Source code for schedium.triggers.sugar.weekly
from __future__ import annotations
from datetime import time
from schedium.triggers.base import BaseTrigger
from schedium.triggers.on import On
from schedium.triggers.sugar._time import parse_at
from schedium.triggers.sugar.tick import Tick
from schedium.types.granularity import Granularity
_WEEKDAY_ALIASES_TO_ISO = {
"mon": 1,
"tue": 2,
"wed": 3,
"thu": 4,
"fri": 5,
"sat": 6,
"sun": 7,
}
def _parse_weekday_to_on_value(day: str | int) -> int:
if isinstance(day, int):
if 1 <= day <= 7:
return day
raise ValueError("weekday integer must be 1..7 (Mon..Sun)")
key = day.strip()[:3].lower()
if key in _WEEKDAY_ALIASES_TO_ISO:
return _WEEKDAY_ALIASES_TO_ISO[key]
raise ValueError(
"weekday must be one of: mon/tue/wed/thu/fri/sat/sun (or full names), or 1..7"
)
[docs]
def Weekly(
day: str | int, *, at: str | time | None = None, force_0_minute: bool = False
) -> BaseTrigger:
"""
Convenience trigger: run weekly on a specific weekday, optionally at a time.
Parameters
----------
day : str | int
Weekday to run on. Can be a string like "mon"/"monday" (case-insensitive,
only first 3 letters are considered) or an integer in ISO format where
Monday=1 and Sunday=7.
at : str | datetime.time, optional
Time of day to run at. If not provided, it will not constrain the time.
Can be a :class:`datetime.time` object, or a string in "HH:MM" or
"HH:MM:SS[.mmm]" format.
force_0_minute : bool, default False
By default, if `at` is provided without a minute component (e.g. "09:00" or
`time(9, 0)`), the trigger does not constrain the minute (i.e., it can run
any minute during the 9 o'clock hour). Setting `force_0_minute=True` makes
it so that the minute is constrained to 0, meaning it will only run at the top
of the hour (e.g. 09:00:37).
Notes
-----
This helper composes triggers to express a weekly schedule.
- `Tick(Granularity.WEEK)` provides a WEEK "bucket" for deduplication.
- `On(day_of_week=...)` and, if `at` is provided, hour/minute
(and optionally second/millisecond).
Using `Tick(WEEK)` (instead of `Every(unit="week", interval=1)`) avoids forcing
alignment to week boundaries (e.g. Monday 00:00). That alignment can make
`next_window()` for AND-combinations like "Monday at 09:30" converge
poorly by repeatedly jumping to week boundaries.
Examples
--------
>>> Weekly("monday")
Specify a time to run at
>>> Weekly("mon", at="09:30")
>>> from datetime import time
>>> Weekly("thursday", at=time(9, 30))
Don't run if overdue by more than a minute (e.g., scheduler was down)
>>> Weekly("thursday", at=time(9, 0), force_0_minute=True)
"""
weekday_value = _parse_weekday_to_on_value(day)
trigger: BaseTrigger = Tick(Granularity.WEEK) & On(
unit="day_of_week", value=weekday_value
)
if at is None:
return trigger
hour, minute, second, millisecond = parse_at(at)
trigger = trigger & On(unit="hour_of_day", value=hour)
if minute != 0 or force_0_minute:
trigger = trigger & On(unit="minute_of_hour", value=minute)
if second is not None:
trigger = trigger & On(unit="second_of_minute", value=second)
if millisecond is not None:
trigger = trigger & On(unit="millisecond_of_second", value=millisecond)
return trigger