API reference¶
Everything public is importable from the package root:
from action0.celery_sched import (
BeatEntry,
DefinitionError,
DuplicateEntryError,
Entry,
Format,
FormatLike,
ScheduleError,
SolarEvent,
Source,
UnknownTaskError,
check_tasks,
load_beat_schedule,
load_entries,
parse_crontab,
parse_duration,
parse_entry,
parse_schedule,
parse_solar,
)
Loading¶
Read schedules and turn them into Celery’s beat_schedule setting.
A schedule is a mapping of entry names to entries (see
entries), written in YAML or TOML (see
formats) or handed over as a mapping (see
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:
.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.
- action0.celery_sched.loader.load_beat_schedule(*sources, replace=False, format=None)[source]¶
Read schedules into a value for Celery’s
beat_schedulesetting.Entries with
enabled: falseare 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': {}}}
- Parameters:
sources (
str|PathLike[str] |IO[str] |IO[bytes] |Mapping[str,Any]) – the files, streams or mappings to read, merged in order (seeload_entries())replace (
bool, default:False) – let a later source redefine an entry instead of raisingformat (
Format|Literal['yaml','toml'] |None, default:None) – the format of every file and stream, instead of telling it from their names (seeformats)
- Return type:
- Returns:
entry name → beat entry, in file order
- Raises:
DefinitionError – if a file or an entry is malformed
DuplicateEntryError – if two entries share a name
ValueError – if the format of a file or stream can’t be told
ImportError – for YAML sources, if PyYAML (the
yamlextra) is not installed
- action0.celery_sched.loader.load_entries(*sources, replace=False, format=None)[source]¶
Read and validate the entries of one or more schedules.
Unlike
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=Truethe later definition wins instead (keeping the position of the first), so an environment-specific file can override or disable entries of a shared one.- Parameters:
sources (
str|PathLike[str] |IO[str] |IO[bytes] |Mapping[str,Any]) – the files, streams or mappings to readreplace (
bool, default:False) – let a later source redefine an entry instead of raisingformat (
Format|Literal['yaml','toml'] |None, default:None) – the format of every file and stream, instead of telling it from their names
- Return type:
- Returns:
the entries, in file order
- Raises:
DefinitionError – if a file or an entry is malformed
DuplicateEntryError – if two entries share a name
ValueError – if the format of a file or stream can’t be told
ImportError – for YAML sources, if PyYAML (the
yamlextra) is not installed
Sources¶
What the loaders read from, and turning each kind of source into data.
A source is one of:
a path (a
stror any path-like) to a YAML or TOML file;an open stream — text or binary, as
tomllibusers are used to;a mapping that is already the parsed document, e.g. a table of a larger TOML configuration, or a dict from a settings system.
- action0.celery_sched.sources.Source: TypeAlias = str | os.PathLike[str] | typing.IO[str] | typing.IO[bytes] | collections.abc.Mapping[str, typing.Any]¶
what the load functions read from: a file path, an open stream, or parsed data
- action0.celery_sched.sources.read_source(source, format=None)[source]¶
Read a source into its parsed document, and name it for error messages.
>>> read_source({"Poll": {"task": "myapp.tasks.poll"}}) (None, {'Poll': {'task': 'myapp.tasks.poll'}})
- Parameters:
- Return type:
- Returns:
the source’s name (the path, the stream’s name, or
None) and its document- Raises:
ValueError – if the format of a file or stream can’t be told
ImportError – for a YAML source, if PyYAML (the
yamlextra) is not installedDefinitionError – if the text is malformed
Formats¶
The file formats a schedule can be written in, and how the loaders tell them apart.
A schedule means the same whatever its format — the entries have the same
keys and values — so the format only decides which parser turns the text into
data. By default a file’s suffix decides: .yaml and .yml are YAML,
.toml is TOML. A stream is judged by its name (which files opened with
open() have); anything else needs an explicit format.
TOML is read with the standard library’s tomllib. YAML needs
PyYAML, which comes with the yaml extra
(pip install "action0-celery-sched[yaml]"); it is imported only once a
YAML source is actually read, so TOML-only installs never need it.
- class action0.celery_sched.formats.Format(*values)[source]¶
A file format for schedules.
>>> Format("toml") is Format.TOML True
- YAML = 'yaml'¶
- TOML = 'toml'¶
- action0.celery_sched.formats.FormatLike: TypeAlias = action0.celery_sched.formats.Format | typing.Literal['yaml', 'toml']¶
a
Format, or its value as a plain string
- action0.celery_sched.formats.SUFFIXES: dict[str, Format] = {'.toml': Format.TOML, '.yaml': Format.YAML, '.yml': Format.YAML}¶
the file suffixes each format is recognised by (compared case-insensitively)
- action0.celery_sched.formats.detect_format(name, format=None)[source]¶
Decide the format of a file: the explicit one if given, else by its suffix.
>>> detect_format("conf/beat.toml") <Format.TOML: 'toml'> >>> detect_format(None, "yaml") <Format.YAML: 'yaml'>
- Parameters:
- Return type:
- Returns:
the format
- Raises:
ValueError – if
formatis not a known format, or it isn’t given and the name doesn’t end in a known suffix
- action0.celery_sched.formats.parse_text(text, format)[source]¶
Parse schedule text in the given format.
>>> parse_text("[Poll]\ntask = 'myapp.tasks.poll'", Format.TOML) {'Poll': {'task': 'myapp.tasks.poll'}}
- Parameters:
- Return type:
- Returns:
the parsed document, as plain Python data
- Raises:
DefinitionError – if the text is malformed
ImportError – for YAML, if PyYAML (the
yamlextra) is not installed
- action0.celery_sched.formats.parser_for(format)[source]¶
Get the parser of a format, importing the YAML one on first use.
The loaders call this before reading a source, so a missing PyYAML is reported without any I/O, like a format that can’t be told.
>>> parser_for(Format.TOML).__name__ 'load_toml'
- Parameters:
format (
Format) – the format- Return type:
- Returns:
a function turning text in that format into plain Python data
- Raises:
ImportError – for YAML, if PyYAML (the
yamlextra) is not installed
Entries¶
One schedule entry: its shape in a file, its parsed form and Celery’s.
An entry is a named mapping, with the same keys in YAML and TOML:
"Nightly report": # the entry's name, unique
task: myapp.reports.tasks.nightly # the registered task name — required
schedule: # when to run it — required
crontab: "0 3 * * *"
params: [daily] # positional arguments
kw: # keyword arguments
recipients: [ops@example.com]
options: # passed on to apply_async()
queue: reports
enabled: true # false keeps the entry but skips it
["Nightly report"] # the entry's name, unique
task = "myapp.reports.tasks.nightly" # the registered task name — required
schedule = { crontab = "0 3 * * *" } # when to run it — required
params = ["daily"] # positional arguments
kw = { recipients = ["ops@example.com"] } # keyword arguments
options = { queue = "reports" } # passed on to apply_async()
enabled = true # false keeps the entry but skips it
Any other key is an error, so a typo like shedule fails at startup
instead of silently dropping the schedule.
- action0.celery_sched.entries.ENTRY_KEYS = ('task', 'schedule', 'params', 'kw', 'options', 'enabled')¶
every key an entry may have
- class action0.celery_sched.entries.BeatEntry[source]¶
One value of Celery’s
beat_schedulesetting, as beat expects it.- schedule: BaseSchedule¶
- class action0.celery_sched.entries.Entry(name, task, schedule, args=(), kwargs=<factory>, options=<factory>, enabled=True, source=None)[source]¶
A parsed and validated schedule entry.
>>> entry = parse_entry("Poll feed", {"task": "myapp.tasks.poll", "schedule": {"every": 30}}) >>> entry.to_celery() {'task': 'myapp.tasks.poll', 'schedule': <freq: 30.00 seconds>, 'args': (), 'kwargs': {}, 'options': {}}
- Parameters:
- schedule: BaseSchedule¶
when the task runs
- action0.celery_sched.entries.parse_entry(name, value, *, source=None)[source]¶
Validate one entry of a schedule file.
The argument containers are deep-copied, so entries sharing a mapping through a YAML anchor never share it at runtime.
- Parameters:
- Return type:
- Returns:
the entry
- Raises:
DefinitionError – if the entry is malformed
Schedules¶
Parse the schedule of an entry.
It is a mapping with exactly one key naming the kind of schedule: every
(an interval, see durations), crontab
(see crontabs) or solar (see
solar). The same three entries in each format:
"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}}}
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.
- action0.celery_sched.schedules.KINDS = ('every', 'crontab', 'solar')¶
the keys naming a kind of schedule; exactly one of them must be given
- action0.celery_sched.schedules.parse_schedule(value)[source]¶
Turn the
scheduleof an entry into a Celery schedule.Intervals become a
celery.schedules.schedule, so every kind comes back as aBaseSchedulethat 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)>
- Parameters:
value (
object) – the parsed value, from either format- Return type:
BaseSchedule- Returns:
the schedule
- Raises:
DefinitionError – if the block is malformed
Intervals¶
Parse the interval of an every schedule.
Three spellings are accepted:
a number of seconds:
30,2.5(or the same as a string, which is what!ENVproduces);a duration string of
<amount><unit>parts:90s,5m,1h30m,1.5h,2d 12h— unitsw,d,h,m,sandms, lowercase only (somcan never be misread as months);a mapping of
timedeltaarguments:{hours: 1, minutes: 30}in YAML,{ hours = 1, minutes = 30 }in TOML.
- action0.celery_sched.durations.parse_duration(value)[source]¶
Turn the value of an
everykey into a positivetimedelta.>>> parse_duration(90) datetime.timedelta(seconds=90) >>> parse_duration("1h30m") datetime.timedelta(seconds=5400) >>> parse_duration({"days": 1, "hours": 12}) datetime.timedelta(days=1, seconds=43200)
- Parameters:
value (
object) – the parsed value, from either format- Return type:
- Returns:
the interval
- Raises:
DefinitionError – if the value is malformed, zero or negative
Crontabs¶
Parse the value of a crontab schedule into a celery.schedules.crontab.
Two spellings are accepted:
a classic five-field cron string,
"minute hour day-of-month month day-of-week"— for example"30 7 * * mon-fri"— or one of the nicknames@hourly,@daily/@midnight,@weekly,@monthlyand@yearly/@annually;a mapping of Celery’s own keyword arguments, where omitted fields mean
*:{minute: 30, hour: 7, day_of_week: mon-fri}in YAML,{ minute = 30, hour = 7, day_of_week = "mon-fri" }in TOML.
Each field takes whatever Celery takes — */15, 1-5, mon,wed, a
plain number — and in the mapping form also a list of numbers. Every field
is validated by Celery while loading, so a typo fails at startup, not when
beat first evaluates the schedule.
- action0.celery_sched.crontabs.parse_crontab(value)[source]¶
Turn the value of a
crontabkey into a Celery crontab.>>> parse_crontab("30 7 * * mon-fri") <crontab: 30 7 * * mon-fri (m/h/dM/MY/d)> >>> parse_crontab({"minute": "*/15"}) <crontab: */15 * * * * (m/h/dM/MY/d)> >>> parse_crontab("@daily") <crontab: 0 0 * * * (m/h/dM/MY/d)>
- Parameters:
value (
object) – the parsed value, from either format- Return type:
- Returns:
the crontab
- Raises:
DefinitionError – if the value is malformed or a field is invalid
Solar events¶
Parse the value of a solar schedule into a celery.schedules.solar.
The value is a mapping of the event and the observer’s position:
solar: {event: sunset, lat: 48.21, lon: 16.37} in YAML,
solar = { event = "sunset", lat = 48.21, lon = 16.37 } in TOML.
Celery computes the event times with ephem,
which is only installed with the solar extra
(pip install "action0-celery-sched[solar]").
- class action0.celery_sched.solar.SolarEvent(*values)[source]¶
The events a solar schedule can fire on, as Celery names them.
>>> SolarEvent("sunset") is SolarEvent.SUNSET True
- DAWN_ASTRONOMICAL = 'dawn_astronomical'¶
- DAWN_NAUTICAL = 'dawn_nautical'¶
- DAWN_CIVIL = 'dawn_civil'¶
- SUNRISE = 'sunrise'¶
- SOLAR_NOON = 'solar_noon'¶
- SUNSET = 'sunset'¶
- DUSK_CIVIL = 'dusk_civil'¶
- DUSK_NAUTICAL = 'dusk_nautical'¶
- DUSK_ASTRONOMICAL = 'dusk_astronomical'¶
- action0.celery_sched.solar.parse_solar(value)[source]¶
Turn the value of a
solarkey into a Celery solar schedule.>>> parse_solar({"event": "sunset", "lat": 48.21, "lon": 16.37}) <solar: sunset at latitude 48.21, longitude: 16.37>
- Parameters:
value (
object) – the parsed value, from either format- Return type:
- Returns:
the solar schedule
- Raises:
DefinitionError – if the value is malformed or out of range
ImportError – if ephem (the
solarextra) is not installed
Task check¶
Check that every scheduled task is actually registered with the Celery app.
Beat sends tasks by name, and a misspelled name is not noticed until the
first run is due — possibly hours after a deploy — and then only as an
error on the worker. check_tasks() catches it at startup.
The app only knows the tasks whose modules were imported. Beat imports them
(imports/include and autodiscovery) right before it sends the
beat_init signal, which makes that the place to check:
from celery.signals import beat_init
@beat_init.connect
def check_schedule(sender, **kwargs):
check_tasks(sender.app)
- action0.celery_sched.tasks.check_tasks(app, schedule=None)[source]¶
Raise if a schedule entry refers to a task the app hasn’t registered.
>>> app = Celery("example") >>> @app.task(name="myapp.tasks.poll") ... def poll() -> None: ... >>> check_tasks(app, {"Poll feed": {"task": "myapp.tasks.poll"}}) >>> check_tasks(app, {"Report": {"task": "myapp.tasks.report"}}) Traceback (most recent call last): ... action0.celery_sched.errors.UnknownTaskError: unregistered tasks: 'Report' -> myapp.tasks.report
- Parameters:
- Raises:
UnknownTaskError – listing every entry whose task is unregistered
- Return type:
Environment variables¶
The !ENV substitution: ${VAR} and ${VAR:-fallback} from the environment.
Substitution is purely textual and happens once, at load time: the result is
always a string and is not parsed as YAML again, so a value like "yes"
or "0123" cannot change type behind your back. The validators accept the
string spellings of numbers and booleans for exactly that reason.
- action0.celery_sched.envvars.substitute_env(value, environ=None)[source]¶
Replace
${VAR}/${VAR:-fallback}occurrences from the environment.>>> substitute_env("${REPORT_HOUR:-3}", environ={}) '3' >>> substitute_env("${QUEUE}-high", environ={"QUEUE": "reports"}) 'reports-high'
- Parameters:
- Return type:
- Returns:
the substituted string
- Raises:
DefinitionError – if a variable without fallback is not set
YAML loader¶
The YAML loader for schedule files: PyYAML’s SafeLoader, plus two things.
The
!ENVtag substitutes environment variables into a scalar (seeenvvars).Duplicate keys are an error. Plain YAML parsers silently keep the last of two equal keys, so a copy-pasted entry that kept its name would quietly replace the original. Keys brought in through a merge (
<<: *base) may still be overridden — that is what merging is for.
Being a SafeLoader, it cannot instantiate arbitrary Python objects.
load_yaml() is the entry point the loaders use.
- class action0.celery_sched.yaml_loader.ScheduleLoader(stream)[source]¶
SafeLoaderwith the!ENVtag and duplicate-key detection.>>> yaml.load("a: 1\na: 2", Loader=ScheduleLoader) Traceback (most recent call last): ... action0.celery_sched.errors.DuplicateEntryError: duplicate entry 'a' on line 2 (first on line 1)
- Parameters:
stream (
Any)stream – the YAML text or stream
- construct_document(node)[source]¶
Remember the root, so a duplicate entry can be told from a duplicate key.
- Parameters:
node (
Node)- Return type:
- flatten_mapping(node)[source]¶
Check a mapping’s own keys for duplicates, then resolve its merges.
This is the one hook PyYAML calls on every mapping before it rewrites the node’s pairs to include the merged ones. A node can be flattened more than once (as a merge source, then again when constructed), so each is checked only on its first, untouched visit.
- Parameters:
node (
MappingNode)- Return type:
- action0.celery_sched.yaml_loader.load_yaml(text)[source]¶
Parse YAML text with the
ScheduleLoader.>>> load_yaml("'Poll feed': {schedule: {every: 5m}}") {'Poll feed': {'schedule': {'every': '5m'}}}
- Parameters:
text (
str) – the YAML document- Return type:
- Returns:
the parsed document (
Nonefor an empty one)- Raises:
DefinitionError – on a syntax error, a duplicate key, or an unset environment variable
TOML loader¶
Parse TOML schedule files, with the standard library’s tomllib.
An entry in TOML has exactly the keys and values it has in YAML:
["Nightly report"]
task = "myapp.reports.tasks.nightly"
params = ["daily"]
kw = { recipients = ["ops@example.com"] }
schedule = { crontab = "0 3 * * *" }
options = { queue = "reports" }
TOML has no tags, so the !ENV substitution is written as a prefix of
a string: crontab = "!ENV ${REPORT_CRON:-0 3 * * *}". The rest of the
string, after !ENV and the whitespace following it, is substituted
exactly like YAML’s !ENV tag does it (see
envvars). Only string values are
substituted, never keys.
TOML itself forbids declaring a key twice, so a duplicate entry is a syntax error here.
- action0.celery_sched.toml_loader.load_toml(text)[source]¶
Parse TOML text and substitute its
!ENV-prefixed strings.>>> load_toml('["Poll feed"]\nschedule = { every = "!ENV ${POLL_EVERY:-5m}" }') {'Poll feed': {'schedule': {'every': '5m'}}}
- Parameters:
text (
str) – the TOML document- Return type:
- Returns:
the parsed document
- Raises:
DefinitionError – on a syntax error, or an unset environment variable
Errors¶
Exceptions raised while reading schedules; everything derives from ScheduleError.
- exception action0.celery_sched.errors.ScheduleError[source]¶
Base class of every error this package raises on purpose.
- exception action0.celery_sched.errors.DefinitionError(reason)[source]¶
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
reason; the location is filled in on the way up (seelocated()), 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
- Parameters:
reason (
str)reason – what is wrong, without the location
- exception action0.celery_sched.errors.DuplicateEntryError(reason)[source]¶
Two entries share a name — within one file, or across files merged without
replace=True.- Parameters:
reason (
str)reason – what is wrong, without the location
- exception action0.celery_sched.errors.UnknownTaskError(missing)[source]¶
Entries refer to tasks the Celery app has not registered.
Raised by
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
- action0.celery_sched.errors.located(*keys, entry=None, source=None)[source]¶
Add location details to any
DefinitionErrorraised 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
- Parameters:
- Return type: