pydocket 0.1.0__tar.gz → 0.1.1__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.0 → pydocket-0.1.1}/PKG-INFO +2 -2
- {pydocket-0.1.0 → pydocket-0.1.1}/pyproject.toml +1 -1
- {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/cli.py +1 -1
- pydocket-0.1.1/tests/cli/test_module.py +22 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/tests/cli/test_snapshot.py +69 -99
- pydocket-0.1.1/tests/cli/test_striking.py +232 -0
- pydocket-0.1.1/tests/cli/test_worker.py +179 -0
- pydocket-0.1.1/tests/cli/test_workers.py +65 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/tests/conftest.py +4 -1
- {pydocket-0.1.0 → pydocket-0.1.1}/uv.lock +2 -1
- pydocket-0.1.0/tests/cli/test_module.py +0 -14
- pydocket-0.1.0/tests/cli/test_striking.py +0 -201
- pydocket-0.1.0/tests/cli/test_worker.py +0 -177
- pydocket-0.1.0/tests/cli/test_workers.py +0 -79
- {pydocket-0.1.0 → pydocket-0.1.1}/.cursorrules +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/.github/codecov.yml +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/.github/workflows/chaos.yml +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/.github/workflows/ci.yml +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/.github/workflows/publish.yml +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/.gitignore +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/.pre-commit-config.yaml +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/LICENSE +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/README.md +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/chaos/README.md +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/chaos/__init__.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/chaos/driver.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/chaos/producer.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/chaos/run +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/chaos/tasks.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/__init__.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/__main__.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/annotations.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/dependencies.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/docket.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/execution.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/instrumentation.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/py.typed +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/tasks.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/worker.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/telemetry/.gitignore +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/telemetry/start +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/telemetry/stop +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/tests/__init__.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/tests/cli/__init__.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/tests/cli/conftest.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/tests/cli/test_parsing.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/tests/cli/test_tasks.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/tests/cli/test_version.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/tests/test_dependencies.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/tests/test_docket.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/tests/test_fundamentals.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/tests/test_instrumentation.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/tests/test_striking.py +0 -0
- {pydocket-0.1.0 → pydocket-0.1.1}/tests/test_worker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydocket
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
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
|
|
@@ -28,7 +28,7 @@ Requires-Dist: opentelemetry-api>=1.30.0
|
|
|
28
28
|
Requires-Dist: opentelemetry-exporter-prometheus>=0.51b0
|
|
29
29
|
Requires-Dist: prometheus-client>=0.21.1
|
|
30
30
|
Requires-Dist: python-json-logger>=3.2.1
|
|
31
|
-
Requires-Dist: redis>=
|
|
31
|
+
Requires-Dist: redis>=4.6
|
|
32
32
|
Requires-Dist: rich>=13.9.4
|
|
33
33
|
Requires-Dist: typer>=0.15.1
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
@@ -365,7 +365,7 @@ def restore(
|
|
|
365
365
|
value_ = interpret_python_value(value)
|
|
366
366
|
if parameter:
|
|
367
367
|
function_name = f"{function or '(all tasks)'}"
|
|
368
|
-
print(f"
|
|
368
|
+
print(f"Restoring {function_name} {parameter} {operator} {value_!r}")
|
|
369
369
|
else:
|
|
370
370
|
print(f"Restoring {function}")
|
|
371
371
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import docket
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def test_module_invocation_as_cli_entrypoint():
|
|
9
|
+
"""Should allow invoking docket as a module with python -m docket."""
|
|
10
|
+
process = await asyncio.create_subprocess_exec(
|
|
11
|
+
sys.executable,
|
|
12
|
+
"-m",
|
|
13
|
+
"docket",
|
|
14
|
+
"version",
|
|
15
|
+
stdout=subprocess.PIPE,
|
|
16
|
+
stderr=subprocess.PIPE,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
stdout, stderr = await process.communicate()
|
|
20
|
+
|
|
21
|
+
assert process.returncode == 0, stderr.decode()
|
|
22
|
+
assert stdout.decode().strip() == docket.__version__
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import sys
|
|
3
2
|
from datetime import datetime, timedelta, timezone
|
|
4
3
|
|
|
5
4
|
import pytest
|
|
6
5
|
from pytest import MonkeyPatch
|
|
6
|
+
from typer.testing import CliRunner
|
|
7
7
|
|
|
8
8
|
from docket import tasks
|
|
9
|
-
from docket.cli import relative_time
|
|
9
|
+
from docket.cli import app, relative_time
|
|
10
10
|
from docket.docket import Docket
|
|
11
11
|
from docket.worker import Worker
|
|
12
12
|
|
|
@@ -19,105 +19,83 @@ async def empty_docket(docket: Docket):
|
|
|
19
19
|
await docket.cancel("initial")
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
async def test_snapshot_empty_docket(docket: Docket):
|
|
22
|
+
async def test_snapshot_empty_docket(docket: Docket, runner: CliRunner):
|
|
23
23
|
"""Should show an empty snapshot when no tasks are scheduled"""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
result = await asyncio.get_running_loop().run_in_executor(
|
|
25
|
+
None,
|
|
26
|
+
runner.invoke,
|
|
27
|
+
app,
|
|
28
|
+
[
|
|
29
|
+
"snapshot",
|
|
30
|
+
"--url",
|
|
31
|
+
docket.url,
|
|
32
|
+
"--docket",
|
|
33
|
+
docket.name,
|
|
34
|
+
],
|
|
35
35
|
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
assert process.stderr
|
|
39
|
-
stderr = await process.stderr.read()
|
|
40
|
-
assert process.returncode == 0, stderr.decode()
|
|
36
|
+
assert result.exit_code == 0, result.output
|
|
41
37
|
|
|
42
|
-
assert
|
|
43
|
-
output = await process.stdout.read()
|
|
44
|
-
output_text = output.decode()
|
|
38
|
+
assert "0 workers, 0/0 running" in result.output
|
|
45
39
|
|
|
46
|
-
assert "0 workers, 0/0 running" in output_text
|
|
47
40
|
|
|
48
|
-
|
|
49
|
-
async def test_snapshot_with_scheduled_tasks(docket: Docket):
|
|
41
|
+
async def test_snapshot_with_scheduled_tasks(docket: Docket, runner: CliRunner):
|
|
50
42
|
"""Should show scheduled tasks in the snapshot"""
|
|
51
43
|
when = datetime.now(timezone.utc) + timedelta(seconds=5)
|
|
52
44
|
await docket.add(tasks.trace, when=when, key="future-task")("hiya!")
|
|
53
45
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
46
|
+
result = await asyncio.get_running_loop().run_in_executor(
|
|
47
|
+
None,
|
|
48
|
+
runner.invoke,
|
|
49
|
+
app,
|
|
50
|
+
[
|
|
51
|
+
"snapshot",
|
|
52
|
+
"--url",
|
|
53
|
+
docket.url,
|
|
54
|
+
"--docket",
|
|
55
|
+
docket.name,
|
|
56
|
+
],
|
|
65
57
|
)
|
|
66
|
-
|
|
58
|
+
assert result.exit_code == 0, result.output
|
|
67
59
|
|
|
68
|
-
assert
|
|
69
|
-
|
|
70
|
-
assert process.returncode == 0, stderr.decode()
|
|
60
|
+
assert "0 workers, 0/1 running" in result.output
|
|
61
|
+
assert "future-task" in result.output
|
|
71
62
|
|
|
72
|
-
assert process.stdout
|
|
73
|
-
output = await process.stdout.read()
|
|
74
|
-
output_text = output.decode()
|
|
75
63
|
|
|
76
|
-
|
|
77
|
-
assert "future-task" in output_text
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
async def test_snapshot_with_running_tasks(docket: Docket):
|
|
64
|
+
async def test_snapshot_with_running_tasks(docket: Docket, runner: CliRunner):
|
|
81
65
|
"""Should show running tasks in the snapshot"""
|
|
82
66
|
heartbeat = timedelta(milliseconds=20)
|
|
83
67
|
docket.heartbeat_interval = heartbeat
|
|
84
68
|
|
|
85
|
-
await docket.add(tasks.sleep)(
|
|
69
|
+
await docket.add(tasks.sleep)(1)
|
|
86
70
|
|
|
87
71
|
async with Worker(docket, name="test-worker") as worker:
|
|
88
72
|
worker_running = asyncio.create_task(worker.run_until_finished())
|
|
89
73
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
74
|
+
await asyncio.sleep(0.1)
|
|
75
|
+
|
|
76
|
+
result = await asyncio.get_running_loop().run_in_executor(
|
|
77
|
+
None,
|
|
78
|
+
runner.invoke,
|
|
79
|
+
app,
|
|
80
|
+
[
|
|
81
|
+
"snapshot",
|
|
82
|
+
"--url",
|
|
83
|
+
docket.url,
|
|
84
|
+
"--docket",
|
|
85
|
+
docket.name,
|
|
86
|
+
],
|
|
101
87
|
)
|
|
102
|
-
|
|
88
|
+
assert result.exit_code == 0, result.output
|
|
103
89
|
|
|
104
|
-
assert
|
|
105
|
-
|
|
106
|
-
assert
|
|
107
|
-
|
|
108
|
-
assert process.stdout
|
|
109
|
-
output = await process.stdout.read()
|
|
110
|
-
output_text = output.decode()
|
|
111
|
-
|
|
112
|
-
assert "1 workers, 1/1 running" in output_text
|
|
113
|
-
assert "sleep" in output_text
|
|
114
|
-
assert "test-worker" in output_text
|
|
90
|
+
assert "1 workers, 1/1 running" in result.output
|
|
91
|
+
assert "sleep" in result.output
|
|
92
|
+
assert "test-worker" in result.output
|
|
115
93
|
|
|
116
94
|
worker_running.cancel()
|
|
117
95
|
await worker_running
|
|
118
96
|
|
|
119
97
|
|
|
120
|
-
async def test_snapshot_with_mixed_tasks(docket: Docket):
|
|
98
|
+
async def test_snapshot_with_mixed_tasks(docket: Docket, runner: CliRunner):
|
|
121
99
|
"""Should show both running and scheduled tasks in the snapshot"""
|
|
122
100
|
heartbeat = timedelta(milliseconds=20)
|
|
123
101
|
docket.heartbeat_interval = heartbeat
|
|
@@ -130,34 +108,26 @@ async def test_snapshot_with_mixed_tasks(docket: Docket):
|
|
|
130
108
|
async with Worker(docket, name="test-worker", concurrency=2) as worker:
|
|
131
109
|
worker_running = asyncio.create_task(worker.run_until_finished())
|
|
132
110
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
],
|
|
144
124
|
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
assert process.stderr
|
|
148
|
-
stderr = await process.stderr.read()
|
|
149
|
-
assert process.returncode == 0, stderr.decode()
|
|
150
|
-
|
|
151
|
-
assert process.stdout
|
|
152
|
-
output = await process.stdout.read()
|
|
153
|
-
output_text = output.decode()
|
|
154
|
-
|
|
155
|
-
print(output_text)
|
|
125
|
+
assert result.exit_code == 0, result.output
|
|
156
126
|
|
|
157
|
-
assert "1 workers, 2/6 running" in
|
|
158
|
-
assert "sleep" in
|
|
159
|
-
assert "test-worker" in
|
|
160
|
-
assert "trace" in
|
|
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
|
|
161
131
|
|
|
162
132
|
worker_running.cancel()
|
|
163
133
|
await worker_running
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import decimal
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from typing import Any
|
|
5
|
+
from uuid import UUID, uuid4
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from typer.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
from docket.cli import app, interpret_python_value
|
|
11
|
+
from docket.docket import Docket
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def test_strike(runner: CliRunner, redis_url: str):
|
|
15
|
+
"""Should strike a task"""
|
|
16
|
+
async with Docket(name=f"test-docket-{uuid4()}", url=redis_url) as docket:
|
|
17
|
+
result = await asyncio.get_running_loop().run_in_executor(
|
|
18
|
+
None,
|
|
19
|
+
runner.invoke,
|
|
20
|
+
app,
|
|
21
|
+
[
|
|
22
|
+
"strike",
|
|
23
|
+
"--url",
|
|
24
|
+
docket.url,
|
|
25
|
+
"--docket",
|
|
26
|
+
docket.name,
|
|
27
|
+
"example_task",
|
|
28
|
+
"some_parameter",
|
|
29
|
+
"==",
|
|
30
|
+
"some_value",
|
|
31
|
+
],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
assert result.exit_code == 0, result.output
|
|
35
|
+
|
|
36
|
+
assert "Striking example_task some_parameter == 'some_value'" in result.output
|
|
37
|
+
|
|
38
|
+
await asyncio.sleep(0.25)
|
|
39
|
+
|
|
40
|
+
assert "example_task" in docket.strike_list.task_strikes
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def test_restore(runner: CliRunner, redis_url: str):
|
|
44
|
+
"""Should restore a task"""
|
|
45
|
+
async with Docket(name=f"test-docket-{uuid4()}", url=redis_url) as docket:
|
|
46
|
+
await docket.strike("example_task", "some_parameter", "==", "some_value")
|
|
47
|
+
assert "example_task" in docket.strike_list.task_strikes
|
|
48
|
+
|
|
49
|
+
result = await asyncio.get_running_loop().run_in_executor(
|
|
50
|
+
None,
|
|
51
|
+
runner.invoke,
|
|
52
|
+
app,
|
|
53
|
+
[
|
|
54
|
+
"restore",
|
|
55
|
+
"--url",
|
|
56
|
+
docket.url,
|
|
57
|
+
"--docket",
|
|
58
|
+
docket.name,
|
|
59
|
+
"example_task",
|
|
60
|
+
"some_parameter",
|
|
61
|
+
"==",
|
|
62
|
+
"some_value",
|
|
63
|
+
],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
assert result.exit_code == 0, result.output
|
|
67
|
+
|
|
68
|
+
assert "Restoring example_task some_parameter == 'some_value'" in result.output
|
|
69
|
+
|
|
70
|
+
await asyncio.sleep(0.25)
|
|
71
|
+
|
|
72
|
+
assert "example_task" not in docket.strike_list.task_strikes
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def test_task_only_strike(runner: CliRunner, redis_url: str):
|
|
76
|
+
"""Should strike a task without specifying parameter conditions"""
|
|
77
|
+
async with Docket(name=f"test-docket-{uuid4()}", url=redis_url) as docket:
|
|
78
|
+
result = await asyncio.get_running_loop().run_in_executor(
|
|
79
|
+
None,
|
|
80
|
+
runner.invoke,
|
|
81
|
+
app,
|
|
82
|
+
[
|
|
83
|
+
"strike",
|
|
84
|
+
"--url",
|
|
85
|
+
docket.url,
|
|
86
|
+
"--docket",
|
|
87
|
+
docket.name,
|
|
88
|
+
"example_task",
|
|
89
|
+
],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
assert result.exit_code == 0, result.output
|
|
93
|
+
assert "Striking example_task" in result.output
|
|
94
|
+
|
|
95
|
+
await asyncio.sleep(0.25)
|
|
96
|
+
|
|
97
|
+
assert "example_task" in docket.strike_list.task_strikes
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def test_task_only_restore(runner: CliRunner, redis_url: str):
|
|
101
|
+
"""Should restore a task without specifying parameter conditions"""
|
|
102
|
+
async with Docket(name=f"test-docket-{uuid4()}", url=redis_url) as docket:
|
|
103
|
+
await docket.strike("example_task")
|
|
104
|
+
|
|
105
|
+
async with Docket(name=f"test-docket-{uuid4()}", url=redis_url) as docket:
|
|
106
|
+
result = await asyncio.get_running_loop().run_in_executor(
|
|
107
|
+
None,
|
|
108
|
+
runner.invoke,
|
|
109
|
+
app,
|
|
110
|
+
[
|
|
111
|
+
"restore",
|
|
112
|
+
"--url",
|
|
113
|
+
docket.url,
|
|
114
|
+
"--docket",
|
|
115
|
+
docket.name,
|
|
116
|
+
"example_task",
|
|
117
|
+
],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
assert result.exit_code == 0, result.output
|
|
121
|
+
assert "Restoring example_task" in result.output
|
|
122
|
+
|
|
123
|
+
await asyncio.sleep(0.25)
|
|
124
|
+
|
|
125
|
+
assert "example_task" not in docket.strike_list.task_strikes
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def test_parameter_only_strike(runner: CliRunner, redis_url: str):
|
|
129
|
+
"""Should strike tasks with matching parameter conditions regardless of task name"""
|
|
130
|
+
async with Docket(name=f"test-docket-{uuid4()}", url=redis_url) as docket:
|
|
131
|
+
result = await asyncio.get_running_loop().run_in_executor(
|
|
132
|
+
None,
|
|
133
|
+
runner.invoke,
|
|
134
|
+
app,
|
|
135
|
+
[
|
|
136
|
+
"strike",
|
|
137
|
+
"--url",
|
|
138
|
+
docket.url,
|
|
139
|
+
"--docket",
|
|
140
|
+
docket.name,
|
|
141
|
+
"",
|
|
142
|
+
"some_parameter",
|
|
143
|
+
"==",
|
|
144
|
+
"some_value",
|
|
145
|
+
],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
assert result.exit_code == 0, result.output
|
|
149
|
+
assert "Striking (all tasks) some_parameter == 'some_value'" in result.output
|
|
150
|
+
|
|
151
|
+
await asyncio.sleep(0.25)
|
|
152
|
+
|
|
153
|
+
assert "some_parameter" in docket.strike_list.parameter_strikes
|
|
154
|
+
parameter_strikes = docket.strike_list.parameter_strikes["some_parameter"]
|
|
155
|
+
assert ("==", "some_value") in parameter_strikes
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def test_parameter_only_restore(runner: CliRunner, redis_url: str):
|
|
159
|
+
"""Should restore tasks with matching parameter conditions regardless of task
|
|
160
|
+
name"""
|
|
161
|
+
async with Docket(name=f"test-docket-{uuid4()}", url=redis_url) as docket:
|
|
162
|
+
await docket.strike("", "some_parameter", "==", "some_value")
|
|
163
|
+
|
|
164
|
+
result = await asyncio.get_running_loop().run_in_executor(
|
|
165
|
+
None,
|
|
166
|
+
runner.invoke,
|
|
167
|
+
app,
|
|
168
|
+
[
|
|
169
|
+
"restore",
|
|
170
|
+
"--url",
|
|
171
|
+
docket.url,
|
|
172
|
+
"--docket",
|
|
173
|
+
docket.name,
|
|
174
|
+
"",
|
|
175
|
+
"some_parameter",
|
|
176
|
+
"==",
|
|
177
|
+
"some_value",
|
|
178
|
+
],
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
assert result.exit_code == 0, result.output
|
|
182
|
+
assert "Restoring (all tasks) some_parameter == 'some_value'" in result.output
|
|
183
|
+
|
|
184
|
+
await asyncio.sleep(0.25)
|
|
185
|
+
|
|
186
|
+
assert "some_parameter" not in docket.strike_list.parameter_strikes
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@pytest.mark.parametrize("operation", ["strike", "restore"])
|
|
190
|
+
async def test_strike_with_no_function_or_parameter(
|
|
191
|
+
runner: CliRunner, redis_url: str, operation: str
|
|
192
|
+
):
|
|
193
|
+
"""Should fail when neither function nor parameter is provided"""
|
|
194
|
+
async with Docket(name=f"test-docket-{uuid4()}", url=redis_url) as docket:
|
|
195
|
+
result = await asyncio.get_running_loop().run_in_executor(
|
|
196
|
+
None,
|
|
197
|
+
runner.invoke,
|
|
198
|
+
app,
|
|
199
|
+
[
|
|
200
|
+
operation,
|
|
201
|
+
"--url",
|
|
202
|
+
docket.url,
|
|
203
|
+
"--docket",
|
|
204
|
+
docket.name,
|
|
205
|
+
"",
|
|
206
|
+
],
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
assert result.exit_code != 0, result.output
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@pytest.mark.parametrize(
|
|
213
|
+
"input_value,expected_result",
|
|
214
|
+
[
|
|
215
|
+
(None, None),
|
|
216
|
+
("hello", "hello"),
|
|
217
|
+
("int:42", 42),
|
|
218
|
+
("float:3.14", 3.14),
|
|
219
|
+
("decimal.Decimal:3.14", decimal.Decimal("3.14")),
|
|
220
|
+
("bool:True", True),
|
|
221
|
+
("bool:False", False),
|
|
222
|
+
("datetime.timedelta:10", timedelta(seconds=10)),
|
|
223
|
+
(
|
|
224
|
+
"uuid.UUID:123e4567-e89b-12d3-a456-426614174000",
|
|
225
|
+
UUID("123e4567-e89b-12d3-a456-426614174000"),
|
|
226
|
+
),
|
|
227
|
+
],
|
|
228
|
+
)
|
|
229
|
+
async def test_interpret_python_value(input_value: str | None, expected_result: Any):
|
|
230
|
+
"""Should interpret Python values correctly from strings"""
|
|
231
|
+
result = interpret_python_value(input_value)
|
|
232
|
+
assert result == expected_result
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from functools import partial
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from typer.testing import CliRunner
|
|
10
|
+
|
|
11
|
+
from docket.cli import app
|
|
12
|
+
from docket.docket import Docket
|
|
13
|
+
from docket.tasks import trace
|
|
14
|
+
from docket.worker import Worker
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(autouse=True)
|
|
18
|
+
def reset_logging() -> None:
|
|
19
|
+
logging.basicConfig(force=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_worker_command_exposes_all_the_options_of_worker():
|
|
23
|
+
"""Should expose all the options of Worker.run in the CLI command"""
|
|
24
|
+
from docket.cli import worker as worker_cli_command
|
|
25
|
+
|
|
26
|
+
cli_signature = inspect.signature(worker_cli_command)
|
|
27
|
+
worker_run_signature = inspect.signature(Worker.run)
|
|
28
|
+
|
|
29
|
+
cli_params = {
|
|
30
|
+
name: (param.default, param.annotation)
|
|
31
|
+
for name, param in cli_signature.parameters.items()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Remove CLI-only parameters
|
|
35
|
+
cli_params.pop("logging_level")
|
|
36
|
+
|
|
37
|
+
worker_params = {
|
|
38
|
+
name: (param.default, param.annotation)
|
|
39
|
+
for name, param in worker_run_signature.parameters.items()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for name, (default, _) in worker_params.items():
|
|
43
|
+
cli_name = name if name != "docket_name" else "docket_"
|
|
44
|
+
|
|
45
|
+
assert cli_name in cli_params, f"Parameter {name} missing from CLI"
|
|
46
|
+
|
|
47
|
+
cli_default, _ = cli_params[cli_name]
|
|
48
|
+
|
|
49
|
+
if name == "name":
|
|
50
|
+
# Skip hostname check for the 'name' parameter as it's machine-specific
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
assert cli_default == default, (
|
|
54
|
+
f"Default for {name} doesn't match: CLI={cli_default}, Worker.run={default}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_worker_command(
|
|
59
|
+
runner: CliRunner,
|
|
60
|
+
docket: Docket,
|
|
61
|
+
):
|
|
62
|
+
"""Should run a worker until there are no more tasks to process"""
|
|
63
|
+
result = runner.invoke(
|
|
64
|
+
app,
|
|
65
|
+
[
|
|
66
|
+
"worker",
|
|
67
|
+
"--until-finished",
|
|
68
|
+
"--url",
|
|
69
|
+
docket.url,
|
|
70
|
+
"--docket",
|
|
71
|
+
docket.name,
|
|
72
|
+
],
|
|
73
|
+
color=True,
|
|
74
|
+
)
|
|
75
|
+
assert result.exit_code == 0
|
|
76
|
+
|
|
77
|
+
assert "Starting worker" in result.output
|
|
78
|
+
assert "trace" in result.output
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def test_rich_logging_format(runner: CliRunner, docket: Docket):
|
|
82
|
+
"""Should use rich formatting for logs by default"""
|
|
83
|
+
await docket.add(trace)("hello")
|
|
84
|
+
|
|
85
|
+
logging.basicConfig(force=True)
|
|
86
|
+
|
|
87
|
+
result = await asyncio.get_running_loop().run_in_executor(
|
|
88
|
+
None,
|
|
89
|
+
partial(
|
|
90
|
+
runner.invoke,
|
|
91
|
+
app,
|
|
92
|
+
[
|
|
93
|
+
"worker",
|
|
94
|
+
"--until-finished",
|
|
95
|
+
"--url",
|
|
96
|
+
docket.url,
|
|
97
|
+
"--docket",
|
|
98
|
+
docket.name,
|
|
99
|
+
"--logging-format",
|
|
100
|
+
"rich",
|
|
101
|
+
],
|
|
102
|
+
color=True,
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
assert result.exit_code == 0, result.output
|
|
107
|
+
|
|
108
|
+
assert "Starting worker" in result.output
|
|
109
|
+
assert "trace" in result.output
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def test_plain_logging_format(runner: CliRunner, docket: Docket):
|
|
113
|
+
"""Should use plain formatting for logs when specified"""
|
|
114
|
+
await docket.add(trace)("hello")
|
|
115
|
+
|
|
116
|
+
result = await asyncio.get_running_loop().run_in_executor(
|
|
117
|
+
None,
|
|
118
|
+
partial(
|
|
119
|
+
runner.invoke,
|
|
120
|
+
app,
|
|
121
|
+
[
|
|
122
|
+
"worker",
|
|
123
|
+
"--until-finished",
|
|
124
|
+
"--url",
|
|
125
|
+
docket.url,
|
|
126
|
+
"--docket",
|
|
127
|
+
docket.name,
|
|
128
|
+
"--logging-format",
|
|
129
|
+
"plain",
|
|
130
|
+
],
|
|
131
|
+
color=True,
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
assert result.exit_code == 0, result.output
|
|
136
|
+
|
|
137
|
+
assert "Starting worker" in result.output
|
|
138
|
+
assert "trace" in result.output
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def test_json_logging_format(runner: CliRunner, docket: Docket):
|
|
142
|
+
"""Should use JSON formatting for logs when specified"""
|
|
143
|
+
await docket.add(trace)("hello")
|
|
144
|
+
|
|
145
|
+
start = datetime.now(timezone.utc)
|
|
146
|
+
|
|
147
|
+
result = await asyncio.get_running_loop().run_in_executor(
|
|
148
|
+
None,
|
|
149
|
+
runner.invoke,
|
|
150
|
+
app,
|
|
151
|
+
[
|
|
152
|
+
"worker",
|
|
153
|
+
"--until-finished",
|
|
154
|
+
"--url",
|
|
155
|
+
docket.url,
|
|
156
|
+
"--docket",
|
|
157
|
+
docket.name,
|
|
158
|
+
"--logging-format",
|
|
159
|
+
"json",
|
|
160
|
+
],
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
assert result.exit_code == 0, result.output
|
|
164
|
+
|
|
165
|
+
# All output lines should be valid JSON
|
|
166
|
+
for line in result.output.strip().split("\n"):
|
|
167
|
+
parsed: dict[str, str] = json.loads(line)
|
|
168
|
+
|
|
169
|
+
assert isinstance(parsed, dict)
|
|
170
|
+
|
|
171
|
+
assert parsed["name"].startswith("docket.")
|
|
172
|
+
assert parsed["levelname"] in ("INFO", "WARNING", "ERROR", "CRITICAL")
|
|
173
|
+
assert "message" in parsed
|
|
174
|
+
assert "exc_info" in parsed
|
|
175
|
+
|
|
176
|
+
timestamp = datetime.strptime(parsed["asctime"], "%Y-%m-%d %H:%M:%S,%f")
|
|
177
|
+
timestamp = timestamp.astimezone()
|
|
178
|
+
assert timestamp >= start
|
|
179
|
+
assert timestamp.tzinfo is not None
|