pydocket 0.11.1__tar.gz → 0.12.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.

Potentially problematic release.


This version of pydocket might be problematic. Click here for more details.

Files changed (87) hide show
  1. pydocket-0.12.0/.coveragerc-memory +10 -0
  2. pydocket-0.12.0/.github/workflows/ci.yml +107 -0
  3. {pydocket-0.11.1 → pydocket-0.12.0}/.github/workflows/claude-code-review.yml +1 -1
  4. {pydocket-0.11.1 → pydocket-0.12.0}/.github/workflows/claude.yml +1 -1
  5. {pydocket-0.11.1 → pydocket-0.12.0}/.gitignore +2 -0
  6. {pydocket-0.11.1 → pydocket-0.12.0}/CLAUDE.md +1 -3
  7. {pydocket-0.11.1 → pydocket-0.12.0}/PKG-INFO +21 -3
  8. {pydocket-0.11.1 → pydocket-0.12.0}/README.md +14 -1
  9. {pydocket-0.11.1 → pydocket-0.12.0}/docs/dependencies.md +135 -40
  10. {pydocket-0.11.1 → pydocket-0.12.0}/docs/getting-started.md +1 -1
  11. {pydocket-0.11.1 → pydocket-0.12.0}/docs/testing.md +70 -0
  12. pydocket-0.12.0/examples/fastapi_background_tasks.py +204 -0
  13. pydocket-0.12.0/examples/local_development.py +98 -0
  14. {pydocket-0.11.1 → pydocket-0.12.0}/pyproject.toml +26 -4
  15. pydocket-0.12.0/sitecustomize.py +7 -0
  16. {pydocket-0.11.1 → pydocket-0.12.0}/src/docket/annotations.py +3 -1
  17. {pydocket-0.11.1 → pydocket-0.12.0}/src/docket/cli.py +35 -8
  18. {pydocket-0.11.1 → pydocket-0.12.0}/src/docket/dependencies.py +83 -22
  19. {pydocket-0.11.1 → pydocket-0.12.0}/src/docket/docket.py +28 -3
  20. {pydocket-0.11.1 → pydocket-0.12.0}/src/docket/execution.py +10 -11
  21. {pydocket-0.11.1 → pydocket-0.12.0}/src/docket/instrumentation.py +8 -1
  22. {pydocket-0.11.1 → pydocket-0.12.0}/src/docket/tasks.py +2 -2
  23. {pydocket-0.11.1 → pydocket-0.12.0}/src/docket/worker.py +29 -19
  24. {pydocket-0.11.1 → pydocket-0.12.0}/tests/cli/test_clear.py +71 -122
  25. pydocket-0.12.0/tests/cli/test_module.py +10 -0
  26. {pydocket-0.11.1 → pydocket-0.12.0}/tests/cli/test_parsing.py +7 -0
  27. {pydocket-0.11.1 → pydocket-0.12.0}/tests/cli/test_snapshot.py +132 -118
  28. {pydocket-0.11.1 → pydocket-0.12.0}/tests/cli/test_striking.py +78 -108
  29. {pydocket-0.11.1 → pydocket-0.12.0}/tests/cli/test_tasks.py +32 -45
  30. pydocket-0.12.0/tests/cli/test_url_validation.py +59 -0
  31. {pydocket-0.11.1 → pydocket-0.12.0}/tests/cli/test_version.py +5 -6
  32. {pydocket-0.11.1 → pydocket-0.12.0}/tests/cli/test_worker.py +45 -68
  33. {pydocket-0.11.1 → pydocket-0.12.0}/tests/cli/test_workers.py +27 -29
  34. pydocket-0.12.0/tests/cli/utils.py +53 -0
  35. pydocket-0.12.0/tests/conftest.py +209 -0
  36. {pydocket-0.11.1 → pydocket-0.12.0}/tests/test_dependencies.py +189 -0
  37. {pydocket-0.11.1 → pydocket-0.12.0}/tests/test_fundamentals.py +222 -4
  38. pydocket-0.12.0/tests/test_memory_backend.py +113 -0
  39. {pydocket-0.11.1 → pydocket-0.12.0}/tests/test_worker.py +12 -0
  40. pydocket-0.12.0/uv.lock +2212 -0
  41. pydocket-0.11.1/.github/workflows/ci.yml +0 -65
  42. pydocket-0.11.1/tests/cli/conftest.py +0 -8
  43. pydocket-0.11.1/tests/cli/test_module.py +0 -22
  44. pydocket-0.11.1/tests/conftest.py +0 -180
  45. pydocket-0.11.1/uv.lock +0 -1444
  46. {pydocket-0.11.1 → pydocket-0.12.0}/.cursor/rules/general.mdc +0 -0
  47. {pydocket-0.11.1 → pydocket-0.12.0}/.cursor/rules/python-style.mdc +0 -0
  48. {pydocket-0.11.1 → pydocket-0.12.0}/.github/codecov.yml +0 -0
  49. {pydocket-0.11.1 → pydocket-0.12.0}/.github/workflows/chaos.yml +0 -0
  50. {pydocket-0.11.1 → pydocket-0.12.0}/.github/workflows/docs.yml +0 -0
  51. {pydocket-0.11.1 → pydocket-0.12.0}/.github/workflows/publish.yml +0 -0
  52. {pydocket-0.11.1 → pydocket-0.12.0}/.pre-commit-config.yaml +0 -0
  53. {pydocket-0.11.1 → pydocket-0.12.0}/LICENSE +0 -0
  54. {pydocket-0.11.1 → pydocket-0.12.0}/chaos/README.md +0 -0
  55. {pydocket-0.11.1 → pydocket-0.12.0}/chaos/__init__.py +0 -0
  56. {pydocket-0.11.1 → pydocket-0.12.0}/chaos/driver.py +0 -0
  57. {pydocket-0.11.1 → pydocket-0.12.0}/chaos/producer.py +0 -0
  58. {pydocket-0.11.1 → pydocket-0.12.0}/chaos/run +0 -0
  59. {pydocket-0.11.1 → pydocket-0.12.0}/chaos/tasks.py +0 -0
  60. {pydocket-0.11.1 → pydocket-0.12.0}/docs/advanced-patterns.md +0 -0
  61. {pydocket-0.11.1 → pydocket-0.12.0}/docs/api-reference.md +0 -0
  62. {pydocket-0.11.1 → pydocket-0.12.0}/docs/index.md +0 -0
  63. {pydocket-0.11.1 → pydocket-0.12.0}/docs/production.md +0 -0
  64. {pydocket-0.11.1 → pydocket-0.12.0}/examples/__init__.py +0 -0
  65. {pydocket-0.11.1 → pydocket-0.12.0}/examples/agenda_scatter.py +0 -0
  66. {pydocket-0.11.1 → pydocket-0.12.0}/examples/common.py +0 -0
  67. {pydocket-0.11.1 → pydocket-0.12.0}/examples/concurrency_control.py +0 -0
  68. {pydocket-0.11.1 → pydocket-0.12.0}/examples/find_and_flood.py +0 -0
  69. {pydocket-0.11.1 → pydocket-0.12.0}/examples/self_perpetuating.py +0 -0
  70. {pydocket-0.11.1 → pydocket-0.12.0}/mkdocs.yml +0 -0
  71. {pydocket-0.11.1 → pydocket-0.12.0}/src/docket/__init__.py +0 -0
  72. {pydocket-0.11.1 → pydocket-0.12.0}/src/docket/__main__.py +0 -0
  73. {pydocket-0.11.1 → pydocket-0.12.0}/src/docket/agenda.py +0 -0
  74. {pydocket-0.11.1 → pydocket-0.12.0}/src/docket/py.typed +0 -0
  75. {pydocket-0.11.1 → pydocket-0.12.0}/telemetry/.gitignore +0 -0
  76. {pydocket-0.11.1 → pydocket-0.12.0}/telemetry/start +0 -0
  77. {pydocket-0.11.1 → pydocket-0.12.0}/telemetry/stop +0 -0
  78. {pydocket-0.11.1 → pydocket-0.12.0}/tests/__init__.py +0 -0
  79. {pydocket-0.11.1 → pydocket-0.12.0}/tests/cli/__init__.py +0 -0
  80. {pydocket-0.11.1 → pydocket-0.12.0}/tests/test_agenda.py +0 -0
  81. {pydocket-0.11.1 → pydocket-0.12.0}/tests/test_concurrency_basic.py +0 -0
  82. {pydocket-0.11.1 → pydocket-0.12.0}/tests/test_concurrency_control.py +0 -0
  83. {pydocket-0.11.1 → pydocket-0.12.0}/tests/test_concurrency_refresh.py +0 -0
  84. {pydocket-0.11.1 → pydocket-0.12.0}/tests/test_docket.py +0 -0
  85. {pydocket-0.11.1 → pydocket-0.12.0}/tests/test_execution.py +0 -0
  86. {pydocket-0.11.1 → pydocket-0.12.0}/tests/test_instrumentation.py +0 -0
  87. {pydocket-0.11.1 → pydocket-0.12.0}/tests/test_striking.py +0 -0
@@ -0,0 +1,10 @@
1
+ # Coverage configuration for memory backend testing
2
+ # CLI tests are skipped with memory:// URLs, so exclude CLI from coverage
3
+
4
+ [run]
5
+ branch = true
6
+ parallel = true
7
+ omit =
8
+ src/docket/__main__.py
9
+ src/docket/cli.py
10
+ tests/cli/test_*.py
@@ -0,0 +1,107 @@
1
+ name: Docket CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_call:
8
+
9
+ jobs:
10
+ test:
11
+ name: Test Python ${{ matrix.python-version }}, ${{ matrix.backend.name }}
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
17
+ backend:
18
+ - name: "Redis 6.2, redis-py <5"
19
+ redis-version: "6.2"
20
+ redis-py-version: ">=4.6,<5"
21
+ - name: "Redis 7.4, redis-py >=5"
22
+ redis-version: "7.4"
23
+ redis-py-version: ">=5"
24
+ - name: "Valkey 8.0, redis-py >=5"
25
+ redis-version: "valkey-8.0"
26
+ redis-py-version: ">=5"
27
+ - name: "Memory (in-memory backend)"
28
+ redis-version: "memory"
29
+ redis-py-version: ">=5"
30
+ exclude:
31
+ # Python 3.10 + Redis 6.2 + redis-py <5 combination is skipped
32
+ - python-version: "3.10"
33
+ backend:
34
+ name: "Redis 6.2, redis-py <5"
35
+ redis-version: "6.2"
36
+ redis-py-version: ">=4.6,<5"
37
+ include:
38
+ - python-version: "3.10"
39
+ cov-threshold: 100
40
+ pytest-args: ""
41
+ # Python 3.11 coverage reporting is unstable, so use 98% threshold
42
+ - python-version: "3.11"
43
+ cov-threshold: 98
44
+ pytest-args: ""
45
+ - python-version: "3.12"
46
+ cov-threshold: 100
47
+ pytest-args: ""
48
+ - python-version: "3.13"
49
+ cov-threshold: 100
50
+ pytest-args: ""
51
+ - python-version: "3.14"
52
+ cov-threshold: 100
53
+ pytest-args: ""
54
+ # Memory backend: CLI tests are skipped via pytest skip markers because
55
+ # CLI rejects memory:// URLs. Use separate coverage config to exclude CLI.
56
+ - backend:
57
+ name: "Memory (in-memory backend)"
58
+ redis-version: "memory"
59
+ redis-py-version: ">=5"
60
+ cov-threshold: 98 # CLI tests are excluded from coverage and some lines are only covered by CLI tests
61
+ pytest-args: "--cov-config=.coveragerc-memory"
62
+
63
+ steps:
64
+ - uses: actions/checkout@v4
65
+
66
+ - name: Install uv and set Python version
67
+ uses: astral-sh/setup-uv@v5
68
+ with:
69
+ python-version: ${{ matrix.python-version }}
70
+ enable-cache: true
71
+ cache-dependency-glob: "pyproject.toml"
72
+
73
+ - name: Install dependencies
74
+ run: uv sync --dev --upgrade-package 'redis${{ matrix.backend.redis-py-version }}'
75
+
76
+ - name: Run tests
77
+ env:
78
+ REDIS_VERSION: ${{ matrix.backend.redis-version }}
79
+ run: uv run pytest --cov-branch --cov-fail-under=${{ matrix.cov-threshold }} --cov-report=xml --cov-report=term-missing:skip-covered ${{ matrix.pytest-args }}
80
+
81
+ - name: Upload coverage reports to Codecov
82
+ uses: codecov/codecov-action@v5
83
+ with:
84
+ token: ${{ secrets.CODECOV_TOKEN }}
85
+ flags: python-${{ matrix.python-version }}
86
+
87
+ pre-commit:
88
+ name: Pre-commit checks
89
+ runs-on: ubuntu-latest
90
+ steps:
91
+ - uses: actions/checkout@v4
92
+
93
+ - name: Install uv and set Python version
94
+ uses: astral-sh/setup-uv@v5
95
+ with:
96
+ python-version: "3.10"
97
+ enable-cache: true
98
+ cache-dependency-glob: "pyproject.toml"
99
+
100
+ - name: Install dependencies
101
+ run: |
102
+ uv sync --dev
103
+ uv pip install pip
104
+
105
+ - uses: pre-commit/action@v3.0.1
106
+ with:
107
+ extra_args: --all-files
@@ -23,7 +23,7 @@ jobs:
23
23
  id: claude-review
24
24
  uses: anthropics/claude-code-action@beta
25
25
  with:
26
- claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
26
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
27
27
  model: "claude-opus-4-1-20250805"
28
28
 
29
29
  # Direct prompt for automated review (no @claude mention needed)
@@ -34,7 +34,7 @@ jobs:
34
34
  id: claude
35
35
  uses: anthropics/claude-code-action@beta
36
36
  with:
37
- claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
37
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
38
38
 
39
39
  additional_permissions: |
40
40
  actions: read
@@ -9,3 +9,5 @@ __pycache__/
9
9
  build/
10
10
  dist/
11
11
  wheels/
12
+
13
+ .coverage.*
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
6
6
 
7
7
  **Docket** (`pydocket` on PyPI) is a distributed background task system for Python functions with Redis-backed persistence. It enables scheduling both immediate and future work with comprehensive dependency injection, retry mechanisms, and fault tolerance.
8
8
 
9
- **Key Requirements**: Python 3.12+, Redis 6.2+ or Valkey 8.0+
9
+ **Key Requirements**: Python 3.10+, Redis 6.2+ or Valkey 8.0+
10
10
 
11
11
  ## Development Commands
12
12
 
@@ -58,7 +58,6 @@ pre-commit install
58
58
  ### Key Classes
59
59
 
60
60
  - **`Docket`** (`src/docket/docket.py`): Central task registry and scheduler
61
-
62
61
  - `add()`: Schedule tasks for execution
63
62
  - `replace()`: Replace existing scheduled tasks
64
63
  - `cancel()`: Cancel pending tasks
@@ -66,7 +65,6 @@ pre-commit install
66
65
  - `snapshot()`: Get current state for observability
67
66
 
68
67
  - **`Worker`** (`src/docket/worker.py`): Task execution engine
69
-
70
68
  - `run_forever()`/`run_until_finished()`: Main execution loops
71
69
  - Handles concurrency, retries, and dependency injection
72
70
  - Maintains heartbeat for liveness tracking
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydocket
3
- Version: 0.11.1
3
+ Version: 0.12.0
4
4
  Summary: A distributed background task system for Python functions
5
5
  Project-URL: Homepage, https://github.com/chrisguidry/docket
6
6
  Project-URL: Bug Tracker, https://github.com/chrisguidry/docket/issues
@@ -19,11 +19,15 @@ Classifier: Development Status :: 4 - Beta
19
19
  Classifier: License :: OSI Approved :: MIT License
20
20
  Classifier: Operating System :: OS Independent
21
21
  Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
22
24
  Classifier: Programming Language :: Python :: 3.12
23
25
  Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Programming Language :: Python :: 3.14
24
27
  Classifier: Typing :: Typed
25
- Requires-Python: >=3.12
28
+ Requires-Python: >=3.10
26
29
  Requires-Dist: cloudpickle>=3.1.1
30
+ Requires-Dist: exceptiongroup>=1.2.0; python_version < '3.11'
27
31
  Requires-Dist: opentelemetry-api>=1.30.0
28
32
  Requires-Dist: opentelemetry-exporter-prometheus>=0.51b0
29
33
  Requires-Dist: prometheus-client>=0.21.1
@@ -31,6 +35,7 @@ Requires-Dist: python-json-logger>=3.2.1
31
35
  Requires-Dist: redis>=4.6
32
36
  Requires-Dist: rich>=13.9.4
33
37
  Requires-Dist: typer>=0.15.1
38
+ Requires-Dist: typing-extensions>=4.12.0
34
39
  Requires-Dist: uuid7>=0.1.0
35
40
  Description-Content-Type: text/markdown
36
41
 
@@ -69,6 +74,7 @@ from docket import Docket, Worker
69
74
 
70
75
  async with Docket() as docket:
71
76
  async with Worker(docket) as worker:
77
+ worker.register(greet)
72
78
  await worker.run_until_finished()
73
79
  ```
74
80
 
@@ -98,7 +104,7 @@ reference](https://chrisguidry.github.io/docket/api-reference/).
98
104
  ## Installing `docket`
99
105
 
100
106
  Docket is [available on PyPI](https://pypi.org/project/pydocket/) under the package name
101
- `pydocket`. It targets Python 3.12 or above.
107
+ `pydocket`. It targets Python 3.10 or above.
102
108
 
103
109
  With [`uv`](https://docs.astral.sh/uv/):
104
110
 
@@ -119,6 +125,18 @@ pip install pydocket
119
125
  Docket requires a [Redis](http://redis.io/) server with Streams support (which was
120
126
  introduced in Redis 5.0.0). Docket is tested with Redis 6 and 7.
121
127
 
128
+ For testing without Redis, Docket includes [fakeredis](https://github.com/cunla/fakeredis-py) for in-memory operation:
129
+
130
+ ```python
131
+ from docket import Docket
132
+
133
+ async with Docket(name="my-docket", url="memory://my-docket") as docket:
134
+ # Use docket normally - all operations are in-memory
135
+ ...
136
+ ```
137
+
138
+ See [Testing with Docket](https://chrisguidry.github.io/docket/testing/#using-in-memory-backend-no-redis-required) for more details.
139
+
122
140
  # Hacking on `docket`
123
141
 
124
142
  We use [`uv`](https://docs.astral.sh/uv/) for project management, so getting set up
@@ -33,6 +33,7 @@ from docket import Docket, Worker
33
33
 
34
34
  async with Docket() as docket:
35
35
  async with Worker(docket) as worker:
36
+ worker.register(greet)
36
37
  await worker.run_until_finished()
37
38
  ```
38
39
 
@@ -62,7 +63,7 @@ reference](https://chrisguidry.github.io/docket/api-reference/).
62
63
  ## Installing `docket`
63
64
 
64
65
  Docket is [available on PyPI](https://pypi.org/project/pydocket/) under the package name
65
- `pydocket`. It targets Python 3.12 or above.
66
+ `pydocket`. It targets Python 3.10 or above.
66
67
 
67
68
  With [`uv`](https://docs.astral.sh/uv/):
68
69
 
@@ -83,6 +84,18 @@ pip install pydocket
83
84
  Docket requires a [Redis](http://redis.io/) server with Streams support (which was
84
85
  introduced in Redis 5.0.0). Docket is tested with Redis 6 and 7.
85
86
 
87
+ For testing without Redis, Docket includes [fakeredis](https://github.com/cunla/fakeredis-py) for in-memory operation:
88
+
89
+ ```python
90
+ from docket import Docket
91
+
92
+ async with Docket(name="my-docket", url="memory://my-docket") as docket:
93
+ # Use docket normally - all operations are in-memory
94
+ ...
95
+ ```
96
+
97
+ See [Testing with Docket](https://chrisguidry.github.io/docket/testing/#using-in-memory-backend-no-redis-required) for more details.
98
+
86
99
  # Hacking on `docket`
87
100
 
88
101
  We use [`uv`](https://docs.astral.sh/uv/) for project management, so getting set up
@@ -160,7 +160,43 @@ Timeouts work alongside retries. If a task times out, it can be retried accordin
160
160
 
161
161
  ## Custom Dependencies
162
162
 
163
- Create your own dependencies using `Depends()` for reusable resources and patterns:
163
+ Create your own dependencies using `Depends()` for reusable resources and patterns. Dependencies can be either synchronous or asynchronous.
164
+
165
+ ### Synchronous Dependencies
166
+
167
+ Use sync dependencies for pure computations and in-memory operations:
168
+
169
+ ```python
170
+ from docket import Depends
171
+
172
+ # In-memory config lookup - no I/O
173
+ def get_config() -> dict:
174
+ """Access configuration from memory."""
175
+ return {"api_url": "https://api.example.com", "timeout": 30}
176
+
177
+ # Pure computation - no I/O
178
+ def build_request_headers(config: dict = Depends(get_config)) -> dict:
179
+ """Construct headers from config."""
180
+ return {
181
+ "User-Agent": "MyApp/1.0",
182
+ "Timeout": str(config["timeout"])
183
+ }
184
+
185
+ async def call_api(
186
+ headers: dict = Depends(build_request_headers)
187
+ ) -> None:
188
+ # Headers are computed without blocking
189
+ # Network I/O happens here (async)
190
+ response = await http_client.get(url, headers=headers)
191
+ ```
192
+
193
+ **Important**: Synchronous dependencies should **NOT** include blocking I/O operations (file access, network calls, database queries, etc.) as it will block the event loop and prevent tasks from being executed. Use async dependencies for any I/O. Sync dependencies are best for:
194
+ - Pure computations
195
+ - In-memory data structure access
196
+ - Configuration lookups from memory
197
+ - Non-blocking transformations
198
+
199
+ ### Asynchronous Dependencies
164
200
 
165
201
  ```python
166
202
  from contextlib import asynccontextmanager
@@ -168,30 +204,78 @@ from docket import Depends
168
204
 
169
205
  @asynccontextmanager
170
206
  async def get_database_connection():
171
- """Simple dependency that returns a database connection."""
207
+ """Async dependency that returns a database connection."""
172
208
  conn = await database.connect()
173
209
  try:
174
210
  yield conn
175
211
  finally:
176
212
  await conn.close()
177
213
 
178
- @asynccontextmanager
179
- async def get_redis_client():
180
- """Another dependency for Redis operations."""
181
- client = redis.Redis(host='localhost', port=6379)
182
- try:
183
- yield client
184
- finally:
185
- client.close()
186
-
187
214
  async def process_user_data(
188
215
  user_id: int,
189
- db=Depends(get_database_connection),
190
- cache=Depends(get_redis_client)
216
+ db=Depends(get_database_connection)
191
217
  ) -> None:
192
- # Both dependencies are automatically provided and cleaned up
218
+ # Database connection is automatically provided and cleaned up
193
219
  user = await db.fetch_user(user_id)
194
- await cache.set(f"user:{user_id}", user.to_json())
220
+ await db.update_user(user_id, {"last_seen": datetime.now()})
221
+ ```
222
+
223
+ ### Synchronous Context Managers
224
+
225
+ Use sync context managers only for managing in-memory resources or quick non-blocking operations:
226
+
227
+ ```python
228
+ from contextlib import contextmanager
229
+ from docket import Depends
230
+
231
+ # In-memory resource tracking - no I/O
232
+ @contextmanager
233
+ def track_operation(operation_name: str):
234
+ """Track operation execution without blocking."""
235
+ operations_in_progress.add(operation_name) # In-memory set
236
+ try:
237
+ yield operation_name
238
+ finally:
239
+ operations_in_progress.remove(operation_name)
240
+
241
+ async def process_data(
242
+ tracker=Depends(lambda: track_operation("data_processing"))
243
+ ) -> None:
244
+ # Operation tracked in memory, no blocking
245
+ await perform_async_work()
246
+ ```
247
+
248
+ ### Mixed Sync and Async Dependencies
249
+
250
+ You can freely mix synchronous and asynchronous dependencies in the same task. Use sync for computations, async for I/O:
251
+
252
+ ```python
253
+ # Sync - in-memory config lookup
254
+ def get_local_config() -> dict:
255
+ """Access local config from memory - no I/O."""
256
+ return {"retry_count": 3, "batch_size": 100}
257
+
258
+ # Async - network I/O
259
+ async def get_remote_config() -> dict:
260
+ """Fetch remote config via network - requires I/O."""
261
+ response = await http_client.get("/api/config")
262
+ return await response.json()
263
+
264
+ # Sync - pure computation
265
+ def merge_configs(
266
+ local: dict = Depends(get_local_config),
267
+ remote: dict = Depends(get_remote_config)
268
+ ) -> dict:
269
+ """Merge configs without blocking - pure computation."""
270
+ return {**local, **remote}
271
+
272
+ async def process_batch(
273
+ config: dict = Depends(merge_configs)
274
+ ) -> None:
275
+ # Config is computed/fetched appropriately
276
+ # Now do the actual I/O work
277
+ for i in range(config["batch_size"]):
278
+ await process_item(i, retries=config["retry_count"])
195
279
  ```
196
280
 
197
281
  ### Nested Dependencies
@@ -220,31 +304,7 @@ async def update_user_profile(
220
304
  await user_service.update_profile(user_id, profile_data)
221
305
  ```
222
306
 
223
- Dependencies are resolved once per task execution and cached, so if multiple parameters depend on the same resource, only one instance is created.
224
-
225
- ### Context Manager Dependencies
226
-
227
- Dependencies can be async context managers for automatic resource cleanup:
228
-
229
- ```python
230
- from contextlib import asynccontextmanager
231
-
232
- @asynccontextmanager
233
- async def get_file_lock(filename: str):
234
- """A dependency that provides file locking."""
235
- lock = await acquire_file_lock(filename)
236
- try:
237
- yield lock
238
- finally:
239
- await release_file_lock(filename)
240
-
241
- async def process_shared_file(
242
- filename: str,
243
- file_lock=Depends(lambda: get_file_lock("shared.txt"))
244
- ) -> None:
245
- # File is locked before task starts, unlocked after task completes
246
- await process_file_safely(filename)
247
- ```
307
+ Dependencies are resolved once per task execution and cached, so if multiple parameters depend on the same resource, only one instance is created. This caching works across both sync and async dependencies.
248
308
 
249
309
  ### Dependencies with Built-in Context
250
310
 
@@ -337,6 +397,41 @@ If `unreliable_dependency` fails, the task won't execute and the error will be l
337
397
 
338
398
  ## Dependency Guidelines
339
399
 
400
+ ### Choose Sync vs Async Appropriately
401
+
402
+ **Use synchronous dependencies for:**
403
+ - Pure computations (math, string manipulation, data transformations)
404
+ - In-memory data structure access (dicts, lists, sets)
405
+ - Configuration lookups from memory
406
+ - Non-blocking operations that complete instantly
407
+
408
+ **Use asynchronous dependencies for:**
409
+ - Network I/O (HTTP requests, API calls)
410
+ - File I/O (reading/writing files)
411
+ - Database queries
412
+ - Any operation that involves `await`
413
+ - Resource management requiring async cleanup
414
+
415
+ ```python
416
+ # ✅ Good: Sync for pure computation
417
+ def calculate_batch_size(item_count: int) -> int:
418
+ return min(item_count, 1000)
419
+
420
+ # ✅ Good: Async for I/O
421
+ async def fetch_user_data(user_id: int) -> dict:
422
+ return await api_client.get(f"/users/{user_id}")
423
+
424
+ # ❌ Bad: Sync with blocking I/O
425
+ def load_config_from_file() -> dict:
426
+ with open("config.json") as f: # Blocks the event loop!
427
+ return json.load(f)
428
+
429
+ # ✅ Good: Use async for file I/O instead
430
+ async def load_config_from_file() -> dict:
431
+ async with aiofiles.open("config.json") as f:
432
+ return json.loads(await f.read())
433
+ ```
434
+
340
435
  ### Design for Reusability
341
436
 
342
437
  Create dependencies that can be used across multiple tasks:
@@ -3,7 +3,7 @@
3
3
  ## Installation
4
4
 
5
5
  Docket is [available on PyPI](https://pypi.org/project/pydocket/) under the package name
6
- `pydocket`. It targets Python 3.12 or above.
6
+ `pydocket`. It targets Python 3.10 or above.
7
7
 
8
8
  With [`uv`](https://docs.astral.sh/uv/):
9
9
 
@@ -2,6 +2,76 @@
2
2
 
3
3
  Docket includes the utilities you need to test all your background task systems in realistic ways. The ergonomic design supports testing complex workflows with minimal setup.
4
4
 
5
+ ## Using In-Memory Backend (No Redis Required)
6
+
7
+ For the fastest tests and simplest setup, Docket supports an in-memory backend using [fakeredis](https://github.com/cunla/fakeredis-py). This is perfect for:
8
+
9
+ - **CI/CD environments** - No need to spin up Redis containers
10
+ - **Local development** - Test without installing/running Redis
11
+ - **Unit tests** - Fast, isolated tests without external dependencies
12
+ - **Educational environments** - Workshops and tutorials without infrastructure
13
+
14
+ ### Installation
15
+
16
+ Fakeredis is included as a standard dependency, so no extra installation is needed.
17
+
18
+ ### Usage
19
+
20
+ Use the `memory://` URL scheme to enable the in-memory backend:
21
+
22
+ ```python
23
+ from docket import Docket
24
+
25
+ async with Docket(name="test-docket", url="memory://test") as docket:
26
+ # Use docket normally - all operations are in-memory
27
+ docket.register(my_task)
28
+ await docket.add(my_task)("arg")
29
+ ```
30
+
31
+ ### Multiple In-Memory Dockets
32
+
33
+ You can run multiple independent in-memory dockets simultaneously by using different URLs:
34
+
35
+ ```python
36
+ async with (
37
+ Docket(name="service-a", url="memory://service-a") as docket_a,
38
+ Docket(name="service-b", url="memory://service-b") as docket_b,
39
+ ):
40
+ # Each docket has its own isolated in-memory data
41
+ await docket_a.add(task_a)()
42
+ await docket_b.add(task_b)()
43
+ ```
44
+
45
+ This is useful for testing multi-service scenarios in a single process.
46
+
47
+ ### Pytest Fixture Example
48
+
49
+ ```python
50
+ import pytest
51
+ from docket import Docket, Worker
52
+ from uuid import uuid4
53
+
54
+ @pytest.fixture
55
+ async def test_docket() -> AsyncGenerator[Docket, None]:
56
+ """Create a test docket with in-memory backend."""
57
+ async with Docket(
58
+ name=f"test-{uuid4()}",
59
+ url=f"memory://test-{uuid4()}"
60
+ ) as docket:
61
+ yield docket
62
+ ```
63
+
64
+ ### Limitations
65
+
66
+ The in-memory backend has some limitations compared to real Redis:
67
+
68
+ - **Single process only** - Cannot distribute work across multiple processes/machines
69
+ - **Data is ephemeral** - Lost when the process exits
70
+ - **Performance may differ** - Timing-sensitive tests may behave differently
71
+ - **Async polling behavior** - Uses non-blocking reads with manual sleeps for proper asyncio integration
72
+
73
+ For integration tests or multi-worker scenarios across processes, use a real Redis instance.
74
+
5
75
  ## Testing Tasks as Simple Functions
6
76
 
7
77
  Often you can test your tasks without running a worker at all! Docket tasks are just Python functions, so you can call them directly and pass test values for dependency parameters: