aqworker 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,615 @@
1
+ Metadata-Version: 2.3
2
+ Name: aqworker
3
+ Version: 0.1.0
4
+ Summary: Async Redis-backed job runner for Python apps. AQWorker orchestrates workers, handlers, and queues with auto-discovery, retries, CLI tooling, and FastAPI-friendly async processing while staying framework agnostic.
5
+ Keywords:
6
+ Author: Do Quoc Vuong
7
+ Author-email: Do Quoc Vuong <vuongtlt13@gmail.com>
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Dist: pydantic>=2.12.4
12
+ Requires-Dist: pytest-asyncio>=1.3.0
13
+ Requires-Dist: redis>=7.1.0
14
+ Requires-Dist: typer>=0.20.0
15
+ Requires-Python: >=3.10, <4.0
16
+ Project-URL: Homepage, https://github.com/vuongtlt13/aqworker
17
+ Project-URL: Issues, https://github.com/vuongtlt13/aqworker/issues
18
+ Project-URL: License, https://opensource.org/licenses/MIT
19
+ Description-Content-Type: text/markdown
20
+
21
+ # AQWorker - Async Queue Worker
22
+
23
+ ## Overview
24
+
25
+ AQWorker is a Redis-based background job processing system for Python applications. It provides a simple, efficient way to handle asynchronous tasks with support for multiple worker types, queue isolation, and job status tracking. While it works seamlessly with FastAPI, it's a standalone package that can be used with any Python application.
26
+
27
+ ## Features
28
+
29
+ - **Framework Agnostic**: Works with FastAPI, Django, Flask, or any Python application
30
+ - **Async/Await Support**: Built with async/await for modern Python applications
31
+ - **Redis-based**: Uses Redis for reliable job queue management
32
+ - **Worker Registry**: Centralized worker management with automatic discovery
33
+ - **Handler Registry**: Organized handler system for job processing
34
+ - **Handler Auto-Discovery**: Import handler packages automatically via `include_packages`
35
+ - **CLI Tools**: Command-line interface for managing workers and jobs
36
+ - **Queue Isolation**: Separate queues for different job types
37
+ - **Job Status Tracking**: Track job lifecycle (PENDING, PROCESSING, COMPLETED, FAILED, CANCELLED)
38
+ - **Automatic Retries**: Built-in retry mechanism with configurable delays
39
+
40
+ ## Architecture
41
+
42
+ ```
43
+ ┌─────────────┐ get_next_job() ┌─────────────┐ dequeue() ┌─────────────┐
44
+ │ Worker │ ────────────────────►│ JobService │ ──────────────► │ JobQueue │
45
+ │ │ │ │ │ │
46
+ │ │ ◄────────────────────│ │ ◄────────────── │ │
47
+ └─────────────┘ complete_job() └─────────────┘ complete_job()└─────────────┘
48
+
49
+ ┌─────────────┐ enqueue_job() ┌─────────────┐ enqueue() ┌─────────────┐
50
+ │ Application │ ────────────────────►│ AQWorker │ ──────────────► │ JobQueue │
51
+ │ (FastAPI/ │ │ │ │ │
52
+ │ Django/ │ │ - Worker │ │ │
53
+ │ Flask/ │ │ Registry │ │ │
54
+ │ Script) │ │ - Handler │ │ │
55
+ │ │ │ Registry │ │ │
56
+ └─────────────┘ │ - Job │ └─────────────┘
57
+ │ Service │
58
+ └─────────────┘
59
+ ```
60
+
61
+ ## Components
62
+
63
+ ### 1. AQWorker (`core.py`)
64
+ - **Purpose**: Main orchestrator class that manages workers, handlers, and job service
65
+ - **Features**:
66
+ - Worker and handler registration
67
+ - Worker instance creation
68
+ - Job service integration
69
+ - Centralized configuration
70
+
71
+ ### 2. WorkerRegistry (`worker/registry.py`)
72
+ - **Purpose**: Manages worker class registrations
73
+ - **Features**:
74
+ - Register worker classes with names
75
+ - List available workers
76
+ - Get worker definitions
77
+
78
+ ### 3. HandlerRegistry (`handler/registry.py`)
79
+ - **Purpose**: Manages handler class registrations
80
+ - **Features**:
81
+ - Register handler classes
82
+ - Lookup handlers by name
83
+ - Snapshot of all registered handlers
84
+
85
+ ### 4. JobService (`job/service.py`)
86
+ - **Purpose**: High-level service layer for job operations
87
+ - **Features**:
88
+ - Enqueue jobs
89
+ - Dequeue jobs for workers
90
+ - Get job status
91
+ - Queue statistics
92
+ - Job lifecycle management
93
+
94
+ ### 5. JobQueue (`job/queue.py`)
95
+ - **Purpose**: Low-level Redis operations for job management
96
+ - **Features**:
97
+ - FIFO queue processing
98
+ - Async/await support
99
+ - Job status tracking
100
+ - Redis operations: enqueue, dequeue, complete_job
101
+
102
+ ### 6. BaseWorker (`worker/base.py`)
103
+ - **Purpose**: Base class for all workers
104
+ - **Features**:
105
+ - Automatic job polling
106
+ - Concurrent job processing
107
+ - Error handling and retries
108
+ - Health monitoring
109
+
110
+ ### 7. BaseHandler (`handler/base.py`)
111
+ - **Purpose**: Base class for all job handlers
112
+ - **Features**:
113
+ - Async/sync handler support
114
+ - Standardized job processing interface
115
+
116
+ ## Installation
117
+
118
+ ```bash
119
+ pip install aqworker
120
+ ```
121
+
122
+ Or install from source:
123
+
124
+ ```bash
125
+ git clone <repository>
126
+ cd aqworker
127
+ pip install -e .
128
+ ```
129
+
130
+ ## Quick Start
131
+
132
+ ### 1. Define Handlers
133
+
134
+ ```python
135
+ # handlers.py
136
+ from aqworker import BaseHandler
137
+
138
+ class EmailHandler(BaseHandler):
139
+ name = "email"
140
+
141
+ async def handle(self, data: dict) -> bool:
142
+ recipient = data.get("recipient")
143
+ subject = data.get("subject")
144
+ body = data.get("body")
145
+
146
+ # Send email logic here
147
+ print(f"Sending email to {recipient}: {subject}")
148
+ return True
149
+ ```
150
+
151
+ You can still use the `@aq_worker.handler` decorator for function-based handlers—just make
152
+ sure the module containing the decorator is imported so the handler gets registered.
153
+
154
+ ### 2. Define Workers
155
+
156
+ ```python
157
+ # workers.py
158
+ from aqworker import BaseWorker, WorkerConfig
159
+
160
+ class EmailWorker(BaseWorker):
161
+ worker_name = "email"
162
+ worker_config = WorkerConfig(
163
+ queue_names=["emails"],
164
+ max_concurrent_jobs=3,
165
+ poll_interval=0.5,
166
+ )
167
+ ```
168
+
169
+ ### 3. Initialize AQWorker
170
+
171
+ ```python
172
+ # aq_worker.py
173
+ from aqworker import AQWorker
174
+ from aqworker.job.service import JobService
175
+ from workers import EmailWorker
176
+ from handlers import EmailHandler # optional when using include_packages
177
+
178
+ # Create JobService
179
+ job_service = JobService()
180
+
181
+ # Initialize AQWorker and auto-import handlers package
182
+ aq_worker = AQWorker(include_packages=["handlers"])
183
+
184
+ # Register workers and handlers
185
+ aq_worker.register_worker(EmailWorker)
186
+ # Handlers inside "handlers" will be discovered automatically, but you can still
187
+ # register manually if desired:
188
+ # aq_worker.register_handler(EmailHandler)
189
+
190
+ # Connect job service
191
+ aq_worker.listen(job_service)
192
+ ```
193
+
194
+ #### Automatic handler discovery
195
+
196
+ If your handlers are spread across multiple modules, you can tell `AQWorker` to
197
+ import them automatically by passing `include_packages`. Every module inside
198
+ those packages will be imported once at startup, so any `@aq_worker.handler`
199
+ decorators (or subclasses of `BaseHandler`) run and register themselves:
200
+
201
+ ```python
202
+ aq_worker = AQWorker(include_packages=[
203
+ "my_project.workers",
204
+ "examples.simple.aq_worker",
205
+ ])
206
+ ```
207
+
208
+ You can also call `aq_worker.autodiscover_handlers([...])` later if you need to
209
+ load additional packages dynamically. After discovery completes AQWorker logs the
210
+ full list of available handlers so you can confirm everything loaded correctly.
211
+
212
+ ### 4. Enqueue Jobs
213
+
214
+ ```python
215
+ # In your application
216
+ from worker import aq_worker
217
+
218
+ # Enqueue a job
219
+ job = await aq_worker.job_service.enqueue_job(
220
+ queue_name="emails",
221
+ handler="email",
222
+ data={
223
+ "recipient": "user@example.com",
224
+ "subject": "Welcome!",
225
+ "body": "Welcome to our service!"
226
+ }
227
+ )
228
+ ```
229
+
230
+ ### 5. Run Workers
231
+
232
+ **Using CLI:**
233
+
234
+ ```bash
235
+ # Option 1: Pass file path
236
+ aqworker start email aq_worker.py
237
+
238
+ # Option 2: Use environment variable
239
+ export AQWORKER_FILE=aq_worker.py
240
+ aqworker start email
241
+ ```
242
+
243
+ **Using Python:**
244
+
245
+ ```python
246
+ import asyncio
247
+ from worker import aq_worker
248
+
249
+ async def main():
250
+ worker = aq_worker.create_worker("email")
251
+ await worker.run()
252
+
253
+ asyncio.run(main())
254
+ ```
255
+
256
+ ## Usage Examples
257
+
258
+ ### Standalone Python Application
259
+
260
+ ```python
261
+ # main.py
262
+ import asyncio
263
+ from worker import aq_worker
264
+
265
+ async def main():
266
+ # Enqueue jobs
267
+ for i in range(10):
268
+ job = await aq_worker.job_service.enqueue_job(
269
+ queue_name="emails",
270
+ handler="email",
271
+ data={"recipient": f"user{i}@example.com", "subject": f"Email {i}"}
272
+ )
273
+ print(f"Enqueued job: {job.id}")
274
+
275
+ # Start aq_worker
276
+ worker = aq_worker.create_worker("email")
277
+ await worker.run()
278
+
279
+ asyncio.run(main())
280
+ ```
281
+
282
+ ### FastAPI Integration
283
+
284
+ The repo ships with a complete FastAPI + AQWorker demo under `examples/simple_fastapi/`.
285
+ Key files:
286
+
287
+ - `worker.py` – configures `AQWorker`, registers the example workers, and wires up the
288
+ shared `JobService`.
289
+ - `main.py` – FastAPI app that exposes `/jobs/email`, `/jobs/notification`, queue stats,
290
+ and discovery endpoints.
291
+ - `client.py` – small CLI client that calls the HTTP endpoints so you can watch jobs
292
+ flow through the queues.
293
+
294
+ Minimal excerpt of the worker wiring:
295
+
296
+ ```python
297
+ # examples/simple_fastapi/worker.py
298
+ from aqworker import AQWorker
299
+ from workers import EmailWorker, NotificationWorker
300
+ from job_service import job_service
301
+
302
+ aq_worker = AQWorker(include_packages=["handlers"])
303
+ aq_worker.register_worker(EmailWorker)
304
+ aq_worker.register_worker(NotificationWorker)
305
+ aq_worker.listen(job_service)
306
+ ```
307
+
308
+ To run the demo API and fire sample requests:
309
+
310
+ ```bash
311
+ cd examples/simple_fastapi
312
+ uv run python main.py # start FastAPI
313
+
314
+ # in another shell: send a job via the bundled client
315
+ uv run python client.py --recipient user@example.com --subject Test --body "Hello!"
316
+ ```
317
+
318
+ ### Django Integration
319
+
320
+ ```python
321
+ # views.py
322
+ from django.http import JsonResponse
323
+ from worker import aq_worker
324
+ import asyncio
325
+
326
+ async def send_email_view(request):
327
+ job = await aq_worker.job_service.enqueue_job(
328
+ queue_name="emails",
329
+ handler="email",
330
+ data={
331
+ "recipient": request.POST.get("recipient"),
332
+ "subject": request.POST.get("subject"),
333
+ "body": request.POST.get("body")
334
+ }
335
+ )
336
+ return JsonResponse({"job_id": job.id})
337
+
338
+ # Use asyncio.run or Django's async support
339
+ ```
340
+
341
+ ## CLI Commands
342
+
343
+ AQWorker provides a CLI tool for managing workers and jobs:
344
+
345
+ ### List Commands
346
+
347
+ ```bash
348
+ # List available workers
349
+ aqworker list:aq_worker [file_path]
350
+
351
+ # List registered handlers
352
+ aqworker list:handlers [file_path]
353
+
354
+ # List queues
355
+ aqworker list:queue [file_path]
356
+ ```
357
+
358
+ ### Worker Commands
359
+
360
+ ```bash
361
+ # Start a aq_worker
362
+ aqworker start <worker_name> [file_path]
363
+
364
+ # Get queue statistics
365
+ aqworker stats <queue_name> [file_path]
366
+ ```
367
+
368
+ ### Using Environment Variable
369
+
370
+ ```bash
371
+ # Set AQWorker file path
372
+ export AQWORKER_FILE=aq_worker.py
373
+
374
+ # Use commands without file path
375
+ aqworker list:aq_worker
376
+ aqworker start email
377
+ aqworker stats emails
378
+ ```
379
+
380
+ **Note**: The file path should point to a Python file containing an `AQWorker` instance. The CLI automatically finds the instance (looking for variables named `aq_worker`, `worker`, `aqworker`, or `aq`).
381
+
382
+ ## Configuration
383
+
384
+ ### Environment Variables
385
+
386
+ ```bash
387
+ # Redis Configuration (used when creating JobService)
388
+ REDIS_HOST=localhost
389
+ REDIS_PORT=6379
390
+ REDIS_DB=0
391
+
392
+ # AQWorker CLI
393
+ AQWORKER_FILE=aq_worker.py # Path to file containing AQWorker instance
394
+ ```
395
+
396
+ ### Worker Configuration
397
+
398
+ ```python
399
+ from aqworker import WorkerConfig
400
+
401
+ class MyWorker(BaseWorker):
402
+ worker_name = "my_worker"
403
+ worker_config = WorkerConfig(
404
+ queue_names=["my_queue"],
405
+ max_concurrent_jobs=5, # Max jobs processed simultaneously
406
+ poll_interval=0.5, # Seconds between queue polls
407
+ job_timeout=300, # Job timeout in seconds
408
+ )
409
+ ```
410
+
411
+ ## Job Lifecycle
412
+
413
+ ```
414
+ 1. ENQUEUE: Job created via JobService.enqueue_job()
415
+ 2. QUEUE: Job added to Redis queue (aqw:{queue_name})
416
+ 3. DEQUEUE: Worker gets job via JobService.get_next_job()
417
+ 4. PROCESS: Job moved to processing queue, handler executed
418
+ 5. COMPLETE: Job marked as completed/failed
419
+ 6. CLEANUP: Old jobs cleaned up automatically
420
+ ```
421
+
422
+ ## Queue System
423
+
424
+ ### Redis key prefixes
425
+
426
+ AQWorker keeps every Redis key namespaced with the `aqw` prefix so multiple apps can
427
+ share the same Redis without collisions. The most important keys are:
428
+
429
+ - `aqw:{queue_name}` – pending FIFO queue for a specific worker queue
430
+ - `aqw:processing`, `aqw:completed`, `aqw:failed` – global processing/completed/failed lists
431
+ - `aqw:job:{job_id}` – job status hash (timestamps, error state, etc.)
432
+ - `aqw:jl:{job_id}` – per-job lock key used to ensure only one worker processes the job
433
+
434
+ All public APIs use helpers from `aqworker.constants` (`get_queue_name`,
435
+ `get_job_status_key`, `get_job_lock_key`) so you rarely need to construct these strings
436
+ manually.
437
+
438
+ ### Queue Structure
439
+
440
+ ```
441
+ Redis Keys:
442
+ ├── aqw:emails # Email jobs (FIFO)
443
+ ├── aqw:notifications # Notification jobs (FIFO)
444
+ ├── aqw:processing # Jobs currently being processed
445
+ ├── aqw:completed # Successfully completed jobs
446
+ └── aqw:failed # Failed jobs
447
+ ```
448
+
449
+ ### Queue Isolation
450
+
451
+ - Each worker processes specific queue names
452
+ - Jobs are isolated by queue name
453
+ - No cross-queue interference
454
+ - Use different queues for different job types
455
+
456
+ ### FIFO Processing
457
+
458
+ - Simple first-in-first-out queue processing
459
+ - Jobs processed in order of arrival
460
+ - All jobs in a queue are processed equally
461
+ - Use different queue names for different priority levels
462
+
463
+ ## Advanced Usage
464
+
465
+ ### Custom Job Data
466
+
467
+ ```python
468
+ job = await aq_worker.job_service.enqueue_job(
469
+ queue_name="emails",
470
+ handler="email",
471
+ data={
472
+ "recipient": "user@example.com",
473
+ "subject": "Welcome",
474
+ "body": "Welcome!",
475
+ "attachments": ["file1.pdf", "file2.jpg"],
476
+ "priority": "high"
477
+ },
478
+ metadata={
479
+ "source": "api",
480
+ "user_id": "12345",
481
+ "campaign_id": "summer2024"
482
+ },
483
+ max_retries=5,
484
+ retry_delay=60
485
+ )
486
+ ```
487
+
488
+ ### Multiple Workers
489
+
490
+ ```python
491
+ # Register multiple workers
492
+ aq_worker.register_worker(EmailWorker)
493
+ aq_worker.register_worker(NotificationWorker)
494
+ aq_worker.register_worker(ReportWorker)
495
+
496
+ # Start specific aq_worker
497
+ worker = aq_worker.create_worker("email")
498
+ await worker.run()
499
+ ```
500
+
501
+ ### Handler with Async Support
502
+
503
+ ```python
504
+ class AsyncHandler(BaseHandler):
505
+ name = "async_task"
506
+
507
+ async def handle(self, data: dict) -> bool:
508
+ # Async operations
509
+ result = await some_async_operation(data)
510
+ return result is not None
511
+ ```
512
+
513
+ ## Best Practices
514
+
515
+ ### 1. Job Design
516
+ - Keep jobs small and focused
517
+ - Use descriptive handler names
518
+ - Include all necessary data in `data` parameter
519
+ - Use `metadata` for tracking/debugging information
520
+
521
+ ### 2. Worker Design
522
+ - Implement proper error handling in handlers
523
+ - Use appropriate concurrency levels
524
+ - Monitor job processing time
525
+ - Log important events
526
+
527
+ ### 3. Queue Management
528
+ - Use descriptive queue names
529
+ - Monitor queue depths
530
+ - Clean up old jobs regularly
531
+ - Balance load across workers
532
+
533
+ ### 4. Handler Design
534
+ - Make handlers idempotent when possible
535
+ - Handle errors gracefully
536
+ - Return `True` on success, `False` on failure
537
+ - Use async handlers for I/O operations
538
+
539
+ ## Troubleshooting
540
+
541
+ ### Common Issues
542
+
543
+ 1. **Jobs not processing**
544
+ - Check worker is running
545
+ - Verify queue names match between enqueue and worker
546
+ - Check Redis connection
547
+ - Verify handler is registered
548
+
549
+ 2. **High memory usage**
550
+ - Clean up old jobs: `await job_service.cleanup_old_jobs(days=7)`
551
+ - Reduce queue depths
552
+ - Optimize job data size
553
+
554
+ 3. **Slow processing**
555
+ - Increase worker concurrency (`max_concurrent_jobs`)
556
+ - Optimize handler logic
557
+ - Check Redis performance
558
+ - Consider multiple workers
559
+
560
+ ### Debugging
561
+
562
+ ```python
563
+ # Check queue statistics
564
+ stats = await aq_worker.job_service.get_queue_stats(["emails"])
565
+ print(f"Queue stats: {stats}")
566
+
567
+ # Get job status
568
+ job = await aq_worker.job_service.get_job(job_id)
569
+ print(f"Job status: {job.status}")
570
+
571
+ # List registered workers
572
+ workers = aq_worker.get_available_workers()
573
+ print(f"Available workers: {workers}")
574
+
575
+ # List registered handlers
576
+ handlers = aq_worker.handler_registry.snapshot()
577
+ print(f"Registered handlers: {list(handlers.keys())}")
578
+ ```
579
+
580
+ ## Examples
581
+
582
+ The `examples/` directory contains end-to-end demos:
583
+
584
+ ### `examples/simple/`
585
+ - Pure Python example with a background thread enqueuing email jobs while a worker
586
+ (created via `create_worker("email")`) processes them.
587
+ - Handlers live in `examples.simple.aq_worker.handlers` and are picked up through
588
+ `AQWorker(include_packages=["examples.simple.aq_worker"])`, so you can keep job logic
589
+ in separate modules without manual registration.
590
+ - Run `python -m examples.simple.main` to start the enqueue loop and worker concurrently
591
+ or use the CLI (`aqworker start email examples/simple/worker.py`) in separate shells.
592
+
593
+ ### `examples/simple_fastapi/`
594
+ - FastAPI service exposing REST endpoints for enqueuing jobs plus health/metadata
595
+ routes (`/handlers`, `/workers`, `/jobs/queues/{queue}/stats`).
596
+ - Worker definition in `examples/simple_fastapi/worker.py` registers worker classes and
597
+ auto-discovers handlers via `include_packages=["examples.simple_fastapi.handlers"]`.
598
+ - `examples/simple_fastapi/handlers.py` simulates real work with 0.5–2 s `asyncio.sleep`
599
+ delays so you can observe concurrent processing.
600
+ - `examples/simple_fastapi/client.py` is an `httpx` script that continuously enqueues
601
+ email + notification jobs every ~0.2 s—perfect for smoke-testing the API while
602
+ watching worker logs scroll.
603
+
604
+ ## Contributing
605
+
606
+ When adding new features:
607
+ 1. Follow async/await patterns
608
+ 2. Add proper error handling
609
+ 3. Update documentation
610
+ 4. Add tests
611
+ 5. Consider performance impact
612
+
613
+ ## License
614
+
615
+ This project is licensed under the terms specified in the LICENSE file.