pydocket 0.1.0__tar.gz → 0.1.2__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 (55) hide show
  1. pydocket-0.1.0/.cursorrules → pydocket-0.1.2/.cursor/rules/general.mdc +7 -17
  2. pydocket-0.1.2/.cursor/rules/python-style.mdc +22 -0
  3. {pydocket-0.1.0 → pydocket-0.1.2}/.github/workflows/ci.yml +1 -0
  4. {pydocket-0.1.0 → pydocket-0.1.2}/PKG-INFO +2 -2
  5. {pydocket-0.1.0 → pydocket-0.1.2}/pyproject.toml +3 -1
  6. {pydocket-0.1.0 → pydocket-0.1.2}/src/docket/cli.py +1 -1
  7. {pydocket-0.1.0 → pydocket-0.1.2}/src/docket/worker.py +1 -1
  8. pydocket-0.1.2/tests/cli/test_module.py +22 -0
  9. {pydocket-0.1.0 → pydocket-0.1.2}/tests/cli/test_snapshot.py +69 -99
  10. pydocket-0.1.2/tests/cli/test_striking.py +232 -0
  11. pydocket-0.1.2/tests/cli/test_worker.py +179 -0
  12. pydocket-0.1.2/tests/cli/test_workers.py +65 -0
  13. {pydocket-0.1.0 → pydocket-0.1.2}/tests/conftest.py +18 -4
  14. {pydocket-0.1.0 → pydocket-0.1.2}/uv.lock +2 -1
  15. pydocket-0.1.0/tests/cli/test_module.py +0 -14
  16. pydocket-0.1.0/tests/cli/test_striking.py +0 -201
  17. pydocket-0.1.0/tests/cli/test_worker.py +0 -177
  18. pydocket-0.1.0/tests/cli/test_workers.py +0 -79
  19. {pydocket-0.1.0 → pydocket-0.1.2}/.github/codecov.yml +0 -0
  20. {pydocket-0.1.0 → pydocket-0.1.2}/.github/workflows/chaos.yml +0 -0
  21. {pydocket-0.1.0 → pydocket-0.1.2}/.github/workflows/publish.yml +0 -0
  22. {pydocket-0.1.0 → pydocket-0.1.2}/.gitignore +0 -0
  23. {pydocket-0.1.0 → pydocket-0.1.2}/.pre-commit-config.yaml +0 -0
  24. {pydocket-0.1.0 → pydocket-0.1.2}/LICENSE +0 -0
  25. {pydocket-0.1.0 → pydocket-0.1.2}/README.md +0 -0
  26. {pydocket-0.1.0 → pydocket-0.1.2}/chaos/README.md +0 -0
  27. {pydocket-0.1.0 → pydocket-0.1.2}/chaos/__init__.py +0 -0
  28. {pydocket-0.1.0 → pydocket-0.1.2}/chaos/driver.py +0 -0
  29. {pydocket-0.1.0 → pydocket-0.1.2}/chaos/producer.py +0 -0
  30. {pydocket-0.1.0 → pydocket-0.1.2}/chaos/run +0 -0
  31. {pydocket-0.1.0 → pydocket-0.1.2}/chaos/tasks.py +0 -0
  32. {pydocket-0.1.0 → pydocket-0.1.2}/src/docket/__init__.py +0 -0
  33. {pydocket-0.1.0 → pydocket-0.1.2}/src/docket/__main__.py +0 -0
  34. {pydocket-0.1.0 → pydocket-0.1.2}/src/docket/annotations.py +0 -0
  35. {pydocket-0.1.0 → pydocket-0.1.2}/src/docket/dependencies.py +0 -0
  36. {pydocket-0.1.0 → pydocket-0.1.2}/src/docket/docket.py +0 -0
  37. {pydocket-0.1.0 → pydocket-0.1.2}/src/docket/execution.py +0 -0
  38. {pydocket-0.1.0 → pydocket-0.1.2}/src/docket/instrumentation.py +0 -0
  39. {pydocket-0.1.0 → pydocket-0.1.2}/src/docket/py.typed +0 -0
  40. {pydocket-0.1.0 → pydocket-0.1.2}/src/docket/tasks.py +0 -0
  41. {pydocket-0.1.0 → pydocket-0.1.2}/telemetry/.gitignore +0 -0
  42. {pydocket-0.1.0 → pydocket-0.1.2}/telemetry/start +0 -0
  43. {pydocket-0.1.0 → pydocket-0.1.2}/telemetry/stop +0 -0
  44. {pydocket-0.1.0 → pydocket-0.1.2}/tests/__init__.py +0 -0
  45. {pydocket-0.1.0 → pydocket-0.1.2}/tests/cli/__init__.py +0 -0
  46. {pydocket-0.1.0 → pydocket-0.1.2}/tests/cli/conftest.py +0 -0
  47. {pydocket-0.1.0 → pydocket-0.1.2}/tests/cli/test_parsing.py +0 -0
  48. {pydocket-0.1.0 → pydocket-0.1.2}/tests/cli/test_tasks.py +0 -0
  49. {pydocket-0.1.0 → pydocket-0.1.2}/tests/cli/test_version.py +0 -0
  50. {pydocket-0.1.0 → pydocket-0.1.2}/tests/test_dependencies.py +0 -0
  51. {pydocket-0.1.0 → pydocket-0.1.2}/tests/test_docket.py +0 -0
  52. {pydocket-0.1.0 → pydocket-0.1.2}/tests/test_fundamentals.py +0 -0
  53. {pydocket-0.1.0 → pydocket-0.1.2}/tests/test_instrumentation.py +0 -0
  54. {pydocket-0.1.0 → pydocket-0.1.2}/tests/test_striking.py +0 -0
  55. {pydocket-0.1.0 → pydocket-0.1.2}/tests/test_worker.py +0 -0
@@ -1,3 +1,10 @@
1
+ ---
2
+ description:
3
+ globs:
4
+ alwaysApply: true
5
+ ---
6
+
7
+ # about docket
1
8
  docket is a distributed background task system for Python functions with a focus
2
9
  on the scheduling of future work as seamlessly and efficiency as immediate work.
3
10
 
@@ -22,20 +29,3 @@ idempotency of an execution.
22
29
 
23
30
  A docket worker should be as easily usable in code as it is from the command line,
24
31
  and should be a breeze to use with test suites.
25
-
26
- # Code style
27
-
28
- When generating production code, always use full parameter and return type hints
29
- for every function. Never generate useless inline comments that just reiterate
30
- what the code is doing. It's okay to include comments in the rare case there is
31
- something tricky going on.
32
-
33
- When generating tests, always use parameter type hints, but never include the
34
- `-> None` return type hint for a test function. For `pytest` fixtures, always
35
- generate both the parameter and return type hints.
36
-
37
- When generating tests, favor smaller, focused tests that use fixtures for reuse.
38
- Don't include extraneous comments in the test code unless something needs more
39
- clarity. Always generate a docstring using "should" language to describe the
40
- aspect of the system the test is checking. Use simple direct language and avoid
41
- sounding stuffy, but make these complete sentences.
@@ -0,0 +1,22 @@
1
+ ---
2
+ description: how to write python
3
+ globs: *.py
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Code style
8
+
9
+ When generating production code, always use full parameter and return type hints
10
+ for every function. Never generate useless inline comments that just reiterate
11
+ what the code is doing. It's okay to include comments in the rare case there is
12
+ something tricky going on.
13
+
14
+ When generating tests, always use parameter type hints, but never include the
15
+ `-> None` return type hint for a test function. For `pytest` fixtures, always
16
+ generate both the parameter and return type hints.
17
+
18
+ When generating tests, favor smaller, focused tests that use fixtures for reuse.
19
+ Don't include extraneous comments in the test code unless something needs more
20
+ clarity. Always generate a docstring using "should" language to describe the
21
+ aspect of the system the test is checking. Use simple direct language and avoid
22
+ sounding stuffy, but make these complete sentences.
@@ -7,6 +7,7 @@ on:
7
7
  branches: [main]
8
8
  workflow_call:
9
9
 
10
+
10
11
  jobs:
11
12
  test:
12
13
  name: Test Python ${{ matrix.python-version }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydocket
3
- Version: 0.1.0
3
+ Version: 0.1.2
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
  ]
@@ -68,6 +68,8 @@ packages = ["src/docket"]
68
68
  addopts = "--cov=src/docket --cov=tests --cov-report=term-missing --cov-branch"
69
69
  asyncio_mode = "auto"
70
70
  asyncio_default_fixture_loop_scope = "session"
71
+ filterwarnings = ["error"]
72
+
71
73
 
72
74
  [tool.pyright]
73
75
  include = ["src", "tests"]
@@ -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
 
@@ -262,7 +262,7 @@ class Worker:
262
262
  )
263
263
 
264
264
  redeliveries: RedisMessages
265
- _, redeliveries, _ = await redis.xautoclaim(
265
+ _, redeliveries, *_ = await redis.xautoclaim(
266
266
  name=self.docket.stream_key,
267
267
  groupname=self.docket.worker_group_name,
268
268
  consumername=self.name,
@@ -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