pydocket 0.1.2__tar.gz → 0.1.3__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.
- {pydocket-0.1.2 → pydocket-0.1.3}/.github/workflows/ci.yml +7 -4
- {pydocket-0.1.2 → pydocket-0.1.3}/PKG-INFO +1 -1
- {pydocket-0.1.2 → pydocket-0.1.3}/pyproject.toml +0 -1
- {pydocket-0.1.2 → pydocket-0.1.3}/src/docket/cli.py +9 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/src/docket/docket.py +18 -8
- {pydocket-0.1.2 → pydocket-0.1.3}/src/docket/worker.py +26 -21
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/cli/test_snapshot.py +0 -39
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/conftest.py +26 -8
- {pydocket-0.1.2 → pydocket-0.1.3}/.cursor/rules/general.mdc +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/.cursor/rules/python-style.mdc +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/.github/codecov.yml +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/.github/workflows/chaos.yml +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/.github/workflows/publish.yml +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/.gitignore +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/.pre-commit-config.yaml +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/LICENSE +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/README.md +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/chaos/README.md +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/chaos/__init__.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/chaos/driver.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/chaos/producer.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/chaos/run +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/chaos/tasks.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/src/docket/__init__.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/src/docket/__main__.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/src/docket/annotations.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/src/docket/dependencies.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/src/docket/execution.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/src/docket/instrumentation.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/src/docket/py.typed +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/src/docket/tasks.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/telemetry/.gitignore +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/telemetry/start +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/telemetry/stop +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/__init__.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/cli/__init__.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/cli/conftest.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/cli/test_module.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/cli/test_parsing.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/cli/test_striking.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/cli/test_tasks.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/cli/test_version.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/cli/test_worker.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/cli/test_workers.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/test_dependencies.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/test_docket.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/test_fundamentals.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/test_instrumentation.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/test_striking.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/tests/test_worker.py +0 -0
- {pydocket-0.1.2 → pydocket-0.1.3}/uv.lock +0 -0
|
@@ -7,15 +7,16 @@ on:
|
|
|
7
7
|
branches: [main]
|
|
8
8
|
workflow_call:
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
jobs:
|
|
12
11
|
test:
|
|
13
|
-
name: Test Python ${{ matrix.python-version }}
|
|
12
|
+
name: Test Python ${{ matrix.python-version }}, Redis ${{ matrix.redis-version }}, redis-py ${{ matrix.redis-py-version }}
|
|
14
13
|
runs-on: ubuntu-latest
|
|
15
14
|
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
16
|
matrix:
|
|
17
17
|
python-version: ["3.12", "3.13"]
|
|
18
|
-
|
|
18
|
+
redis-version: ["6.2", "7.4"]
|
|
19
|
+
redis-py-version: [">=4.6,<5", ">=5"]
|
|
19
20
|
|
|
20
21
|
steps:
|
|
21
22
|
- uses: actions/checkout@v4
|
|
@@ -28,9 +29,11 @@ jobs:
|
|
|
28
29
|
cache-dependency-glob: "pyproject.toml"
|
|
29
30
|
|
|
30
31
|
- name: Install dependencies
|
|
31
|
-
run: uv sync --dev
|
|
32
|
+
run: uv sync --dev --upgrade-package 'redis${{ matrix.redis-py-version }}'
|
|
32
33
|
|
|
33
34
|
- name: Run tests
|
|
35
|
+
env:
|
|
36
|
+
REDIS_VERSION: ${{ matrix.redis-version }}
|
|
34
37
|
run: uv run pytest --cov-branch --cov-report=xml --cov-report=term-missing:skip-covered
|
|
35
38
|
|
|
36
39
|
- name: Upload coverage reports to Codecov
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydocket
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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
|
|
@@ -228,6 +228,14 @@ def worker(
|
|
|
228
228
|
envvar="DOCKET_WORKER_RECONNECTION_DELAY",
|
|
229
229
|
),
|
|
230
230
|
] = timedelta(seconds=5),
|
|
231
|
+
minimum_check_interval: Annotated[
|
|
232
|
+
timedelta,
|
|
233
|
+
typer.Option(
|
|
234
|
+
parser=duration,
|
|
235
|
+
help="The minimum interval to check for tasks",
|
|
236
|
+
envvar="DOCKET_WORKER_MINIMUM_CHECK_INTERVAL",
|
|
237
|
+
),
|
|
238
|
+
] = timedelta(milliseconds=100),
|
|
231
239
|
until_finished: Annotated[
|
|
232
240
|
bool,
|
|
233
241
|
typer.Option(
|
|
@@ -244,6 +252,7 @@ def worker(
|
|
|
244
252
|
concurrency=concurrency,
|
|
245
253
|
redelivery_timeout=redelivery_timeout,
|
|
246
254
|
reconnection_delay=reconnection_delay,
|
|
255
|
+
minimum_check_interval=minimum_check_interval,
|
|
247
256
|
until_finished=until_finished,
|
|
248
257
|
tasks=tasks,
|
|
249
258
|
)
|
|
@@ -147,17 +147,17 @@ class Docket:
|
|
|
147
147
|
self._monitor_strikes_task = asyncio.create_task(self._monitor_strikes())
|
|
148
148
|
|
|
149
149
|
# Ensure that the stream and worker group exist
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
try:
|
|
151
|
+
async with self.redis() as r:
|
|
152
152
|
await r.xgroup_create(
|
|
153
153
|
groupname=self.worker_group_name,
|
|
154
154
|
name=self.stream_key,
|
|
155
155
|
id="0-0",
|
|
156
156
|
mkstream=True,
|
|
157
157
|
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
except redis.exceptions.RedisError as e:
|
|
159
|
+
if "BUSYGROUP" not in repr(e):
|
|
160
|
+
raise
|
|
161
161
|
|
|
162
162
|
return self
|
|
163
163
|
|
|
@@ -178,8 +178,18 @@ class Docket:
|
|
|
178
178
|
|
|
179
179
|
@asynccontextmanager
|
|
180
180
|
async def redis(self) -> AsyncGenerator[Redis, None]:
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
redis: Redis | None = None
|
|
182
|
+
try:
|
|
183
|
+
redis = await Redis.from_url(
|
|
184
|
+
self.url,
|
|
185
|
+
single_connection_client=True,
|
|
186
|
+
)
|
|
187
|
+
async with redis:
|
|
188
|
+
yield redis
|
|
189
|
+
finally:
|
|
190
|
+
# redis 4.6.0 doesn't automatically disconnect and leaves connections open
|
|
191
|
+
if redis:
|
|
192
|
+
await redis.connection_pool.disconnect()
|
|
183
193
|
|
|
184
194
|
def register(self, function: Callable[..., Awaitable[Any]]) -> None:
|
|
185
195
|
from .dependencies import validate_dependencies
|
|
@@ -513,7 +523,7 @@ class Docket:
|
|
|
513
523
|
)
|
|
514
524
|
running.append(RunningExecution(execution, worker_name, started))
|
|
515
525
|
else:
|
|
516
|
-
future.append(execution)
|
|
526
|
+
future.append(execution) # pragma: no cover
|
|
517
527
|
|
|
518
528
|
for message in queued_messages:
|
|
519
529
|
function = self.tasks[message[b"function"].decode()]
|
|
@@ -67,7 +67,7 @@ class Worker:
|
|
|
67
67
|
concurrency: int = 10,
|
|
68
68
|
redelivery_timeout: timedelta = timedelta(minutes=5),
|
|
69
69
|
reconnection_delay: timedelta = timedelta(seconds=5),
|
|
70
|
-
minimum_check_interval: timedelta = timedelta(milliseconds=
|
|
70
|
+
minimum_check_interval: timedelta = timedelta(milliseconds=100),
|
|
71
71
|
) -> None:
|
|
72
72
|
self.docket = docket
|
|
73
73
|
self.name = name or f"worker:{uuid4()}"
|
|
@@ -110,6 +110,7 @@ class Worker:
|
|
|
110
110
|
concurrency: int = 10,
|
|
111
111
|
redelivery_timeout: timedelta = timedelta(minutes=5),
|
|
112
112
|
reconnection_delay: timedelta = timedelta(seconds=5),
|
|
113
|
+
minimum_check_interval: timedelta = timedelta(milliseconds=100),
|
|
113
114
|
until_finished: bool = False,
|
|
114
115
|
tasks: list[str] = ["docket.tasks:standard_tasks"],
|
|
115
116
|
) -> None:
|
|
@@ -123,6 +124,7 @@ class Worker:
|
|
|
123
124
|
concurrency=concurrency,
|
|
124
125
|
redelivery_timeout=redelivery_timeout,
|
|
125
126
|
reconnection_delay=reconnection_delay,
|
|
127
|
+
minimum_check_interval=minimum_check_interval,
|
|
126
128
|
) as worker:
|
|
127
129
|
if until_finished:
|
|
128
130
|
await worker.run_until_finished()
|
|
@@ -170,28 +172,31 @@ class Worker:
|
|
|
170
172
|
"""
|
|
171
173
|
local total_work = redis.call('ZCARD', KEYS[1])
|
|
172
174
|
local due_work = 0
|
|
173
|
-
local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1])
|
|
174
175
|
|
|
175
|
-
|
|
176
|
-
local
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
local
|
|
181
|
-
|
|
182
|
-
|
|
176
|
+
if total_work > 0 then
|
|
177
|
+
local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1])
|
|
178
|
+
|
|
179
|
+
for i, key in ipairs(tasks) do
|
|
180
|
+
local hash_key = ARGV[2] .. ":" .. key
|
|
181
|
+
local task_data = redis.call('HGETALL', hash_key)
|
|
182
|
+
|
|
183
|
+
if #task_data > 0 then
|
|
184
|
+
local task = {}
|
|
185
|
+
for j = 1, #task_data, 2 do
|
|
186
|
+
task[task_data[j]] = task_data[j+1]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
redis.call('XADD', KEYS[2], '*',
|
|
190
|
+
'key', task['key'],
|
|
191
|
+
'when', task['when'],
|
|
192
|
+
'function', task['function'],
|
|
193
|
+
'args', task['args'],
|
|
194
|
+
'kwargs', task['kwargs'],
|
|
195
|
+
'attempt', task['attempt']
|
|
196
|
+
)
|
|
197
|
+
redis.call('DEL', hash_key)
|
|
198
|
+
due_work = due_work + 1
|
|
183
199
|
end
|
|
184
|
-
|
|
185
|
-
redis.call('XADD', KEYS[2], '*',
|
|
186
|
-
'key', task['key'],
|
|
187
|
-
'when', task['when'],
|
|
188
|
-
'function', task['function'],
|
|
189
|
-
'args', task['args'],
|
|
190
|
-
'kwargs', task['kwargs'],
|
|
191
|
-
'attempt', task['attempt']
|
|
192
|
-
)
|
|
193
|
-
redis.call('DEL', hash_key)
|
|
194
|
-
due_work = due_work + 1
|
|
195
200
|
end
|
|
196
201
|
end
|
|
197
202
|
|
|
@@ -91,45 +91,6 @@ async def test_snapshot_with_running_tasks(docket: Docket, runner: CliRunner):
|
|
|
91
91
|
assert "sleep" in result.output
|
|
92
92
|
assert "test-worker" in result.output
|
|
93
93
|
|
|
94
|
-
worker_running.cancel()
|
|
95
|
-
await worker_running
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
async def test_snapshot_with_mixed_tasks(docket: Docket, runner: CliRunner):
|
|
99
|
-
"""Should show both running and scheduled tasks in the snapshot"""
|
|
100
|
-
heartbeat = timedelta(milliseconds=20)
|
|
101
|
-
docket.heartbeat_interval = heartbeat
|
|
102
|
-
|
|
103
|
-
future = datetime.now(timezone.utc) + timedelta(seconds=5)
|
|
104
|
-
await docket.add(tasks.trace, when=future)("hi!")
|
|
105
|
-
for _ in range(5): # more than the concurrency allows
|
|
106
|
-
await docket.add(tasks.sleep)(2)
|
|
107
|
-
|
|
108
|
-
async with Worker(docket, name="test-worker", concurrency=2) as worker:
|
|
109
|
-
worker_running = asyncio.create_task(worker.run_until_finished())
|
|
110
|
-
|
|
111
|
-
await asyncio.sleep(0.1)
|
|
112
|
-
|
|
113
|
-
result = await asyncio.get_running_loop().run_in_executor(
|
|
114
|
-
None,
|
|
115
|
-
runner.invoke,
|
|
116
|
-
app,
|
|
117
|
-
[
|
|
118
|
-
"snapshot",
|
|
119
|
-
"--url",
|
|
120
|
-
docket.url,
|
|
121
|
-
"--docket",
|
|
122
|
-
docket.name,
|
|
123
|
-
],
|
|
124
|
-
)
|
|
125
|
-
assert result.exit_code == 0, result.output
|
|
126
|
-
|
|
127
|
-
assert "1 workers, 2/6 running" in result.output
|
|
128
|
-
assert "sleep" in result.output
|
|
129
|
-
assert "test-worker" in result.output
|
|
130
|
-
assert "trace" in result.output
|
|
131
|
-
|
|
132
|
-
worker_running.cancel()
|
|
133
94
|
await worker_running
|
|
134
95
|
|
|
135
96
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import time
|
|
3
|
+
from contextlib import contextmanager
|
|
2
4
|
from datetime import datetime, timezone
|
|
3
5
|
from functools import partial
|
|
4
6
|
from typing import Any, AsyncGenerator, Callable, Generator, Iterable, cast
|
|
@@ -9,10 +11,12 @@ import pytest
|
|
|
9
11
|
import redis.exceptions
|
|
10
12
|
from docker import DockerClient
|
|
11
13
|
from docker.models.containers import Container
|
|
12
|
-
from redis import Redis
|
|
14
|
+
from redis import ConnectionPool, Redis
|
|
13
15
|
|
|
14
16
|
from docket import Docket, Worker
|
|
15
17
|
|
|
18
|
+
REDIS_VERSION = os.environ.get("REDIS_VERSION", "7.4")
|
|
19
|
+
|
|
16
20
|
|
|
17
21
|
@pytest.fixture
|
|
18
22
|
def now() -> Callable[[], datetime]:
|
|
@@ -24,6 +28,19 @@ def redis_port(unused_tcp_port_factory: Callable[[], int]) -> int:
|
|
|
24
28
|
return unused_tcp_port_factory()
|
|
25
29
|
|
|
26
30
|
|
|
31
|
+
@contextmanager
|
|
32
|
+
def _sync_redis(url: str) -> Generator[Redis, None, None]:
|
|
33
|
+
pool: ConnectionPool | None = None
|
|
34
|
+
redis = Redis.from_url(url, single_connection_client=True) # type: ignore
|
|
35
|
+
try:
|
|
36
|
+
with redis:
|
|
37
|
+
pool = redis.connection_pool # type: ignore
|
|
38
|
+
yield redis
|
|
39
|
+
finally:
|
|
40
|
+
if pool: # pragma: no branch
|
|
41
|
+
pool.disconnect()
|
|
42
|
+
|
|
43
|
+
|
|
27
44
|
@pytest.fixture(scope="session")
|
|
28
45
|
def redis_server(redis_port: int) -> Generator[Container, None, None]:
|
|
29
46
|
client = DockerClient.from_env()
|
|
@@ -39,7 +56,7 @@ def redis_server(redis_port: int) -> Generator[Container, None, None]:
|
|
|
39
56
|
container.remove(force=True)
|
|
40
57
|
|
|
41
58
|
container = client.containers.run(
|
|
42
|
-
"redis:
|
|
59
|
+
f"redis:{REDIS_VERSION}",
|
|
43
60
|
detach=True,
|
|
44
61
|
ports={"6379/tcp": redis_port},
|
|
45
62
|
labels={"source": "docket-unit-tests"},
|
|
@@ -50,8 +67,9 @@ def redis_server(redis_port: int) -> Generator[Container, None, None]:
|
|
|
50
67
|
|
|
51
68
|
while True:
|
|
52
69
|
try:
|
|
53
|
-
with
|
|
54
|
-
|
|
70
|
+
with _sync_redis(url) as r:
|
|
71
|
+
success = r.ping() # type: ignore
|
|
72
|
+
if success: # pragma: no branch
|
|
55
73
|
break
|
|
56
74
|
except redis.exceptions.ConnectionError: # pragma: no cover
|
|
57
75
|
pass
|
|
@@ -61,7 +79,7 @@ def redis_server(redis_port: int) -> Generator[Container, None, None]:
|
|
|
61
79
|
try:
|
|
62
80
|
yield container
|
|
63
81
|
finally:
|
|
64
|
-
with
|
|
82
|
+
with _sync_redis(url) as r:
|
|
65
83
|
info: dict[str, Any] = r.info() # type: ignore
|
|
66
84
|
|
|
67
85
|
container.stop()
|
|
@@ -75,10 +93,10 @@ def redis_server(redis_port: int) -> Generator[Container, None, None]:
|
|
|
75
93
|
|
|
76
94
|
@pytest.fixture
|
|
77
95
|
def redis_url(redis_server: Container, redis_port: int) -> str:
|
|
78
|
-
|
|
96
|
+
url = f"redis://localhost:{redis_port}/0"
|
|
97
|
+
with _sync_redis(url) as r:
|
|
79
98
|
r.flushdb() # type: ignore
|
|
80
|
-
|
|
81
|
-
return f"redis://localhost:{redis_port}/0"
|
|
99
|
+
return url
|
|
82
100
|
|
|
83
101
|
|
|
84
102
|
@pytest.fixture
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|