hawkapi-celery 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.
- hawkapi_celery-0.1.0/.github/workflows/ci.yml +52 -0
- hawkapi_celery-0.1.0/.github/workflows/release.yml +25 -0
- hawkapi_celery-0.1.0/.gitignore +35 -0
- hawkapi_celery-0.1.0/CHANGELOG.md +15 -0
- hawkapi_celery-0.1.0/LICENSE +21 -0
- hawkapi_celery-0.1.0/PKG-INFO +224 -0
- hawkapi_celery-0.1.0/README.md +170 -0
- hawkapi_celery-0.1.0/pyproject.toml +76 -0
- hawkapi_celery-0.1.0/src/hawkapi_celery/__init__.py +54 -0
- hawkapi_celery-0.1.0/src/hawkapi_celery/_app.py +62 -0
- hawkapi_celery-0.1.0/src/hawkapi_celery/_beat.py +49 -0
- hawkapi_celery-0.1.0/src/hawkapi_celery/_context.py +106 -0
- hawkapi_celery-0.1.0/src/hawkapi_celery/_health.py +56 -0
- hawkapi_celery-0.1.0/src/hawkapi_celery/_plugin.py +74 -0
- hawkapi_celery-0.1.0/src/hawkapi_celery/_tasks.py +93 -0
- hawkapi_celery-0.1.0/src/hawkapi_celery/_testing.py +86 -0
- hawkapi_celery-0.1.0/src/hawkapi_celery/py.typed +0 -0
- hawkapi_celery-0.1.0/tests/__init__.py +0 -0
- hawkapi_celery-0.1.0/tests/conftest.py +20 -0
- hawkapi_celery-0.1.0/tests/test_app.py +35 -0
- hawkapi_celery-0.1.0/tests/test_beat.py +49 -0
- hawkapi_celery-0.1.0/tests/test_context.py +71 -0
- hawkapi_celery-0.1.0/tests/test_health.py +46 -0
- hawkapi_celery-0.1.0/tests/test_plugin.py +66 -0
- hawkapi_celery-0.1.0/tests/test_tasks.py +46 -0
- hawkapi_celery-0.1.0/tests/test_testing.py +37 -0
- hawkapi_celery-0.1.0/uv.lock +405 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
name: Lint
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v4
|
|
15
|
+
with:
|
|
16
|
+
enable-cache: true
|
|
17
|
+
- name: Install dependencies
|
|
18
|
+
run: uv sync --extra dev
|
|
19
|
+
- name: ruff check
|
|
20
|
+
run: uv run ruff check .
|
|
21
|
+
- name: ruff format check
|
|
22
|
+
run: uv run ruff format --check .
|
|
23
|
+
|
|
24
|
+
typecheck:
|
|
25
|
+
name: Typecheck
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: astral-sh/setup-uv@v4
|
|
30
|
+
with:
|
|
31
|
+
enable-cache: true
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
run: uv sync --extra dev
|
|
34
|
+
- name: pyright
|
|
35
|
+
run: uv run pyright src/
|
|
36
|
+
|
|
37
|
+
test:
|
|
38
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
strategy:
|
|
41
|
+
matrix:
|
|
42
|
+
python-version: ["3.12", "3.13"]
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
- uses: astral-sh/setup-uv@v4
|
|
46
|
+
with:
|
|
47
|
+
enable-cache: true
|
|
48
|
+
python-version: ${{ matrix.python-version }}
|
|
49
|
+
- name: Install dependencies
|
|
50
|
+
run: uv sync --extra dev
|
|
51
|
+
- name: Run tests
|
|
52
|
+
run: uv run pytest tests/ -q
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build-and-publish:
|
|
9
|
+
name: Build and publish to PyPI
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: release
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # required for trusted publishing
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v4
|
|
18
|
+
with:
|
|
19
|
+
enable-cache: true
|
|
20
|
+
- name: Build package
|
|
21
|
+
run: uv build
|
|
22
|
+
- name: Publish to PyPI
|
|
23
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
24
|
+
with:
|
|
25
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.so
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
*.egg
|
|
9
|
+
.eggs/
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
env/
|
|
13
|
+
.env
|
|
14
|
+
*.log
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.pyright/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
htmlcov/
|
|
20
|
+
.coverage
|
|
21
|
+
.coverage.*
|
|
22
|
+
coverage.xml
|
|
23
|
+
*.cover
|
|
24
|
+
.hypothesis/
|
|
25
|
+
.tox/
|
|
26
|
+
.nox/
|
|
27
|
+
*.swp
|
|
28
|
+
*.swo
|
|
29
|
+
*~
|
|
30
|
+
.DS_Store
|
|
31
|
+
.idea/
|
|
32
|
+
.vscode/
|
|
33
|
+
.history/
|
|
34
|
+
site/
|
|
35
|
+
.remember/
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-05-16
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `init_celery(app, ...)` + `app.state.celery`.
|
|
8
|
+
- `@task(celery_app, ...)` decorator with `async def` support (runs coroutines on a private event loop) and auto-retry knobs.
|
|
9
|
+
- Beat schedule helpers — `Periodic`, `add_periodic`, `every`, `every_seconds`, re-exported `crontab`.
|
|
10
|
+
- Healthchecks — `check_broker`, `check_workers`, `healthcheck` → `HealthReport`.
|
|
11
|
+
- Request-context propagation between HTTP handlers and workers (`bind_context`, `current_context`, `attach_context_signals`).
|
|
12
|
+
- DI helpers — `Depends(get_celery)`, `get_task_result(task_id, request)`.
|
|
13
|
+
- Test fixtures — `eager_mode`, `record_tasks` / `TaskRecorder`.
|
|
14
|
+
- Retry helper — `compute_backoff` with decorrelated jitter.
|
|
15
|
+
- Extras: `[redis]`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HawkAPI Contributors
|
|
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,224 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hawkapi-celery
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Celery integration for HawkAPI — async tasks, beat scheduler, context propagation, healthchecks, eager-mode fixtures
|
|
5
|
+
Project-URL: Homepage, https://pypi.org/project/hawkapi-celery/
|
|
6
|
+
Project-URL: Repository, https://github.com/ashimov/hawkapi-celery
|
|
7
|
+
Project-URL: Issues, https://github.com/ashimov/hawkapi-celery/issues
|
|
8
|
+
Author-email: HawkAPI Contributors <hawkapi@users.noreply.github.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 HawkAPI Contributors
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: async,background,beat,celery,hawkapi,queue,tasks
|
|
32
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
33
|
+
Classifier: Framework :: AsyncIO
|
|
34
|
+
Classifier: Framework :: Celery
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
40
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
41
|
+
Classifier: Typing :: Typed
|
|
42
|
+
Requires-Python: >=3.12
|
|
43
|
+
Requires-Dist: celery>=5.4
|
|
44
|
+
Requires-Dist: hawkapi>=0.1.7
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: pyright>=1.1; extra == 'dev'
|
|
47
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
48
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
49
|
+
Requires-Dist: redis>=5.0; extra == 'dev'
|
|
50
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
51
|
+
Provides-Extra: redis
|
|
52
|
+
Requires-Dist: redis>=5.0; extra == 'redis'
|
|
53
|
+
Description-Content-Type: text/markdown
|
|
54
|
+
|
|
55
|
+
# hawkapi-celery
|
|
56
|
+
|
|
57
|
+
Celery integration for [HawkAPI](https://github.com/ashimov/HawkAPI). Async tasks, beat scheduler, request-context propagation, broker/worker healthchecks, and eager-mode fixtures for tests.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install hawkapi-celery
|
|
63
|
+
pip install 'hawkapi-celery[redis]' # adds redis client
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Quickstart
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from hawkapi import Depends, HawkAPI
|
|
70
|
+
from celery import Celery
|
|
71
|
+
from hawkapi_celery import (
|
|
72
|
+
CeleryConfig, bind_context, get_celery, init_celery, task,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
celery_app: Celery # populated below
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def make_app() -> HawkAPI:
|
|
80
|
+
app = HawkAPI()
|
|
81
|
+
global celery_app
|
|
82
|
+
celery_app = init_celery(
|
|
83
|
+
app,
|
|
84
|
+
config=CeleryConfig(
|
|
85
|
+
broker_url="redis://localhost:6379/0",
|
|
86
|
+
result_backend="redis://localhost:6379/0",
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@task(celery_app, name="emails.send")
|
|
92
|
+
async def send_email(to: str, subject: str, body: str) -> None:
|
|
93
|
+
... # any await-able send logic
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.post("/notify")
|
|
97
|
+
async def notify(email: str, c: Celery = Depends(get_celery)):
|
|
98
|
+
with bind_context(request_id="…"):
|
|
99
|
+
send_email.delay(email, "Welcome", "Hello!")
|
|
100
|
+
return {"ok": True}
|
|
101
|
+
|
|
102
|
+
return app
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Tasks
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from hawkapi_celery import task
|
|
109
|
+
|
|
110
|
+
@task(celery_app, name="myapp.work", queue="default",
|
|
111
|
+
autoretry_for=(ConnectionError,), retry_backoff=True, max_retries=5)
|
|
112
|
+
async def work(x: int) -> int: # async def — runs on a private event loop
|
|
113
|
+
...
|
|
114
|
+
return x * 2
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@task(celery_app, bind=True)
|
|
118
|
+
def slow_work(self, payload): # sync — bound `self` for retry handling
|
|
119
|
+
try:
|
|
120
|
+
do_thing(payload)
|
|
121
|
+
except TransientError as exc:
|
|
122
|
+
raise self.retry(exc=exc, countdown=compute_backoff(self.request.retries))
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Beat (periodic tasks)
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from datetime import timedelta
|
|
129
|
+
from hawkapi_celery import Periodic, add_periodic, crontab, every
|
|
130
|
+
|
|
131
|
+
add_periodic(celery_app, "cleanup",
|
|
132
|
+
Periodic(task="myapp.cleanup", schedule=every(timedelta(hours=1))))
|
|
133
|
+
|
|
134
|
+
add_periodic(celery_app, "nightly_report",
|
|
135
|
+
Periodic(task="myapp.report", schedule=crontab(hour=2, minute=0),
|
|
136
|
+
kwargs={"date": "yesterday"}))
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Context propagation
|
|
140
|
+
|
|
141
|
+
`bind_context()` carries a dict from the HTTP handler to the worker process via the task headers. Inside the task call `current_context()` to read it back.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from hawkapi_celery import bind_context, current_context
|
|
145
|
+
|
|
146
|
+
@task(celery_app, name="log.event")
|
|
147
|
+
def log_event(payload: dict) -> None:
|
|
148
|
+
ctx = current_context() # {"request_id": "…", "user_id": "…"}
|
|
149
|
+
log.info("event", **ctx, **payload)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@app.post("/event")
|
|
153
|
+
async def post_event(p: Payload):
|
|
154
|
+
with bind_context(request_id=p.request_id, user_id=p.user_id):
|
|
155
|
+
log_event.delay(p.model_dump())
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Wired automatically by `init_celery(..., propagate_context=True)` (default).
|
|
159
|
+
|
|
160
|
+
## Healthchecks
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from hawkapi_celery import healthcheck
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@app.get("/healthz")
|
|
167
|
+
async def healthz():
|
|
168
|
+
report = healthcheck(celery_app, timeout=2.0)
|
|
169
|
+
return {
|
|
170
|
+
"broker": report.broker_ok,
|
|
171
|
+
"workers_alive": report.workers_alive,
|
|
172
|
+
"workers": list(report.workers),
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Testing
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from hawkapi_celery import eager_mode, record_tasks
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_signup_enqueues_welcome_email(client, celery_app):
|
|
183
|
+
with record_tasks(celery_app) as recorder:
|
|
184
|
+
client.post("/signup", json={"email": "x@y.z"})
|
|
185
|
+
assert any(t.name == "emails.send" for t in recorder.captured)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_signup_runs_welcome_email_inline(client, celery_app):
|
|
189
|
+
with eager_mode(celery_app):
|
|
190
|
+
client.post("/signup", json={"email": "x@y.z"})
|
|
191
|
+
# All tasks executed synchronously in-process — assert their side-effects directly.
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## CeleryConfig
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
CeleryConfig(
|
|
198
|
+
broker_url="redis://localhost:6379/0",
|
|
199
|
+
result_backend="redis://localhost:6379/0",
|
|
200
|
+
task_serializer="json",
|
|
201
|
+
timezone="UTC",
|
|
202
|
+
task_time_limit=600,
|
|
203
|
+
task_soft_time_limit=540,
|
|
204
|
+
worker_prefetch_multiplier=1,
|
|
205
|
+
worker_max_tasks_per_child=1000,
|
|
206
|
+
task_default_queue="default",
|
|
207
|
+
extra_kwargs={...}, # forwarded to celery.conf.update
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Development
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
git clone https://github.com/ashimov/hawkapi-celery.git
|
|
215
|
+
cd hawkapi-celery
|
|
216
|
+
uv sync --extra dev
|
|
217
|
+
uv run pytest -q
|
|
218
|
+
uv run ruff check . && uv run ruff format --check .
|
|
219
|
+
uv run pyright src/
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
MIT.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# hawkapi-celery
|
|
2
|
+
|
|
3
|
+
Celery integration for [HawkAPI](https://github.com/ashimov/HawkAPI). Async tasks, beat scheduler, request-context propagation, broker/worker healthchecks, and eager-mode fixtures for tests.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install hawkapi-celery
|
|
9
|
+
pip install 'hawkapi-celery[redis]' # adds redis client
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quickstart
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from hawkapi import Depends, HawkAPI
|
|
16
|
+
from celery import Celery
|
|
17
|
+
from hawkapi_celery import (
|
|
18
|
+
CeleryConfig, bind_context, get_celery, init_celery, task,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
celery_app: Celery # populated below
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def make_app() -> HawkAPI:
|
|
26
|
+
app = HawkAPI()
|
|
27
|
+
global celery_app
|
|
28
|
+
celery_app = init_celery(
|
|
29
|
+
app,
|
|
30
|
+
config=CeleryConfig(
|
|
31
|
+
broker_url="redis://localhost:6379/0",
|
|
32
|
+
result_backend="redis://localhost:6379/0",
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@task(celery_app, name="emails.send")
|
|
38
|
+
async def send_email(to: str, subject: str, body: str) -> None:
|
|
39
|
+
... # any await-able send logic
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.post("/notify")
|
|
43
|
+
async def notify(email: str, c: Celery = Depends(get_celery)):
|
|
44
|
+
with bind_context(request_id="…"):
|
|
45
|
+
send_email.delay(email, "Welcome", "Hello!")
|
|
46
|
+
return {"ok": True}
|
|
47
|
+
|
|
48
|
+
return app
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Tasks
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from hawkapi_celery import task
|
|
55
|
+
|
|
56
|
+
@task(celery_app, name="myapp.work", queue="default",
|
|
57
|
+
autoretry_for=(ConnectionError,), retry_backoff=True, max_retries=5)
|
|
58
|
+
async def work(x: int) -> int: # async def — runs on a private event loop
|
|
59
|
+
...
|
|
60
|
+
return x * 2
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@task(celery_app, bind=True)
|
|
64
|
+
def slow_work(self, payload): # sync — bound `self` for retry handling
|
|
65
|
+
try:
|
|
66
|
+
do_thing(payload)
|
|
67
|
+
except TransientError as exc:
|
|
68
|
+
raise self.retry(exc=exc, countdown=compute_backoff(self.request.retries))
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Beat (periodic tasks)
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from datetime import timedelta
|
|
75
|
+
from hawkapi_celery import Periodic, add_periodic, crontab, every
|
|
76
|
+
|
|
77
|
+
add_periodic(celery_app, "cleanup",
|
|
78
|
+
Periodic(task="myapp.cleanup", schedule=every(timedelta(hours=1))))
|
|
79
|
+
|
|
80
|
+
add_periodic(celery_app, "nightly_report",
|
|
81
|
+
Periodic(task="myapp.report", schedule=crontab(hour=2, minute=0),
|
|
82
|
+
kwargs={"date": "yesterday"}))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Context propagation
|
|
86
|
+
|
|
87
|
+
`bind_context()` carries a dict from the HTTP handler to the worker process via the task headers. Inside the task call `current_context()` to read it back.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from hawkapi_celery import bind_context, current_context
|
|
91
|
+
|
|
92
|
+
@task(celery_app, name="log.event")
|
|
93
|
+
def log_event(payload: dict) -> None:
|
|
94
|
+
ctx = current_context() # {"request_id": "…", "user_id": "…"}
|
|
95
|
+
log.info("event", **ctx, **payload)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@app.post("/event")
|
|
99
|
+
async def post_event(p: Payload):
|
|
100
|
+
with bind_context(request_id=p.request_id, user_id=p.user_id):
|
|
101
|
+
log_event.delay(p.model_dump())
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Wired automatically by `init_celery(..., propagate_context=True)` (default).
|
|
105
|
+
|
|
106
|
+
## Healthchecks
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from hawkapi_celery import healthcheck
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@app.get("/healthz")
|
|
113
|
+
async def healthz():
|
|
114
|
+
report = healthcheck(celery_app, timeout=2.0)
|
|
115
|
+
return {
|
|
116
|
+
"broker": report.broker_ok,
|
|
117
|
+
"workers_alive": report.workers_alive,
|
|
118
|
+
"workers": list(report.workers),
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Testing
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from hawkapi_celery import eager_mode, record_tasks
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_signup_enqueues_welcome_email(client, celery_app):
|
|
129
|
+
with record_tasks(celery_app) as recorder:
|
|
130
|
+
client.post("/signup", json={"email": "x@y.z"})
|
|
131
|
+
assert any(t.name == "emails.send" for t in recorder.captured)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_signup_runs_welcome_email_inline(client, celery_app):
|
|
135
|
+
with eager_mode(celery_app):
|
|
136
|
+
client.post("/signup", json={"email": "x@y.z"})
|
|
137
|
+
# All tasks executed synchronously in-process — assert their side-effects directly.
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## CeleryConfig
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
CeleryConfig(
|
|
144
|
+
broker_url="redis://localhost:6379/0",
|
|
145
|
+
result_backend="redis://localhost:6379/0",
|
|
146
|
+
task_serializer="json",
|
|
147
|
+
timezone="UTC",
|
|
148
|
+
task_time_limit=600,
|
|
149
|
+
task_soft_time_limit=540,
|
|
150
|
+
worker_prefetch_multiplier=1,
|
|
151
|
+
worker_max_tasks_per_child=1000,
|
|
152
|
+
task_default_queue="default",
|
|
153
|
+
extra_kwargs={...}, # forwarded to celery.conf.update
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
git clone https://github.com/ashimov/hawkapi-celery.git
|
|
161
|
+
cd hawkapi-celery
|
|
162
|
+
uv sync --extra dev
|
|
163
|
+
uv run pytest -q
|
|
164
|
+
uv run ruff check . && uv run ruff format --check .
|
|
165
|
+
uv run pyright src/
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hawkapi-celery"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Celery integration for HawkAPI — async tasks, beat scheduler, context propagation, healthchecks, eager-mode fixtures"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "HawkAPI Contributors", email = "hawkapi@users.noreply.github.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["hawkapi", "celery", "tasks", "queue", "beat", "background", "async"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
|
+
"Framework :: AsyncIO",
|
|
19
|
+
"Framework :: Celery",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: System :: Distributed Computing",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"hawkapi>=0.1.7",
|
|
30
|
+
"celery>=5.4",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
redis = ["redis>=5.0"]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8.0",
|
|
37
|
+
"pytest-asyncio>=0.24",
|
|
38
|
+
"redis>=5.0",
|
|
39
|
+
"ruff>=0.8",
|
|
40
|
+
"pyright>=1.1",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://pypi.org/project/hawkapi-celery/"
|
|
45
|
+
Repository = "https://github.com/ashimov/hawkapi-celery"
|
|
46
|
+
Issues = "https://github.com/ashimov/hawkapi-celery/issues"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/hawkapi_celery"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
asyncio_mode = "auto"
|
|
54
|
+
filterwarnings = ["ignore::DeprecationWarning"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
target-version = "py312"
|
|
58
|
+
line-length = 100
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "S"]
|
|
62
|
+
ignore = ["S101", "B008"]
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint.per-file-ignores]
|
|
65
|
+
"tests/**" = ["S"]
|
|
66
|
+
|
|
67
|
+
[tool.pyright]
|
|
68
|
+
pythonVersion = "3.12"
|
|
69
|
+
typeCheckingMode = "strict"
|
|
70
|
+
reportUnknownVariableType = false
|
|
71
|
+
reportUnknownMemberType = false
|
|
72
|
+
reportUnknownArgumentType = false
|
|
73
|
+
reportMissingTypeStubs = false
|
|
74
|
+
reportUntypedFunctionDecorator = false
|
|
75
|
+
reportUnusedFunction = false
|
|
76
|
+
reportInvalidTypeVarUse = false
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""hawkapi-celery — Celery integration for HawkAPI.
|
|
2
|
+
|
|
3
|
+
Async tasks, beat scheduler, broker/worker healthchecks, request-context
|
|
4
|
+
propagation between HTTP handlers and Celery workers, and eager-mode fixtures
|
|
5
|
+
for testing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ._app import CeleryConfig, create_celery
|
|
11
|
+
from ._beat import Periodic, add_periodic, crontab, every, every_seconds
|
|
12
|
+
from ._context import (
|
|
13
|
+
attach_context_signals,
|
|
14
|
+
bind_context,
|
|
15
|
+
current_context,
|
|
16
|
+
reset_context,
|
|
17
|
+
set_context,
|
|
18
|
+
)
|
|
19
|
+
from ._health import HealthReport, check_broker, check_workers, healthcheck
|
|
20
|
+
from ._plugin import get_celery, get_task_result, init_celery, resolve_celery
|
|
21
|
+
from ._tasks import compute_backoff, task
|
|
22
|
+
from ._testing import CapturedTask, TaskRecorder, eager_mode, record_tasks
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"CapturedTask",
|
|
28
|
+
"CeleryConfig",
|
|
29
|
+
"HealthReport",
|
|
30
|
+
"Periodic",
|
|
31
|
+
"TaskRecorder",
|
|
32
|
+
"__version__",
|
|
33
|
+
"add_periodic",
|
|
34
|
+
"attach_context_signals",
|
|
35
|
+
"bind_context",
|
|
36
|
+
"check_broker",
|
|
37
|
+
"check_workers",
|
|
38
|
+
"compute_backoff",
|
|
39
|
+
"create_celery",
|
|
40
|
+
"crontab",
|
|
41
|
+
"current_context",
|
|
42
|
+
"eager_mode",
|
|
43
|
+
"every",
|
|
44
|
+
"every_seconds",
|
|
45
|
+
"get_celery",
|
|
46
|
+
"get_task_result",
|
|
47
|
+
"healthcheck",
|
|
48
|
+
"init_celery",
|
|
49
|
+
"record_tasks",
|
|
50
|
+
"reset_context",
|
|
51
|
+
"resolve_celery",
|
|
52
|
+
"set_context",
|
|
53
|
+
"task",
|
|
54
|
+
]
|