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.

Files changed (54) hide show
  1. {pydocket-0.1.0 → pydocket-0.1.1}/PKG-INFO +2 -2
  2. {pydocket-0.1.0 → pydocket-0.1.1}/pyproject.toml +1 -1
  3. {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/cli.py +1 -1
  4. pydocket-0.1.1/tests/cli/test_module.py +22 -0
  5. {pydocket-0.1.0 → pydocket-0.1.1}/tests/cli/test_snapshot.py +69 -99
  6. pydocket-0.1.1/tests/cli/test_striking.py +232 -0
  7. pydocket-0.1.1/tests/cli/test_worker.py +179 -0
  8. pydocket-0.1.1/tests/cli/test_workers.py +65 -0
  9. {pydocket-0.1.0 → pydocket-0.1.1}/tests/conftest.py +4 -1
  10. {pydocket-0.1.0 → pydocket-0.1.1}/uv.lock +2 -1
  11. pydocket-0.1.0/tests/cli/test_module.py +0 -14
  12. pydocket-0.1.0/tests/cli/test_striking.py +0 -201
  13. pydocket-0.1.0/tests/cli/test_worker.py +0 -177
  14. pydocket-0.1.0/tests/cli/test_workers.py +0 -79
  15. {pydocket-0.1.0 → pydocket-0.1.1}/.cursorrules +0 -0
  16. {pydocket-0.1.0 → pydocket-0.1.1}/.github/codecov.yml +0 -0
  17. {pydocket-0.1.0 → pydocket-0.1.1}/.github/workflows/chaos.yml +0 -0
  18. {pydocket-0.1.0 → pydocket-0.1.1}/.github/workflows/ci.yml +0 -0
  19. {pydocket-0.1.0 → pydocket-0.1.1}/.github/workflows/publish.yml +0 -0
  20. {pydocket-0.1.0 → pydocket-0.1.1}/.gitignore +0 -0
  21. {pydocket-0.1.0 → pydocket-0.1.1}/.pre-commit-config.yaml +0 -0
  22. {pydocket-0.1.0 → pydocket-0.1.1}/LICENSE +0 -0
  23. {pydocket-0.1.0 → pydocket-0.1.1}/README.md +0 -0
  24. {pydocket-0.1.0 → pydocket-0.1.1}/chaos/README.md +0 -0
  25. {pydocket-0.1.0 → pydocket-0.1.1}/chaos/__init__.py +0 -0
  26. {pydocket-0.1.0 → pydocket-0.1.1}/chaos/driver.py +0 -0
  27. {pydocket-0.1.0 → pydocket-0.1.1}/chaos/producer.py +0 -0
  28. {pydocket-0.1.0 → pydocket-0.1.1}/chaos/run +0 -0
  29. {pydocket-0.1.0 → pydocket-0.1.1}/chaos/tasks.py +0 -0
  30. {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/__init__.py +0 -0
  31. {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/__main__.py +0 -0
  32. {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/annotations.py +0 -0
  33. {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/dependencies.py +0 -0
  34. {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/docket.py +0 -0
  35. {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/execution.py +0 -0
  36. {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/instrumentation.py +0 -0
  37. {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/py.typed +0 -0
  38. {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/tasks.py +0 -0
  39. {pydocket-0.1.0 → pydocket-0.1.1}/src/docket/worker.py +0 -0
  40. {pydocket-0.1.0 → pydocket-0.1.1}/telemetry/.gitignore +0 -0
  41. {pydocket-0.1.0 → pydocket-0.1.1}/telemetry/start +0 -0
  42. {pydocket-0.1.0 → pydocket-0.1.1}/telemetry/stop +0 -0
  43. {pydocket-0.1.0 → pydocket-0.1.1}/tests/__init__.py +0 -0
  44. {pydocket-0.1.0 → pydocket-0.1.1}/tests/cli/__init__.py +0 -0
  45. {pydocket-0.1.0 → pydocket-0.1.1}/tests/cli/conftest.py +0 -0
  46. {pydocket-0.1.0 → pydocket-0.1.1}/tests/cli/test_parsing.py +0 -0
  47. {pydocket-0.1.0 → pydocket-0.1.1}/tests/cli/test_tasks.py +0 -0
  48. {pydocket-0.1.0 → pydocket-0.1.1}/tests/cli/test_version.py +0 -0
  49. {pydocket-0.1.0 → pydocket-0.1.1}/tests/test_dependencies.py +0 -0
  50. {pydocket-0.1.0 → pydocket-0.1.1}/tests/test_docket.py +0 -0
  51. {pydocket-0.1.0 → pydocket-0.1.1}/tests/test_fundamentals.py +0 -0
  52. {pydocket-0.1.0 → pydocket-0.1.1}/tests/test_instrumentation.py +0 -0
  53. {pydocket-0.1.0 → pydocket-0.1.1}/tests/test_striking.py +0 -0
  54. {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.0
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>=5.2.1
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
@@ -25,7 +25,7 @@ dependencies = [
25
25
  "opentelemetry-exporter-prometheus>=0.51b0",
26
26
  "prometheus-client>=0.21.1",
27
27
  "python-json-logger>=3.2.1",
28
- "redis>=5.2.1",
28
+ "redis>=4.6",
29
29
  "rich>=13.9.4",
30
30
  "typer>=0.15.1",
31
31
  ]
@@ -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"Striking {function_name} {parameter} {operator} {value_!r}")
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
- process = await asyncio.create_subprocess_exec(
25
- sys.executable,
26
- "-m",
27
- "docket",
28
- "snapshot",
29
- "--url",
30
- docket.url,
31
- "--docket",
32
- docket.name,
33
- stdout=asyncio.subprocess.PIPE,
34
- stderr=asyncio.subprocess.PIPE,
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
- await process.wait()
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 process.stdout
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
- process = await asyncio.create_subprocess_exec(
55
- sys.executable,
56
- "-m",
57
- "docket",
58
- "snapshot",
59
- "--url",
60
- docket.url,
61
- "--docket",
62
- docket.name,
63
- stdout=asyncio.subprocess.PIPE,
64
- stderr=asyncio.subprocess.PIPE,
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
- await process.wait()
58
+ assert result.exit_code == 0, result.output
67
59
 
68
- assert process.stderr
69
- stderr = await process.stderr.read()
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
- assert "0 workers, 0/1 running" in output_text
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)(2)
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
- process = await asyncio.create_subprocess_exec(
91
- sys.executable,
92
- "-m",
93
- "docket",
94
- "snapshot",
95
- "--url",
96
- docket.url,
97
- "--docket",
98
- docket.name,
99
- stdout=asyncio.subprocess.PIPE,
100
- stderr=asyncio.subprocess.PIPE,
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
- await process.wait()
88
+ assert result.exit_code == 0, result.output
103
89
 
104
- assert process.stderr
105
- stderr = await process.stderr.read()
106
- assert process.returncode == 0, stderr.decode()
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
- process = await asyncio.create_subprocess_exec(
134
- sys.executable,
135
- "-m",
136
- "docket",
137
- "snapshot",
138
- "--url",
139
- docket.url,
140
- "--docket",
141
- docket.name,
142
- stdout=asyncio.subprocess.PIPE,
143
- stderr=asyncio.subprocess.PIPE,
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
- await process.wait()
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 output_text
158
- assert "sleep" in output_text
159
- assert "test-worker" in output_text
160
- assert "trace" in output_text
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