ardiq 0.1.1__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.
ardiq-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 17tayyy
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.
ardiq-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: ardiq
3
+ Version: 0.1.1
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Programming Language :: Python :: 3.13
8
+ Classifier: Programming Language :: Rust
9
+ Classifier: Framework :: AsyncIO
10
+ Classifier: Topic :: System :: Distributed Computing
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Requires-Dist: msgpack>=1.1.2
13
+ Requires-Dist: typer>=0.26.0
14
+ License-File: LICENSE
15
+ Summary: A fast distributed task queue with a Rust core and a Python API, backed by Redis streams.
16
+ Keywords: task-queue,redis,rust,async,asyncio,distributed,jobs,worker
17
+ Author-email: 17tayyy <oscarfdst@proton.me>
18
+ Requires-Python: >=3.13
19
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
20
+ Project-URL: Repository, https://github.com/17tayyy/ardiq
21
+
22
+ # ArdiQ
23
+
24
+ A fast distributed task queue with a **Rust core** and a clean **Python API**, backed by Redis streams.
25
+
26
+ ArdiQ runs the worker loop and all Redis I/O in Rust (via [PyO3](https://pyo3.rs) + [tokio](https://tokio.rs)); you write tasks in plain Python. The two meet at a single async callback, with the GIL held only for the microseconds it takes to start a task and read its result — so a single process handles high concurrency.
27
+
28
+ > **Early stage.** The engine is solid — priorities, retries with backoff, delayed/scheduled tasks, crash recovery, result storage — but the ergonomics layer is still growing.
29
+
30
+ ## Features
31
+
32
+ - 🦀 **Rust core** — the loop and Redis I/O run on tokio, off the GIL
33
+ - **Priority queues** — higher-priority tasks are consumed first
34
+ - **Delayed & scheduled** tasks (`delay_ms` / `schedule_ms`)
35
+ - **Automatic retries** with quadratic backoff, configurable per task
36
+ - **Crash recovery** — in-flight tasks of a dead worker are reclaimed (`XAUTOCLAIM`)
37
+ - **Results** with TTL, plus task **status** (`queued` / `running` / `complete` / `not_found`)
38
+ - **Sync & async tasks** — blocking sync functions run in a thread pool
39
+ - **CLI worker** (`ardiq run module:app`) and **burst mode** (drain the queue and exit)
40
+
41
+ ## Installation
42
+
43
+ ArdiQ isn't on PyPI yet. Build it from source — you'll need [Rust](https://rustup.rs) and [uv](https://docs.astral.sh/uv/):
44
+
45
+ ```console
46
+ $ git clone https://github.com/17tayyy/ardiq
47
+ $ cd ardiq
48
+ $ uv sync # builds the Rust extension and installs dependencies
49
+ ```
50
+
51
+ You also need a Redis server. For local development:
52
+
53
+ ```console
54
+ $ docker compose up -d
55
+ ```
56
+
57
+ ## Quickstart
58
+
59
+ Define an app and some tasks (`example.py`):
60
+
61
+ ```python
62
+ from ardiq import Ardiq
63
+
64
+ app = Ardiq(redis_url="redis://localhost:6379", queue_name="example")
65
+
66
+
67
+ @app.task()
68
+ async def add(a: int, b: int) -> int:
69
+ return a + b
70
+
71
+
72
+ @app.task(max_retries=3)
73
+ def slow_double(x: int) -> int: # sync task — runs in a thread
74
+ return x * 2
75
+ ```
76
+
77
+ Start a worker:
78
+
79
+ ```console
80
+ $ ardiq run example:app
81
+ ```
82
+
83
+ Enqueue tasks from anywhere and read their results:
84
+
85
+ ```python
86
+ import asyncio
87
+ from example import add
88
+
89
+
90
+ async def main():
91
+ job = await add.enqueue(2, 3) # returns a Job handle
92
+ print(job.id)
93
+ print(await job.status()) # 'queued' | 'running' | 'complete'
94
+ print(await job.result(timeout=5)) # waits → TaskResult(success=True, value=5, tries=1)
95
+
96
+
97
+ asyncio.run(main())
98
+ ```
99
+
100
+ Or run the whole thing in one process with `python example.py`, which enqueues a
101
+ few tasks and processes them in burst mode.
102
+
103
+ ## Configuration
104
+
105
+ `Ardiq(...)` accepts:
106
+
107
+ | Option | Default | Description |
108
+ |---|---|---|
109
+ | `redis_url` | `redis://localhost:6379` | Redis connection URL |
110
+ | `queue_name` | `"default"` | Logical queue (key namespace) |
111
+ | `priorities` | `["default"]` | Priority names, **lowest-first** |
112
+ | `concurrency` | `16` | Max tasks running at once |
113
+ | `prefetch` | `concurrency * 2` | Max tasks held in memory (drives backpressure) |
114
+ | `idle_timeout_ms` | `60000` | When an unrenewed in-flight task may be reclaimed |
115
+ | `result_ttl_ms` | `300000` | How long results live (`0` drops, negative keeps forever) |
116
+ | `burst` | `False` | Exit once the queue drains |
117
+ | `serializer` / `deserializer` | msgpack | Wire codec; pass `pickle.dumps`/`pickle.loads` to send datetimes/objects |
118
+
119
+ `@app.task(...)` accepts `name`, `max_retries` (default 3), `backoff_ms`, `timeout` (seconds), and `priority`.
120
+ Use `task.options(delay_ms=…, schedule_ms=…, priority=…, task_id=…).enqueue(...)` for one-off overrides.
121
+
122
+ ## Development
123
+
124
+ ```console
125
+ $ docker compose up -d # Redis on localhost:6379
126
+ $ uv run pytest # test suite (needs Redis)
127
+ $ uv run ruff check . # lint
128
+ $ uv run ty check ardiq tests # type-check
129
+ ```
130
+
131
+ After changing the Rust core, rebuild with `uv sync --reinstall-package ardiq`.
132
+
133
+ ## License
134
+
135
+ [MIT](LICENSE)
136
+
ardiq-0.1.1/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # ArdiQ
2
+
3
+ A fast distributed task queue with a **Rust core** and a clean **Python API**, backed by Redis streams.
4
+
5
+ ArdiQ runs the worker loop and all Redis I/O in Rust (via [PyO3](https://pyo3.rs) + [tokio](https://tokio.rs)); you write tasks in plain Python. The two meet at a single async callback, with the GIL held only for the microseconds it takes to start a task and read its result — so a single process handles high concurrency.
6
+
7
+ > **Early stage.** The engine is solid — priorities, retries with backoff, delayed/scheduled tasks, crash recovery, result storage — but the ergonomics layer is still growing.
8
+
9
+ ## Features
10
+
11
+ - 🦀 **Rust core** — the loop and Redis I/O run on tokio, off the GIL
12
+ - **Priority queues** — higher-priority tasks are consumed first
13
+ - **Delayed & scheduled** tasks (`delay_ms` / `schedule_ms`)
14
+ - **Automatic retries** with quadratic backoff, configurable per task
15
+ - **Crash recovery** — in-flight tasks of a dead worker are reclaimed (`XAUTOCLAIM`)
16
+ - **Results** with TTL, plus task **status** (`queued` / `running` / `complete` / `not_found`)
17
+ - **Sync & async tasks** — blocking sync functions run in a thread pool
18
+ - **CLI worker** (`ardiq run module:app`) and **burst mode** (drain the queue and exit)
19
+
20
+ ## Installation
21
+
22
+ ArdiQ isn't on PyPI yet. Build it from source — you'll need [Rust](https://rustup.rs) and [uv](https://docs.astral.sh/uv/):
23
+
24
+ ```console
25
+ $ git clone https://github.com/17tayyy/ardiq
26
+ $ cd ardiq
27
+ $ uv sync # builds the Rust extension and installs dependencies
28
+ ```
29
+
30
+ You also need a Redis server. For local development:
31
+
32
+ ```console
33
+ $ docker compose up -d
34
+ ```
35
+
36
+ ## Quickstart
37
+
38
+ Define an app and some tasks (`example.py`):
39
+
40
+ ```python
41
+ from ardiq import Ardiq
42
+
43
+ app = Ardiq(redis_url="redis://localhost:6379", queue_name="example")
44
+
45
+
46
+ @app.task()
47
+ async def add(a: int, b: int) -> int:
48
+ return a + b
49
+
50
+
51
+ @app.task(max_retries=3)
52
+ def slow_double(x: int) -> int: # sync task — runs in a thread
53
+ return x * 2
54
+ ```
55
+
56
+ Start a worker:
57
+
58
+ ```console
59
+ $ ardiq run example:app
60
+ ```
61
+
62
+ Enqueue tasks from anywhere and read their results:
63
+
64
+ ```python
65
+ import asyncio
66
+ from example import add
67
+
68
+
69
+ async def main():
70
+ job = await add.enqueue(2, 3) # returns a Job handle
71
+ print(job.id)
72
+ print(await job.status()) # 'queued' | 'running' | 'complete'
73
+ print(await job.result(timeout=5)) # waits → TaskResult(success=True, value=5, tries=1)
74
+
75
+
76
+ asyncio.run(main())
77
+ ```
78
+
79
+ Or run the whole thing in one process with `python example.py`, which enqueues a
80
+ few tasks and processes them in burst mode.
81
+
82
+ ## Configuration
83
+
84
+ `Ardiq(...)` accepts:
85
+
86
+ | Option | Default | Description |
87
+ |---|---|---|
88
+ | `redis_url` | `redis://localhost:6379` | Redis connection URL |
89
+ | `queue_name` | `"default"` | Logical queue (key namespace) |
90
+ | `priorities` | `["default"]` | Priority names, **lowest-first** |
91
+ | `concurrency` | `16` | Max tasks running at once |
92
+ | `prefetch` | `concurrency * 2` | Max tasks held in memory (drives backpressure) |
93
+ | `idle_timeout_ms` | `60000` | When an unrenewed in-flight task may be reclaimed |
94
+ | `result_ttl_ms` | `300000` | How long results live (`0` drops, negative keeps forever) |
95
+ | `burst` | `False` | Exit once the queue drains |
96
+ | `serializer` / `deserializer` | msgpack | Wire codec; pass `pickle.dumps`/`pickle.loads` to send datetimes/objects |
97
+
98
+ `@app.task(...)` accepts `name`, `max_retries` (default 3), `backoff_ms`, `timeout` (seconds), and `priority`.
99
+ Use `task.options(delay_ms=…, schedule_ms=…, priority=…, task_id=…).enqueue(...)` for one-off overrides.
100
+
101
+ ## Development
102
+
103
+ ```console
104
+ $ docker compose up -d # Redis on localhost:6379
105
+ $ uv run pytest # test suite (needs Redis)
106
+ $ uv run ruff check . # lint
107
+ $ uv run ty check ardiq tests # type-check
108
+ ```
109
+
110
+ After changing the Rust core, rebuild with `uv sync --reinstall-package ardiq`.
111
+
112
+ ## License
113
+
114
+ [MIT](LICENSE)
@@ -0,0 +1,346 @@
1
+ """ArdiQ Python API: the Ardiq app, the @task decorator, and task handles.
2
+
3
+ The Rust core owns the loop and Redis I/O; an `Ardiq` app owns its task registry,
4
+ its wire codec, and the executor the core calls back into for every task.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import time
11
+ import uuid
12
+ from collections.abc import Callable
13
+ from dataclasses import dataclass
14
+ from typing import Any, NamedTuple
15
+
16
+ import msgpack
17
+
18
+ from ardiq._core import ArdiqCore
19
+
20
+ __all__ = ["Ardiq", "Job", "Task", "TaskInfo", "TaskResult"]
21
+
22
+ # Outcome codes for the Rust core's executor protocol.
23
+ SUCCESS, FAILURE, RETRY = 0, 1, 2
24
+ DEFAULT_MAX_RETRIES = 3
25
+
26
+
27
+ def _now_ms() -> int:
28
+ return int(time.time() * 1000)
29
+
30
+
31
+ def _default_dumps(obj: Any) -> bytes:
32
+ return msgpack.packb(obj)
33
+
34
+
35
+ def _default_loads(data: bytes) -> Any:
36
+ return msgpack.unpackb(data, raw=False)
37
+
38
+
39
+ @dataclass(slots=True)
40
+ class _Registered:
41
+ fn: Callable[..., Any]
42
+ max_retries: int
43
+ backoff_ms: int
44
+ is_async: bool
45
+ timeout: float | None # seconds; None = no timeout
46
+
47
+
48
+ class TaskResult(NamedTuple):
49
+ """A decoded result envelope. On failure, `value` holds the error repr.
50
+
51
+ Times are epoch ms: `enqueue_time` when enqueued, `start`/`finish` around
52
+ execution.
53
+ """
54
+
55
+ success: bool
56
+ value: Any
57
+ tries: int
58
+ enqueue_time: int = 0
59
+ start: int = 0
60
+ finish: int = 0
61
+
62
+ @property
63
+ def duration_ms(self) -> int:
64
+ return self.finish - self.start
65
+
66
+
67
+ class TaskInfo(NamedTuple):
68
+ """Snapshot of an unfinished task (queued, scheduled, or running)."""
69
+
70
+ task_id: str
71
+ fn_name: str
72
+ args: tuple
73
+ kwargs: dict
74
+ enqueue_time: int
75
+ tries: int
76
+ status: str
77
+ scheduled_at: int | None = None # epoch ms if waiting in the delayed queue
78
+
79
+
80
+ @dataclass(frozen=True, slots=True)
81
+ class Job:
82
+ """Handle to an enqueued task."""
83
+
84
+ app: Ardiq
85
+ id: str
86
+
87
+ async def result(self, timeout: float | None = None) -> TaskResult | None:
88
+ return await self.app.result(self.id, timeout=timeout)
89
+
90
+ async def status(self) -> str:
91
+ return await self.app.status(self.id)
92
+
93
+ async def info(self) -> TaskInfo | None:
94
+ return await self.app.info(self.id)
95
+
96
+
97
+ class Task:
98
+ """A registered task. Call it to run inline, or `.enqueue` to dispatch."""
99
+
100
+ def __init__(
101
+ self, app: Ardiq, name: str, fn: Callable[..., Any], priority: str | None
102
+ ):
103
+ self.app = app
104
+ self.name = name
105
+ self.fn = fn
106
+ self.priority = priority
107
+
108
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
109
+ return self.fn(*args, **kwargs)
110
+
111
+ async def enqueue(self, *args: Any, **kwargs: Any) -> Job:
112
+ return await self.app._enqueue(self, args, kwargs)
113
+
114
+ def options(
115
+ self,
116
+ *,
117
+ task_id: str | None = None,
118
+ priority: str | None = None,
119
+ delay_ms: int = 0,
120
+ schedule_ms: int = 0,
121
+ expire_ms: int = 0,
122
+ ) -> _BoundTask:
123
+ return _BoundTask(self, task_id, priority, delay_ms, schedule_ms, expire_ms)
124
+
125
+
126
+ @dataclass(frozen=True, slots=True)
127
+ class _BoundTask:
128
+ """A task plus enqueue options, kept off `enqueue(*args, **kwargs)`."""
129
+
130
+ task: Task
131
+ task_id: str | None
132
+ priority: str | None
133
+ delay_ms: int
134
+ schedule_ms: int
135
+ expire_ms: int
136
+
137
+ async def enqueue(self, *args: Any, **kwargs: Any) -> Job:
138
+ return await self.task.app._enqueue(
139
+ self.task,
140
+ args,
141
+ kwargs,
142
+ task_id=self.task_id,
143
+ priority=self.priority,
144
+ delay_ms=self.delay_ms,
145
+ schedule_ms=self.schedule_ms,
146
+ expire_ms=self.expire_ms,
147
+ )
148
+
149
+
150
+ class Ardiq:
151
+ """App: owns the core, its task registry, and its wire codec."""
152
+
153
+ def __init__(
154
+ self,
155
+ redis_url: str | None = None,
156
+ queue_name: str = "default",
157
+ priorities: list[str] | None = None,
158
+ *,
159
+ serializer: Callable[[Any], bytes] | None = None,
160
+ deserializer: Callable[[bytes], Any] | None = None,
161
+ **core_kwargs: Any,
162
+ ):
163
+ self._dumps = serializer or _default_dumps
164
+ self._loads = deserializer or _default_loads
165
+ self._registry: dict[str, _Registered] = {}
166
+ config = {
167
+ "redis_url": redis_url,
168
+ "queue_name": queue_name,
169
+ "priorities": priorities,
170
+ **core_kwargs,
171
+ }
172
+ self._core = ArdiqCore({k: v for k, v in config.items() if v is not None})
173
+
174
+ @property
175
+ def worker_id(self) -> str:
176
+ return self._core.worker_id
177
+
178
+ @property
179
+ def burst(self) -> bool:
180
+ return self._core.burst
181
+
182
+ @burst.setter
183
+ def burst(self, value: bool) -> None:
184
+ self._core.burst = value
185
+
186
+ @property
187
+ def tasks(self) -> list[str]:
188
+ """Names of the registered tasks."""
189
+ return list(self._registry)
190
+
191
+ def task(
192
+ self,
193
+ fn: Callable[..., Any] | None = None,
194
+ *,
195
+ name: str | None = None,
196
+ max_retries: int = DEFAULT_MAX_RETRIES,
197
+ backoff_ms: int = 0,
198
+ timeout: float | None = None,
199
+ priority: str | None = None,
200
+ ) -> Any:
201
+ def wrap(fn: Callable[..., Any]) -> Task:
202
+ task_name = name or getattr(fn, "__name__", None)
203
+ if task_name is None:
204
+ raise TypeError("@task needs an explicit name for this callable")
205
+ self._registry[task_name] = _Registered(
206
+ fn,
207
+ max_retries,
208
+ backoff_ms,
209
+ asyncio.iscoroutinefunction(fn),
210
+ timeout,
211
+ )
212
+ return Task(self, task_name, fn, priority)
213
+
214
+ return wrap(fn) if fn is not None else wrap
215
+
216
+ async def _enqueue(
217
+ self,
218
+ task: Task,
219
+ args: tuple,
220
+ kwargs: dict,
221
+ *,
222
+ task_id: str | None = None,
223
+ priority: str | None = None,
224
+ delay_ms: int = 0,
225
+ schedule_ms: int = 0,
226
+ expire_ms: int = 0,
227
+ ) -> Job:
228
+ job_id = task_id or uuid.uuid4().hex
229
+ payload = self._pack(task.name, args, kwargs)
230
+ await self._core.enqueue(
231
+ job_id, payload, priority or task.priority, delay_ms, schedule_ms, expire_ms
232
+ )
233
+ return Job(self, job_id)
234
+
235
+ def _pack(self, fn_name: str, args: tuple, kwargs: dict) -> bytes:
236
+ return self._dumps(
237
+ {"f": fn_name, "a": list(args), "k": kwargs, "t": _now_ms()}
238
+ )
239
+
240
+ def _envelope(
241
+ self, success: bool, result: Any, tries: int, enqueue_time: int, start: int
242
+ ) -> bytes:
243
+ return self._dumps(
244
+ {
245
+ "s": success,
246
+ "r": result,
247
+ "t": tries,
248
+ "et": enqueue_time,
249
+ "st": start,
250
+ "ft": _now_ms(),
251
+ }
252
+ )
253
+
254
+ def _unpack(self, raw: bytes | None) -> TaskResult | None:
255
+ if raw is None:
256
+ return None
257
+ env = self._loads(raw)
258
+ return TaskResult(
259
+ env["s"],
260
+ env["r"],
261
+ env["t"],
262
+ env.get("et", 0),
263
+ env.get("st", 0),
264
+ env.get("ft", 0),
265
+ )
266
+
267
+ async def _execute(
268
+ self, task_id: str, payload: bytes, tries: int
269
+ ) -> tuple[int, bytes, int]:
270
+ """The core's per-task callback. Returns (outcome, result_bytes, retry_ms)."""
271
+ data = self._loads(payload)
272
+ enqueue_time = int(data.get("t", 0))
273
+ start = _now_ms()
274
+ reg = self._registry.get(data["f"])
275
+ if reg is None:
276
+ env = self._envelope(
277
+ False, f"unknown task {data['f']!r}", tries, enqueue_time, start
278
+ )
279
+ return FAILURE, env, 0
280
+
281
+ try:
282
+ if reg.is_async:
283
+ coro = reg.fn(*data["a"], **data["k"])
284
+ else:
285
+ coro = asyncio.to_thread(reg.fn, *data["a"], **data["k"])
286
+ if reg.timeout is not None:
287
+ result = await asyncio.wait_for(coro, reg.timeout)
288
+ else:
289
+ result = await coro
290
+ except Exception as exc:
291
+ if isinstance(exc, TimeoutError) and reg.timeout is not None:
292
+ err = f"timed out after {reg.timeout}s"
293
+ else:
294
+ err = repr(exc)
295
+ if tries <= reg.max_retries:
296
+ return RETRY, b"", reg.backoff_ms # 0 = core's default backoff
297
+ return FAILURE, self._envelope(False, err, tries, enqueue_time, start), 0
298
+
299
+ return SUCCESS, self._envelope(True, result, tries, enqueue_time, start), 0
300
+
301
+ async def run(self) -> None:
302
+ await self._core.run(self._execute)
303
+
304
+ def stop(self) -> None:
305
+ self._core.stop()
306
+
307
+ async def queue_size(self) -> int:
308
+ return await self._core.queue_size()
309
+
310
+ async def result(
311
+ self, task_id: str, timeout: float | None = None
312
+ ) -> TaskResult | None:
313
+ """Fetch a task's result. With `timeout` (seconds), wait for it to be
314
+ stored, raising `TimeoutError` if it isn't in time; without, return the
315
+ result now or `None` if it isn't ready."""
316
+ if timeout is None:
317
+ return self._unpack(await self._core.result(task_id))
318
+ deadline = time.monotonic() + timeout
319
+ while True:
320
+ raw = await self._core.result(task_id)
321
+ if raw is not None:
322
+ return self._unpack(raw)
323
+ if time.monotonic() >= deadline:
324
+ raise TimeoutError(f"no result for {task_id!r} within {timeout}s")
325
+ await asyncio.sleep(0.05)
326
+
327
+ async def status(self, task_id: str) -> str:
328
+ return await self._core.status(task_id)
329
+
330
+ async def info(self, task_id: str) -> TaskInfo | None:
331
+ """Metadata for an unfinished task, or `None` if it's finished/unknown
332
+ (use `result` for finished tasks)."""
333
+ payload, tries, scheduled_at = await self._core.task_info(task_id)
334
+ if payload is None:
335
+ return None
336
+ data = self._loads(payload)
337
+ return TaskInfo(
338
+ task_id=task_id,
339
+ fn_name=data["f"],
340
+ args=tuple(data["a"]),
341
+ kwargs=data["k"],
342
+ enqueue_time=int(data["t"]),
343
+ tries=tries,
344
+ status=await self.status(task_id),
345
+ scheduled_at=scheduled_at or None,
346
+ )
@@ -0,0 +1,4 @@
1
+ from ardiq.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,29 @@
1
+ """Type stubs for the Rust extension `ardiq._core`."""
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from typing import Any
5
+
6
+ Executor = Callable[[str, bytes, int], Awaitable[tuple[int, bytes, int]]]
7
+
8
+ def init_logging(verbose: bool) -> None: ...
9
+
10
+ class ArdiqCore:
11
+ def __init__(self, config: dict[str, Any]) -> None: ...
12
+ @property
13
+ def worker_id(self) -> str: ...
14
+ burst: bool
15
+ def enqueue(
16
+ self,
17
+ task_id: str,
18
+ payload: bytes,
19
+ priority: str | None = None,
20
+ delay_ms: int = 0,
21
+ schedule_ms: int = 0,
22
+ expire_ms: int = 0,
23
+ ) -> Awaitable[bool]: ...
24
+ def run(self, callback: Executor) -> Awaitable[None]: ...
25
+ def stop(self) -> None: ...
26
+ def queue_size(self) -> Awaitable[int]: ...
27
+ def result(self, task_id: str) -> Awaitable[bytes | None]: ... # core: no timeout
28
+ def status(self, task_id: str) -> Awaitable[str]: ...
29
+ def task_info(self, task_id: str) -> Awaitable[tuple[bytes | None, int, int]]: ...