Source code for action0.celery_sched.errors

"""
Exceptions raised while reading schedules; everything derives from :py:class:`ScheduleError`.
"""

from collections.abc import Iterator
from collections.abc import Mapping
from contextlib import contextmanager


[docs] class ScheduleError(Exception): """ Base class of every error this package raises on purpose. """
[docs] class DefinitionError(ScheduleError): """ A schedule file, or one entry in it, is malformed. The error knows *where* the problem is: the file it came from, the entry and the key path inside that entry. The parsers only state the :py:attr:`reason`; the location is filled in on the way up (see :py:func:`located`), so the message reads like:: beat.yaml: entry 'Nightly report': schedule.crontab: expected 5 fields, got 4 >>> error = DefinitionError("expected 5 fields, got 4") >>> error.source, error.entry, error.path = "beat.yaml", "Nightly report", ("schedule", "crontab") >>> print(error) beat.yaml: entry 'Nightly report': schedule.crontab: expected 5 fields, got 4 """ def __init__(self, reason: str) -> None: """ :param reason: what is wrong, without the location """ super().__init__(reason) self.reason = reason #: the file (or stream name) the entry was read from, if known self.source: str | None = None #: the name of the entry, if the error is inside one self.entry: str | None = None #: the keys leading from the entry to the offending value self.path: tuple[str, ...] = () def __str__(self) -> str: parts = [] if self.source is not None: parts.append(self.source) if self.entry is not None: parts.append(f"entry {self.entry!r}") if self.path: parts.append(".".join(self.path)) parts.append(self.reason) return ": ".join(parts)
[docs] class DuplicateEntryError(DefinitionError): """ Two entries share a name — within one file, or across files merged without ``replace=True``. """
[docs] class UnknownTaskError(ScheduleError): """ Entries refer to tasks the Celery app has not registered. Raised by :py:func:`~action0.celery_sched.tasks.check_tasks`, listing *every* offender at once rather than only the first. >>> print(UnknownTaskError({"Nightly report": "myapp.tasks.nightly"})) unregistered tasks: 'Nightly report' -> myapp.tasks.nightly """ def __init__(self, missing: Mapping[str, str]) -> None: """ :param missing: entry name → the task name nothing is registered under """ self.missing = dict(missing) listing = ", ".join(f"{entry!r} -> {task}" for entry, task in self.missing.items()) super().__init__(f"unregistered tasks: {listing}")
[docs] @contextmanager def located(*keys: str, entry: str | None = None, source: str | None = None) -> Iterator[None]: """ Add location details to any :py:class:`DefinitionError` raised inside the block. Nest these as the parsers descend into a value, so each layer only needs to know its own key (or entry name, or file): >>> try: ... with located(entry="Poll feed"): ... with located("schedule"): ... with located("every"): ... raise DefinitionError("must be positive") ... except DefinitionError as error: ... print(error) entry 'Poll feed': schedule.every: must be positive :param keys: the keys this block descends into, prepended to the path :param entry: the entry this block parses, unless the error already has one :param source: the file this block reads, unless the error already has one """ try: yield except DefinitionError as error: error.path = (*keys, *error.path) if error.entry is None: error.entry = entry if error.source is None: error.source = source raise