aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/apscheduler/triggers
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/apscheduler/triggers')
-rw-r--r--.venv/lib/python3.12/site-packages/apscheduler/triggers/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/apscheduler/triggers/base.py35
-rw-r--r--.venv/lib/python3.12/site-packages/apscheduler/triggers/calendarinterval.py186
-rw-r--r--.venv/lib/python3.12/site-packages/apscheduler/triggers/combining.py114
-rw-r--r--.venv/lib/python3.12/site-packages/apscheduler/triggers/cron/__init__.py289
-rw-r--r--.venv/lib/python3.12/site-packages/apscheduler/triggers/cron/expressions.py285
-rw-r--r--.venv/lib/python3.12/site-packages/apscheduler/triggers/cron/fields.py149
-rw-r--r--.venv/lib/python3.12/site-packages/apscheduler/triggers/date.py51
-rw-r--r--.venv/lib/python3.12/site-packages/apscheduler/triggers/interval.py138
9 files changed, 1247 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/apscheduler/triggers/__init__.py b/.venv/lib/python3.12/site-packages/apscheduler/triggers/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/apscheduler/triggers/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/apscheduler/triggers/base.py b/.venv/lib/python3.12/site-packages/apscheduler/triggers/base.py
new file mode 100644
index 00000000..917af8ca
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/apscheduler/triggers/base.py
@@ -0,0 +1,35 @@
+import random
+from abc import ABCMeta, abstractmethod
+from datetime import timedelta
+
+
+class BaseTrigger(metaclass=ABCMeta):
+ """Abstract base class that defines the interface that every trigger must implement."""
+
+ __slots__ = ()
+
+ @abstractmethod
+ def get_next_fire_time(self, previous_fire_time, now):
+ """
+ Returns the next datetime to fire on, If no such datetime can be calculated, returns
+ ``None``.
+
+ :param datetime.datetime previous_fire_time: the previous time the trigger was fired
+ :param datetime.datetime now: current datetime
+ """
+
+ def _apply_jitter(self, next_fire_time, jitter, now):
+ """
+ Randomize ``next_fire_time`` by adding a random value (the jitter).
+
+ :param datetime.datetime|None next_fire_time: next fire time without jitter applied. If
+ ``None``, returns ``None``.
+ :param int|None jitter: maximum number of seconds to add to ``next_fire_time``
+ (if ``None`` or ``0``, returns ``next_fire_time``)
+ :param datetime.datetime now: current datetime
+ :return datetime.datetime|None: next fire time with a jitter.
+ """
+ if next_fire_time is None or not jitter:
+ return next_fire_time
+
+ return next_fire_time + timedelta(seconds=random.uniform(0, jitter))
diff --git a/.venv/lib/python3.12/site-packages/apscheduler/triggers/calendarinterval.py b/.venv/lib/python3.12/site-packages/apscheduler/triggers/calendarinterval.py
new file mode 100644
index 00000000..cd860489
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/apscheduler/triggers/calendarinterval.py
@@ -0,0 +1,186 @@
+from __future__ import annotations
+
+from datetime import date, datetime, time, timedelta, tzinfo
+from typing import Any
+
+from tzlocal import get_localzone
+
+from apscheduler.triggers.base import BaseTrigger
+from apscheduler.util import (
+ asdate,
+ astimezone,
+ timezone_repr,
+)
+
+
+class CalendarIntervalTrigger(BaseTrigger):
+ """
+ Runs the task on specified calendar-based intervals always at the same exact time of
+ day.
+
+ When calculating the next date, the ``years`` and ``months`` parameters are first
+ added to the previous date while keeping the day of the month constant. This is
+ repeated until the resulting date is valid. After that, the ``weeks`` and ``days``
+ parameters are added to that date. Finally, the date is combined with the given time
+ (hour, minute, second) to form the final datetime.
+
+ This means that if the ``days`` or ``weeks`` parameters are not used, the task will
+ always be executed on the same day of the month at the same wall clock time,
+ assuming the date and time are valid.
+
+ If the resulting datetime is invalid due to a daylight saving forward shift, the
+ date is discarded and the process moves on to the next date. If instead the datetime
+ is ambiguous due to a backward DST shift, the earlier of the two resulting datetimes
+ is used.
+
+ If no previous run time is specified when requesting a new run time (like when
+ starting for the first time or resuming after being paused), ``start_date`` is used
+ as a reference and the next valid datetime equal to or later than the current time
+ will be returned. Otherwise, the next valid datetime starting from the previous run
+ time is returned, even if it's in the past.
+
+ .. warning:: Be wary of setting a start date near the end of the month (29. – 31.)
+ if you have ``months`` specified in your interval, as this will skip the months
+ when those days do not exist. Likewise, setting the start date on the leap day
+ (February 29th) and having ``years`` defined may cause some years to be skipped.
+
+ Users are also discouraged from using a time inside the target timezone's DST
+ switching period (typically around 2 am) since a date could either be skipped or
+ repeated due to the specified wall clock time either occurring twice or not at
+ all.
+
+ :param years: number of years to wait
+ :param months: number of months to wait
+ :param weeks: number of weeks to wait
+ :param days: number of days to wait
+ :param hour: hour to run the task at
+ :param minute: minute to run the task at
+ :param second: second to run the task at
+ :param start_date: first date to trigger on (defaults to current date if omitted)
+ :param end_date: latest possible date to trigger on
+ :param timezone: time zone to use for calculating the next fire time (defaults
+ to scheduler timezone if created via the scheduler, otherwise the local time
+ zone)
+ :param jitter: delay the job execution by ``jitter`` seconds at most
+ """
+
+ __slots__ = (
+ "years",
+ "months",
+ "weeks",
+ "days",
+ "start_date",
+ "end_date",
+ "timezone",
+ "jitter",
+ "_time",
+ )
+
+ def __init__(
+ self,
+ *,
+ years: int = 0,
+ months: int = 0,
+ weeks: int = 0,
+ days: int = 0,
+ hour: int = 0,
+ minute: int = 0,
+ second: int = 0,
+ start_date: date | str | None = None,
+ end_date: date | str | None = None,
+ timezone: str | tzinfo | None = None,
+ jitter: int | None = None,
+ ):
+ if timezone:
+ self.timezone = astimezone(timezone)
+ else:
+ self.timezone = astimezone(get_localzone())
+
+ self.years = years
+ self.months = months
+ self.weeks = weeks
+ self.days = days
+ self.start_date = asdate(start_date) or date.today()
+ self.end_date = asdate(end_date)
+ self.jitter = jitter
+ self._time = time(hour, minute, second, tzinfo=self.timezone)
+
+ if self.years == self.months == self.weeks == self.days == 0:
+ raise ValueError("interval must be at least 1 day long")
+
+ if self.end_date and self.start_date > self.end_date:
+ raise ValueError("end_date cannot be earlier than start_date")
+
+ def get_next_fire_time(
+ self, previous_fire_time: datetime | None, now: datetime
+ ) -> datetime | None:
+ while True:
+ if previous_fire_time:
+ year, month = previous_fire_time.year, previous_fire_time.month
+ while True:
+ month += self.months
+ year += self.years + (month - 1) // 12
+ month = (month - 1) % 12 + 1
+ try:
+ next_date = date(year, month, previous_fire_time.day)
+ except ValueError:
+ pass # Nonexistent date
+ else:
+ next_date += timedelta(self.days + self.weeks * 7)
+ break
+ else:
+ next_date = self.start_date
+
+ # Don't return any date past end_date
+ if self.end_date and next_date > self.end_date:
+ return None
+
+ # Combine the date with the designated time and normalize the result
+ timestamp = datetime.combine(next_date, self._time).timestamp()
+ next_time = datetime.fromtimestamp(timestamp, self.timezone)
+
+ # Check if the time is off due to normalization and a forward DST shift
+ if next_time.timetz() != self._time:
+ previous_fire_time = next_time.date()
+ else:
+ return self._apply_jitter(next_time, self.jitter, now)
+
+ def __getstate__(self) -> dict[str, Any]:
+ return {
+ "version": 1,
+ "interval": [self.years, self.months, self.weeks, self.days],
+ "time": [self._time.hour, self._time.minute, self._time.second],
+ "start_date": self.start_date,
+ "end_date": self.end_date,
+ "timezone": self.timezone,
+ "jitter": self.jitter,
+ }
+
+ def __setstate__(self, state: dict[str, Any]) -> None:
+ if state.get("version", 1) > 1:
+ raise ValueError(
+ f"Got serialized data for version {state['version']} of "
+ f"{self.__class__.__name__}, but only versions up to 1 can be handled"
+ )
+
+ self.years, self.months, self.weeks, self.days = state["interval"]
+ self.start_date = state["start_date"]
+ self.end_date = state["end_date"]
+ self.timezone = state["timezone"]
+ self.jitter = state["jitter"]
+ self._time = time(*state["time"], tzinfo=self.timezone)
+
+ def __repr__(self) -> str:
+ fields = []
+ for field in "years", "months", "weeks", "days":
+ value = getattr(self, field)
+ if value > 0:
+ fields.append(f"{field}={value}")
+
+ fields.append(f"time={self._time.isoformat()!r}")
+ fields.append(f"start_date='{self.start_date}'")
+ if self.end_date:
+ fields.append(f"end_date='{self.end_date}'")
+
+ fields.append(f"timezone={timezone_repr(self.timezone)!r}")
+ return f'{self.__class__.__name__}({", ".join(fields)})'
diff --git a/.venv/lib/python3.12/site-packages/apscheduler/triggers/combining.py b/.venv/lib/python3.12/site-packages/apscheduler/triggers/combining.py
new file mode 100644
index 00000000..653f9b57
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/apscheduler/triggers/combining.py
@@ -0,0 +1,114 @@
+from apscheduler.triggers.base import BaseTrigger
+from apscheduler.util import obj_to_ref, ref_to_obj
+
+
+class BaseCombiningTrigger(BaseTrigger):
+ __slots__ = ("triggers", "jitter")
+
+ def __init__(self, triggers, jitter=None):
+ self.triggers = triggers
+ self.jitter = jitter
+
+ def __getstate__(self):
+ return {
+ "version": 1,
+ "triggers": [
+ (obj_to_ref(trigger.__class__), trigger.__getstate__())
+ for trigger in self.triggers
+ ],
+ "jitter": self.jitter,
+ }
+
+ def __setstate__(self, state):
+ if state.get("version", 1) > 1:
+ raise ValueError(
+ f"Got serialized data for version {state['version']} of "
+ f"{self.__class__.__name__}, but only versions up to 1 can be handled"
+ )
+
+ self.jitter = state["jitter"]
+ self.triggers = []
+ for clsref, state in state["triggers"]:
+ cls = ref_to_obj(clsref)
+ trigger = cls.__new__(cls)
+ trigger.__setstate__(state)
+ self.triggers.append(trigger)
+
+ def __repr__(self):
+ return "<{}({}{})>".format(
+ self.__class__.__name__,
+ self.triggers,
+ f", jitter={self.jitter}" if self.jitter else "",
+ )
+
+
+class AndTrigger(BaseCombiningTrigger):
+ """
+ Always returns the earliest next fire time that all the given triggers can agree on.
+ The trigger is considered to be finished when any of the given triggers has finished its
+ schedule.
+
+ Trigger alias: ``and``
+
+ .. warning:: This trigger should only be used to combine triggers that fire on
+ specific times of day, such as
+ :class:`~apscheduler.triggers.cron.CronTrigger` and
+ class:`~apscheduler.triggers.calendarinterval.CalendarIntervalTrigger`.
+ Attempting to use it with
+ :class:`~apscheduler.triggers.interval.IntervalTrigger` will likely result in
+ the scheduler hanging as it tries to find a fire time that matches exactly
+ between fire times produced by all the given triggers.
+
+ :param list triggers: triggers to combine
+ :param int|None jitter: delay the job execution by ``jitter`` seconds at most
+ """
+
+ __slots__ = ()
+
+ def get_next_fire_time(self, previous_fire_time, now):
+ while True:
+ fire_times = [
+ trigger.get_next_fire_time(previous_fire_time, now)
+ for trigger in self.triggers
+ ]
+ if None in fire_times:
+ return None
+ elif min(fire_times) == max(fire_times):
+ return self._apply_jitter(fire_times[0], self.jitter, now)
+ else:
+ now = max(fire_times)
+
+ def __str__(self):
+ return "and[{}]".format(", ".join(str(trigger) for trigger in self.triggers))
+
+
+class OrTrigger(BaseCombiningTrigger):
+ """
+ Always returns the earliest next fire time produced by any of the given triggers.
+ The trigger is considered finished when all the given triggers have finished their schedules.
+
+ Trigger alias: ``or``
+
+ :param list triggers: triggers to combine
+ :param int|None jitter: delay the job execution by ``jitter`` seconds at most
+
+ .. note:: Triggers that depends on the previous fire time, such as the interval trigger, may
+ seem to behave strangely since they are always passed the previous fire time produced by
+ any of the given triggers.
+ """
+
+ __slots__ = ()
+
+ def get_next_fire_time(self, previous_fire_time, now):
+ fire_times = [
+ trigger.get_next_fire_time(previous_fire_time, now)
+ for trigger in self.triggers
+ ]
+ fire_times = [fire_time for fire_time in fire_times if fire_time is not None]
+ if fire_times:
+ return self._apply_jitter(min(fire_times), self.jitter, now)
+ else:
+ return None
+
+ def __str__(self):
+ return "or[{}]".format(", ".join(str(trigger) for trigger in self.triggers))
diff --git a/.venv/lib/python3.12/site-packages/apscheduler/triggers/cron/__init__.py b/.venv/lib/python3.12/site-packages/apscheduler/triggers/cron/__init__.py
new file mode 100644
index 00000000..03be8196
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/apscheduler/triggers/cron/__init__.py
@@ -0,0 +1,289 @@
+from datetime import datetime, timedelta
+
+from tzlocal import get_localzone
+
+from apscheduler.triggers.base import BaseTrigger
+from apscheduler.triggers.cron.fields import (
+ DEFAULT_VALUES,
+ BaseField,
+ DayOfMonthField,
+ DayOfWeekField,
+ MonthField,
+ WeekField,
+)
+from apscheduler.util import (
+ astimezone,
+ convert_to_datetime,
+ datetime_ceil,
+ datetime_repr,
+)
+
+
+class CronTrigger(BaseTrigger):
+ """
+ Triggers when current time matches all specified time constraints,
+ similarly to how the UNIX cron scheduler works.
+
+ :param int|str year: 4-digit year
+ :param int|str month: month (1-12)
+ :param int|str day: day of month (1-31)
+ :param int|str week: ISO week (1-53)
+ :param int|str day_of_week: number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun)
+ :param int|str hour: hour (0-23)
+ :param int|str minute: minute (0-59)
+ :param int|str second: second (0-59)
+ :param datetime|str start_date: earliest possible date/time to trigger on (inclusive)
+ :param datetime|str end_date: latest possible date/time to trigger on (inclusive)
+ :param datetime.tzinfo|str timezone: time zone to use for the date/time calculations (defaults
+ to scheduler timezone)
+ :param int|None jitter: delay the job execution by ``jitter`` seconds at most
+
+ .. note:: The first weekday is always **monday**.
+ """
+
+ FIELD_NAMES = (
+ "year",
+ "month",
+ "day",
+ "week",
+ "day_of_week",
+ "hour",
+ "minute",
+ "second",
+ )
+ FIELDS_MAP = {
+ "year": BaseField,
+ "month": MonthField,
+ "week": WeekField,
+ "day": DayOfMonthField,
+ "day_of_week": DayOfWeekField,
+ "hour": BaseField,
+ "minute": BaseField,
+ "second": BaseField,
+ }
+
+ __slots__ = "timezone", "start_date", "end_date", "fields", "jitter"
+
+ def __init__(
+ self,
+ year=None,
+ month=None,
+ day=None,
+ week=None,
+ day_of_week=None,
+ hour=None,
+ minute=None,
+ second=None,
+ start_date=None,
+ end_date=None,
+ timezone=None,
+ jitter=None,
+ ):
+ if timezone:
+ self.timezone = astimezone(timezone)
+ elif isinstance(start_date, datetime) and start_date.tzinfo:
+ self.timezone = astimezone(start_date.tzinfo)
+ elif isinstance(end_date, datetime) and end_date.tzinfo:
+ self.timezone = astimezone(end_date.tzinfo)
+ else:
+ self.timezone = get_localzone()
+
+ self.start_date = convert_to_datetime(start_date, self.timezone, "start_date")
+ self.end_date = convert_to_datetime(end_date, self.timezone, "end_date")
+
+ self.jitter = jitter
+
+ values = dict(
+ (key, value)
+ for (key, value) in locals().items()
+ if key in self.FIELD_NAMES and value is not None
+ )
+ self.fields = []
+ assign_defaults = False
+ for field_name in self.FIELD_NAMES:
+ if field_name in values:
+ exprs = values.pop(field_name)
+ is_default = False
+ assign_defaults = not values
+ elif assign_defaults:
+ exprs = DEFAULT_VALUES[field_name]
+ is_default = True
+ else:
+ exprs = "*"
+ is_default = True
+
+ field_class = self.FIELDS_MAP[field_name]
+ field = field_class(field_name, exprs, is_default)
+ self.fields.append(field)
+
+ @classmethod
+ def from_crontab(cls, expr, timezone=None):
+ """
+ Create a :class:`~CronTrigger` from a standard crontab expression.
+
+ See https://en.wikipedia.org/wiki/Cron for more information on the format accepted here.
+
+ :param expr: minute, hour, day of month, month, day of week
+ :param datetime.tzinfo|str timezone: time zone to use for the date/time calculations (
+ defaults to scheduler timezone)
+ :return: a :class:`~CronTrigger` instance
+
+ """
+ values = expr.split()
+ if len(values) != 5:
+ raise ValueError(f"Wrong number of fields; got {len(values)}, expected 5")
+
+ return cls(
+ minute=values[0],
+ hour=values[1],
+ day=values[2],
+ month=values[3],
+ day_of_week=values[4],
+ timezone=timezone,
+ )
+
+ def _increment_field_value(self, dateval, fieldnum):
+ """
+ Increments the designated field and resets all less significant fields to their minimum
+ values.
+
+ :type dateval: datetime
+ :type fieldnum: int
+ :return: a tuple containing the new date, and the number of the field that was actually
+ incremented
+ :rtype: tuple
+ """
+
+ values = {}
+ i = 0
+ while i < len(self.fields):
+ field = self.fields[i]
+ if not field.REAL:
+ if i == fieldnum:
+ fieldnum -= 1
+ i -= 1
+ else:
+ i += 1
+ continue
+
+ if i < fieldnum:
+ values[field.name] = field.get_value(dateval)
+ i += 1
+ elif i > fieldnum:
+ values[field.name] = field.get_min(dateval)
+ i += 1
+ else:
+ value = field.get_value(dateval)
+ maxval = field.get_max(dateval)
+ if value == maxval:
+ fieldnum -= 1
+ i -= 1
+ else:
+ values[field.name] = value + 1
+ i += 1
+
+ difference = datetime(**values) - dateval.replace(tzinfo=None)
+ dateval = datetime.fromtimestamp(
+ dateval.timestamp() + difference.total_seconds(), self.timezone
+ )
+ return dateval, fieldnum
+
+ def _set_field_value(self, dateval, fieldnum, new_value):
+ values = {}
+ for i, field in enumerate(self.fields):
+ if field.REAL:
+ if i < fieldnum:
+ values[field.name] = field.get_value(dateval)
+ elif i > fieldnum:
+ values[field.name] = field.get_min(dateval)
+ else:
+ values[field.name] = new_value
+
+ return datetime(**values, tzinfo=self.timezone, fold=dateval.fold)
+
+ def get_next_fire_time(self, previous_fire_time, now):
+ if previous_fire_time:
+ start_date = min(now, previous_fire_time + timedelta(microseconds=1))
+ if start_date == previous_fire_time:
+ start_date += timedelta(microseconds=1)
+ else:
+ start_date = max(now, self.start_date) if self.start_date else now
+
+ fieldnum = 0
+ next_date = datetime_ceil(start_date).astimezone(self.timezone)
+ while 0 <= fieldnum < len(self.fields):
+ field = self.fields[fieldnum]
+ curr_value = field.get_value(next_date)
+ next_value = field.get_next_value(next_date)
+
+ if next_value is None:
+ # No valid value was found
+ next_date, fieldnum = self._increment_field_value(
+ next_date, fieldnum - 1
+ )
+ elif next_value > curr_value:
+ # A valid, but higher than the starting value, was found
+ if field.REAL:
+ next_date = self._set_field_value(next_date, fieldnum, next_value)
+ fieldnum += 1
+ else:
+ next_date, fieldnum = self._increment_field_value(
+ next_date, fieldnum
+ )
+ else:
+ # A valid value was found, no changes necessary
+ fieldnum += 1
+
+ # Return if the date has rolled past the end date
+ if self.end_date and next_date > self.end_date:
+ return None
+
+ if fieldnum >= 0:
+ next_date = self._apply_jitter(next_date, self.jitter, now)
+ return min(next_date, self.end_date) if self.end_date else next_date
+
+ def __getstate__(self):
+ return {
+ "version": 2,
+ "timezone": self.timezone,
+ "start_date": self.start_date,
+ "end_date": self.end_date,
+ "fields": self.fields,
+ "jitter": self.jitter,
+ }
+
+ def __setstate__(self, state):
+ # This is for compatibility with APScheduler 3.0.x
+ if isinstance(state, tuple):
+ state = state[1]
+
+ if state.get("version", 1) > 2:
+ raise ValueError(
+ f"Got serialized data for version {state['version']} of "
+ f"{self.__class__.__name__}, but only versions up to 2 can be handled"
+ )
+
+ self.timezone = astimezone(state["timezone"])
+ self.start_date = state["start_date"]
+ self.end_date = state["end_date"]
+ self.fields = state["fields"]
+ self.jitter = state.get("jitter")
+
+ def __str__(self):
+ options = [f"{f.name}='{f}'" for f in self.fields if not f.is_default]
+ return "cron[{}]".format(", ".join(options))
+
+ def __repr__(self):
+ options = [f"{f.name}='{f}'" for f in self.fields if not f.is_default]
+ if self.start_date:
+ options.append(f"start_date={datetime_repr(self.start_date)!r}")
+ if self.end_date:
+ options.append(f"end_date={datetime_repr(self.end_date)!r}")
+ if self.jitter:
+ options.append(f"jitter={self.jitter}")
+
+ return "<{} ({}, timezone='{}')>".format(
+ self.__class__.__name__,
+ ", ".join(options),
+ self.timezone,
+ )
diff --git a/.venv/lib/python3.12/site-packages/apscheduler/triggers/cron/expressions.py b/.venv/lib/python3.12/site-packages/apscheduler/triggers/cron/expressions.py
new file mode 100644
index 00000000..0d84ec23
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/apscheduler/triggers/cron/expressions.py
@@ -0,0 +1,285 @@
+"""This module contains the expressions applicable for CronTrigger's fields."""
+
+__all__ = (
+ "AllExpression",
+ "RangeExpression",
+ "WeekdayRangeExpression",
+ "WeekdayPositionExpression",
+ "LastDayOfMonthExpression",
+)
+
+import re
+from calendar import monthrange
+
+from apscheduler.util import asint
+
+WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
+MONTHS = [
+ "jan",
+ "feb",
+ "mar",
+ "apr",
+ "may",
+ "jun",
+ "jul",
+ "aug",
+ "sep",
+ "oct",
+ "nov",
+ "dec",
+]
+
+
+class AllExpression:
+ value_re = re.compile(r"\*(?:/(?P<step>\d+))?$")
+
+ def __init__(self, step=None):
+ self.step = asint(step)
+ if self.step == 0:
+ raise ValueError("Increment must be higher than 0")
+
+ def validate_range(self, field_name):
+ from apscheduler.triggers.cron.fields import MAX_VALUES, MIN_VALUES
+
+ value_range = MAX_VALUES[field_name] - MIN_VALUES[field_name]
+ if self.step and self.step > value_range:
+ raise ValueError(
+ f"the step value ({self.step}) is higher than the total range of the "
+ f"expression ({value_range})"
+ )
+
+ def get_next_value(self, date, field):
+ start = field.get_value(date)
+ minval = field.get_min(date)
+ maxval = field.get_max(date)
+ start = max(start, minval)
+
+ if not self.step:
+ next = start
+ else:
+ distance_to_next = (self.step - (start - minval)) % self.step
+ next = start + distance_to_next
+
+ if next <= maxval:
+ return next
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__) and self.step == other.step
+
+ def __str__(self):
+ if self.step:
+ return "*/%d" % self.step
+ return "*"
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({self.step})"
+
+
+class RangeExpression(AllExpression):
+ value_re = re.compile(r"(?P<first>\d+)(?:-(?P<last>\d+))?(?:/(?P<step>\d+))?$")
+
+ def __init__(self, first, last=None, step=None):
+ super().__init__(step)
+ first = asint(first)
+ last = asint(last)
+ if last is None and step is None:
+ last = first
+ if last is not None and first > last:
+ raise ValueError(
+ "The minimum value in a range must not be higher than the maximum"
+ )
+ self.first = first
+ self.last = last
+
+ def validate_range(self, field_name):
+ from apscheduler.triggers.cron.fields import MAX_VALUES, MIN_VALUES
+
+ super().validate_range(field_name)
+ if self.first < MIN_VALUES[field_name]:
+ raise ValueError(
+ f"the first value ({self.first}) is lower than the minimum value ({MIN_VALUES[field_name]})"
+ )
+ if self.last is not None and self.last > MAX_VALUES[field_name]:
+ raise ValueError(
+ f"the last value ({self.last}) is higher than the maximum value ({MAX_VALUES[field_name]})"
+ )
+ value_range = (self.last or MAX_VALUES[field_name]) - self.first
+ if self.step and self.step > value_range:
+ raise ValueError(
+ f"the step value ({self.step}) is higher than the total range of the "
+ f"expression ({value_range})"
+ )
+
+ def get_next_value(self, date, field):
+ startval = field.get_value(date)
+ minval = field.get_min(date)
+ maxval = field.get_max(date)
+
+ # Apply range limits
+ minval = max(minval, self.first)
+ maxval = min(maxval, self.last) if self.last is not None else maxval
+ nextval = max(minval, startval)
+
+ # Apply the step if defined
+ if self.step:
+ distance_to_next = (self.step - (nextval - minval)) % self.step
+ nextval += distance_to_next
+
+ return nextval if nextval <= maxval else None
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, self.__class__)
+ and self.first == other.first
+ and self.last == other.last
+ )
+
+ def __str__(self):
+ if self.last != self.first and self.last is not None:
+ range = "%d-%d" % (self.first, self.last)
+ else:
+ range = str(self.first)
+
+ if self.step:
+ return "%s/%d" % (range, self.step)
+
+ return range
+
+ def __repr__(self):
+ args = [str(self.first)]
+ if self.last != self.first and self.last is not None or self.step:
+ args.append(str(self.last))
+
+ if self.step:
+ args.append(str(self.step))
+
+ return "{}({})".format(self.__class__.__name__, ", ".join(args))
+
+
+class MonthRangeExpression(RangeExpression):
+ value_re = re.compile(r"(?P<first>[a-z]+)(?:-(?P<last>[a-z]+))?", re.IGNORECASE)
+
+ def __init__(self, first, last=None):
+ try:
+ first_num = MONTHS.index(first.lower()) + 1
+ except ValueError:
+ raise ValueError(f'Invalid month name "{first}"')
+
+ if last:
+ try:
+ last_num = MONTHS.index(last.lower()) + 1
+ except ValueError:
+ raise ValueError(f'Invalid month name "{last}"')
+ else:
+ last_num = None
+
+ super().__init__(first_num, last_num)
+
+ def __str__(self):
+ if self.last != self.first and self.last is not None:
+ return f"{MONTHS[self.first - 1]}-{MONTHS[self.last - 1]}"
+ return MONTHS[self.first - 1]
+
+ def __repr__(self):
+ args = [f"'{MONTHS[self.first]}'"]
+ if self.last != self.first and self.last is not None:
+ args.append(f"'{MONTHS[self.last - 1]}'")
+ return "{}({})".format(self.__class__.__name__, ", ".join(args))
+
+
+class WeekdayRangeExpression(RangeExpression):
+ value_re = re.compile(r"(?P<first>[a-z]+)(?:-(?P<last>[a-z]+))?", re.IGNORECASE)
+
+ def __init__(self, first, last=None):
+ try:
+ first_num = WEEKDAYS.index(first.lower())
+ except ValueError:
+ raise ValueError(f'Invalid weekday name "{first}"')
+
+ if last:
+ try:
+ last_num = WEEKDAYS.index(last.lower())
+ except ValueError:
+ raise ValueError(f'Invalid weekday name "{last}"')
+ else:
+ last_num = None
+
+ super().__init__(first_num, last_num)
+
+ def __str__(self):
+ if self.last != self.first and self.last is not None:
+ return f"{WEEKDAYS[self.first]}-{WEEKDAYS[self.last]}"
+ return WEEKDAYS[self.first]
+
+ def __repr__(self):
+ args = [f"'{WEEKDAYS[self.first]}'"]
+ if self.last != self.first and self.last is not None:
+ args.append(f"'{WEEKDAYS[self.last]}'")
+ return "{}({})".format(self.__class__.__name__, ", ".join(args))
+
+
+class WeekdayPositionExpression(AllExpression):
+ options = ["1st", "2nd", "3rd", "4th", "5th", "last"]
+ value_re = re.compile(
+ r"(?P<option_name>{}) +(?P<weekday_name>(?:\d+|\w+))".format("|".join(options)),
+ re.IGNORECASE,
+ )
+
+ def __init__(self, option_name, weekday_name):
+ super().__init__(None)
+ try:
+ self.option_num = self.options.index(option_name.lower())
+ except ValueError:
+ raise ValueError(f'Invalid weekday position "{option_name}"')
+
+ try:
+ self.weekday = WEEKDAYS.index(weekday_name.lower())
+ except ValueError:
+ raise ValueError(f'Invalid weekday name "{weekday_name}"')
+
+ def get_next_value(self, date, field):
+ # Figure out the weekday of the month's first day and the number of days in that month
+ first_day_wday, last_day = monthrange(date.year, date.month)
+
+ # Calculate which day of the month is the first of the target weekdays
+ first_hit_day = self.weekday - first_day_wday + 1
+ if first_hit_day <= 0:
+ first_hit_day += 7
+
+ # Calculate what day of the month the target weekday would be
+ if self.option_num < 5:
+ target_day = first_hit_day + self.option_num * 7
+ else:
+ target_day = first_hit_day + ((last_day - first_hit_day) // 7) * 7
+
+ if target_day <= last_day and target_day >= date.day:
+ return target_day
+
+ def __eq__(self, other):
+ return (
+ super().__eq__(other)
+ and self.option_num == other.option_num
+ and self.weekday == other.weekday
+ )
+
+ def __str__(self):
+ return f"{self.options[self.option_num]} {WEEKDAYS[self.weekday]}"
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}('{self.options[self.option_num]}', '{WEEKDAYS[self.weekday]}')"
+
+
+class LastDayOfMonthExpression(AllExpression):
+ value_re = re.compile(r"last", re.IGNORECASE)
+
+ def __init__(self):
+ super().__init__(None)
+
+ def get_next_value(self, date, field):
+ return monthrange(date.year, date.month)[1]
+
+ def __str__(self):
+ return "last"
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}()"
diff --git a/.venv/lib/python3.12/site-packages/apscheduler/triggers/cron/fields.py b/.venv/lib/python3.12/site-packages/apscheduler/triggers/cron/fields.py
new file mode 100644
index 00000000..4c081797
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/apscheduler/triggers/cron/fields.py
@@ -0,0 +1,149 @@
+"""Fields represent CronTrigger options which map to :class:`~datetime.datetime` fields."""
+
+__all__ = (
+ "MIN_VALUES",
+ "MAX_VALUES",
+ "DEFAULT_VALUES",
+ "BaseField",
+ "WeekField",
+ "DayOfMonthField",
+ "DayOfWeekField",
+)
+
+import re
+from calendar import monthrange
+
+from apscheduler.triggers.cron.expressions import (
+ AllExpression,
+ LastDayOfMonthExpression,
+ MonthRangeExpression,
+ RangeExpression,
+ WeekdayPositionExpression,
+ WeekdayRangeExpression,
+)
+
+MIN_VALUES = {
+ "year": 1970,
+ "month": 1,
+ "day": 1,
+ "week": 1,
+ "day_of_week": 0,
+ "hour": 0,
+ "minute": 0,
+ "second": 0,
+}
+MAX_VALUES = {
+ "year": 9999,
+ "month": 12,
+ "day": 31,
+ "week": 53,
+ "day_of_week": 6,
+ "hour": 23,
+ "minute": 59,
+ "second": 59,
+}
+DEFAULT_VALUES = {
+ "year": "*",
+ "month": 1,
+ "day": 1,
+ "week": "*",
+ "day_of_week": "*",
+ "hour": 0,
+ "minute": 0,
+ "second": 0,
+}
+SEPARATOR = re.compile(" *, *")
+
+
+class BaseField:
+ REAL = True
+ COMPILERS = [AllExpression, RangeExpression]
+
+ def __init__(self, name, exprs, is_default=False):
+ self.name = name
+ self.is_default = is_default
+ self.compile_expressions(exprs)
+
+ def get_min(self, dateval):
+ return MIN_VALUES[self.name]
+
+ def get_max(self, dateval):
+ return MAX_VALUES[self.name]
+
+ def get_value(self, dateval):
+ return getattr(dateval, self.name)
+
+ def get_next_value(self, dateval):
+ smallest = None
+ for expr in self.expressions:
+ value = expr.get_next_value(dateval, self)
+ if smallest is None or (value is not None and value < smallest):
+ smallest = value
+
+ return smallest
+
+ def compile_expressions(self, exprs):
+ self.expressions = []
+
+ # Split a comma-separated expression list, if any
+ for expr in SEPARATOR.split(str(exprs).strip()):
+ self.compile_expression(expr)
+
+ def compile_expression(self, expr):
+ for compiler in self.COMPILERS:
+ match = compiler.value_re.match(expr)
+ if match:
+ compiled_expr = compiler(**match.groupdict())
+
+ try:
+ compiled_expr.validate_range(self.name)
+ except ValueError as e:
+ raise ValueError(
+ f"Error validating expression {expr!r}: {e}"
+ ) from None
+
+ self.expressions.append(compiled_expr)
+ return
+
+ raise ValueError(f'Unrecognized expression "{expr}" for field "{self.name}"')
+
+ def __eq__(self, other):
+ return (
+ isinstance(self, self.__class__) and self.expressions == other.expressions
+ )
+
+ def __str__(self):
+ expr_strings = (str(e) for e in self.expressions)
+ return ",".join(expr_strings)
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}('{self.name}', '{self}')"
+
+
+class WeekField(BaseField):
+ REAL = False
+
+ def get_value(self, dateval):
+ return dateval.isocalendar()[1]
+
+
+class DayOfMonthField(BaseField):
+ COMPILERS = BaseField.COMPILERS + [
+ WeekdayPositionExpression,
+ LastDayOfMonthExpression,
+ ]
+
+ def get_max(self, dateval):
+ return monthrange(dateval.year, dateval.month)[1]
+
+
+class DayOfWeekField(BaseField):
+ REAL = False
+ COMPILERS = BaseField.COMPILERS + [WeekdayRangeExpression]
+
+ def get_value(self, dateval):
+ return dateval.weekday()
+
+
+class MonthField(BaseField):
+ COMPILERS = BaseField.COMPILERS + [MonthRangeExpression]
diff --git a/.venv/lib/python3.12/site-packages/apscheduler/triggers/date.py b/.venv/lib/python3.12/site-packages/apscheduler/triggers/date.py
new file mode 100644
index 00000000..a9302da5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/apscheduler/triggers/date.py
@@ -0,0 +1,51 @@
+from datetime import datetime
+
+from tzlocal import get_localzone
+
+from apscheduler.triggers.base import BaseTrigger
+from apscheduler.util import astimezone, convert_to_datetime, datetime_repr
+
+
+class DateTrigger(BaseTrigger):
+ """
+ Triggers once on the given datetime. If ``run_date`` is left empty, current time is used.
+
+ :param datetime|str run_date: the date/time to run the job at
+ :param datetime.tzinfo|str timezone: time zone for ``run_date`` if it doesn't have one already
+ """
+
+ __slots__ = "run_date"
+
+ def __init__(self, run_date=None, timezone=None):
+ timezone = astimezone(timezone) or get_localzone()
+ if run_date is not None:
+ self.run_date = convert_to_datetime(run_date, timezone, "run_date")
+ else:
+ self.run_date = datetime.now(timezone)
+
+ def get_next_fire_time(self, previous_fire_time, now):
+ return self.run_date if previous_fire_time is None else None
+
+ def __getstate__(self):
+ return {"version": 1, "run_date": self.run_date}
+
+ def __setstate__(self, state):
+ # This is for compatibility with APScheduler 3.0.x
+ if isinstance(state, tuple):
+ state = state[1]
+
+ if state.get("version", 1) > 1:
+ raise ValueError(
+ f"Got serialized data for version {state['version']} of "
+ f"{self.__class__.__name__}, but only version 1 can be handled"
+ )
+
+ self.run_date = state["run_date"]
+
+ def __str__(self):
+ return f"date[{datetime_repr(self.run_date)}]"
+
+ def __repr__(self):
+ return (
+ f"<{self.__class__.__name__} (run_date='{datetime_repr(self.run_date)}')>"
+ )
diff --git a/.venv/lib/python3.12/site-packages/apscheduler/triggers/interval.py b/.venv/lib/python3.12/site-packages/apscheduler/triggers/interval.py
new file mode 100644
index 00000000..9264c4ac
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/apscheduler/triggers/interval.py
@@ -0,0 +1,138 @@
+import random
+from datetime import datetime, timedelta
+from math import ceil
+
+from tzlocal import get_localzone
+
+from apscheduler.triggers.base import BaseTrigger
+from apscheduler.util import (
+ astimezone,
+ convert_to_datetime,
+ datetime_repr,
+)
+
+
+class IntervalTrigger(BaseTrigger):
+ """
+ Triggers on specified intervals, starting on ``start_date`` if specified, ``datetime.now()`` +
+ interval otherwise.
+
+ :param int weeks: number of weeks to wait
+ :param int days: number of days to wait
+ :param int hours: number of hours to wait
+ :param int minutes: number of minutes to wait
+ :param int seconds: number of seconds to wait
+ :param datetime|str start_date: starting point for the interval calculation
+ :param datetime|str end_date: latest possible date/time to trigger on
+ :param datetime.tzinfo|str timezone: time zone to use for the date/time calculations
+ :param int|None jitter: delay the job execution by ``jitter`` seconds at most
+ """
+
+ __slots__ = (
+ "timezone",
+ "start_date",
+ "end_date",
+ "interval",
+ "interval_length",
+ "jitter",
+ )
+
+ def __init__(
+ self,
+ weeks=0,
+ days=0,
+ hours=0,
+ minutes=0,
+ seconds=0,
+ start_date=None,
+ end_date=None,
+ timezone=None,
+ jitter=None,
+ ):
+ self.interval = timedelta(
+ weeks=weeks, days=days, hours=hours, minutes=minutes, seconds=seconds
+ )
+ self.interval_length = self.interval.total_seconds()
+ if self.interval_length == 0:
+ self.interval = timedelta(seconds=1)
+ self.interval_length = 1
+
+ if timezone:
+ self.timezone = astimezone(timezone)
+ elif isinstance(start_date, datetime) and start_date.tzinfo:
+ self.timezone = astimezone(start_date.tzinfo)
+ elif isinstance(end_date, datetime) and end_date.tzinfo:
+ self.timezone = astimezone(end_date.tzinfo)
+ else:
+ self.timezone = get_localzone()
+
+ start_date = start_date or (datetime.now(self.timezone) + self.interval)
+ self.start_date = convert_to_datetime(start_date, self.timezone, "start_date")
+ self.end_date = convert_to_datetime(end_date, self.timezone, "end_date")
+
+ self.jitter = jitter
+
+ def get_next_fire_time(self, previous_fire_time, now):
+ if previous_fire_time:
+ next_fire_time = previous_fire_time.timestamp() + self.interval_length
+ elif self.start_date > now:
+ next_fire_time = self.start_date.timestamp()
+ else:
+ timediff = now.timestamp() - self.start_date.timestamp()
+ next_interval_num = int(ceil(timediff / self.interval_length))
+ next_fire_time = (
+ self.start_date.timestamp() + self.interval_length * next_interval_num
+ )
+
+ if self.jitter is not None:
+ next_fire_time += random.uniform(0, self.jitter)
+
+ if not self.end_date or next_fire_time <= self.end_date.timestamp():
+ return datetime.fromtimestamp(next_fire_time, tz=self.timezone)
+
+ def __getstate__(self):
+ return {
+ "version": 2,
+ "timezone": astimezone(self.timezone),
+ "start_date": self.start_date,
+ "end_date": self.end_date,
+ "interval": self.interval,
+ "jitter": self.jitter,
+ }
+
+ def __setstate__(self, state):
+ # This is for compatibility with APScheduler 3.0.x
+ if isinstance(state, tuple):
+ state = state[1]
+
+ if state.get("version", 1) > 2:
+ raise ValueError(
+ f"Got serialized data for version {state['version']} of "
+ f"{self.__class__.__name__}, but only versions up to 2 can be handled"
+ )
+
+ self.timezone = state["timezone"]
+ self.start_date = state["start_date"]
+ self.end_date = state["end_date"]
+ self.interval = state["interval"]
+ self.interval_length = self.interval.total_seconds()
+ self.jitter = state.get("jitter")
+
+ def __str__(self):
+ return f"interval[{self.interval!s}]"
+
+ def __repr__(self):
+ options = [
+ f"interval={self.interval!r}",
+ f"start_date={datetime_repr(self.start_date)!r}",
+ ]
+ if self.end_date:
+ options.append(f"end_date={datetime_repr(self.end_date)!r}")
+ if self.jitter:
+ options.append(f"jitter={self.jitter}")
+
+ return "<{} ({}, timezone='{}')>".format(
+ self.__class__.__name__,
+ ", ".join(options),
+ self.timezone,
+ )