Source code for action0.celery_sched.schedules

"""
Parse the ``schedule`` of an entry.

It is a mapping with exactly one key naming the kind of schedule: ``every``
(an interval, see :py:mod:`~action0.celery_sched.durations`), ``crontab``
(see :py:mod:`~action0.celery_sched.crontabs`) or ``solar`` (see
:py:mod:`~action0.celery_sched.solar`). The same three entries in each format:

.. code-block:: yaml

    "Poll":   {task: myapp.tasks.poll, schedule: {every: 1h, relative: true}}
    "Report": {task: myapp.tasks.report, schedule: {crontab: "0 3 * * *"}}
    "Lights": {task: myapp.tasks.lights, schedule: {solar: {event: sunset, lat: 48.21, lon: 16.37}}}

.. code-block:: toml

    Poll = { task = "myapp.tasks.poll", schedule = { every = "1h", relative = true } }
    Report = { task = "myapp.tasks.report", schedule = { crontab = "0 3 * * *" } }
    Lights = { task = "myapp.tasks.lights", schedule = { solar = { event = "sunset", lat = 48.21, lon = 16.37 } } }

``relative`` is Celery's own flag for intervals: when true, the time of the
next run is rounded to the resolution of the interval (a ``1h`` interval runs
on the hour). It is only allowed next to ``every``.
"""

from collections.abc import Callable
from typing import Any

from celery.schedules import BaseSchedule
from celery.schedules import schedule

from action0.celery_sched.crontabs import parse_crontab
from action0.celery_sched.durations import parse_duration
from action0.celery_sched.errors import DefinitionError
from action0.celery_sched.errors import located
from action0.celery_sched.solar import parse_solar
from action0.celery_sched.values import as_bool
from action0.celery_sched.values import check_keys
from action0.celery_sched.values import expect_mapping

#: the keys naming a kind of schedule; exactly one of them must be given
KINDS = ("every", "crontab", "solar")


[docs] def parse_schedule(value: object) -> BaseSchedule: """ Turn the ``schedule`` of an entry into a Celery schedule. Intervals become a :py:class:`celery.schedules.schedule`, so every kind comes back as a :py:class:`~celery.schedules.BaseSchedule` that beat accepts as it is. >>> parse_schedule({"every": "5m"}) <freq: 5.00 minutes> >>> parse_schedule({"crontab": "0 3 * * *"}) <crontab: 0 3 * * * (m/h/dM/MY/d)> :param value: the parsed value, from either format :returns: the schedule :raises DefinitionError: if the block is malformed """ mapping = expect_mapping(value) check_keys(mapping, allowed=(*KINDS, "relative")) kind = _kind_of(mapping) if kind == "every": return _parse_every(mapping) if "relative" in mapping: raise DefinitionError("'relative' only applies to 'every' schedules") with located(kind): return _PARSERS[kind](mapping[kind])
def _kind_of(mapping: dict[str, Any]) -> str: """Find the one key naming the kind of schedule.""" kinds = [kind for kind in KINDS if kind in mapping] if len(kinds) != 1: found = f"got {', '.join(kinds)}" if kinds else "got none" raise DefinitionError(f"expected exactly one of {', '.join(KINDS)}, {found}") return kinds[0] def _parse_every(mapping: dict[str, Any]) -> schedule: """Build an interval schedule from ``every`` and the optional ``relative``.""" with located("every"): interval = parse_duration(mapping["every"]) with located("relative"): relative = as_bool(mapping.get("relative", False)) return schedule(interval, relative=relative) # the parsers of the kinds that take nothing but their own value _PARSERS: dict[str, Callable[[object], BaseSchedule]] = { "crontab": parse_crontab, "solar": parse_solar, }