Source code for action0.celery_sched.loader

"""
Read schedules and turn them into Celery's ``beat_schedule`` setting.

A schedule is a mapping of entry names to entries (see
:py:mod:`~action0.celery_sched.entries`), written in YAML or TOML (see
:py:mod:`~action0.celery_sched.formats`) or handed over as a mapping (see
:py:mod:`~action0.celery_sched.sources`); an empty file is an empty
schedule. Entries whose name starts with a ``.`` are *templates*: they are
never scheduled. In YAML, that makes them the place for anchors that other
entries merge in:

.. code-block:: yaml

    .reports: &reports
      task: myapp.reports.tasks.build
      options: {queue: reports}

    "Daily report":
      <<: *reports
      kw: {period: day}
      schedule: {crontab: "@daily"}

TOML cannot refer to another table, so there templates are merely skipped.
"""

from typing import NoReturn

from action0.celery_sched.entries import BeatEntry
from action0.celery_sched.entries import Entry
from action0.celery_sched.entries import parse_entry
from action0.celery_sched.errors import DefinitionError
from action0.celery_sched.errors import DuplicateEntryError
from action0.celery_sched.errors import located
from action0.celery_sched.formats import FormatLike
from action0.celery_sched.sources import Source
from action0.celery_sched.sources import read_source
from action0.celery_sched.values import expect_mapping


[docs] def load_beat_schedule( *sources: Source, replace: bool = False, format: FormatLike | None = None ) -> dict[str, BeatEntry]: """ Read schedules into a value for Celery's ``beat_schedule`` setting. Entries with ``enabled: false`` are left out:: app.conf.beat_schedule = load_beat_schedule("beat.yaml") # or "beat.toml" >>> load_beat_schedule({"Poll feed": {"task": "myapp.tasks.poll", "schedule": {"every": "5m"}}}) {'Poll feed': {'task': 'myapp.tasks.poll', 'schedule': <freq: 5.00 minutes>, 'args': (), 'kwargs': {}, 'options': {}}} :param sources: the files, streams or mappings to read, merged in order (see :py:func:`load_entries`) :param replace: let a later source redefine an entry instead of raising :param format: the format of every file and stream, instead of telling it from their names (see :py:mod:`~action0.celery_sched.formats`) :returns: entry name → beat entry, in file order :raises DefinitionError: if a file or an entry is malformed :raises DuplicateEntryError: if two entries share a name :raises ValueError: if the format of a file or stream can't be told :raises ImportError: for YAML sources, if PyYAML (the ``yaml`` extra) is not installed """ entries = load_entries(*sources, replace=replace, format=format) return {entry.name: entry.to_celery() for entry in entries if entry.enabled}
[docs] def load_entries( *sources: Source, replace: bool = False, format: FormatLike | None = None ) -> list[Entry]: """ Read and validate the entries of one or more schedules. Unlike :py:func:`load_beat_schedule`, disabled entries are included, and each one knows the file it came from — handy for tooling and tests. Sources are merged in order, whatever their format. An entry name defined twice is an error; with ``replace=True`` the later definition wins instead (keeping the position of the first), so an environment-specific file can override or disable entries of a shared one. :param sources: the files, streams or mappings to read :param replace: let a later source redefine an entry instead of raising :param format: the format of every file and stream, instead of telling it from their names :returns: the entries, in file order :raises DefinitionError: if a file or an entry is malformed :raises DuplicateEntryError: if two entries share a name :raises ValueError: if the format of a file or stream can't be told :raises ImportError: for YAML sources, if PyYAML (the ``yaml`` extra) is not installed """ merged: dict[str, Entry] = {} for source in sources: name, document = read_source(source, format) for entry in _parse_document(document, source=name): previous = merged.get(entry.name) if previous is not None and not replace: _raise_duplicate(entry, previous) merged[entry.name] = entry return list(merged.values())
def _parse_document(document: object, *, source: str | None) -> list[Entry]: """Validate the entries of one parsed document, whatever its format.""" with located(source=source): if document is None: # an empty YAML file, or one holding only comments return [] if not isinstance(document, dict): raise DefinitionError("the top level must be a mapping of entry names") return [ parse_entry(entry_name, value, source=source) for entry_name, value in expect_mapping(document).items() if not entry_name.startswith(".") # a template, only there for its anchor ] def _raise_duplicate(entry: Entry, previous: Entry) -> NoReturn: """Report an entry name that an earlier source already defined.""" where = f" in {previous.source}" if previous.source is not None else "" error = DuplicateEntryError(f"already defined{where} (pass replace=True to override)") error.entry, error.source = entry.name, entry.source raise error