action0-celery-sched 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- action0_celery_sched-0.1.0/.github/workflows/ci.yml +99 -0
- action0_celery_sched-0.1.0/.github/workflows/release.yml +62 -0
- action0_celery_sched-0.1.0/.gitignore +20 -0
- action0_celery_sched-0.1.0/.python-version +1 -0
- action0_celery_sched-0.1.0/CLAUDE.md +83 -0
- action0_celery_sched-0.1.0/LICENSE +21 -0
- action0_celery_sched-0.1.0/PKG-INFO +218 -0
- action0_celery_sched-0.1.0/README.md +185 -0
- action0_celery_sched-0.1.0/docs/api.md +120 -0
- action0_celery_sched-0.1.0/docs/conf.py +48 -0
- action0_celery_sched-0.1.0/docs/index.md +112 -0
- action0_celery_sched-0.1.0/docs/usage.md +858 -0
- action0_celery_sched-0.1.0/pyproject.toml +154 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/__init__.py +45 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/crontabs.py +119 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/durations.py +117 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/entries.py +160 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/envvars.py +45 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/errors.py +114 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/formats.py +126 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/loader.py +122 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/py.typed +0 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/schedules.py +95 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/solar.py +94 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/sources.py +67 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/tasks.py +58 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/toml_loader.py +77 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/values.py +131 -0
- action0_celery_sched-0.1.0/src/action0/celery_sched/yaml_loader.py +112 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_crontabs.py +111 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_durations.py +148 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_entries.py +164 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_envvars.py +48 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_errors.py +84 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_format_parity.py +136 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_formats.py +129 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_init.py +23 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_loader.py +354 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_schedules.py +96 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_solar.py +80 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_sources.py +64 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_tasks.py +60 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_toml_loader.py +127 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_values.py +119 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_without_pyyaml.py +46 -0
- action0_celery_sched-0.1.0/tests/action0/celery_sched/test_yaml_loader.py +174 -0
- action0_celery_sched-0.1.0/uv.lock +1599 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
# cancel superseded runs of the same branch / PR
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: true
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
checks:
|
|
16
|
+
name: checks (python ${{ matrix.python-version }})
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
continue-on-error: ${{ matrix.experimental }}
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
23
|
+
experimental: [false]
|
|
24
|
+
include:
|
|
25
|
+
# 3.15 is still a pre-release: run it, but don't fail the whole
|
|
26
|
+
# workflow while parts of the toolchain don't support it yet
|
|
27
|
+
- python-version: "3.15"
|
|
28
|
+
experimental: true
|
|
29
|
+
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v7
|
|
32
|
+
|
|
33
|
+
- name: Install uv
|
|
34
|
+
uses: astral-sh/setup-uv@v10.0.1
|
|
35
|
+
with:
|
|
36
|
+
python-version: ${{ matrix.python-version }}
|
|
37
|
+
|
|
38
|
+
- name: Install dependencies
|
|
39
|
+
run: uv sync --locked
|
|
40
|
+
|
|
41
|
+
- name: ruff format
|
|
42
|
+
run: uv run ruff format --check
|
|
43
|
+
|
|
44
|
+
- name: ruff check
|
|
45
|
+
run: uv run ruff check
|
|
46
|
+
|
|
47
|
+
- name: mypy
|
|
48
|
+
run: uv run mypy
|
|
49
|
+
|
|
50
|
+
- name: pyright
|
|
51
|
+
run: uv run pyright
|
|
52
|
+
|
|
53
|
+
- name: ty
|
|
54
|
+
run: uv run ty check
|
|
55
|
+
|
|
56
|
+
- name: pytest
|
|
57
|
+
run: uv run pytest
|
|
58
|
+
|
|
59
|
+
docs:
|
|
60
|
+
name: docs build
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v7
|
|
64
|
+
|
|
65
|
+
- name: Install uv
|
|
66
|
+
uses: astral-sh/setup-uv@v10.0.1
|
|
67
|
+
|
|
68
|
+
- name: Install dependencies
|
|
69
|
+
run: uv sync --locked --group docs
|
|
70
|
+
|
|
71
|
+
- name: Build documentation (warnings are errors)
|
|
72
|
+
run: uv run --group docs sphinx-build -W --keep-going -b html docs docs/_build/html
|
|
73
|
+
|
|
74
|
+
- name: Upload pages artifact
|
|
75
|
+
uses: actions/upload-pages-artifact@v5
|
|
76
|
+
with:
|
|
77
|
+
path: docs/_build/html
|
|
78
|
+
|
|
79
|
+
deploy-docs:
|
|
80
|
+
name: deploy docs
|
|
81
|
+
# publish only what landed on main
|
|
82
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
83
|
+
needs: docs
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
permissions:
|
|
86
|
+
contents: read
|
|
87
|
+
pages: write
|
|
88
|
+
id-token: write
|
|
89
|
+
environment:
|
|
90
|
+
name: github-pages
|
|
91
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
92
|
+
# the Pages site itself was enabled once via
|
|
93
|
+
# `gh api repos/<owner>/<repo>/pages -X POST -f build_type=workflow`;
|
|
94
|
+
# creating it from a workflow is impossible (GITHUB_TOKEN cannot get
|
|
95
|
+
# repo-administration rights)
|
|
96
|
+
steps:
|
|
97
|
+
- name: Deploy to GitHub Pages
|
|
98
|
+
id: deployment
|
|
99
|
+
uses: actions/deploy-pages@v5
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: check and build
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v7
|
|
13
|
+
|
|
14
|
+
- name: Install uv
|
|
15
|
+
uses: astral-sh/setup-uv@v10.0.1
|
|
16
|
+
|
|
17
|
+
- name: Install dependencies
|
|
18
|
+
run: uv sync --locked
|
|
19
|
+
|
|
20
|
+
- name: Run all checks
|
|
21
|
+
run: |
|
|
22
|
+
uv run ruff format --check
|
|
23
|
+
uv run ruff check
|
|
24
|
+
uv run mypy
|
|
25
|
+
uv run pyright
|
|
26
|
+
uv run ty check
|
|
27
|
+
uv run pytest
|
|
28
|
+
|
|
29
|
+
- name: Check that the tag matches __version__
|
|
30
|
+
run: |
|
|
31
|
+
version=$(uv run python -c "from action0.celery_sched import __version__; print(__version__)")
|
|
32
|
+
if [ "v$version" != "$GITHUB_REF_NAME" ]; then
|
|
33
|
+
echo "tag $GITHUB_REF_NAME does not match __version__ $version" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
- name: Build sdist and wheel
|
|
38
|
+
run: uv build
|
|
39
|
+
|
|
40
|
+
- uses: actions/upload-artifact@v7
|
|
41
|
+
with:
|
|
42
|
+
name: dist
|
|
43
|
+
path: dist/
|
|
44
|
+
|
|
45
|
+
publish:
|
|
46
|
+
name: publish to PyPI
|
|
47
|
+
needs: build
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
environment:
|
|
50
|
+
name: pypi
|
|
51
|
+
url: https://pypi.org/p/action0-celery-sched
|
|
52
|
+
permissions:
|
|
53
|
+
# OIDC token for PyPI trusted publishing
|
|
54
|
+
id-token: write
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/download-artifact@v8
|
|
57
|
+
with:
|
|
58
|
+
name: dist
|
|
59
|
+
path: dist/
|
|
60
|
+
|
|
61
|
+
- name: Publish to PyPI
|
|
62
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# Editor files
|
|
13
|
+
.idea
|
|
14
|
+
|
|
15
|
+
# Claude Code local state
|
|
16
|
+
.claude/settings.local.json
|
|
17
|
+
.claude/worktrees/
|
|
18
|
+
|
|
19
|
+
# Sphinx build output
|
|
20
|
+
docs/_build/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
`action0-celery-sched` is a Python library that reads Celery beat schedules from YAML or TOML files (or already-parsed mappings) and turns them into the value of Celery's `beat_schedule` setting (`app.conf.beat_schedule = load_beat_schedule("beat.yaml")`). It ships the `action0.celery_sched` package (`action0` is a PEP 420 namespace package) from a `src/` layout, is built with hatchling, and uses `uv` for environment/dependency management. The only runtime dependency is `celery`: TOML uses the stdlib `tomllib`, YAML needs the optional `yaml` extra (PyYAML), and the optional `solar` extra adds `ephem`, which Celery needs to build `solar` schedules. Importing the package must never import PyYAML — `test_without_pyyaml.py` guards that.
|
|
8
|
+
|
|
9
|
+
## Rules
|
|
10
|
+
|
|
11
|
+
- **Never commit without asking.** Also never push, tag, or publish on your own.
|
|
12
|
+
- **Branches + PRs.** All changes go through feature branches and GitHub pull requests that Simon reviews and merges — never commit to `main` directly. (Only the initial implementation is built directly on `main`; once that phase is over, this applies without exception.)
|
|
13
|
+
- **Discuss first.** Always present the plan and the intended edits and get agreement before changing files.
|
|
14
|
+
- Every code change comes with: tests, docstrings, inline comments where the code isn't self-explanatory, and updated usage examples in `README.md` and the Sphinx docs (`docs/usage.md`).
|
|
15
|
+
- Before considering work done, run ruff, mypy, pyright, ty and pytest (commands below) and fix what they report.
|
|
16
|
+
- Supported Python versions: 3.11 up to the latest release. Don't use syntax or stdlib features introduced after 3.11 (no PEP 695 `type` aliases or generics — use `TypeAlias`), and don't rely on behavior removed in newer versions.
|
|
17
|
+
- Prefer many small modules and short functions over large ones.
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
`uv run` syncs the environment automatically (the dev dependency group is installed by default), so no separate install step is needed.
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
uv run pytest # all tests
|
|
25
|
+
uv run pytest tests/action0/celery_sched/test_loader.py # one file
|
|
26
|
+
uv run pytest tests/action0/celery_sched/test_loader.py::MergeTestCase::test_replace # one test
|
|
27
|
+
|
|
28
|
+
uv run ruff check # lint (add --fix to autofix)
|
|
29
|
+
uv run ruff format # format
|
|
30
|
+
uv run mypy # type-check (strict; files are configured in pyproject.toml)
|
|
31
|
+
uv run pyright # type-check
|
|
32
|
+
uv run ty check # type-check
|
|
33
|
+
|
|
34
|
+
uv run --group docs sphinx-build -W --keep-going -b html docs docs/_build/html # build docs
|
|
35
|
+
|
|
36
|
+
uv build # build sdist + wheel into dist/
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`pytest` also runs the `>>>` examples in the docstrings as doctests (`--doctest-modules` over `src/`), so docstring examples must produce their shown output exactly.
|
|
40
|
+
|
|
41
|
+
## Architecture
|
|
42
|
+
|
|
43
|
+
The file format is the same in YAML and TOML — Simon's requirement: an entry is identical regardless of format. A top-level mapping of entry name → entry; entry keys `task` (required), `schedule` (required), `params` (positional args), `kw` (keyword args), `options` (passed to `apply_async`), `enabled`. The `schedule` has exactly one of `every` / `crontab` / `solar` (plus `relative`, only next to `every`). The names `kw`/`params` are the user-facing names chosen by Simon; they map to Celery's `kwargs`/`args`.
|
|
44
|
+
|
|
45
|
+
Data flow: source (path / stream / mapping) → `sources.read_source` → `formats.detect_format` + `formats.parse_text` → `yaml_loader.load_yaml` or `toml_loader.load_toml` → plain Python data → `loader._parse_document` → `entries.parse_entry` (and below it the format-agnostic parsers) → `Entry` → `to_celery()`. Everything from `_parse_document` down never knows which format the data came from; keep it that way.
|
|
46
|
+
|
|
47
|
+
Modules under `src/action0/celery_sched/`, one concern each, from the leaves up:
|
|
48
|
+
|
|
49
|
+
- `errors.py` — `ScheduleError` base; `DefinitionError` carries `reason` plus a location (`source` file, `entry` name, key `path`) rendered as `beat.yaml: entry 'X': schedule.crontab.hour: reason`; `DuplicateEntryError`; `UnknownTaskError` (with `missing`). `located(*keys, entry=, source=)` is the context manager that fills the location in on the way up — parsers only ever raise with a reason and wrap each descent in `with located("key"):`.
|
|
50
|
+
- `values.py` — shared validators: `expect_mapping`, `check_keys` (unknown keys are errors — the typo guard), `as_number`, `as_bool`, `describe`. Number/boolean validators accept string spellings because `!ENV` always yields strings.
|
|
51
|
+
- `durations.py` — `parse_duration`: seconds (number or numeric string), duration strings (`1h30m`, units `w d h m s ms`, lowercase only), or a `timedelta`-kwargs mapping; must be positive and finite.
|
|
52
|
+
- `crontabs.py` — `parse_crontab`: five-field cron string (cron order; own whitespace split because Celery's `crontab.from_string` splits on single spaces only), `@daily`-style nicknames, or a mapping of Celery's field names. Each field is validated by constructing a one-field `crontab` so the error is pinned to its key.
|
|
53
|
+
- `solar.py` — `SolarEvent` (StrEnum of Celery's events) and `parse_solar`; range checks happen before construction, and a missing `ephem` becomes an `ImportError` naming the `solar` extra.
|
|
54
|
+
- `schedules.py` — `parse_schedule` dispatches on the one kind key. Intervals become `celery.schedules.schedule(timedelta, relative=...)` objects, so every kind comes out as a `BaseSchedule` that beat takes as-is.
|
|
55
|
+
- `entries.py` — `Entry` (frozen dataclass; `source` excluded from equality), `BeatEntry` (TypedDict of one `beat_schedule` value), `parse_entry`. Argument containers are deep-copied so YAML-anchor-shared mappings are never shared at runtime.
|
|
56
|
+
- `envvars.py` — `substitute_env` for `${VAR}` / `${VAR:-fallback}`; textual, never re-parsed (same semantics as action0-service's `!ENV`).
|
|
57
|
+
- `yaml_loader.py` — `load_yaml` (wraps `yaml.YAMLError` into `DefinitionError`) and `ScheduleLoader`, a `SafeLoader` with the `!ENV` tag (unset variables are reported with their line — entry/key aren't known while parsing) and duplicate-key detection. Duplicates are checked in `flatten_mapping` (the one hook that sees a mapping's own pairs before merge keys are spliced in), once per node — a node can be flattened again as a merge source after it was rewritten, which would otherwise produce false duplicates (there are tests for exactly that). A duplicate at the root is a `DuplicateEntryError`.
|
|
58
|
+
- `toml_loader.py` — `load_toml`: `tomllib.loads` (syntax errors, which include TOML's own duplicate-key errors, become `DefinitionError`), then a walk substituting strings that start with `!ENV` + whitespace — TOML has no tags, so the tag is spelled as a string prefix (precedent: dynaconf's `"@int ..."`). The walk runs under `located()`, so unset variables report entry and key path. Keys are never substituted.
|
|
59
|
+
- `formats.py` — `Format` (StrEnum YAML/TOML), `FormatLike` (`Format | Literal[...]`, like action0-pipeline's `ErrorPolicyLike`), `SUFFIXES`, `detect_format` (explicit `format` wins, else suffix — case-insensitive `.yaml`/`.yml`/`.toml`; anything else raises `ValueError`, deliberately no default), `parser_for` (imports `yaml_loader` lazily; an `ImportError` whose `name` is `"yaml"` becomes one naming the `yaml` extra, any other `ImportError` propagates untouched) and `parse_text` (dispatch through `parser_for`). Nothing may import `yaml_loader` at module level.
|
|
60
|
+
- `sources.py` — `Source` (path | `IO[str]` | `IO[bytes]` | `Mapping`) and `read_source`: mappings pass through unnamed (no `!ENV`, no format); paths and streams get their format detected and their parser obtained *before* any I/O (so an unknown format or a missing PyYAML fails without touching the file), binary streams are decoded as UTF-8 (tomllib users open files `"rb"`).
|
|
61
|
+
- `loader.py` — `load_entries(*sources, replace=False, format=None)` (all entries incl. disabled, merged in order across formats; cross-source duplicates raise unless `replace`, which keeps the first position) and `load_beat_schedule` (enabled entries → `to_celery()`). `_parse_document` is the format-agnostic document layer: `None` → empty, top level must be a mapping, top-level keys starting with `.` are templates (anchor carriers in YAML, merely skipped in TOML).
|
|
62
|
+
- `tasks.py` — `check_tasks(app, schedule=None)` compares task names with `app.tasks` (default: the app's own `beat_schedule`). Documented to run in a `beat_init` handler, because beat imports task modules right before sending that signal.
|
|
63
|
+
|
|
64
|
+
Deliberate decisions worth keeping:
|
|
65
|
+
|
|
66
|
+
- **Strict over lenient**: unknown keys, duplicate keys and invalid crontab fields fail at load time — a silently dropped schedule is the failure mode this library exists to prevent.
|
|
67
|
+
- **Explicit schedule kinds** (`every` / `crontab` / `solar`), no shape-inferred scalar shorthand — chosen by Simon.
|
|
68
|
+
- `replace=True` replaces whole entries; there is no field-level patching across files.
|
|
69
|
+
- Format differences that are inherent and documented, not papered over: anchors/templates are YAML-only; a duplicate entry within one TOML file is a TOML syntax error (a `DefinitionError`, not a `DuplicateEntryError` — parsing tomllib's message would be fragile); TOML has no null, so empty `kw:`-style values are simply omitted.
|
|
70
|
+
- `tests/action0/celery_sched/test_format_parity.py` loads one feature-complete schedule written in both formats and requires identical entries (including `schedule.relative`, which Celery's `schedule.__eq__` ignores). Any new entry feature must be added to both spellings there.
|
|
71
|
+
|
|
72
|
+
Conventions:
|
|
73
|
+
|
|
74
|
+
- The version is single-sourced as `__version__` in `src/action0/celery_sched/__init__.py`; hatch extracts it with the regex in `[tool.hatch.version]`. Bump it only there.
|
|
75
|
+
- Releases: pushing a `vX.Y.Z` tag triggers `.github/workflows/release.yml`, which re-runs all checks, verifies the tag matches `__version__`, builds, and publishes to PyPI via trusted publishing (environment `pypi`). Never bump the version, tag, or publish on your own — releasing is the user's call.
|
|
76
|
+
- Tests mirror the `src/` layout under `tests/action0/celery_sched/` and are `unittest.TestCase` classes, executed via pytest. `test_loader.py` includes an integration test that feeds the result (from both formats) into Celery's own `beat.Scheduler`.
|
|
77
|
+
- `test_without_pyyaml.py` runs a subprocess with `sys.modules["yaml"] = None` (stricter than uninstalling: it also catches indirect imports) and checks that the package imports, TOML and mapping sources load, and a YAML source raises the extra's `ImportError` before any I/O.
|
|
78
|
+
- Celery ships no type information; the dev group includes `celery-types` (stubs), `pyyaml` (the `yaml` extra, for tests and docs) and `types-pyyaml`. Where the stubs lack something (e.g. `solar._all_events`), use `getattr` with a comment rather than an ignore. When an ignore is unavoidable, silence each checker with its own syntax (`# type: ignore[code] # ty: ignore[code]`).
|
|
79
|
+
- Ruff enforces one import per line (isort `force-single-line`), line length 99, `action0` as first-party. Ruff only honours `.gitignore` inside a git repository.
|
|
80
|
+
- Docs live in `docs/` (Sphinx + Furo, MyST Markdown pages, sphinx-design tabs, autodoc for the API reference, intersphinx to Python and Celery). Docstrings are Sphinx-reST (`:param:`, `:py:func:` roles). CI builds them with `-W` on every run and deploys to GitHub Pages on pushes to `main`. Examples in `docs/usage.md` and `README.md` show real outputs and error messages — keep them truthful, and keep every fenced `yaml`/`toml` block valid.
|
|
81
|
+
- **Both formats, equally (Simon's rule):** every example is given in YAML *and* TOML — in the guide, the index, the README and docstring examples. YAML comes first, TOML second. In Sphinx pages, use a sphinx-design tab-set with `:sync-group: format` and tab items `YAML` (`:sync: yaml`) then `TOML` (`:sync: toml`); a shown error message goes inside the tab next to the input that causes it. On GitHub-rendered files (README) and in docstrings, a YAML block followed by the equivalent TOML block. Each pair must mean the same (identical entries). Prose stays format-neutral: write `schedule`, not `schedule:` or `schedule =`. Only features that exist in one format alone (YAML anchors/templates) are shown in one format, next to how the other format does without.
|
|
82
|
+
- The GitHub Pages site must be enabled once per repo before `deploy-docs` can run: `gh api repos/LaughInJar/action0-celery-sched/pages -X POST -f build_type=workflow`.
|
|
83
|
+
- The README carries an AI-usage disclosure section — keep it accurate when the development workflow changes.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LaughInJar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: action0-celery-sched
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Celery beat schedules defined in YAML or TOML files
|
|
5
|
+
Project-URL: Homepage, https://github.com/LaughInJar/action0-celery-sched
|
|
6
|
+
Project-URL: Documentation, https://laughinjar.github.io/action0-celery-sched/
|
|
7
|
+
Project-URL: Source, https://github.com/LaughInJar/action0-celery-sched
|
|
8
|
+
Project-URL: Issues, https://github.com/LaughInJar/action0-celery-sched/issues
|
|
9
|
+
Author: Simon Lachinger
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: beat,celery,celery-beat,crontab,periodic-tasks,schedule,toml,yaml
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: Celery
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
24
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Requires-Dist: celery>=5.3
|
|
28
|
+
Provides-Extra: solar
|
|
29
|
+
Requires-Dist: ephem>=4.1; extra == 'solar'
|
|
30
|
+
Provides-Extra: yaml
|
|
31
|
+
Requires-Dist: pyyaml>=6.0; extra == 'yaml'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# Action0-Celery-Sched
|
|
35
|
+
|
|
36
|
+
[](https://github.com/LaughInJar/action0-celery-sched/actions/workflows/ci.yml)
|
|
37
|
+
[](https://pypi.org/project/action0-celery-sched/)
|
|
38
|
+
|
|
39
|
+
Celery beat schedules defined in YAML or TOML files: which task runs when,
|
|
40
|
+
with which arguments, kept out of the code and validated when the app starts.
|
|
41
|
+
|
|
42
|
+
Requires Python 3.11 or newer and Celery 5.3 or newer.
|
|
43
|
+
|
|
44
|
+
Full documentation including the API reference:
|
|
45
|
+
<https://laughinjar.github.io/action0-celery-sched/>
|
|
46
|
+
|
|
47
|
+
**Status:** early. The file format in YAML and TOML, all three kinds of Celery
|
|
48
|
+
schedule, the validation and the task check work and are covered by tests; the
|
|
49
|
+
API may still move.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```shell
|
|
54
|
+
pip install action0-celery-sched # TOML schedules
|
|
55
|
+
pip install "action0-celery-sched[yaml]" # YAML schedules too (PyYAML)
|
|
56
|
+
pip install "action0-celery-sched[solar]" # solar schedules (ephem)
|
|
57
|
+
pip install "action0-celery-sched[yaml,solar]" # all of it
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
TOML needs nothing beyond the standard library's `tomllib`; YAML needs PyYAML,
|
|
61
|
+
which the `yaml` extra brings along. (`uv add` works the same way.)
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
Write the schedule down, in YAML or in TOML — the entries are the same in
|
|
66
|
+
both:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
# beat.yaml
|
|
70
|
+
"Poll feed":
|
|
71
|
+
task: myapp.feeds.tasks.poll
|
|
72
|
+
schedule:
|
|
73
|
+
every: 5m # or 300, 90s, 1h30m, {minutes: 5}
|
|
74
|
+
|
|
75
|
+
"Nightly report":
|
|
76
|
+
task: myapp.reports.tasks.nightly
|
|
77
|
+
kw: # keyword arguments
|
|
78
|
+
recipients: [ops@example.com]
|
|
79
|
+
params: # positional arguments
|
|
80
|
+
- daily
|
|
81
|
+
schedule:
|
|
82
|
+
crontab: "0 3 * * *" # or {minute: 0, hour: 3}, or @daily
|
|
83
|
+
options: # passed on to apply_async()
|
|
84
|
+
queue: reports
|
|
85
|
+
|
|
86
|
+
"Sunset lights":
|
|
87
|
+
task: myapp.home.tasks.lights_on
|
|
88
|
+
schedule:
|
|
89
|
+
solar: {event: sunset, lat: 48.21, lon: 16.37}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```toml
|
|
93
|
+
# beat.toml
|
|
94
|
+
["Poll feed"]
|
|
95
|
+
task = "myapp.feeds.tasks.poll"
|
|
96
|
+
schedule = { every = "5m" } # or 300, "90s", "1h30m", { minutes = 5 }
|
|
97
|
+
|
|
98
|
+
["Nightly report"]
|
|
99
|
+
task = "myapp.reports.tasks.nightly"
|
|
100
|
+
kw = { recipients = ["ops@example.com"] } # keyword arguments
|
|
101
|
+
params = ["daily"] # positional arguments
|
|
102
|
+
schedule = { crontab = "0 3 * * *" } # or { minute = 0, hour = 3 }, or "@daily"
|
|
103
|
+
options = { queue = "reports" } # passed on to apply_async()
|
|
104
|
+
|
|
105
|
+
["Sunset lights"]
|
|
106
|
+
task = "myapp.home.tasks.lights_on"
|
|
107
|
+
schedule = { solar = { event = "sunset", lat = 48.21, lon = 16.37 } }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
And hand it to Celery:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from celery import Celery
|
|
114
|
+
from action0.celery_sched import load_beat_schedule
|
|
115
|
+
|
|
116
|
+
app = Celery("myapp")
|
|
117
|
+
app.conf.beat_schedule = load_beat_schedule("beat.yaml")
|
|
118
|
+
# or
|
|
119
|
+
app.conf.beat_schedule = load_beat_schedule("beat.toml")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The suffix decides the format (`format="yaml"` or `format="toml"` for anything
|
|
123
|
+
else). A mapping works too, e.g. the `beat` part of a larger YAML or TOML
|
|
124
|
+
config: `load_beat_schedule(settings["beat"])`.
|
|
125
|
+
|
|
126
|
+
The result is plain Celery, a `beat_schedule` dict of `schedule`, `crontab`
|
|
127
|
+
and `solar` objects:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
{'Poll feed': {'task': 'myapp.feeds.tasks.poll',
|
|
131
|
+
'schedule': <freq: 5.00 minutes>,
|
|
132
|
+
'args': (),
|
|
133
|
+
'kwargs': {},
|
|
134
|
+
'options': {}},
|
|
135
|
+
...}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Everything is validated while loading. An unknown key such as a misspelled
|
|
139
|
+
`shedule`, an impossible crontab field, or an entry name used twice is an
|
|
140
|
+
error naming the file, the entry and the key — the same message for both
|
|
141
|
+
formats, here for the crontab `"0 25 * * *"`:
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
beat.yaml: entry 'Nightly report': schedule.crontab.hour: invalid value '25': Invalid end range: 25 > 23.
|
|
145
|
+
beat.toml: entry 'Nightly report': schedule.crontab.hour: invalid value '25': Invalid end range: 25 > 23.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Values that differ per environment can come from environment variables, and
|
|
149
|
+
entries can be switched off without deleting them. In YAML `!ENV` is a tag;
|
|
150
|
+
TOML has no tags, so there it is a prefix of the string:
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
"Nightly report":
|
|
154
|
+
task: myapp.reports.tasks.nightly
|
|
155
|
+
schedule:
|
|
156
|
+
crontab: !ENV ${REPORT_CRON:-0 3 * * *}
|
|
157
|
+
enabled: !ENV ${REPORTS_ENABLED:-true}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```toml
|
|
161
|
+
["Nightly report"]
|
|
162
|
+
task = "myapp.reports.tasks.nightly"
|
|
163
|
+
schedule = { crontab = "!ENV ${REPORT_CRON:-0 3 * * *}" }
|
|
164
|
+
enabled = "!ENV ${REPORTS_ENABLED:-true}"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Several files merge in order, whatever their format. With `replace=True` a
|
|
168
|
+
later file may override or disable entries of an earlier one:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
app.conf.beat_schedule = load_beat_schedule(
|
|
172
|
+
"beat/common.yaml", f"beat/{environment}.toml", replace=True
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
A misspelled `task` name would only surface when the task is first due. Catch
|
|
177
|
+
it when beat starts instead:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from celery.signals import beat_init
|
|
181
|
+
from action0.celery_sched import check_tasks
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@beat_init.connect
|
|
185
|
+
def check_schedule(sender, **kwargs):
|
|
186
|
+
check_tasks(sender.app) # raises UnknownTaskError listing every unknown name
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
See the [usage guide](https://laughinjar.github.io/action0-celery-sched/usage.html)
|
|
190
|
+
for the full file format, every example in both YAML and TOML: every interval
|
|
191
|
+
and crontab spelling, `relative` intervals, how the two formats differ,
|
|
192
|
+
templates and anchors, and the error types.
|
|
193
|
+
|
|
194
|
+
The `action0` namespace is simply the one the author likes to use for
|
|
195
|
+
personal projects.
|
|
196
|
+
|
|
197
|
+
## Development
|
|
198
|
+
|
|
199
|
+
```shell
|
|
200
|
+
uv run pytest # tests (incl. doctests in src/)
|
|
201
|
+
uv run ruff check # lint
|
|
202
|
+
uv run ruff format # format
|
|
203
|
+
uv run mypy # type-check (strict)
|
|
204
|
+
uv run pyright # type-check
|
|
205
|
+
uv run ty check # type-check
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## AI disclosure
|
|
209
|
+
|
|
210
|
+
This library is developed with heavy use of AI coding tools: the code,
|
|
211
|
+
tests, and documentation are largely written by
|
|
212
|
+
[Claude Code](https://claude.com/claude-code), working from the author's
|
|
213
|
+
design brief and reviewed by the author. If that changes how much you want
|
|
214
|
+
to rely on this package, that's a fair call — read the source, it's small.
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
MIT — see [LICENSE](https://github.com/LaughInJar/action0-celery-sched/blob/main/LICENSE).
|