hawkapi-taskiq 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_taskiq-0.1.0/.github/workflows/ci.yml +52 -0
- hawkapi_taskiq-0.1.0/.github/workflows/release.yml +25 -0
- hawkapi_taskiq-0.1.0/.gitignore +35 -0
- hawkapi_taskiq-0.1.0/CHANGELOG.md +24 -0
- hawkapi_taskiq-0.1.0/LICENSE +21 -0
- hawkapi_taskiq-0.1.0/PKG-INFO +178 -0
- hawkapi_taskiq-0.1.0/README.md +121 -0
- hawkapi_taskiq-0.1.0/pyproject.toml +76 -0
- hawkapi_taskiq-0.1.0/src/hawkapi_taskiq/__init__.py +35 -0
- hawkapi_taskiq-0.1.0/src/hawkapi_taskiq/_broker.py +68 -0
- hawkapi_taskiq-0.1.0/src/hawkapi_taskiq/_config.py +38 -0
- hawkapi_taskiq-0.1.0/src/hawkapi_taskiq/_health.py +60 -0
- hawkapi_taskiq-0.1.0/src/hawkapi_taskiq/_plugin.py +98 -0
- hawkapi_taskiq-0.1.0/src/hawkapi_taskiq/_schedule.py +36 -0
- hawkapi_taskiq-0.1.0/src/hawkapi_taskiq/_tasks.py +53 -0
- hawkapi_taskiq-0.1.0/src/hawkapi_taskiq/_testing.py +24 -0
- hawkapi_taskiq-0.1.0/src/hawkapi_taskiq/py.typed +1 -0
- hawkapi_taskiq-0.1.0/tests/__init__.py +1 -0
- hawkapi_taskiq-0.1.0/tests/test_config.py +67 -0
- hawkapi_taskiq-0.1.0/tests/test_health.py +13 -0
- hawkapi_taskiq-0.1.0/tests/test_plugin.py +66 -0
- hawkapi_taskiq-0.1.0/tests/test_schedule.py +37 -0
- hawkapi_taskiq-0.1.0/tests/test_tasks.py +72 -0
- hawkapi_taskiq-0.1.0/uv.lock +974 -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,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-05-17
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
Security review applied before ship:
|
|
8
|
+
|
|
9
|
+
- `task_name` smuggled via `**task_kwargs` no longer bypasses the duplicate-name guard (defense in depth).
|
|
10
|
+
- `extra={"serializer": ...}` is rejected to prevent a future broker release accepting that kwarg from bypassing the JSON-only enforcement (CWE-502 hardening).
|
|
11
|
+
- `result_backend_url` now raises `NotImplementedError` rather than silently dropping the value — a misconfigured result backend can no longer be ignored.
|
|
12
|
+
- Double-init guard on `init_taskiq` — calling it twice on the same app raises rather than double-registering startup/shutdown hooks.
|
|
13
|
+
- `check_broker` actually probes the broker (was a static `True` before) and scrubs Redis/NATS credentials from any error string.
|
|
14
|
+
- `add_scheduled` removed from the public API — it set labels in a format `TaskiqScheduler` does not consume. `Scheduled` remains as a cron-validation value type; README shows the direct `LabelScheduleSource` wiring.
|
|
15
|
+
|
|
16
|
+
Features:
|
|
17
|
+
|
|
18
|
+
- `create_broker(TaskIQConfig(...))` — URL-scheme allowlist (`memory://`, `redis://`, `rediss://`, `nats://`), JSON serializer enforced at construction.
|
|
19
|
+
- `@task(broker, ...)` decorator with duplicate-name detection.
|
|
20
|
+
- `Scheduled` value type with cron-syntax validation via `croniter` (extras `[cron]`).
|
|
21
|
+
- `init_taskiq(app, ...)` wires startup/shutdown into the app lifecycle. `Depends(get_broker)` + `WeakKeyDictionary` registry.
|
|
22
|
+
- `check_broker()` health probe + `HealthReport` with credential scrubbing.
|
|
23
|
+
- `in_memory_broker()` async context manager for tests.
|
|
24
|
+
- Extras: `[redis]` (taskiq-redis), `[nats]` (taskiq-nats), `[cron]` (croniter).
|
|
@@ -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,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hawkapi-taskiq
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TaskIQ integration for HawkAPI — modern async-native task queue, DI, scheduling, JSON-only formatter
|
|
5
|
+
Project-URL: Homepage, https://pypi.org/project/hawkapi-taskiq/
|
|
6
|
+
Project-URL: Repository, https://github.com/ashimov/hawkapi-taskiq
|
|
7
|
+
Project-URL: Issues, https://github.com/ashimov/hawkapi-taskiq/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,hawkapi,queue,scheduling,taskiq,tasks
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Framework :: AsyncIO
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
39
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
40
|
+
Classifier: Typing :: Typed
|
|
41
|
+
Requires-Python: >=3.12
|
|
42
|
+
Requires-Dist: hawkapi>=0.1.7
|
|
43
|
+
Requires-Dist: taskiq>=0.11
|
|
44
|
+
Provides-Extra: cron
|
|
45
|
+
Requires-Dist: croniter>=2.0; extra == 'cron'
|
|
46
|
+
Provides-Extra: dev
|
|
47
|
+
Requires-Dist: croniter>=2.0; extra == 'dev'
|
|
48
|
+
Requires-Dist: pyright>=1.1; extra == 'dev'
|
|
49
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
50
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
52
|
+
Provides-Extra: nats
|
|
53
|
+
Requires-Dist: taskiq-nats>=0.5; extra == 'nats'
|
|
54
|
+
Provides-Extra: redis
|
|
55
|
+
Requires-Dist: taskiq-redis>=1.0; extra == 'redis'
|
|
56
|
+
Description-Content-Type: text/markdown
|
|
57
|
+
|
|
58
|
+
# hawkapi-taskiq
|
|
59
|
+
|
|
60
|
+
[TaskIQ](https://taskiq-python.github.io/) integration for [HawkAPI](https://github.com/ashimov/HawkAPI). Modern async-native task queue — a lighter, async-first alternative to Celery.
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install hawkapi-taskiq
|
|
66
|
+
pip install 'hawkapi-taskiq[redis]' # + taskiq-redis
|
|
67
|
+
pip install 'hawkapi-taskiq[nats]' # + taskiq-nats
|
|
68
|
+
pip install 'hawkapi-taskiq[cron]' # + croniter for schedule validation
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quickstart
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from hawkapi import Depends, HawkAPI
|
|
75
|
+
from hawkapi_taskiq import TaskIQConfig, get_broker, init_taskiq, task
|
|
76
|
+
|
|
77
|
+
app = HawkAPI()
|
|
78
|
+
broker = init_taskiq(app, config=TaskIQConfig(broker_url="redis://localhost:6379/0"))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@task(broker, name="emails.send")
|
|
82
|
+
async def send_email(to: str, subject: str) -> None:
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.post("/notify")
|
|
87
|
+
async def notify(email: str, b = Depends(get_broker)):
|
|
88
|
+
await send_email.kiq(email, "Hello")
|
|
89
|
+
return {"ok": True}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Broker selection
|
|
93
|
+
|
|
94
|
+
Choose by URL scheme — all others are rejected:
|
|
95
|
+
|
|
96
|
+
| URL | Broker |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `memory://` | `InMemoryBroker` (tests, single-process) |
|
|
99
|
+
| `redis://host:6379/0` | `ListQueueBroker` (taskiq-redis) |
|
|
100
|
+
| `rediss://...` | same, with TLS |
|
|
101
|
+
| `nats://server:4222` | `NatsBroker` (taskiq-nats) |
|
|
102
|
+
|
|
103
|
+
Any other scheme raises `ValueError` at `create_broker()` — this is a security feature, not a limitation. The allowlist prevents accidentally enabling brokers that use unsafe deserialization formats.
|
|
104
|
+
|
|
105
|
+
## Scheduling
|
|
106
|
+
|
|
107
|
+
v0.1.0 ships a `Scheduled` value type with cron-syntax validation. Wire it to TaskIQ's native scheduler yourself — we deliberately avoid a "magic" registration helper that doesn't compose cleanly with the upstream `TaskiqScheduler`:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from hawkapi_taskiq import Scheduled
|
|
111
|
+
from taskiq import TaskiqScheduler
|
|
112
|
+
from taskiq.schedule_sources import LabelScheduleSource
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@task(broker, name="myapp.cleanup")
|
|
116
|
+
async def cleanup() -> None:
|
|
117
|
+
...
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# 1. Validate the schedule (cron syntax) up front.
|
|
121
|
+
schedule = Scheduled(cron="0 * * * *") # raises ValueError if malformed
|
|
122
|
+
|
|
123
|
+
# 2. Apply it as a label LabelScheduleSource reads.
|
|
124
|
+
cleanup.labels["schedule"] = [{"cron": schedule.cron, "args": [], "kwargs": {}}]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# 3. Run a scheduler process alongside the worker.
|
|
128
|
+
scheduler = TaskiqScheduler(broker=broker, sources=[LabelScheduleSource(broker)])
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`Scheduled(cron="...")` validates via [`croniter`](https://github.com/kiorky/croniter) (install with `[cron]` extra). Both `cron` and `interval_seconds` set are rejected (exactly one is required).
|
|
132
|
+
|
|
133
|
+
## Health
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from hawkapi_taskiq import check_broker
|
|
137
|
+
|
|
138
|
+
report = await check_broker(broker)
|
|
139
|
+
# HealthReport(broker_ok=True, broker_type="ListQueueBroker", error="")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Testing
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from hawkapi_taskiq import in_memory_broker, task
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def test_my_task():
|
|
149
|
+
async with in_memory_broker() as broker:
|
|
150
|
+
@task(broker, name="t.work")
|
|
151
|
+
async def work(x: int) -> int:
|
|
152
|
+
return x * 2
|
|
153
|
+
|
|
154
|
+
await work.kiq(21)
|
|
155
|
+
# Execute pending tasks via TaskIQ's normal flow.
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Security
|
|
159
|
+
|
|
160
|
+
- **JSON-only serialization** — TaskIQ defaults are fine; we explicitly reject any other serializer via `TaskIQConfig.serializer` to prevent arbitrary-deserialization at consume time (CWE-502).
|
|
161
|
+
- **Broker URL scheme allowlist** — only `memory://`, `redis://`, `rediss://`, `nats://`.
|
|
162
|
+
- **Task name registry** — duplicate `@task(name=...)` raises at registration. TaskIQ silently overrides; we disallow.
|
|
163
|
+
- **Cron expression validation** at registration time — malformed expressions fail fast.
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
git clone https://github.com/ashimov/hawkapi-taskiq.git
|
|
169
|
+
cd hawkapi-taskiq
|
|
170
|
+
uv sync --extra dev
|
|
171
|
+
uv run pytest -q
|
|
172
|
+
uv run ruff check . && uv run ruff format --check .
|
|
173
|
+
uv run pyright src/
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# hawkapi-taskiq
|
|
2
|
+
|
|
3
|
+
[TaskIQ](https://taskiq-python.github.io/) integration for [HawkAPI](https://github.com/ashimov/HawkAPI). Modern async-native task queue — a lighter, async-first alternative to Celery.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install hawkapi-taskiq
|
|
9
|
+
pip install 'hawkapi-taskiq[redis]' # + taskiq-redis
|
|
10
|
+
pip install 'hawkapi-taskiq[nats]' # + taskiq-nats
|
|
11
|
+
pip install 'hawkapi-taskiq[cron]' # + croniter for schedule validation
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from hawkapi import Depends, HawkAPI
|
|
18
|
+
from hawkapi_taskiq import TaskIQConfig, get_broker, init_taskiq, task
|
|
19
|
+
|
|
20
|
+
app = HawkAPI()
|
|
21
|
+
broker = init_taskiq(app, config=TaskIQConfig(broker_url="redis://localhost:6379/0"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@task(broker, name="emails.send")
|
|
25
|
+
async def send_email(to: str, subject: str) -> None:
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.post("/notify")
|
|
30
|
+
async def notify(email: str, b = Depends(get_broker)):
|
|
31
|
+
await send_email.kiq(email, "Hello")
|
|
32
|
+
return {"ok": True}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Broker selection
|
|
36
|
+
|
|
37
|
+
Choose by URL scheme — all others are rejected:
|
|
38
|
+
|
|
39
|
+
| URL | Broker |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `memory://` | `InMemoryBroker` (tests, single-process) |
|
|
42
|
+
| `redis://host:6379/0` | `ListQueueBroker` (taskiq-redis) |
|
|
43
|
+
| `rediss://...` | same, with TLS |
|
|
44
|
+
| `nats://server:4222` | `NatsBroker` (taskiq-nats) |
|
|
45
|
+
|
|
46
|
+
Any other scheme raises `ValueError` at `create_broker()` — this is a security feature, not a limitation. The allowlist prevents accidentally enabling brokers that use unsafe deserialization formats.
|
|
47
|
+
|
|
48
|
+
## Scheduling
|
|
49
|
+
|
|
50
|
+
v0.1.0 ships a `Scheduled` value type with cron-syntax validation. Wire it to TaskIQ's native scheduler yourself — we deliberately avoid a "magic" registration helper that doesn't compose cleanly with the upstream `TaskiqScheduler`:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from hawkapi_taskiq import Scheduled
|
|
54
|
+
from taskiq import TaskiqScheduler
|
|
55
|
+
from taskiq.schedule_sources import LabelScheduleSource
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@task(broker, name="myapp.cleanup")
|
|
59
|
+
async def cleanup() -> None:
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# 1. Validate the schedule (cron syntax) up front.
|
|
64
|
+
schedule = Scheduled(cron="0 * * * *") # raises ValueError if malformed
|
|
65
|
+
|
|
66
|
+
# 2. Apply it as a label LabelScheduleSource reads.
|
|
67
|
+
cleanup.labels["schedule"] = [{"cron": schedule.cron, "args": [], "kwargs": {}}]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# 3. Run a scheduler process alongside the worker.
|
|
71
|
+
scheduler = TaskiqScheduler(broker=broker, sources=[LabelScheduleSource(broker)])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`Scheduled(cron="...")` validates via [`croniter`](https://github.com/kiorky/croniter) (install with `[cron]` extra). Both `cron` and `interval_seconds` set are rejected (exactly one is required).
|
|
75
|
+
|
|
76
|
+
## Health
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from hawkapi_taskiq import check_broker
|
|
80
|
+
|
|
81
|
+
report = await check_broker(broker)
|
|
82
|
+
# HealthReport(broker_ok=True, broker_type="ListQueueBroker", error="")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Testing
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from hawkapi_taskiq import in_memory_broker, task
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def test_my_task():
|
|
92
|
+
async with in_memory_broker() as broker:
|
|
93
|
+
@task(broker, name="t.work")
|
|
94
|
+
async def work(x: int) -> int:
|
|
95
|
+
return x * 2
|
|
96
|
+
|
|
97
|
+
await work.kiq(21)
|
|
98
|
+
# Execute pending tasks via TaskIQ's normal flow.
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Security
|
|
102
|
+
|
|
103
|
+
- **JSON-only serialization** — TaskIQ defaults are fine; we explicitly reject any other serializer via `TaskIQConfig.serializer` to prevent arbitrary-deserialization at consume time (CWE-502).
|
|
104
|
+
- **Broker URL scheme allowlist** — only `memory://`, `redis://`, `rediss://`, `nats://`.
|
|
105
|
+
- **Task name registry** — duplicate `@task(name=...)` raises at registration. TaskIQ silently overrides; we disallow.
|
|
106
|
+
- **Cron expression validation** at registration time — malformed expressions fail fast.
|
|
107
|
+
|
|
108
|
+
## Development
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
git clone https://github.com/ashimov/hawkapi-taskiq.git
|
|
112
|
+
cd hawkapi-taskiq
|
|
113
|
+
uv sync --extra dev
|
|
114
|
+
uv run pytest -q
|
|
115
|
+
uv run ruff check . && uv run ruff format --check .
|
|
116
|
+
uv run pyright src/
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hawkapi-taskiq"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "TaskIQ integration for HawkAPI — modern async-native task queue, DI, scheduling, JSON-only formatter"
|
|
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", "taskiq", "tasks", "queue", "async", "scheduling"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Framework :: AsyncIO",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: System :: Distributed Computing",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"hawkapi>=0.1.7",
|
|
29
|
+
"taskiq>=0.11",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
redis = ["taskiq-redis>=1.0"]
|
|
34
|
+
nats = ["taskiq-nats>=0.5"]
|
|
35
|
+
cron = ["croniter>=2.0"]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=8.0",
|
|
38
|
+
"pytest-asyncio>=0.24",
|
|
39
|
+
"croniter>=2.0",
|
|
40
|
+
"ruff>=0.8",
|
|
41
|
+
"pyright>=1.1",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://pypi.org/project/hawkapi-taskiq/"
|
|
46
|
+
Repository = "https://github.com/ashimov/hawkapi-taskiq"
|
|
47
|
+
Issues = "https://github.com/ashimov/hawkapi-taskiq/issues"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.wheel]
|
|
50
|
+
packages = ["src/hawkapi_taskiq"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
asyncio_mode = "auto"
|
|
55
|
+
filterwarnings = ["ignore::DeprecationWarning"]
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
target-version = "py312"
|
|
59
|
+
line-length = 100
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "S"]
|
|
63
|
+
ignore = ["S101", "S105", "S110", "B008", "SIM105", "SIM108", "SIM113"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint.per-file-ignores]
|
|
66
|
+
"tests/**" = ["S"]
|
|
67
|
+
|
|
68
|
+
[tool.pyright]
|
|
69
|
+
pythonVersion = "3.12"
|
|
70
|
+
typeCheckingMode = "strict"
|
|
71
|
+
reportUnknownVariableType = false
|
|
72
|
+
reportUnknownMemberType = false
|
|
73
|
+
reportUnknownArgumentType = false
|
|
74
|
+
reportMissingTypeStubs = false
|
|
75
|
+
reportUntypedFunctionDecorator = false
|
|
76
|
+
reportGeneralTypeIssues = false
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""hawkapi-taskiq — TaskIQ integration for HawkAPI.
|
|
2
|
+
|
|
3
|
+
Modern async-native task queue. URL-scheme allowlist enforces JSON-only
|
|
4
|
+
serialization to prevent arbitrary-deserialization vulnerabilities. Broker
|
|
5
|
+
DI via ``init_taskiq(app, ...)`` + ``Depends(get_broker)``. ``Scheduled``
|
|
6
|
+
provides cron-syntax validation; wire it into TaskIQ's own scheduler
|
|
7
|
+
sources (see README).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from ._broker import create_broker
|
|
13
|
+
from ._config import ALLOWED_BROKER_SCHEMES, TaskIQConfig
|
|
14
|
+
from ._health import HealthReport, check_broker
|
|
15
|
+
from ._plugin import get_broker, init_taskiq, resolve_broker
|
|
16
|
+
from ._schedule import Scheduled
|
|
17
|
+
from ._tasks import task
|
|
18
|
+
from ._testing import in_memory_broker
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"ALLOWED_BROKER_SCHEMES",
|
|
24
|
+
"HealthReport",
|
|
25
|
+
"Scheduled",
|
|
26
|
+
"TaskIQConfig",
|
|
27
|
+
"__version__",
|
|
28
|
+
"check_broker",
|
|
29
|
+
"create_broker",
|
|
30
|
+
"get_broker",
|
|
31
|
+
"in_memory_broker",
|
|
32
|
+
"init_taskiq",
|
|
33
|
+
"resolve_broker",
|
|
34
|
+
"task",
|
|
35
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Broker factory — URL-scheme dispatch with strict allowlist."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ._config import ALLOWED_BROKER_SCHEMES, TaskIQConfig, _scheme
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_broker(config: TaskIQConfig | None = None) -> Any:
|
|
11
|
+
"""Build an :class:`AsyncBroker` from ``config``. Raises ``ValueError`` for
|
|
12
|
+
any URL scheme not in :data:`ALLOWED_BROKER_SCHEMES`."""
|
|
13
|
+
cfg = config or TaskIQConfig()
|
|
14
|
+
|
|
15
|
+
if cfg.serializer != "json":
|
|
16
|
+
raise ValueError(
|
|
17
|
+
f"hawkapi-taskiq only supports the JSON serializer in v0.1.0; got {cfg.serializer!r}"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Defense in depth — block ``serializer`` from ``extra`` so a future broker
|
|
21
|
+
# release that accepts the kwarg cannot bypass the JSON-only guarantee above.
|
|
22
|
+
if "serializer" in cfg.extra:
|
|
23
|
+
raise ValueError("'serializer' in extra is not permitted; JSON is enforced")
|
|
24
|
+
|
|
25
|
+
broker_url = cfg.broker_url.strip()
|
|
26
|
+
scheme = _scheme(broker_url)
|
|
27
|
+
if scheme not in ALLOWED_BROKER_SCHEMES:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
f"broker_url scheme {scheme!r} is not in the allowlist "
|
|
30
|
+
f"{sorted(ALLOWED_BROKER_SCHEMES)!r}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if cfg.result_backend_url:
|
|
34
|
+
# v0.1.0 does not yet plumb result_backend_url through to the broker —
|
|
35
|
+
# raise early so a misconfigured result backend cannot be silently dropped.
|
|
36
|
+
raise NotImplementedError(
|
|
37
|
+
"result_backend_url is not wired in v0.1.0; configure the result backend "
|
|
38
|
+
"on the broker directly via TaskIQConfig.extra"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if scheme == "memory":
|
|
42
|
+
from taskiq import InMemoryBroker
|
|
43
|
+
|
|
44
|
+
return InMemoryBroker()
|
|
45
|
+
|
|
46
|
+
if scheme in ("redis", "rediss"):
|
|
47
|
+
try:
|
|
48
|
+
from taskiq_redis import ListQueueBroker # type: ignore[import-not-found]
|
|
49
|
+
except ImportError as exc: # pragma: no cover
|
|
50
|
+
raise ImportError(
|
|
51
|
+
"taskiq-redis is required for redis brokers; pip install 'hawkapi-taskiq[redis]'"
|
|
52
|
+
) from exc
|
|
53
|
+
return ListQueueBroker(url=broker_url, **cfg.extra)
|
|
54
|
+
|
|
55
|
+
if scheme == "nats":
|
|
56
|
+
try:
|
|
57
|
+
from taskiq_nats import NatsBroker # type: ignore[import-not-found]
|
|
58
|
+
except ImportError as exc: # pragma: no cover
|
|
59
|
+
raise ImportError(
|
|
60
|
+
"taskiq-nats is required for nats brokers; pip install 'hawkapi-taskiq[nats]'"
|
|
61
|
+
) from exc
|
|
62
|
+
return NatsBroker(servers=[broker_url], **cfg.extra)
|
|
63
|
+
|
|
64
|
+
# Unreachable — the allowlist check above gates this.
|
|
65
|
+
raise ValueError(f"unsupported scheme {scheme!r}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__all__ = ["create_broker"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""TaskIQ configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class TaskIQConfig:
|
|
11
|
+
"""Builder for a TaskIQ broker via :func:`hawkapi_taskiq.create_broker`."""
|
|
12
|
+
|
|
13
|
+
broker_url: str = "memory://"
|
|
14
|
+
"""Broker connection string. Only ``memory://``, ``redis://``, ``rediss://``,
|
|
15
|
+
``nats://`` are accepted — anything else raises ``ValueError`` to prevent
|
|
16
|
+
accidentally enabling unsafe-deserialization brokers."""
|
|
17
|
+
|
|
18
|
+
result_backend_url: str = ""
|
|
19
|
+
"""Optional result-backend URL. Same scheme allowlist as ``broker_url``."""
|
|
20
|
+
|
|
21
|
+
timezone: str = "UTC"
|
|
22
|
+
task_default_queue: str = "default"
|
|
23
|
+
serializer: str = "json"
|
|
24
|
+
"""Always ``"json"`` for v0.1.0. Other serializers are rejected at runtime
|
|
25
|
+
to prevent arbitrary-deserialization vulns (CWE-502)."""
|
|
26
|
+
|
|
27
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
28
|
+
"""Forwarded as keyword args to the broker constructor."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
ALLOWED_BROKER_SCHEMES = frozenset({"memory", "redis", "rediss", "nats"})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _scheme(url: str) -> str:
|
|
35
|
+
return url.split("://", 1)[0].lower() if "://" in url else ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = ["ALLOWED_BROKER_SCHEMES", "TaskIQConfig", "_scheme"]
|