flask-async-celery 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,597 @@
1
+ Metadata-Version: 2.4
2
+ Name: flask-async-celery
3
+ Version: 0.1.0
4
+ Summary: AsyncIO execution pool for Celery with Flask integration
5
+ Author: Mazhar Ali
6
+ License: MIT
7
+ Keywords: celery,asyncio,flask,async,tasks,redis
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Framework :: Flask
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Topic :: System :: Distributed Computing
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: celery<5.7,>=5.6
22
+ Requires-Dist: Flask>=2.3
23
+ Provides-Extra: redis
24
+ Requires-Dist: redis>=5.0; extra == "redis"
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest>=8.0; extra == "test"
27
+ Requires-Dist: pytest-asyncio>=0.24; extra == "test"
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8.0; extra == "dev"
30
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
31
+ Requires-Dist: build>=1.2; extra == "dev"
32
+ Requires-Dist: twine>=5.0; extra == "dev"
33
+
34
+ # Flask Async Celery
35
+
36
+ Run `async def` Celery tasks on a persistent asyncio event loop with bounded concurrency, Flask integration, and Celery consumer backpressure.
37
+
38
+ ## Features
39
+
40
+ * Persistent `asyncio` event loop in a dedicated thread per Celery worker process.
41
+ * Run native `async def` Celery tasks.
42
+ * Bounded asynchronous concurrency with `max_tasks`.
43
+ * Celery request context propagation into the asyncio execution thread.
44
+ * `self.retry()` support for async tasks.
45
+ * Normal synchronous Celery tasks continue to work.
46
+ * Redis consumer-side backpressure through Celery's `worker_disable_prefetch`.
47
+ * Flask extension with simple configuration.
48
+ * Graceful asyncio executor shutdown.
49
+ * Compatible with Celery 5.6.x and Python 3.10+.
50
+
51
+ ## Architecture
52
+
53
+ ```text
54
+ Redis
55
+ │
56
+ ▼
57
+ Celery Consumer
58
+ │
59
+ worker_disable_prefetch
60
+ │
61
+ ▼
62
+ AsyncIOPool
63
+ max_tasks = N
64
+ │
65
+ ▼
66
+ AsyncExecutor
67
+ asyncio.Semaphore(N)
68
+ │
69
+ Persistent loop
70
+ │
71
+ ┌────────────┼────────────┐
72
+ ▼ ▼ ▼
73
+ Async task Async task Async task
74
+ ```
75
+
76
+ The package separates Celery's worker execution from asyncio execution:
77
+
78
+ 1. Celery receives and traces the task.
79
+ 2. `AsyncIOPool` bridges Celery execution into the asyncio executor.
80
+ 3. `AsyncExecutor` owns a persistent asyncio event loop.
81
+ 4. A semaphore limits active async tasks.
82
+ 5. Celery's Redis `worker_disable_prefetch` option can prevent the consumer from reserving work beyond the available pool capacity.
83
+
84
+ ## Requirements
85
+
86
+ * Python 3.10+
87
+ * Celery 5.6.x
88
+ * Flask 2.3+
89
+ * Redis when using the Redis broker/result backend and consumer backpressure.
90
+
91
+ ## Installation
92
+
93
+ From PyPI:
94
+
95
+ ```bash
96
+ pip install flask-async-celery
97
+ ```
98
+
99
+ For Redis support:
100
+
101
+ ```bash
102
+ pip install "flask-async-celery[redis]"
103
+ ```
104
+
105
+ For development and testing:
106
+
107
+ ```bash
108
+ pip install "flask-async-celery[test]"
109
+ ```
110
+
111
+ ## Basic Flask Setup
112
+
113
+ ```python
114
+ import asyncio
115
+
116
+ from flask import Flask
117
+
118
+ from flask_async_celery import AsyncCelery, AsyncTask
119
+
120
+
121
+ app = Flask(__name__)
122
+
123
+ celery = AsyncCelery(
124
+ app,
125
+ broker_url="redis://127.0.0.1:6379/0",
126
+ result_backend="redis://127.0.0.1:6379/1",
127
+ max_tasks=5,
128
+ disable_prefetch=True,
129
+ )
130
+
131
+
132
+ @celery.task(base=AsyncTask)
133
+ async def my_task(value):
134
+ await asyncio.sleep(1)
135
+ return value * 2
136
+ ```
137
+
138
+ Send the task normally:
139
+
140
+ ```python
141
+ result = my_task.delay(10)
142
+
143
+ print(result.get(timeout=30))
144
+ # 20
145
+ ```
146
+
147
+ ## Flask Configuration
148
+
149
+ Configuration can be supplied through Flask:
150
+
151
+ ```python
152
+ app.config["ASYNC_CELERY_MAX_TASKS"] = 10
153
+ app.config["ASYNC_CELERY_DISABLE_PREFETCH"] = True
154
+
155
+ celery = AsyncCelery(
156
+ app,
157
+ broker_url="redis://127.0.0.1:6379/0",
158
+ result_backend="redis://127.0.0.1:6379/1",
159
+ )
160
+ ```
161
+
162
+ Available settings:
163
+
164
+ | Setting | Default | Description |
165
+ | ------------------------------- | ------: | ----------------------------------------------------- |
166
+ | `ASYNC_CELERY_MAX_TASKS` | `20` | Maximum number of concurrently executing async tasks. |
167
+ | `ASYNC_CELERY_DISABLE_PREFETCH` | `True` | Enables Celery consumer-side backpressure. |
168
+
169
+ Constructor arguments can also be used directly:
170
+
171
+ ```python
172
+ celery = AsyncCelery(
173
+ app,
174
+ max_tasks=10,
175
+ disable_prefetch=True,
176
+ )
177
+ ```
178
+
179
+ Flask configuration takes precedence over the constructor defaults when the extension is initialized.
180
+
181
+ ## Worker Configuration
182
+
183
+ Run the worker using the package's custom pool:
184
+
185
+ ```bash
186
+ celery -A your_app.celery worker \
187
+ -P flask_async_celery.pool:AsyncIOPool \
188
+ -c 5 \
189
+ --loglevel=INFO
190
+ ```
191
+
192
+ For example:
193
+
194
+ ```bash
195
+ celery -A your_app.celery worker \
196
+ -P flask_async_celery.pool:AsyncIOPool \
197
+ -c 10 \
198
+ --loglevel=INFO
199
+ ```
200
+
201
+ The `-c` value should match the desired async concurrency.
202
+
203
+ The custom pool exposes its configured concurrency through `num_processes`, allowing Celery's consumer to use the same capacity when consumer-side prefetch is disabled.
204
+
205
+ ## Concurrency
206
+
207
+ Set the maximum number of simultaneously executing async tasks with:
208
+
209
+ ```python
210
+ celery = AsyncCelery(
211
+ app,
212
+ max_tasks=5,
213
+ )
214
+ ```
215
+
216
+ With:
217
+
218
+ ```text
219
+ max_tasks = 5
220
+ ```
221
+
222
+ the asyncio executor allows at most five active coroutines at once.
223
+
224
+ Additional work waits for an available execution slot.
225
+
226
+ This is different from simply creating more threads. The package uses one persistent asyncio event loop and runs async coroutines concurrently on that loop.
227
+
228
+ ## Consumer Backpressure
229
+
230
+ For Redis, Celery 5.6 supports:
231
+
232
+ ```python
233
+ worker_disable_prefetch = True
234
+ ```
235
+
236
+ The extension enables this by default:
237
+
238
+ ```python
239
+ celery = AsyncCelery(
240
+ app,
241
+ max_tasks=5,
242
+ disable_prefetch=True,
243
+ )
244
+ ```
245
+
246
+ This provides two levels of protection:
247
+
248
+ ```text
249
+ Celery Consumer
250
+ │
251
+ │ Don't reserve beyond available capacity
252
+ ▼
253
+ AsyncIOPool
254
+ │
255
+ │ max_tasks
256
+ ▼
257
+ AsyncExecutor
258
+ │
259
+ │ semaphore
260
+ ▼
261
+ asyncio tasks
262
+ ```
263
+
264
+ You can disable the consumer-side behavior:
265
+
266
+ ```python
267
+ celery = AsyncCelery(
268
+ app,
269
+ max_tasks=5,
270
+ disable_prefetch=False,
271
+ )
272
+ ```
273
+
274
+ When disabled, the asyncio executor still enforces its own concurrency limit.
275
+
276
+ ### Redis Requirement
277
+
278
+ `worker_disable_prefetch` is intended for supported Redis worker configurations. If you use another broker, verify that your Celery version and broker transport support this feature before relying on consumer-side backpressure.
279
+
280
+ The executor-level concurrency limit remains independent of consumer prefetch behavior.
281
+
282
+ ## Async Tasks
283
+
284
+ Use `AsyncTask` as the base class for native async tasks:
285
+
286
+ ```python
287
+ from flask_async_celery import AsyncTask
288
+
289
+
290
+ @celery.task(base=AsyncTask)
291
+ async def fetch_data():
292
+ await some_async_operation()
293
+ return "done"
294
+ ```
295
+
296
+ The task can use normal Celery task features:
297
+
298
+ ```python
299
+ @celery.task(
300
+ base=AsyncTask,
301
+ bind=True,
302
+ max_retries=3,
303
+ )
304
+ async def process_item(self, item_id):
305
+ try:
306
+ return await process(item_id)
307
+ except TemporaryError as exc:
308
+ raise self.retry(
309
+ exc=exc,
310
+ countdown=5,
311
+ )
312
+ ```
313
+
314
+ The Celery request context is propagated from the Celery worker thread into the asyncio execution thread, so task information such as the task ID, retry count, delivery information, and retry context remains available.
315
+
316
+ ## Synchronous Tasks
317
+
318
+ Normal synchronous tasks can still be used:
319
+
320
+ ```python
321
+ @celery.task
322
+ def sync_task(value):
323
+ return value * 2
324
+ ```
325
+
326
+ The package does not require every task to be asynchronous.
327
+
328
+ ## Retries
329
+
330
+ Async `self.retry()` is supported:
331
+
332
+ ```python
333
+ @celery.task(
334
+ base=AsyncTask,
335
+ bind=True,
336
+ max_retries=3,
337
+ )
338
+ async def retrying_task(self):
339
+ if should_retry():
340
+ raise self.retry(countdown=5)
341
+
342
+ return "success"
343
+ ```
344
+
345
+ The Celery task request is preserved when execution moves from the Celery worker thread to the asyncio event loop. This allows Celery retry metadata and delivery information to remain available to the async task.
346
+
347
+ ## Exceptions
348
+
349
+ Exceptions raised by an async task propagate through the Celery execution path:
350
+
351
+ ```python
352
+ @celery.task(base=AsyncTask)
353
+ async def failing_task():
354
+ raise RuntimeError("something went wrong")
355
+ ```
356
+
357
+ Celery remains responsible for task failure state, result handling, retry behavior, and worker-level task tracing.
358
+
359
+ ## Graceful Shutdown
360
+
361
+ The asyncio executor runs in a dedicated daemon thread.
362
+
363
+ When the pool stops, the executor:
364
+
365
+ 1. Stops accepting new work.
366
+ 2. Stops the asyncio event loop.
367
+ 3. Cancels pending asyncio tasks.
368
+ 4. Waits for the loop thread to terminate.
369
+ 5. Closes the event loop.
370
+
371
+ ## Public API
372
+
373
+ The main public API is intentionally small:
374
+
375
+ ```python
376
+ from flask_async_celery import AsyncCelery, AsyncTask
377
+ ```
378
+
379
+ ### `AsyncCelery`
380
+
381
+ Flask integration and Celery configuration.
382
+
383
+ ```python
384
+ AsyncCelery(
385
+ app=None,
386
+ *,
387
+ celery=None,
388
+ broker_url=None,
389
+ result_backend=None,
390
+ max_tasks=20,
391
+ disable_prefetch=True,
392
+ )
393
+ ```
394
+
395
+ ### `AsyncTask`
396
+
397
+ Base class for asynchronous Celery tasks:
398
+
399
+ ```python
400
+ @celery.task(base=AsyncTask)
401
+ async def my_task():
402
+ ...
403
+ ```
404
+
405
+ ## Development
406
+
407
+ Clone the repository and install the project in editable mode:
408
+
409
+ ```bash
410
+ git clone <repository-url>
411
+ cd flask-async-celery
412
+
413
+ pip install -e ".[test]"
414
+ ```
415
+
416
+ Run the test suite:
417
+
418
+ ```bash
419
+ pytest -v
420
+ ```
421
+
422
+ The test suite covers:
423
+
424
+ * asyncio executor concurrency
425
+ * AsyncIOPool execution
426
+ * async task exceptions
427
+ * async retries
428
+ * synchronous retries
429
+ * real Celery worker integration
430
+ * Redis consumer backpressure
431
+ * Flask extension configuration
432
+ * end-to-end Flask/Celery/async execution
433
+
434
+ ## Project Structure
435
+
436
+ ```text
437
+ flask-async-celery/
438
+ ├── pyproject.toml
439
+ ├── README.md
440
+ ├── src/
441
+ │ └── flask_async_celery/
442
+ │ ├── __init__.py
443
+ │ ├── extension.py
444
+ │ ├── executor.py
445
+ │ ├── bootstep.py
446
+ │ ├── pool.py
447
+ │ └── task.py
448
+ └── test/
449
+ ├── conftest.py
450
+ ├── test_executor.py
451
+ ├── test_tasks.py
452
+ ├── test_backpressure.py
453
+ ├── test_celery_pool.py
454
+ ├── test_worker_integration.py
455
+ ├── test_extension.py
456
+ └── test_extension_integration.py
457
+ ```
458
+
459
+ ## Design Notes
460
+
461
+ This package does not replace Celery's task tracing and lifecycle handling.
462
+
463
+ Celery remains responsible for:
464
+
465
+ * task delivery
466
+ * task acknowledgment
467
+ * retries
468
+ * result state
469
+ * task IDs
470
+ * worker lifecycle
471
+ * task tracing
472
+
473
+ The package provides the asyncio execution layer and integrates it with Celery's pool interface.
474
+
475
+ The asyncio event loop is persistent for the lifetime of the worker process rather than creating a new event loop for every task.
476
+
477
+ ### Why a Persistent Event Loop?
478
+
479
+ Creating a new event loop for every task adds unnecessary setup and teardown overhead.
480
+
481
+ Instead, each worker process owns one persistent asyncio event loop:
482
+
483
+ ```text
484
+ Celery Worker Process
485
+ │
486
+ ├── Celery Consumer
487
+ ├── Celery task execution
488
+ ├── bridge threads
489
+ │
490
+ └── asyncio event loop thread
491
+ ├── Task A
492
+ ├── Task B
493
+ └── Task C
494
+ ```
495
+
496
+ Async tasks can therefore share the same event loop while still being bounded by `max_tasks`.
497
+
498
+ ### Why Request Propagation?
499
+
500
+ Celery's request context is associated with the worker execution context.
501
+
502
+ The asyncio event loop runs in a separate thread, so the package explicitly transfers the current Celery request into that execution context.
503
+
504
+ This preserves information required by features such as:
505
+
506
+ ```python
507
+ self.request.id
508
+ self.request.retries
509
+ self.request.delivery_info
510
+ self.retry()
511
+ ```
512
+
513
+ The request is pushed before async execution and removed afterward.
514
+
515
+ ## Limitations
516
+
517
+ ### Broker-specific Backpressure
518
+
519
+ Consumer-side `worker_disable_prefetch` support depends on Celery and the broker transport.
520
+
521
+ The asyncio executor's own `max_tasks` limit remains the final execution boundary.
522
+
523
+ ### Worker Pool
524
+
525
+ The worker must use:
526
+
527
+ ```text
528
+ flask_async_celery.pool:AsyncIOPool
529
+ ```
530
+
531
+ for the package's asyncio execution model.
532
+
533
+ ### One Event Loop Per Worker Process
534
+
535
+ Each worker process owns its own asyncio event loop and concurrency limit.
536
+
537
+ For example:
538
+
539
+ ```bash
540
+ celery -A your_app.celery worker \
541
+ -P flask_async_celery.pool:AsyncIOPool \
542
+ -c 5
543
+ ```
544
+
545
+ creates a worker configuration with five execution slots.
546
+
547
+ If you run multiple worker processes, each process has its own pool and event loop.
548
+
549
+ ## Testing
550
+
551
+ Run the complete test suite:
552
+
553
+ ```bash
554
+ pytest -v
555
+ ```
556
+
557
+ The project currently tests the execution model with real Celery workers in addition to unit-level executor and pool tests.
558
+
559
+ A successful test run should show all tests passing.
560
+
561
+ ## Building the Package
562
+
563
+ Install the build tooling:
564
+
565
+ ```bash
566
+ python -m pip install build
567
+ ```
568
+
569
+ Build the source distribution and wheel:
570
+
571
+ ```bash
572
+ python -m build
573
+ ```
574
+
575
+ The generated files will be placed in:
576
+
577
+ ```text
578
+ dist/
579
+ ```
580
+
581
+ You can inspect the generated wheel with:
582
+
583
+ ```bash
584
+ python -m pip install dist/*.whl
585
+ ```
586
+
587
+ ## Version
588
+
589
+ Current version:
590
+
591
+ ```text
592
+ 0.1.0
593
+ ```
594
+
595
+ ## License
596
+
597
+ Add the project's license information here.