diff --git a/README.rst b/README.rst index 0c3bffe2..d591ec51 100644 --- a/README.rst +++ b/README.rst @@ -42,6 +42,7 @@ Usage schedule.every().monday.do(job) schedule.every().wednesday.at("13:15").do(job) schedule.every().minute.at(":17").do(job) + schedule.every().monthThe(5).at("05:08:17").do(job) while True: schedule.run_pending() diff --git a/schedule/__init__.py b/schedule/__init__.py index 83a95815..5937de51 100644 --- a/schedule/__init__.py +++ b/schedule/__init__.py @@ -286,9 +286,10 @@ def is_repr(j): call_repr = job_func_name + "(" + ", ".join(args + kwargs) + ")" if self.at_time is not None: - return "Every %s %s at %s do %s %s" % ( + return "Every %s %s %s at %s do %s %s" % ( self.interval, self.unit[:-1] if self.interval == 1 else self.unit, + self.monthDay if self.unit == "monthThe" else "", self.at_time, call_repr, timestats, @@ -439,6 +440,17 @@ def sunday(self): ) self.start_day = "sunday" return self.weeks + + def monthThe(self, monthDay): + if self.interval != 1: + raise IntervalError( + "Scheduling .monthThe(d) jobs is only allowed for monthly jobs. " + "Using .monthThe(d) on a job scheduled to run every 2 or more months " + "is not supported." + ) + self.unit = "monthThe" + self.monthDay = monthDay + return self def tag(self, *tags: Hashable): """ @@ -474,13 +486,13 @@ def at(self, time_str): :return: The invoked job instance """ - if self.unit not in ("days", "hours", "minutes") and not self.start_day: + if self.unit not in ("days", "hours", "minutes", "monthThe") and not self.start_day: raise ScheduleValueError( - "Invalid unit (valid units are `days`, `hours`, and `minutes`)" + "Invalid unit (valid units are `days`, `hours`, and `minutes` and 'monthThe')" ) if not isinstance(time_str, str): raise TypeError("at() should be passed a string") - if self.unit == "days" or self.start_day: + if self.unit == "days" or self.start_day or self.unit == "monthThe": if not re.match(r"^([0-2]\d:)?[0-5]\d:[0-5]\d$", time_str): raise ScheduleValueError( "Invalid time format for a daily job (valid format is HH:MM(:SS)?)" @@ -512,7 +524,7 @@ def at(self, time_str): else: hour, minute = time_values second = 0 - if self.unit == "days" or self.start_day: + if self.unit == "days" or self.start_day or self.unit == "monthThe": hour = int(hour) if not (0 <= hour <= 23): raise ScheduleValueError( @@ -671,10 +683,10 @@ def _schedule_next_run(self) -> None: """ Compute the instant when this job should run next. """ - if self.unit not in ("seconds", "minutes", "hours", "days", "weeks"): + if self.unit not in ("seconds", "minutes", "hours", "days", "weeks", "monthThe"): raise ScheduleValueError( "Invalid unit (valid units are `seconds`, `minutes`, `hours`, " - "`days`, and `weeks`)" + "`days`, and `weeks` and 'monthThe')" ) if self.latest is not None: @@ -684,7 +696,19 @@ def _schedule_next_run(self) -> None: else: interval = self.interval - self.period = datetime.timedelta(**{self.unit: interval}) + # Special case for .monthThe(d) + if self.unit == "monthThe": + # We search for le period of the next run. to know if already run or not. + start = 0 if not self.last_run else 1 + nextDate = datetime.datetime.now() + for day in range(start, 32): + nextDate += datetime.timedelta(days=day) + if nextDate.day == self.monthDay: + break + self.period = datetime.timedelta(days=day) + else: + self.period = datetime.timedelta(**{self.unit: interval}) + self.next_run = datetime.datetime.now() + self.period if self.start_day is not None: if self.unit != "weeks": @@ -708,17 +732,18 @@ def _schedule_next_run(self) -> None: days_ahead += 7 self.next_run += datetime.timedelta(days_ahead) - self.period if self.at_time is not None: - if self.unit not in ("days", "hours", "minutes") and self.start_day is None: + if self.unit not in ("days", "hours", "minutes", "monthThe") and self.start_day is None: raise ScheduleValueError("Invalid unit without specifying start day") kwargs = {"second": self.at_time.second, "microsecond": 0} - if self.unit == "days" or self.start_day is not None: + if self.unit == "days" or self.start_day is not None or self.unit == "monthThe": kwargs["hour"] = self.at_time.hour - if self.unit in ["days", "hours"] or self.start_day is not None: + if self.unit in ["days", "hours", "monthThe"] or self.start_day is not None: kwargs["minute"] = self.at_time.minute self.next_run = self.next_run.replace(**kwargs) # type: ignore # Make sure we run at the specified time *today* (or *this hour*) # as well. This accounts for when a job takes so long it finished # in the next period. + # With monthThe we consider a job can’t run so long. if not self.last_run or (self.next_run - self.last_run) > self.period: now = datetime.datetime.now() if (