flashq 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.
- flashq-0.1.0/.github/workflows/ci.yml +77 -0
- flashq-0.1.0/.gitignore +43 -0
- flashq-0.1.0/CONTRIBUTING.md +82 -0
- flashq-0.1.0/LICENSE +21 -0
- flashq-0.1.0/PKG-INFO +389 -0
- flashq-0.1.0/README.md +343 -0
- flashq-0.1.0/docs/research/CELERY-DEEP-ANALYSIS.md +474 -0
- flashq-0.1.0/docs/research/FINAL-REALITY-CHECK.md +309 -0
- flashq-0.1.0/docs/research/LAUNCH-STRATEGY.md +256 -0
- flashq-0.1.0/docs/research/OPPORTUNITY-ANALYSIS.md +326 -0
- flashq-0.1.0/docs/research/REALITY-CHECK-REPORT.md +232 -0
- flashq-0.1.0/docs/research/UPDATED-MARKET-ANALYSIS.md +154 -0
- flashq-0.1.0/examples/advanced.py +102 -0
- flashq-0.1.0/examples/basic.py +43 -0
- flashq-0.1.0/examples/fastapi_app.py +59 -0
- flashq-0.1.0/flashq/__init__.py +91 -0
- flashq-0.1.0/flashq/__main__.py +5 -0
- flashq-0.1.0/flashq/_version.py +3 -0
- flashq-0.1.0/flashq/app.py +281 -0
- flashq-0.1.0/flashq/backends/__init__.py +174 -0
- flashq-0.1.0/flashq/backends/postgres.py +363 -0
- flashq-0.1.0/flashq/backends/redis.py +288 -0
- flashq-0.1.0/flashq/backends/sqlite.py +473 -0
- flashq-0.1.0/flashq/canvas.py +302 -0
- flashq-0.1.0/flashq/cli.py +219 -0
- flashq-0.1.0/flashq/contrib/__init__.py +1 -0
- flashq-0.1.0/flashq/dashboard/__init__.py +1 -0
- flashq-0.1.0/flashq/dlq.py +173 -0
- flashq-0.1.0/flashq/enums.py +58 -0
- flashq-0.1.0/flashq/exceptions.py +59 -0
- flashq-0.1.0/flashq/middleware.py +212 -0
- flashq-0.1.0/flashq/models.py +146 -0
- flashq-0.1.0/flashq/py.typed +0 -0
- flashq-0.1.0/flashq/ratelimit.py +192 -0
- flashq-0.1.0/flashq/scheduler.py +216 -0
- flashq-0.1.0/flashq/serializers.py +75 -0
- flashq-0.1.0/flashq/task.py +259 -0
- flashq-0.1.0/flashq/worker.py +420 -0
- flashq-0.1.0/pyproject.toml +103 -0
- flashq-0.1.0/tests/__init__.py +0 -0
- flashq-0.1.0/tests/test_backends.py +149 -0
- flashq-0.1.0/tests/test_canvas.py +307 -0
- flashq-0.1.0/tests/test_cli.py +271 -0
- flashq-0.1.0/tests/test_core.py +510 -0
- flashq-0.1.0/tests/test_dlq.py +214 -0
- flashq-0.1.0/tests/test_middleware.py +401 -0
- flashq-0.1.0/tests/test_ratelimit.py +183 -0
- flashq-0.1.0/tests/test_scheduler.py +147 -0
- flashq-0.1.0/tests/test_serializers.py +61 -0
- flashq-0.1.0/tests/test_worker.py +258 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
runs-on: ${{ matrix.os }}
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
17
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: pip install -e ".[dev]"
|
|
29
|
+
|
|
30
|
+
- name: Lint
|
|
31
|
+
run: ruff check flashq/
|
|
32
|
+
|
|
33
|
+
- name: Format check
|
|
34
|
+
run: ruff format flashq/ --check
|
|
35
|
+
|
|
36
|
+
- name: Type check
|
|
37
|
+
run: mypy flashq/ --ignore-missing-imports
|
|
38
|
+
continue-on-error: true
|
|
39
|
+
|
|
40
|
+
- name: Test
|
|
41
|
+
run: pytest tests/ -v --tb=short
|
|
42
|
+
|
|
43
|
+
coverage:
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
needs: test
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@v4
|
|
48
|
+
- uses: actions/setup-python@v5
|
|
49
|
+
with:
|
|
50
|
+
python-version: "3.12"
|
|
51
|
+
- run: pip install -e ".[dev]"
|
|
52
|
+
- run: pytest tests/ --cov=flashq --cov-report=term-missing --cov-report=xml
|
|
53
|
+
- name: Upload coverage
|
|
54
|
+
uses: codecov/codecov-action@v4
|
|
55
|
+
with:
|
|
56
|
+
file: coverage.xml
|
|
57
|
+
continue-on-error: true
|
|
58
|
+
|
|
59
|
+
publish:
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
needs: [test, coverage]
|
|
62
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
63
|
+
permissions:
|
|
64
|
+
id-token: write
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@v4
|
|
67
|
+
- uses: actions/setup-python@v5
|
|
68
|
+
with:
|
|
69
|
+
python-version: "3.12"
|
|
70
|
+
- name: Install build tools
|
|
71
|
+
run: pip install build
|
|
72
|
+
- name: Build
|
|
73
|
+
run: python -m build
|
|
74
|
+
- name: Publish to PyPI
|
|
75
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
76
|
+
with:
|
|
77
|
+
password: ${{ secrets.PYPI_TOKEN }}
|
flashq-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Byte-compiled
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.egg
|
|
11
|
+
|
|
12
|
+
# Virtual env
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
env/
|
|
16
|
+
|
|
17
|
+
# IDE
|
|
18
|
+
.vscode/
|
|
19
|
+
.idea/
|
|
20
|
+
*.swp
|
|
21
|
+
*.swo
|
|
22
|
+
|
|
23
|
+
# Testing
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
htmlcov/
|
|
26
|
+
.coverage
|
|
27
|
+
.coverage.*
|
|
28
|
+
|
|
29
|
+
# mypy
|
|
30
|
+
.mypy_cache/
|
|
31
|
+
|
|
32
|
+
# ruff
|
|
33
|
+
.ruff_cache/
|
|
34
|
+
|
|
35
|
+
# FlashQ database files
|
|
36
|
+
*.db
|
|
37
|
+
*.db-wal
|
|
38
|
+
*.db-shm
|
|
39
|
+
|
|
40
|
+
# OS
|
|
41
|
+
.DS_Store
|
|
42
|
+
Thumbs.db
|
|
43
|
+
reddit_research.txt
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Contributing to FlashQ
|
|
2
|
+
|
|
3
|
+
Thanks for considering contributing to FlashQ! Here's how to get started.
|
|
4
|
+
|
|
5
|
+
## Development Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/bysiber/flashq.git
|
|
9
|
+
cd flashq
|
|
10
|
+
python -m venv .venv
|
|
11
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
12
|
+
pip install -e ".[dev]"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Running Tests
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# All tests
|
|
19
|
+
pytest tests/ -v
|
|
20
|
+
|
|
21
|
+
# With coverage
|
|
22
|
+
pytest tests/ --cov=flashq --cov-report=term-missing
|
|
23
|
+
|
|
24
|
+
# Specific test file
|
|
25
|
+
pytest tests/test_core.py -v
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Code Quality
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Lint
|
|
32
|
+
ruff check flashq/
|
|
33
|
+
|
|
34
|
+
# Format
|
|
35
|
+
ruff format flashq/
|
|
36
|
+
|
|
37
|
+
# Type check
|
|
38
|
+
mypy flashq/ --ignore-missing-imports
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Pull Request Process
|
|
42
|
+
|
|
43
|
+
1. Fork the repository
|
|
44
|
+
2. Create a feature branch (`git checkout -b feature/my-feature`)
|
|
45
|
+
3. Write tests for your changes
|
|
46
|
+
4. Ensure all tests pass and linting is clean
|
|
47
|
+
5. Submit a PR with a clear description
|
|
48
|
+
|
|
49
|
+
## Architecture Overview
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
flashq/
|
|
53
|
+
├── app.py # FlashQ class, task registry
|
|
54
|
+
├── task.py # Task decorator, TaskHandle
|
|
55
|
+
├── models.py # TaskMessage, TaskResult dataclasses
|
|
56
|
+
├── worker.py # Worker process, task execution
|
|
57
|
+
├── middleware.py # Middleware system
|
|
58
|
+
├── scheduler.py # Periodic/cron task scheduler
|
|
59
|
+
├── backends/
|
|
60
|
+
│ ├── __init__.py # BaseBackend ABC
|
|
61
|
+
│ ├── sqlite.py # SQLite backend (default)
|
|
62
|
+
│ ├── postgres.py # PostgreSQL backend
|
|
63
|
+
│ └── redis.py # Redis backend
|
|
64
|
+
├── serializers.py # JSON/Pickle serialization
|
|
65
|
+
├── cli.py # Command-line interface
|
|
66
|
+
├── enums.py # TaskState, TaskPriority
|
|
67
|
+
└── exceptions.py # Exception hierarchy
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Adding a New Backend
|
|
71
|
+
|
|
72
|
+
1. Subclass `BaseBackend` from `flashq.backends`
|
|
73
|
+
2. Implement all abstract methods
|
|
74
|
+
3. Add tests in `tests/test_backend_yourname.py`
|
|
75
|
+
4. Add optional dependency in `pyproject.toml`
|
|
76
|
+
|
|
77
|
+
## Code Style
|
|
78
|
+
|
|
79
|
+
- Python 3.10+ features are welcome
|
|
80
|
+
- Type hints everywhere
|
|
81
|
+
- Docstrings for public APIs
|
|
82
|
+
- Keep imports sorted (ruff handles this)
|
flashq-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Ozden
|
|
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.
|
flashq-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flashq
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The task queue that works out of the box — no Redis, no RabbitMQ, just pip install and go.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ozden/flashq
|
|
6
|
+
Project-URL: Documentation, https://flashq.readthedocs.io
|
|
7
|
+
Project-URL: Repository, https://github.com/ozden/flashq
|
|
8
|
+
Project-URL: Issues, https://github.com/ozden/flashq/issues
|
|
9
|
+
Author: Ozden
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: async,background-tasks,celery,job-queue,task-queue
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Provides-Extra: all
|
|
27
|
+
Requires-Dist: psycopg[binary,pool]>=3.1; extra == 'all'
|
|
28
|
+
Requires-Dist: redis>=5.0; extra == 'all'
|
|
29
|
+
Requires-Dist: starlette>=0.36; extra == 'all'
|
|
30
|
+
Requires-Dist: uvicorn>=0.27; extra == 'all'
|
|
31
|
+
Provides-Extra: dashboard
|
|
32
|
+
Requires-Dist: starlette>=0.36; extra == 'dashboard'
|
|
33
|
+
Requires-Dist: uvicorn>=0.27; extra == 'dashboard'
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
36
|
+
Requires-Dist: pre-commit>=3.6; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: ruff>=0.2; extra == 'dev'
|
|
41
|
+
Provides-Extra: postgres
|
|
42
|
+
Requires-Dist: psycopg[binary,pool]>=3.1; extra == 'postgres'
|
|
43
|
+
Provides-Extra: redis
|
|
44
|
+
Requires-Dist: redis>=5.0; extra == 'redis'
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
# ⚡ FlashQ
|
|
48
|
+
|
|
49
|
+
**The task queue that works out of the box — no Redis, no RabbitMQ, just `pip install flashq` and go.**
|
|
50
|
+
|
|
51
|
+
[](https://github.com/bysiber/flashq/actions/workflows/ci.yml)
|
|
52
|
+
[](https://pypi.org/project/flashq/)
|
|
53
|
+
[](LICENSE)
|
|
54
|
+
[]()
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Why FlashQ?
|
|
59
|
+
|
|
60
|
+
Every Python developer has been there: you need background tasks, you look at Celery, and suddenly you need Redis or RabbitMQ running, a separate broker config, and 200 lines of boilerplate before your first task runs.
|
|
61
|
+
|
|
62
|
+
**FlashQ changes that.** SQLite is the default backend — zero external dependencies, zero config. Your tasks persist across restarts, and you can scale to PostgreSQL or Redis when you need to.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from flashq import FlashQ
|
|
66
|
+
|
|
67
|
+
app = FlashQ() # That's it. Uses SQLite by default.
|
|
68
|
+
|
|
69
|
+
@app.task()
|
|
70
|
+
def send_email(to: str, subject: str) -> None:
|
|
71
|
+
print(f"Sending email to {to}: {subject}")
|
|
72
|
+
|
|
73
|
+
# Enqueue a task
|
|
74
|
+
send_email.delay(to="user@example.com", subject="Welcome!")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Features
|
|
78
|
+
|
|
79
|
+
| Feature | FlashQ | Celery | Dramatiq | Huey | TaskIQ |
|
|
80
|
+
|---------|:------:|:------:|:--------:|:----:|:------:|
|
|
81
|
+
| Zero-config setup | ✅ | ❌ | ❌ | ⚠️ | ❌ |
|
|
82
|
+
| SQLite backend | ✅ | ❌ | ❌ | ✅ | ❌ |
|
|
83
|
+
| PostgreSQL backend | ✅ | ❌ | ❌ | ❌ | ⚠️ |
|
|
84
|
+
| Redis backend | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
85
|
+
| Async + Sync tasks | ✅ | ❌ | ❌ | ❌ | Async only |
|
|
86
|
+
| Type-safe `.delay()` | ✅ | ❌ | ⚠️ | ❌ | ✅ |
|
|
87
|
+
| Task chains/groups | ✅ | ✅ | ❌ | ❌ | ❌ |
|
|
88
|
+
| Middleware system | ✅ | ✅ | ✅ | ❌ | ✅ |
|
|
89
|
+
| Rate limiting | ✅ | ✅ | ❌ | ❌ | ❌ |
|
|
90
|
+
| Dead letter queue | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
91
|
+
| Task timeouts | ✅ | ✅ | ✅ | ❌ | ✅ |
|
|
92
|
+
| Periodic/cron scheduler | ✅ | ⚠️ | ❌ | ✅ | ⚠️ |
|
|
93
|
+
| Zero dependencies | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
94
|
+
|
|
95
|
+
## Installation
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Core (SQLite only — zero dependencies!)
|
|
99
|
+
pip install flashq
|
|
100
|
+
|
|
101
|
+
# With Redis
|
|
102
|
+
pip install "flashq[redis]"
|
|
103
|
+
|
|
104
|
+
# With PostgreSQL
|
|
105
|
+
pip install "flashq[postgres]"
|
|
106
|
+
|
|
107
|
+
# Development
|
|
108
|
+
pip install "flashq[dev]"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Quick Start
|
|
112
|
+
|
|
113
|
+
### 1. Define tasks
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# tasks.py
|
|
117
|
+
from flashq import FlashQ
|
|
118
|
+
|
|
119
|
+
app = FlashQ()
|
|
120
|
+
|
|
121
|
+
@app.task()
|
|
122
|
+
def add(x: int, y: int) -> int:
|
|
123
|
+
return x + y
|
|
124
|
+
|
|
125
|
+
@app.task(queue="emails", max_retries=5, retry_delay=30.0)
|
|
126
|
+
def send_email(to: str, subject: str, body: str) -> dict:
|
|
127
|
+
return {"status": "sent", "to": to}
|
|
128
|
+
|
|
129
|
+
@app.task(timeout=120.0) # Kill if takes >2 min
|
|
130
|
+
async def process_image(url: str) -> str:
|
|
131
|
+
# Async tasks just work™
|
|
132
|
+
result = await download_and_resize(url)
|
|
133
|
+
return result
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 2. Enqueue tasks
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from tasks import add, send_email
|
|
140
|
+
|
|
141
|
+
# Simple dispatch
|
|
142
|
+
handle = add.delay(2, 3)
|
|
143
|
+
|
|
144
|
+
# With options
|
|
145
|
+
handle = send_email.apply(
|
|
146
|
+
kwargs={"to": "user@example.com", "subject": "Hi", "body": "Hello!"},
|
|
147
|
+
countdown=60, # delay by 60 seconds
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Check result
|
|
151
|
+
result = handle.get_result()
|
|
152
|
+
if result and result.is_success:
|
|
153
|
+
print(f"Result: {result.result}")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 3. Start the worker
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
flashq worker tasks:app
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
⚡ FlashQ Worker
|
|
164
|
+
├─ name: worker-12345
|
|
165
|
+
├─ backend: SQLiteBackend
|
|
166
|
+
├─ queues: default
|
|
167
|
+
├─ concurrency: 4
|
|
168
|
+
├─ tasks: 3
|
|
169
|
+
│ └─ tasks.add
|
|
170
|
+
│ └─ tasks.send_email
|
|
171
|
+
│ └─ tasks.process_image
|
|
172
|
+
└─ Ready! Waiting for tasks...
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Task Composition
|
|
176
|
+
|
|
177
|
+
Chain tasks sequentially, run them in parallel, or combine both:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from flashq import chain, group, chord
|
|
181
|
+
|
|
182
|
+
# Chain: sequential — result of each passed to next
|
|
183
|
+
pipe = chain(
|
|
184
|
+
download.s("https://example.com/data.csv"),
|
|
185
|
+
parse_csv.s(),
|
|
186
|
+
store_results.s(table="imports"),
|
|
187
|
+
)
|
|
188
|
+
pipe.delay(app)
|
|
189
|
+
|
|
190
|
+
# Group: parallel execution
|
|
191
|
+
batch = group(
|
|
192
|
+
send_email.s(to="alice@test.com", subject="Hi"),
|
|
193
|
+
send_email.s(to="bob@test.com", subject="Hi"),
|
|
194
|
+
send_email.s(to="carol@test.com", subject="Hi"),
|
|
195
|
+
)
|
|
196
|
+
handle = batch.delay(app)
|
|
197
|
+
results = handle.get_results(timeout=30)
|
|
198
|
+
|
|
199
|
+
# Chord: parallel + callback when all complete
|
|
200
|
+
workflow = chord(
|
|
201
|
+
group(fetch_price.s("AAPL"), fetch_price.s("GOOG"), fetch_price.s("MSFT")),
|
|
202
|
+
aggregate_prices.s(),
|
|
203
|
+
)
|
|
204
|
+
workflow.delay(app)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Middleware
|
|
208
|
+
|
|
209
|
+
Intercept task lifecycle events for logging, monitoring, or custom logic:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from flashq import FlashQ, Middleware
|
|
213
|
+
|
|
214
|
+
class MetricsMiddleware(Middleware):
|
|
215
|
+
def before_execute(self, message):
|
|
216
|
+
self.start = time.time()
|
|
217
|
+
return message
|
|
218
|
+
|
|
219
|
+
def after_execute(self, message, result):
|
|
220
|
+
duration = time.time() - self.start
|
|
221
|
+
statsd.timing(f"task.{message.task_name}.duration", duration)
|
|
222
|
+
|
|
223
|
+
def on_error(self, message, exc):
|
|
224
|
+
sentry.capture_exception(exc)
|
|
225
|
+
return False # Don't suppress
|
|
226
|
+
|
|
227
|
+
def on_dead(self, message, exc):
|
|
228
|
+
alert_ops_team(f"Task {message.task_name} permanently failed: {exc}")
|
|
229
|
+
|
|
230
|
+
app = FlashQ()
|
|
231
|
+
app.add_middleware(MetricsMiddleware())
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Built-in middlewares: `LoggingMiddleware`, `TimeoutMiddleware`, `RateLimiter`.
|
|
235
|
+
|
|
236
|
+
## Rate Limiting
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from flashq.ratelimit import RateLimiter
|
|
240
|
+
|
|
241
|
+
limiter = RateLimiter(default_rate="100/m") # 100 tasks/minute global
|
|
242
|
+
limiter.configure("send_email", rate="10/m") # 10 emails/minute
|
|
243
|
+
limiter.configure("api_call", rate="60/h") # 60 API calls/hour
|
|
244
|
+
|
|
245
|
+
app.add_middleware(limiter)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Dead Letter Queue
|
|
249
|
+
|
|
250
|
+
Inspect and replay permanently failed tasks:
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
from flashq.dlq import DeadLetterQueue
|
|
254
|
+
|
|
255
|
+
dlq = DeadLetterQueue(app)
|
|
256
|
+
app.add_middleware(dlq.middleware()) # Auto-capture dead tasks
|
|
257
|
+
|
|
258
|
+
# Later...
|
|
259
|
+
for task in dlq.list():
|
|
260
|
+
print(f"{task.task_name}: {task.error}")
|
|
261
|
+
|
|
262
|
+
dlq.replay(task_id="abc123") # Re-enqueue with reset retries
|
|
263
|
+
dlq.replay_all() # Replay everything
|
|
264
|
+
dlq.purge() # Clear DLQ
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Periodic Tasks
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
from flashq import FlashQ, every, cron
|
|
271
|
+
from flashq.scheduler import Scheduler
|
|
272
|
+
|
|
273
|
+
app = FlashQ()
|
|
274
|
+
|
|
275
|
+
@app.task(name="cleanup")
|
|
276
|
+
def cleanup_old_data():
|
|
277
|
+
delete_old_records(days=30)
|
|
278
|
+
|
|
279
|
+
@app.task(name="daily_report")
|
|
280
|
+
def daily_report():
|
|
281
|
+
generate_and_send_report()
|
|
282
|
+
|
|
283
|
+
scheduler = Scheduler(app)
|
|
284
|
+
scheduler.add("cleanup", every(hours=6))
|
|
285
|
+
scheduler.add("daily_report", cron("0 9 * * 1-5")) # 9 AM weekdays
|
|
286
|
+
scheduler.start()
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Retry & Error Handling
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
@app.task(max_retries=5, retry_delay=30.0, retry_backoff=True)
|
|
293
|
+
def flaky_task():
|
|
294
|
+
# Retries: 30s → 60s → 120s → 240s → 480s (exponential backoff)
|
|
295
|
+
response = requests.get("https://unreliable-api.com")
|
|
296
|
+
response.raise_for_status()
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
from flashq.exceptions import TaskRetryError
|
|
301
|
+
|
|
302
|
+
@app.task(max_retries=10)
|
|
303
|
+
def smart_retry():
|
|
304
|
+
try:
|
|
305
|
+
do_something()
|
|
306
|
+
except TemporaryError:
|
|
307
|
+
raise TaskRetryError(countdown=5.0) # Custom retry delay
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Backends
|
|
311
|
+
|
|
312
|
+
### SQLite (Default — Zero Config)
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
app = FlashQ() # Creates flashq.db in current dir
|
|
316
|
+
app = FlashQ(backend=SQLiteBackend(path="/var/lib/flashq/tasks.db"))
|
|
317
|
+
app = FlashQ(backend=SQLiteBackend(path=":memory:")) # For testing
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### PostgreSQL
|
|
321
|
+
|
|
322
|
+
Uses `LISTEN/NOTIFY` for instant task delivery + `FOR UPDATE SKIP LOCKED` for atomic dequeue.
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
from flashq.backends.postgres import PostgresBackend
|
|
326
|
+
app = FlashQ(backend=PostgresBackend("postgresql://localhost/mydb"))
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Redis
|
|
330
|
+
|
|
331
|
+
Uses sorted sets for scheduling and Lua scripts for atomic operations.
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
from flashq.backends.redis import RedisBackend
|
|
335
|
+
app = FlashQ(backend=RedisBackend("redis://localhost:6379/0"))
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## CLI
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
flashq worker myapp:app # Start worker
|
|
342
|
+
flashq worker myapp:app -q emails,sms # Specific queues
|
|
343
|
+
flashq worker myapp:app -c 16 # 16 concurrent threads
|
|
344
|
+
flashq info myapp:app # Queue stats
|
|
345
|
+
flashq purge myapp:app -f # Purge queue
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Architecture
|
|
349
|
+
|
|
350
|
+
```
|
|
351
|
+
Your App → FlashQ → Backend (SQLite/PG/Redis) → Worker(s)
|
|
352
|
+
↕
|
|
353
|
+
Middleware Stack
|
|
354
|
+
Rate Limiter
|
|
355
|
+
Scheduler
|
|
356
|
+
Dead Letter Queue
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
FlashQ uses a clean, modular architecture:
|
|
360
|
+
- **Backend**: Pluggable storage (SQLite, PostgreSQL, Redis)
|
|
361
|
+
- **Worker**: Thread pool executor with graceful shutdown
|
|
362
|
+
- **Middleware**: Intercepts every stage of task lifecycle
|
|
363
|
+
- **Scheduler**: Interval and cron-based periodic dispatch
|
|
364
|
+
- **Canvas**: Task composition (chain, group, chord)
|
|
365
|
+
|
|
366
|
+
## Roadmap
|
|
367
|
+
|
|
368
|
+
- [x] Core engine with SQLite backend
|
|
369
|
+
- [x] PostgreSQL backend (LISTEN/NOTIFY)
|
|
370
|
+
- [x] Redis backend (Lua scripts)
|
|
371
|
+
- [x] Task timeouts (non-blocking)
|
|
372
|
+
- [x] Middleware system
|
|
373
|
+
- [x] Rate limiting (token bucket)
|
|
374
|
+
- [x] Dead letter queue
|
|
375
|
+
- [x] Task chains, groups, chords
|
|
376
|
+
- [x] Periodic/cron scheduler
|
|
377
|
+
- [x] CLI (worker, info, purge)
|
|
378
|
+
- [x] 226 tests, 95% core coverage
|
|
379
|
+
- [ ] Web dashboard
|
|
380
|
+
- [ ] Task result streaming
|
|
381
|
+
- [ ] PyPI publish
|
|
382
|
+
|
|
383
|
+
## Contributing
|
|
384
|
+
|
|
385
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
|
|
386
|
+
|
|
387
|
+
## License
|
|
388
|
+
|
|
389
|
+
MIT License. See [LICENSE](LICENSE) for details.
|