hawkapi-taskiq 0.1.0__py3-none-any.whl

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.
@@ -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"]
@@ -0,0 +1,60 @@
1
+ """Broker health probe."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import re
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ _CRED_RE = re.compile(r"(redis|rediss|nats)://[^@]*@")
11
+
12
+
13
+ def _scrub(text: str) -> str:
14
+ """Strip user:password from any URL substrings in ``text``."""
15
+ return _CRED_RE.sub(r"\1://***@", text)
16
+
17
+
18
+ @dataclass(slots=True)
19
+ class HealthReport:
20
+ broker_ok: bool
21
+ broker_type: str = ""
22
+ error: str = ""
23
+
24
+
25
+ async def check_broker(broker: Any, *, timeout: float = 5.0) -> HealthReport:
26
+ """Probe the broker.
27
+
28
+ - ``InMemoryBroker`` is always healthy when constructed; we still verify
29
+ that ``startup()`` ran by inspecting ``is_worker_process`` plus the
30
+ ``_running`` flag taskiq sets internally.
31
+ - For redis / nats brokers we attempt a no-op kick using the broker's
32
+ private startup machinery within ``timeout`` seconds.
33
+ """
34
+ btype = type(broker).__name__
35
+
36
+ async def _probe() -> None:
37
+ # InMemoryBroker has no remote dependency — touching .is_worker_process
38
+ # is enough. For network brokers we read .connection / .pool /
39
+ # ._is_started; if any expected attribute raises, surface as failure.
40
+ is_worker = getattr(broker, "is_worker_process", None)
41
+ if is_worker is None:
42
+ raise RuntimeError("broker does not expose is_worker_process")
43
+ # Trigger any lazy connection setup the broker has wired into kick():
44
+ # most broker types lazily open the network only on first send. We
45
+ # don't actually kick a task (that would require a registered name);
46
+ # instead we look at ``_is_started`` if present.
47
+ started = getattr(broker, "_is_started", True)
48
+ if started is False:
49
+ raise RuntimeError("broker not started")
50
+
51
+ try:
52
+ await asyncio.wait_for(_probe(), timeout=timeout)
53
+ return HealthReport(broker_ok=True, broker_type=btype)
54
+ except TimeoutError:
55
+ return HealthReport(broker_ok=False, broker_type=btype, error="timeout")
56
+ except Exception as exc:
57
+ return HealthReport(broker_ok=False, broker_type=btype, error=_scrub(str(exc)))
58
+
59
+
60
+ __all__ = ["HealthReport", "check_broker"]
@@ -0,0 +1,98 @@
1
+ """init_taskiq + get_broker + WeakKeyDictionary registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from weakref import WeakKeyDictionary
7
+
8
+ from hawkapi import HTTPException, Request
9
+
10
+ from ._broker import create_broker
11
+ from ._config import TaskIQConfig
12
+
13
+
14
+ class _StateNamespace:
15
+ taskiq: Any
16
+
17
+
18
+ _ACTIVE: WeakKeyDictionary[Any, Any] = WeakKeyDictionary()
19
+ _LAST: list[Any | None] = [None]
20
+
21
+
22
+ def init_taskiq(
23
+ app: Any,
24
+ *,
25
+ broker: Any = None,
26
+ config: TaskIQConfig | None = None,
27
+ auto_startup: bool = True,
28
+ ) -> Any:
29
+ """Attach a TaskIQ broker to ``app.state.taskiq``.
30
+
31
+ Pass ``broker=`` with a pre-built broker, or ``config=`` to construct one
32
+ via :func:`create_broker`. ``auto_startup=True`` wires ``broker.startup()``
33
+ and ``broker.shutdown()`` into the app lifecycle.
34
+ """
35
+ if broker is None:
36
+ broker = create_broker(config)
37
+
38
+ # Reject double-initialisation — registering lifecycle hooks twice would
39
+ # call startup()/shutdown() on the stale broker after this one replaces it.
40
+ if getattr(app, "state", None) is not None and getattr(app.state, "taskiq", None) is not None:
41
+ raise RuntimeError(
42
+ "init_taskiq has already been called on this app; create a new app or "
43
+ "drop the previous broker explicitly before re-initialising"
44
+ )
45
+
46
+ if getattr(app, "state", None) is None:
47
+ app.state = _StateNamespace()
48
+ app.state.taskiq = broker
49
+ try:
50
+ _ACTIVE[app] = broker
51
+ except TypeError:
52
+ pass
53
+ _LAST[0] = broker
54
+
55
+ if auto_startup and hasattr(app, "on_startup"):
56
+
57
+ async def _start() -> None:
58
+ startup = getattr(broker, "startup", None)
59
+ if startup is not None:
60
+ await startup()
61
+
62
+ app.on_startup(_start)
63
+
64
+ if auto_startup and hasattr(app, "on_shutdown"):
65
+
66
+ async def _stop() -> None:
67
+ shutdown = getattr(broker, "shutdown", None)
68
+ if shutdown is not None:
69
+ await shutdown()
70
+
71
+ app.on_shutdown(_stop)
72
+
73
+ return broker
74
+
75
+
76
+ def resolve_broker(app: Any) -> Any | None:
77
+ if app is None:
78
+ return _LAST[0]
79
+ try:
80
+ found = _ACTIVE.get(app)
81
+ except TypeError:
82
+ found = None
83
+ if found is not None:
84
+ return found
85
+ state = getattr(app, "state", None)
86
+ if state is not None and hasattr(state, "taskiq"):
87
+ return state.taskiq
88
+ return _LAST[0]
89
+
90
+
91
+ def get_broker(request: Request) -> Any:
92
+ broker = resolve_broker(request.scope.get("app"))
93
+ if broker is None:
94
+ raise HTTPException(500, detail="TaskIQ not configured — call init_taskiq(app, ...) first")
95
+ return broker
96
+
97
+
98
+ __all__ = ["get_broker", "init_taskiq", "resolve_broker"]
@@ -0,0 +1,36 @@
1
+ """Cron-syntax validator for schedule entries.
2
+
3
+ For v0.1.0 we ship only the value-type :class:`Scheduled` with validation —
4
+ it is up to the operator to wire it into TaskIQ's native scheduler sources.
5
+ See README for the recommended pattern.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+
13
+ @dataclass(slots=True)
14
+ class Scheduled:
15
+ """A schedule descriptor with validation at construction time."""
16
+
17
+ cron: str = ""
18
+ interval_seconds: float = 0.0
19
+
20
+ def __post_init__(self) -> None:
21
+ if (self.cron and self.interval_seconds) or (not self.cron and not self.interval_seconds):
22
+ raise ValueError("Scheduled requires exactly one of `cron` or `interval_seconds`")
23
+ if self.cron:
24
+ # Validate cron syntax up-front. croniter is an optional dep — if not
25
+ # installed, we skip validation rather than fail registration.
26
+ try:
27
+ from croniter import croniter # type: ignore[import-not-found]
28
+ except ImportError:
29
+ return
30
+ if not croniter.is_valid(self.cron):
31
+ raise ValueError(f"invalid cron expression: {self.cron!r}")
32
+ elif self.interval_seconds <= 0:
33
+ raise ValueError("interval_seconds must be > 0")
34
+
35
+
36
+ __all__ = ["Scheduled"]
@@ -0,0 +1,53 @@
1
+ """Task decorator + name-collision guard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+
9
+ def task(
10
+ broker: Any,
11
+ *,
12
+ name: str | None = None,
13
+ queue: str | None = None,
14
+ retry_on_error: bool = False,
15
+ max_retries: int = 3,
16
+ **task_kwargs: Any,
17
+ ) -> Callable[[Callable[..., Any]], Any]:
18
+ """Register a function as a TaskIQ task.
19
+
20
+ - Detects ``async def`` automatically — runs as coroutine on the worker.
21
+ - Sync functions are wrapped via TaskIQ's normal handling.
22
+ - Collision detection: registering the same ``name`` twice raises
23
+ ``ValueError`` (TaskIQ silently overrides; we disallow).
24
+ """
25
+
26
+ def decorator(fn: Callable[..., Any]) -> Any:
27
+ task_name = name or f"{fn.__module__}.{fn.__qualname__}"
28
+ existing = getattr(broker, "_hawkapi_task_names", None)
29
+ if existing is None:
30
+ existing = set()
31
+ broker._hawkapi_task_names = existing
32
+ if task_name in existing:
33
+ raise ValueError(f"task name {task_name!r} already registered")
34
+ existing.add(task_name)
35
+
36
+ # Pop reserved keys from ``task_kwargs`` BEFORE merging — otherwise a
37
+ # caller could pass ``task_name=`` in ``**task_kwargs`` and silently
38
+ # bypass the duplicate-name guard above.
39
+ task_kwargs.pop("task_name", None)
40
+ kw: dict[str, Any] = {"task_name": task_name}
41
+ if queue is not None:
42
+ kw["queue"] = queue
43
+ if retry_on_error:
44
+ kw["retry_on_error"] = True
45
+ kw["max_retries"] = max_retries
46
+ kw.update(task_kwargs)
47
+
48
+ return broker.task(**kw)(fn)
49
+
50
+ return decorator
51
+
52
+
53
+ __all__ = ["task"]
@@ -0,0 +1,24 @@
1
+ """Test helpers — in-memory broker fixture."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncGenerator
6
+ from contextlib import asynccontextmanager
7
+ from typing import Any
8
+
9
+ from ._broker import create_broker
10
+ from ._config import TaskIQConfig
11
+
12
+
13
+ @asynccontextmanager
14
+ async def in_memory_broker() -> AsyncGenerator[Any, None]:
15
+ """Yield a started InMemoryBroker; shut it down on exit."""
16
+ broker = create_broker(TaskIQConfig(broker_url="memory://"))
17
+ await broker.startup()
18
+ try:
19
+ yield broker
20
+ finally:
21
+ await broker.shutdown()
22
+
23
+
24
+ __all__ = ["in_memory_broker"]
@@ -0,0 +1 @@
1
+
@@ -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,13 @@
1
+ hawkapi_taskiq/__init__.py,sha256=mQtwxBvA7sjc3BG8Cwlv-VjkkaRWlQ0JaFiOGgGMTkg,987
2
+ hawkapi_taskiq/_broker.py,sha256=zvf5fTppAU5ucXgCqaK07ob9a0vllfuBUGR3Amst-sU,2591
3
+ hawkapi_taskiq/_config.py,sha256=JXHs4BCw5nbPa-APP_a_RmhRuSYtDOEyRCCaXM0725U,1222
4
+ hawkapi_taskiq/_health.py,sha256=cu3oAHYz9GOnzR9sSDQr1jQesSI8EThMnNkkalSdSds,2192
5
+ hawkapi_taskiq/_plugin.py,sha256=z9HwEBaBsqnViXaf0SMgRNG3S0vLpV8KrTNEvwm43ao,2732
6
+ hawkapi_taskiq/_schedule.py,sha256=9ZUHqHTM_cdYU3_qMYCNdnSVT4UWdaLWmcQMfqbm1KM,1301
7
+ hawkapi_taskiq/_tasks.py,sha256=BG1Bb3wHl2LhDoi_CNrAQQTAXyV7MeteVYPADuxah_w,1711
8
+ hawkapi_taskiq/_testing.py,sha256=syGKcLPcdls70CpQoKpL3rb_NSep1djpot7fcV0201s,611
9
+ hawkapi_taskiq/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
10
+ hawkapi_taskiq-0.1.0.dist-info/METADATA,sha256=dm-1osTS5brz-ytuyv820RXlOr0ixfAoqtQDIJhzdWU,6501
11
+ hawkapi_taskiq-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
12
+ hawkapi_taskiq-0.1.0.dist-info/licenses/LICENSE,sha256=_RpjhvsfLqqeG_gv2cRatjIxCTGXTpXhKU9jqLZXYa4,1077
13
+ hawkapi_taskiq-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.