taskito 0.2.0__tar.gz → 0.2.2__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.
Files changed (58) hide show
  1. {taskito-0.2.0 → taskito-0.2.2}/Cargo.lock +10 -84
  2. taskito-0.2.2/PKG-INFO +286 -0
  3. taskito-0.2.2/README.md +263 -0
  4. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/Cargo.toml +1 -1
  5. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-python/Cargo.toml +1 -1
  6. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/canvas.py +3 -9
  7. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/context.py +1 -3
  8. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/dashboard.py +6 -2
  9. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/mixins.py +1 -3
  10. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/result.py +1 -3
  11. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/testing.py +1 -3
  12. {taskito-0.2.0 → taskito-0.2.2}/pyproject.toml +3 -2
  13. taskito-0.2.0/PKG-INFO +0 -20
  14. {taskito-0.2.0 → taskito-0.2.2}/Cargo.toml +0 -0
  15. {taskito-0.2.0 → taskito-0.2.2}/LICENSE +0 -0
  16. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/error.rs +0 -0
  17. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/job.rs +0 -0
  18. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/lib.rs +0 -0
  19. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/periodic.rs +0 -0
  20. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/resilience/circuit_breaker.rs +0 -0
  21. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/resilience/dlq.rs +0 -0
  22. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/resilience/mod.rs +0 -0
  23. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/resilience/rate_limiter.rs +0 -0
  24. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/resilience/retry.rs +0 -0
  25. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/scheduler.rs +0 -0
  26. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/storage/mod.rs +0 -0
  27. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/storage/models.rs +0 -0
  28. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/storage/schema.rs +0 -0
  29. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/storage/sqlite/circuit_breakers.rs +0 -0
  30. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/storage/sqlite/dead_letter.rs +0 -0
  31. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/storage/sqlite/jobs.rs +0 -0
  32. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/storage/sqlite/logs.rs +0 -0
  33. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/storage/sqlite/metrics.rs +0 -0
  34. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/storage/sqlite/mod.rs +0 -0
  35. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/storage/sqlite/periodic.rs +0 -0
  36. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/storage/sqlite/rate_limits.rs +0 -0
  37. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/storage/sqlite/tests.rs +0 -0
  38. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/storage/sqlite/trait_impl.rs +0 -0
  39. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/storage/sqlite/workers.rs +0 -0
  40. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-core/src/storage/traits.rs +0 -0
  41. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-python/src/lib.rs +0 -0
  42. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-python/src/py_config.rs +0 -0
  43. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-python/src/py_job.rs +0 -0
  44. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-python/src/py_queue.rs +0 -0
  45. {taskito-0.2.0 → taskito-0.2.2}/crates/taskito-python/src/py_worker.rs +0 -0
  46. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/__init__.py +0 -0
  47. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/_taskito.pyi +0 -0
  48. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/app.py +0 -0
  49. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/cli.py +0 -0
  50. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/contrib/__init__.py +0 -0
  51. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/contrib/fastapi.py +0 -0
  52. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/contrib/otel.py +0 -0
  53. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/exceptions.py +0 -0
  54. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/middleware.py +0 -0
  55. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/py.typed +0 -0
  56. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/serializers.py +0 -0
  57. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/task.py +0 -0
  58. {taskito-0.2.0 → taskito-0.2.2}/py_src/taskito/templates/dashboard.html +0 -0
@@ -221,7 +221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
221
221
  checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
222
222
  dependencies = [
223
223
  "libc",
224
- "windows-sys 0.61.2",
224
+ "windows-sys",
225
225
  ]
226
226
 
227
227
  [[package]]
@@ -448,7 +448,7 @@ checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
448
448
  dependencies = [
449
449
  "libc",
450
450
  "wasi",
451
- "windows-sys 0.61.2",
451
+ "windows-sys",
452
452
  ]
453
453
 
454
454
  [[package]]
@@ -705,7 +705,7 @@ dependencies = [
705
705
  "errno",
706
706
  "libc",
707
707
  "linux-raw-sys",
708
- "windows-sys 0.61.2",
708
+ "windows-sys",
709
709
  ]
710
710
 
711
711
  [[package]]
@@ -802,12 +802,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
802
802
 
803
803
  [[package]]
804
804
  name = "socket2"
805
- version = "0.6.2"
805
+ version = "0.6.3"
806
806
  source = "registry+https://github.com/rust-lang/crates.io-index"
807
- checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
807
+ checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
808
808
  dependencies = [
809
809
  "libc",
810
- "windows-sys 0.60.2",
810
+ "windows-sys",
811
811
  ]
812
812
 
813
813
  [[package]]
@@ -847,7 +847,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
847
847
 
848
848
  [[package]]
849
849
  name = "taskito-core"
850
- version = "0.2.0"
850
+ version = "0.2.2"
851
851
  dependencies = [
852
852
  "chrono",
853
853
  "cron",
@@ -866,7 +866,7 @@ dependencies = [
866
866
 
867
867
  [[package]]
868
868
  name = "taskito-python"
869
- version = "0.2.0"
869
+ version = "0.2.2"
870
870
  dependencies = [
871
871
  "crossbeam-channel",
872
872
  "pyo3",
@@ -885,7 +885,7 @@ dependencies = [
885
885
  "getrandom 0.4.2",
886
886
  "once_cell",
887
887
  "rustix",
888
- "windows-sys 0.61.2",
888
+ "windows-sys",
889
889
  ]
890
890
 
891
891
  [[package]]
@@ -973,7 +973,7 @@ dependencies = [
973
973
  "signal-hook-registry",
974
974
  "socket2",
975
975
  "tokio-macros",
976
- "windows-sys 0.61.2",
976
+ "windows-sys",
977
977
  ]
978
978
 
979
979
  [[package]]
@@ -1184,15 +1184,6 @@ dependencies = [
1184
1184
  "windows-link",
1185
1185
  ]
1186
1186
 
1187
- [[package]]
1188
- name = "windows-sys"
1189
- version = "0.60.2"
1190
- source = "registry+https://github.com/rust-lang/crates.io-index"
1191
- checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
1192
- dependencies = [
1193
- "windows-targets",
1194
- ]
1195
-
1196
1187
  [[package]]
1197
1188
  name = "windows-sys"
1198
1189
  version = "0.61.2"
@@ -1202,71 +1193,6 @@ dependencies = [
1202
1193
  "windows-link",
1203
1194
  ]
1204
1195
 
1205
- [[package]]
1206
- name = "windows-targets"
1207
- version = "0.53.5"
1208
- source = "registry+https://github.com/rust-lang/crates.io-index"
1209
- checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
1210
- dependencies = [
1211
- "windows-link",
1212
- "windows_aarch64_gnullvm",
1213
- "windows_aarch64_msvc",
1214
- "windows_i686_gnu",
1215
- "windows_i686_gnullvm",
1216
- "windows_i686_msvc",
1217
- "windows_x86_64_gnu",
1218
- "windows_x86_64_gnullvm",
1219
- "windows_x86_64_msvc",
1220
- ]
1221
-
1222
- [[package]]
1223
- name = "windows_aarch64_gnullvm"
1224
- version = "0.53.1"
1225
- source = "registry+https://github.com/rust-lang/crates.io-index"
1226
- checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
1227
-
1228
- [[package]]
1229
- name = "windows_aarch64_msvc"
1230
- version = "0.53.1"
1231
- source = "registry+https://github.com/rust-lang/crates.io-index"
1232
- checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
1233
-
1234
- [[package]]
1235
- name = "windows_i686_gnu"
1236
- version = "0.53.1"
1237
- source = "registry+https://github.com/rust-lang/crates.io-index"
1238
- checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
1239
-
1240
- [[package]]
1241
- name = "windows_i686_gnullvm"
1242
- version = "0.53.1"
1243
- source = "registry+https://github.com/rust-lang/crates.io-index"
1244
- checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
1245
-
1246
- [[package]]
1247
- name = "windows_i686_msvc"
1248
- version = "0.53.1"
1249
- source = "registry+https://github.com/rust-lang/crates.io-index"
1250
- checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
1251
-
1252
- [[package]]
1253
- name = "windows_x86_64_gnu"
1254
- version = "0.53.1"
1255
- source = "registry+https://github.com/rust-lang/crates.io-index"
1256
- checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
1257
-
1258
- [[package]]
1259
- name = "windows_x86_64_gnullvm"
1260
- version = "0.53.1"
1261
- source = "registry+https://github.com/rust-lang/crates.io-index"
1262
- checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
1263
-
1264
- [[package]]
1265
- name = "windows_x86_64_msvc"
1266
- version = "0.53.1"
1267
- source = "registry+https://github.com/rust-lang/crates.io-index"
1268
- checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
1269
-
1270
1196
  [[package]]
1271
1197
  name = "wit-bindgen"
1272
1198
  version = "0.51.0"
taskito-0.2.2/PKG-INFO ADDED
@@ -0,0 +1,286 @@
1
+ Metadata-Version: 2.4
2
+ Name: taskito
3
+ Version: 0.2.2
4
+ Requires-Dist: cloudpickle>=3.0
5
+ Requires-Dist: pytest>=7.0 ; extra == 'dev'
6
+ Requires-Dist: pytest-asyncio>=0.21 ; extra == 'dev'
7
+ Requires-Dist: ruff>=0.8 ; extra == 'dev'
8
+ Requires-Dist: mypy>=1.13 ; extra == 'dev'
9
+ Requires-Dist: zensical ; extra == 'docs'
10
+ Requires-Dist: fastapi>=0.100.0 ; extra == 'fastapi'
11
+ Requires-Dist: pydantic>=2.0 ; extra == 'fastapi'
12
+ Requires-Dist: opentelemetry-api ; extra == 'otel'
13
+ Requires-Dist: opentelemetry-sdk ; extra == 'otel'
14
+ Provides-Extra: dev
15
+ Provides-Extra: docs
16
+ Provides-Extra: fastapi
17
+ Provides-Extra: otel
18
+ License-File: LICENSE
19
+ Summary: Rust-powered task queue for Python. No broker required.
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
22
+
23
+ # taskito
24
+
25
+ [![PyPI version](https://img.shields.io/pypi/v/taskito.svg)](https://pypi.org/project/taskito/)
26
+ [![Python versions](https://img.shields.io/pypi/pyversions/taskito.svg)](https://pypi.org/project/taskito/)
27
+ [![License](https://img.shields.io/pypi/l/taskito.svg)](https://github.com/pratyush618/taskito/blob/master/LICENSE)
28
+
29
+ A Rust-powered task queue for Python. No broker required — just SQLite.
30
+
31
+ ```
32
+ pip install taskito
33
+ ```
34
+
35
+ ## Quickstart
36
+
37
+ **1. Define tasks** in `tasks.py`:
38
+
39
+ ```python
40
+ from taskito import Queue
41
+
42
+ queue = Queue(db_path="tasks.db")
43
+
44
+ @queue.task()
45
+ def add(a: int, b: int) -> int:
46
+ return a + b
47
+ ```
48
+
49
+ **2. Start a worker** in one terminal:
50
+
51
+ ```bash
52
+ taskito worker --app tasks:queue
53
+ ```
54
+
55
+ **3. Enqueue jobs** from another terminal or script:
56
+
57
+ ```python
58
+ from tasks import add
59
+
60
+ job = add.delay(2, 3)
61
+ print(job.result(timeout=10)) # 5
62
+ ```
63
+
64
+ ## Why taskito?
65
+
66
+ Most Python task queues require a separate broker (Redis, RabbitMQ) even for single-machine workloads. taskito embeds everything — storage, scheduling, and worker management — into a single `pip install` with no external dependencies beyond Python itself.
67
+
68
+ The heavy lifting runs in Rust: a Tokio async scheduler, OS thread worker pool with crossbeam channels, and Diesel ORM over SQLite in WAL mode. Python's GIL is only held during task execution.
69
+
70
+ ## Features
71
+
72
+ - **Priority queues** — higher priority jobs run first
73
+ - **Retry with exponential backoff** — automatic retries with jitter
74
+ - **Dead letter queue** — inspect and replay failed jobs
75
+ - **Rate limiting** — token bucket with `"100/m"` syntax
76
+ - **Task dependencies** — `depends_on` for DAG workflows with cascade cancel
77
+ - **Task workflows** — `chain`, `group`, `chord` primitives
78
+ - **Periodic tasks** — cron scheduling with seconds granularity
79
+ - **Progress tracking** — report and read progress from inside tasks
80
+ - **Job cancellation** — cancel pending or running jobs
81
+ - **Unique tasks** — deduplicate active jobs by key
82
+ - **Batch enqueue** — `task.map()` for high-throughput bulk inserts
83
+ - **Named queues** — route tasks to isolated queues
84
+ - **Hooks** — before/after/success/failure middleware
85
+ - **Per-task middleware** — `TaskMiddleware` with `before`/`after`/`on_retry` hooks
86
+ - **Pluggable serializers** — `CloudpickleSerializer` (default), `JsonSerializer`, or custom
87
+ - **Cancel running tasks** — cooperative cancellation with `check_cancelled()`
88
+ - **Soft timeouts** — `check_timeout()` inside tasks
89
+ - **Worker heartbeat** — monitor worker health via `queue.workers()`
90
+ - **Job expiration** — `expires` parameter for time-sensitive jobs
91
+ - **Exception filtering** — `retry_on` / `dont_retry_on` for selective retries
92
+ - **OpenTelemetry** — optional tracing integration via `pip install taskito[otel]`
93
+ - **Async support** — `await job.aresult()`, `await queue.astats()`
94
+ - **Web dashboard** — `taskito dashboard --app myapp:queue` serves a built-in monitoring UI
95
+ - **FastAPI integration** — `TaskitoRouter` for instant REST API over the queue
96
+ - **CLI** — `taskito worker`, `taskito info --watch`, `taskito dashboard`
97
+
98
+ ## Examples
99
+
100
+ ### Retry with Backoff
101
+
102
+ ```python
103
+ @queue.task(max_retries=5, retry_backoff=2.0)
104
+ def fetch_url(url: str) -> str:
105
+ return requests.get(url).text
106
+ ```
107
+
108
+ ### Priority Queues
109
+
110
+ ```python
111
+ urgent_report.apply_async(args=[data], priority=10)
112
+ bulk_report.delay(data) # default priority 0
113
+ ```
114
+
115
+ ### Rate Limiting
116
+
117
+ ```python
118
+ @queue.task(rate_limit="100/m")
119
+ def call_api(endpoint: str) -> dict:
120
+ return requests.get(endpoint).json()
121
+ ```
122
+
123
+ ### Task Dependencies
124
+
125
+ ```python
126
+ download = fetch_file.delay("data.csv")
127
+ parsed = parse_file.apply_async(
128
+ args=["data.csv"],
129
+ depends_on=[download.id],
130
+ )
131
+ # parsed waits until download completes; if download fails, parsed is cancelled
132
+ ```
133
+
134
+ ### Workflows
135
+
136
+ ```python
137
+ from taskito import chain, group, chord
138
+
139
+ # Sequential pipeline — each step receives the previous result
140
+ chain(fetch.s(url), parse.s(), store.s()).apply()
141
+
142
+ # Parallel fan-out
143
+ group(process.s(chunk) for chunk in chunks).apply()
144
+
145
+ # Parallel + callback when all complete
146
+ chord([download.s(u) for u in urls], merge.s()).apply()
147
+ ```
148
+
149
+ ### Periodic Tasks
150
+
151
+ ```python
152
+ @queue.periodic(cron="0 */6 * * *")
153
+ def cleanup_temp_files():
154
+ ...
155
+ ```
156
+
157
+ ### Progress Tracking
158
+
159
+ ```python
160
+ from taskito import current_job
161
+
162
+ @queue.task()
163
+ def train_model(epochs: int):
164
+ for i in range(epochs):
165
+ ...
166
+ current_job.update_progress(int((i + 1) / epochs * 100))
167
+ ```
168
+
169
+ ### Hooks
170
+
171
+ ```python
172
+ @queue.on_failure
173
+ def alert_on_failure(task_name, args, kwargs, error):
174
+ slack.post(f"Task {task_name} failed: {error}")
175
+ ```
176
+
177
+ ### Exception Filtering
178
+
179
+ ```python
180
+ @queue.task(
181
+ max_retries=5,
182
+ retry_on=[ConnectionError, TimeoutError],
183
+ dont_retry_on=[ValueError],
184
+ )
185
+ def fetch_data(url: str) -> dict:
186
+ return requests.get(url).json()
187
+ ```
188
+
189
+ ### Per-Task Middleware
190
+
191
+ ```python
192
+ from taskito import TaskMiddleware
193
+
194
+ class TimingMiddleware(TaskMiddleware):
195
+ def before(self, ctx):
196
+ ctx._start = time.time()
197
+
198
+ def after(self, ctx, result, error):
199
+ elapsed = time.time() - ctx._start
200
+ print(f"{ctx.task_name} took {elapsed:.2f}s")
201
+
202
+ @queue.task(middleware=[TimingMiddleware()])
203
+ def process(data):
204
+ ...
205
+ ```
206
+
207
+ ### Delayed Scheduling
208
+
209
+ ```python
210
+ # Run 30 minutes from now
211
+ reminder.apply_async(args=[user_id, msg], delay=1800)
212
+ ```
213
+
214
+ ### Unique Tasks
215
+
216
+ ```python
217
+ report.apply_async(args=[user_id], unique_key=f"report:{user_id}")
218
+ # Second enqueue with same key is silently deduplicated while first is active
219
+ ```
220
+
221
+ ### FastAPI Integration
222
+
223
+ ```python
224
+ from fastapi import FastAPI
225
+ from taskito.contrib.fastapi import TaskitoRouter
226
+
227
+ app = FastAPI()
228
+ app.include_router(TaskitoRouter(queue), prefix="/tasks")
229
+ # GET /tasks/stats, GET /tasks/jobs/{id}, GET /tasks/jobs/{id}/progress (SSE), ...
230
+ ```
231
+
232
+ ### Batch Enqueue
233
+
234
+ ```python
235
+ jobs = send_email.map([("alice@x.com",), ("bob@x.com",), ("carol@x.com",)])
236
+ ```
237
+
238
+ ### Async Support
239
+
240
+ ```python
241
+ job = expensive_task.delay(data)
242
+ result = await job.aresult(timeout=30)
243
+ stats = await queue.astats()
244
+ ```
245
+
246
+ ## Testing
247
+
248
+ taskito includes a built-in test mode — no worker needed:
249
+
250
+ ```python
251
+ def test_add():
252
+ with queue.test_mode() as results:
253
+ add.delay(2, 3)
254
+ assert results[0].return_value == 5
255
+ ```
256
+
257
+ ## Documentation
258
+
259
+ Full documentation with guides, API reference, architecture diagrams, and examples:
260
+
261
+ **[Read the docs →](https://taskito-sepia.vercel.app)**
262
+
263
+ Coming from Celery? See the **[Migration Guide](https://taskito-sepia.vercel.app/guide/migration/)**.
264
+
265
+ ## Comparison
266
+
267
+ | Feature | taskito | Celery | RQ | Dramatiq | Huey |
268
+ |---|---|---|---|---|---|
269
+ | Broker required | **No** | Yes | Yes | Yes | Yes |
270
+ | Core language | **Rust + Python** | Python | Python | Python | Python |
271
+ | Priority queues | **Yes** | Yes | No | No | Yes |
272
+ | Rate limiting | **Yes** | Yes | No | Yes | No |
273
+ | Dead letter queue | **Yes** | No | Yes | No | No |
274
+ | Task dependencies | **Yes** | No | No | No | No |
275
+ | Task chaining | **Yes** | Yes | No | Yes | No |
276
+ | Built-in dashboard | **Yes** | No | No | No | No |
277
+ | FastAPI integration | **Yes** | No | No | No | No |
278
+ | Per-task middleware | **Yes** | No | No | Yes | No |
279
+ | Cancel running tasks | **Yes** | Yes | No | No | No |
280
+ | Custom serializers | **Yes** | Yes | No | No | No |
281
+ | Setup | **`pip install`** | Broker + backend | Redis | Broker | Redis |
282
+
283
+ ## License
284
+
285
+ MIT
286
+
@@ -0,0 +1,263 @@
1
+ # taskito
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/taskito.svg)](https://pypi.org/project/taskito/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/taskito.svg)](https://pypi.org/project/taskito/)
5
+ [![License](https://img.shields.io/pypi/l/taskito.svg)](https://github.com/pratyush618/taskito/blob/master/LICENSE)
6
+
7
+ A Rust-powered task queue for Python. No broker required — just SQLite.
8
+
9
+ ```
10
+ pip install taskito
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ **1. Define tasks** in `tasks.py`:
16
+
17
+ ```python
18
+ from taskito import Queue
19
+
20
+ queue = Queue(db_path="tasks.db")
21
+
22
+ @queue.task()
23
+ def add(a: int, b: int) -> int:
24
+ return a + b
25
+ ```
26
+
27
+ **2. Start a worker** in one terminal:
28
+
29
+ ```bash
30
+ taskito worker --app tasks:queue
31
+ ```
32
+
33
+ **3. Enqueue jobs** from another terminal or script:
34
+
35
+ ```python
36
+ from tasks import add
37
+
38
+ job = add.delay(2, 3)
39
+ print(job.result(timeout=10)) # 5
40
+ ```
41
+
42
+ ## Why taskito?
43
+
44
+ Most Python task queues require a separate broker (Redis, RabbitMQ) even for single-machine workloads. taskito embeds everything — storage, scheduling, and worker management — into a single `pip install` with no external dependencies beyond Python itself.
45
+
46
+ The heavy lifting runs in Rust: a Tokio async scheduler, OS thread worker pool with crossbeam channels, and Diesel ORM over SQLite in WAL mode. Python's GIL is only held during task execution.
47
+
48
+ ## Features
49
+
50
+ - **Priority queues** — higher priority jobs run first
51
+ - **Retry with exponential backoff** — automatic retries with jitter
52
+ - **Dead letter queue** — inspect and replay failed jobs
53
+ - **Rate limiting** — token bucket with `"100/m"` syntax
54
+ - **Task dependencies** — `depends_on` for DAG workflows with cascade cancel
55
+ - **Task workflows** — `chain`, `group`, `chord` primitives
56
+ - **Periodic tasks** — cron scheduling with seconds granularity
57
+ - **Progress tracking** — report and read progress from inside tasks
58
+ - **Job cancellation** — cancel pending or running jobs
59
+ - **Unique tasks** — deduplicate active jobs by key
60
+ - **Batch enqueue** — `task.map()` for high-throughput bulk inserts
61
+ - **Named queues** — route tasks to isolated queues
62
+ - **Hooks** — before/after/success/failure middleware
63
+ - **Per-task middleware** — `TaskMiddleware` with `before`/`after`/`on_retry` hooks
64
+ - **Pluggable serializers** — `CloudpickleSerializer` (default), `JsonSerializer`, or custom
65
+ - **Cancel running tasks** — cooperative cancellation with `check_cancelled()`
66
+ - **Soft timeouts** — `check_timeout()` inside tasks
67
+ - **Worker heartbeat** — monitor worker health via `queue.workers()`
68
+ - **Job expiration** — `expires` parameter for time-sensitive jobs
69
+ - **Exception filtering** — `retry_on` / `dont_retry_on` for selective retries
70
+ - **OpenTelemetry** — optional tracing integration via `pip install taskito[otel]`
71
+ - **Async support** — `await job.aresult()`, `await queue.astats()`
72
+ - **Web dashboard** — `taskito dashboard --app myapp:queue` serves a built-in monitoring UI
73
+ - **FastAPI integration** — `TaskitoRouter` for instant REST API over the queue
74
+ - **CLI** — `taskito worker`, `taskito info --watch`, `taskito dashboard`
75
+
76
+ ## Examples
77
+
78
+ ### Retry with Backoff
79
+
80
+ ```python
81
+ @queue.task(max_retries=5, retry_backoff=2.0)
82
+ def fetch_url(url: str) -> str:
83
+ return requests.get(url).text
84
+ ```
85
+
86
+ ### Priority Queues
87
+
88
+ ```python
89
+ urgent_report.apply_async(args=[data], priority=10)
90
+ bulk_report.delay(data) # default priority 0
91
+ ```
92
+
93
+ ### Rate Limiting
94
+
95
+ ```python
96
+ @queue.task(rate_limit="100/m")
97
+ def call_api(endpoint: str) -> dict:
98
+ return requests.get(endpoint).json()
99
+ ```
100
+
101
+ ### Task Dependencies
102
+
103
+ ```python
104
+ download = fetch_file.delay("data.csv")
105
+ parsed = parse_file.apply_async(
106
+ args=["data.csv"],
107
+ depends_on=[download.id],
108
+ )
109
+ # parsed waits until download completes; if download fails, parsed is cancelled
110
+ ```
111
+
112
+ ### Workflows
113
+
114
+ ```python
115
+ from taskito import chain, group, chord
116
+
117
+ # Sequential pipeline — each step receives the previous result
118
+ chain(fetch.s(url), parse.s(), store.s()).apply()
119
+
120
+ # Parallel fan-out
121
+ group(process.s(chunk) for chunk in chunks).apply()
122
+
123
+ # Parallel + callback when all complete
124
+ chord([download.s(u) for u in urls], merge.s()).apply()
125
+ ```
126
+
127
+ ### Periodic Tasks
128
+
129
+ ```python
130
+ @queue.periodic(cron="0 */6 * * *")
131
+ def cleanup_temp_files():
132
+ ...
133
+ ```
134
+
135
+ ### Progress Tracking
136
+
137
+ ```python
138
+ from taskito import current_job
139
+
140
+ @queue.task()
141
+ def train_model(epochs: int):
142
+ for i in range(epochs):
143
+ ...
144
+ current_job.update_progress(int((i + 1) / epochs * 100))
145
+ ```
146
+
147
+ ### Hooks
148
+
149
+ ```python
150
+ @queue.on_failure
151
+ def alert_on_failure(task_name, args, kwargs, error):
152
+ slack.post(f"Task {task_name} failed: {error}")
153
+ ```
154
+
155
+ ### Exception Filtering
156
+
157
+ ```python
158
+ @queue.task(
159
+ max_retries=5,
160
+ retry_on=[ConnectionError, TimeoutError],
161
+ dont_retry_on=[ValueError],
162
+ )
163
+ def fetch_data(url: str) -> dict:
164
+ return requests.get(url).json()
165
+ ```
166
+
167
+ ### Per-Task Middleware
168
+
169
+ ```python
170
+ from taskito import TaskMiddleware
171
+
172
+ class TimingMiddleware(TaskMiddleware):
173
+ def before(self, ctx):
174
+ ctx._start = time.time()
175
+
176
+ def after(self, ctx, result, error):
177
+ elapsed = time.time() - ctx._start
178
+ print(f"{ctx.task_name} took {elapsed:.2f}s")
179
+
180
+ @queue.task(middleware=[TimingMiddleware()])
181
+ def process(data):
182
+ ...
183
+ ```
184
+
185
+ ### Delayed Scheduling
186
+
187
+ ```python
188
+ # Run 30 minutes from now
189
+ reminder.apply_async(args=[user_id, msg], delay=1800)
190
+ ```
191
+
192
+ ### Unique Tasks
193
+
194
+ ```python
195
+ report.apply_async(args=[user_id], unique_key=f"report:{user_id}")
196
+ # Second enqueue with same key is silently deduplicated while first is active
197
+ ```
198
+
199
+ ### FastAPI Integration
200
+
201
+ ```python
202
+ from fastapi import FastAPI
203
+ from taskito.contrib.fastapi import TaskitoRouter
204
+
205
+ app = FastAPI()
206
+ app.include_router(TaskitoRouter(queue), prefix="/tasks")
207
+ # GET /tasks/stats, GET /tasks/jobs/{id}, GET /tasks/jobs/{id}/progress (SSE), ...
208
+ ```
209
+
210
+ ### Batch Enqueue
211
+
212
+ ```python
213
+ jobs = send_email.map([("alice@x.com",), ("bob@x.com",), ("carol@x.com",)])
214
+ ```
215
+
216
+ ### Async Support
217
+
218
+ ```python
219
+ job = expensive_task.delay(data)
220
+ result = await job.aresult(timeout=30)
221
+ stats = await queue.astats()
222
+ ```
223
+
224
+ ## Testing
225
+
226
+ taskito includes a built-in test mode — no worker needed:
227
+
228
+ ```python
229
+ def test_add():
230
+ with queue.test_mode() as results:
231
+ add.delay(2, 3)
232
+ assert results[0].return_value == 5
233
+ ```
234
+
235
+ ## Documentation
236
+
237
+ Full documentation with guides, API reference, architecture diagrams, and examples:
238
+
239
+ **[Read the docs →](https://taskito-sepia.vercel.app)**
240
+
241
+ Coming from Celery? See the **[Migration Guide](https://taskito-sepia.vercel.app/guide/migration/)**.
242
+
243
+ ## Comparison
244
+
245
+ | Feature | taskito | Celery | RQ | Dramatiq | Huey |
246
+ |---|---|---|---|---|---|
247
+ | Broker required | **No** | Yes | Yes | Yes | Yes |
248
+ | Core language | **Rust + Python** | Python | Python | Python | Python |
249
+ | Priority queues | **Yes** | Yes | No | No | Yes |
250
+ | Rate limiting | **Yes** | Yes | No | Yes | No |
251
+ | Dead letter queue | **Yes** | No | Yes | No | No |
252
+ | Task dependencies | **Yes** | No | No | No | No |
253
+ | Task chaining | **Yes** | Yes | No | Yes | No |
254
+ | Built-in dashboard | **Yes** | No | No | No | No |
255
+ | FastAPI integration | **Yes** | No | No | No | No |
256
+ | Per-task middleware | **Yes** | No | No | Yes | No |
257
+ | Cancel running tasks | **Yes** | Yes | No | No | No |
258
+ | Custom serializers | **Yes** | Yes | No | No | No |
259
+ | Setup | **`pip install`** | Broker + backend | Redis | Broker | Redis |
260
+
261
+ ## License
262
+
263
+ MIT
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "taskito-core"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "taskito-python"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -79,9 +79,7 @@ class group:
79
79
  concurrently. Members are dispatched in waves.
80
80
  """
81
81
 
82
- def __init__(
83
- self, *signatures: Signature, max_concurrency: int | None = None
84
- ):
82
+ def __init__(self, *signatures: Signature, max_concurrency: int | None = None):
85
83
  if len(signatures) < 1:
86
84
  raise ValueError("group requires at least one signature")
87
85
  self.signatures = list(signatures)
@@ -154,9 +152,7 @@ class chord:
154
152
  )
155
153
 
156
154
 
157
- def chunks(
158
- task: TaskWrapper, items: list[Any], chunk_size: int
159
- ) -> group:
155
+ def chunks(task: TaskWrapper, items: list[Any], chunk_size: int) -> group:
160
156
  """Split items into chunks and create a group of tasks processing each chunk.
161
157
 
162
158
  Args:
@@ -179,9 +175,7 @@ def chunks(
179
175
  return group(*sigs)
180
176
 
181
177
 
182
- def starmap(
183
- task: TaskWrapper, args_list: list[tuple[Any, ...]]
184
- ) -> group:
178
+ def starmap(task: TaskWrapper, args_list: list[tuple[Any, ...]]) -> group:
185
179
  """Create a group with one task per args tuple.
186
180
 
187
181
  Args:
@@ -75,9 +75,7 @@ class JobContext:
75
75
  if _queue_ref is None:
76
76
  raise RuntimeError("Queue reference not set. Cannot write log.")
77
77
  extra_str = json.dumps(extra) if extra else None
78
- _queue_ref._inner.write_task_log(
79
- ctx.job_id, ctx.task_name, level, message, extra_str
80
- )
78
+ _queue_ref._inner.write_task_log(ctx.job_id, ctx.task_name, level, message, extra_str)
81
79
 
82
80
  def check_cancelled(self) -> None:
83
81
  """Check if cancellation has been requested for this job.
@@ -25,7 +25,9 @@ if TYPE_CHECKING:
25
25
 
26
26
  def _load_spa_html() -> str:
27
27
  """Load the dashboard SPA HTML from the bundled template file."""
28
- return resources.files("taskito").joinpath("templates/dashboard.html").read_text(encoding="utf-8")
28
+ return (
29
+ resources.files("taskito").joinpath("templates/dashboard.html").read_text(encoding="utf-8")
30
+ )
29
31
 
30
32
 
31
33
  _SPA_HTML: str | None = None
@@ -106,7 +108,9 @@ def _make_handler(queue: Queue) -> type:
106
108
  level = qs.get("level", [None])[0]
107
109
  since = int(qs.get("since", ["3600"])[0])
108
110
  limit = int(qs.get("limit", ["100"])[0])
109
- self._json_response(queue.query_logs(task_name=task, level=level, since=since, limit=limit))
111
+ self._json_response(
112
+ queue.query_logs(task_name=task, level=level, since=since, limit=limit)
113
+ )
110
114
  elif path == "/api/circuit-breakers":
111
115
  self._json_response(queue.circuit_breakers())
112
116
  elif path == "/api/workers":
@@ -19,9 +19,7 @@ class _AsyncMixin:
19
19
  async def _run_sync(self, fn: Any, *args: Any, **kwargs: Any) -> Any:
20
20
  loop = asyncio.get_event_loop()
21
21
  if kwargs:
22
- return await loop.run_in_executor(
23
- self._executor, lambda: fn(*args, **kwargs)
24
- )
22
+ return await loop.run_in_executor(self._executor, lambda: fn(*args, **kwargs))
25
23
  return await loop.run_in_executor(self._executor, lambda: fn(*args))
26
24
 
27
25
 
@@ -90,9 +90,7 @@ class JobResult:
90
90
  return status, cloudpickle.loads(result_bytes)
91
91
 
92
92
  if status in ("failed", "dead"):
93
- raise RuntimeError(
94
- f"Job {self.id} {status}: {self._py_job.error or 'unknown error'}"
95
- )
93
+ raise RuntimeError(f"Job {self.id} {status}: {self._py_job.error or 'unknown error'}")
96
94
 
97
95
  return status, None
98
96
 
@@ -181,9 +181,7 @@ class _FakeJobResult:
181
181
 
182
182
  def result(self, timeout: float = 30.0, **kwargs: Any) -> Any:
183
183
  if self._tr.error:
184
- raise RuntimeError(
185
- f"Job {self._tr.job_id} failed: {self._tr.error}"
186
- )
184
+ raise RuntimeError(f"Job {self._tr.job_id} failed: {self._tr.error}")
187
185
  return self._tr.return_value
188
186
 
189
187
  async def aresult(self, timeout: float = 30.0, **kwargs: Any) -> Any:
@@ -4,10 +4,11 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "taskito"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Rust-powered task queue for Python. No broker required."
9
9
  requires-python = ">=3.9"
10
10
  license = { file = "LICENSE" }
11
+ readme = "README.md"
11
12
  dependencies = ["cloudpickle>=3.0"]
12
13
 
13
14
  [project.optional-dependencies]
@@ -23,7 +24,7 @@ module-name = "taskito._taskito"
23
24
  features = ["pyo3/extension-module"]
24
25
 
25
26
  [project.scripts]
26
- taskito = "taskito.cli:main"
27
+ taskito = "taskito.cli:main"
27
28
 
28
29
  [tool.pytest.ini_options]
29
30
  asyncio_mode = "auto"
taskito-0.2.0/PKG-INFO DELETED
@@ -1,20 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: taskito
3
- Version: 0.2.0
4
- Requires-Dist: cloudpickle>=3.0
5
- Requires-Dist: pytest>=7.0 ; extra == 'dev'
6
- Requires-Dist: pytest-asyncio>=0.21 ; extra == 'dev'
7
- Requires-Dist: ruff>=0.8 ; extra == 'dev'
8
- Requires-Dist: mypy>=1.13 ; extra == 'dev'
9
- Requires-Dist: zensical ; extra == 'docs'
10
- Requires-Dist: fastapi>=0.100.0 ; extra == 'fastapi'
11
- Requires-Dist: pydantic>=2.0 ; extra == 'fastapi'
12
- Requires-Dist: opentelemetry-api ; extra == 'otel'
13
- Requires-Dist: opentelemetry-sdk ; extra == 'otel'
14
- Provides-Extra: dev
15
- Provides-Extra: docs
16
- Provides-Extra: fastapi
17
- Provides-Extra: otel
18
- License-File: LICENSE
19
- Summary: Rust-powered task queue for Python. No broker required.
20
- Requires-Python: >=3.9
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes