docstr: :return: Return the next run datetime, None if no scheduling needed
docstr_tok: [":","return",":","Return","the","next","run","datetime",",","None","if","no","scheduling","needed"]
code: def next(self):
      # type: () -> Optional[datetime]
      """
      :return: Return the next run datetime, None if no scheduling needed
      """
      if not self.recurring and self._last_executed:
          self._next_run = None
          return self._next_run
  
      # make sure we have a starting time
      if not self.starting_time:
          self.starting_time = datetime.utcnow()
  
      # check if we have a specific date
      if self.year and self.year > 2000:
          # this is by definition a single execution only
          if self._last_executed:
              return None
  
          self._next_run = datetime(
              year=int(self.year),
              month=int(self.month or 1),
              day=int(self.day or 1),
              hour=int(self.hour or 0),
              minute=int(self.minute or 0),
          )
          if self.weekdays:
              self._next_run += relativedelta(
                  weekday=self.get_weekday_ord(self.weekdays[0])
              )
  
          return self._next_run
  
      # check if we have a specific day of the week
      weekday = None
      if self.weekdays:
          # get previous weekday
          _weekdays = [self.get_weekday_ord(w) for w in self.weekdays]
          try:
              prev_weekday_ind = (
                  _weekdays.index(self._last_executed.weekday())
                  if self._last_executed
                  else -1
              )
          except ValueError:
              # in case previous execution was not in the weekday (for example executed immediately at scheduling)
              prev_weekday_ind = -1
          weekday = _weekdays[(prev_weekday_ind + 1) % len(_weekdays)]
  
      prev_timestamp = self._last_executed or self.starting_time
      # fix first scheduled job should be as close as possible to starting time
      if self._schedule_counter < 1:
          # we should get here the first time we need to schedule a job, after that the delta is fixed
          # If we have execute_immediately we need to get here after the first execution
          # (so even through we have self._last_executed)
  
          # if this is a daily schedule and we can still run it today, then we should
          run0 = self._calc_next_run(self.starting_time, weekday)
          run1 = self._calc_next_run(run0, weekday)
          delta = run1 - run0
          optional_first_timestamp = self._calc_next_run(
              prev_timestamp - delta, weekday
          )
          if optional_first_timestamp > prev_timestamp:
              # this is us, we can still run it
              self._next_run = optional_first_timestamp
              return self._next_run
  
      self._next_run = self._calc_next_run(prev_timestamp, weekday)
      return self._next_run
  
imports: import json
  import logging
  from datetime import datetime
  from threading import Thread, enumerate
  from time import sleep, time
  from typing import List, Union, Optional, Callable, Sequence, Dict
  from attr import attrs, attrib
  from dateutil.relativedelta import relativedelta
  from .job import ClearmlJob
  from .controller import PipelineController
  from ..backend_interface.util import datetime_from_isoformat, datetime_to_isoformat, mutually_exclusive
  from ..task import Task
label_desc: Use image node_id 4 for calling the ScheduleJob obj's underlying member method code with example usage: obj.next() and returns: self, self, self, None, self