crazy-workers 1.4.2__tar.gz → 1.5.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.
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/PKG-INFO +21 -6
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/README.md +20 -5
- crazy_workers-1.5.0/crazy_workers/__init__.py +24 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/commands/status.py +22 -2
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/engine.py +76 -0
- crazy_workers-1.5.0/crazy_workers/params.py +45 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers.egg-info/PKG-INFO +21 -6
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers.egg-info/SOURCES.txt +3 -1
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/pyproject.toml +2 -2
- crazy_workers-1.5.0/tests/test_params.py +51 -0
- crazy_workers-1.4.2/crazy_workers/__init__.py +0 -6
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/LICENSE +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/_bootstrap.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/__init__.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/__main__.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/base.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/detect.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/entry.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/orchestrator.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/systemd.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/windows.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/__init__.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/commands/__init__.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/commands/params.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/commands/starter.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/commands/stopper.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/discovery.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/main.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/ui.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/client.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/__init__.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/backend.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/manager/__init__.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/manager/lister.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/manager/recoverer.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/manager/starter.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/manager/stopper.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/recovery.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/daemon/__init__.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/daemon/__main__.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/daemon/reconciler.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/daemon/runner.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/database/__init__.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/database/schema.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/database/storage.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/testing/__init__.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/testing/backends.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/testing/polling.py +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers.egg-info/dependency_links.txt +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers.egg-info/entry_points.txt +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers.egg-info/requires.txt +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers.egg-info/top_level.txt +0 -0
- {crazy_workers-1.4.2 → crazy_workers-1.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crazy-workers
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: A Python library for managing background worker processes with persistent state, automatic recovery, and a CLI.
|
|
5
5
|
Author: GioVanni Colasanto
|
|
6
6
|
License: MIT
|
|
@@ -76,9 +76,11 @@ pip install .
|
|
|
76
76
|
|
|
77
77
|
```python
|
|
78
78
|
# workers/my_worker.py
|
|
79
|
-
import
|
|
79
|
+
import time
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
from crazy_workers import parse_params
|
|
82
|
+
|
|
83
|
+
params = parse_params()
|
|
82
84
|
duration = params.get('duration', 60)
|
|
83
85
|
|
|
84
86
|
for _ in range(duration):
|
|
@@ -176,15 +178,28 @@ Closes the database connection and clears internal process references. Does **no
|
|
|
176
178
|
|
|
177
179
|
## Worker Script Contract
|
|
178
180
|
|
|
179
|
-
A worker receives its parameters as a JSON string in `sys.argv[1]
|
|
181
|
+
A worker receives its parameters as a JSON string in `sys.argv[1]`. Use
|
|
182
|
+
`parse_params` instead of decoding it by hand:
|
|
180
183
|
|
|
181
184
|
```python
|
|
182
|
-
import
|
|
185
|
+
from crazy_workers import parse_params
|
|
183
186
|
|
|
184
|
-
params =
|
|
187
|
+
params = parse_params()
|
|
185
188
|
# ... do work ...
|
|
186
189
|
```
|
|
187
190
|
|
|
191
|
+
Without arguments it is lenient: a worker launched with no parameters gets
|
|
192
|
+
`{}`. Pass `required=` to abort before any real work when a parameter the
|
|
193
|
+
worker cannot run without is missing or empty — the worker exits with code 1
|
|
194
|
+
and a message on stderr:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
params = parse_params(required=('device_id', 'output_dir'))
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`parse_params(argv=...)` accepts an explicit argv list, which makes the
|
|
201
|
+
parsing trivially testable without patching `sys.argv`.
|
|
202
|
+
|
|
188
203
|
A worker is a separate process, so it cannot be handed a live object (e.g. a DB
|
|
189
204
|
connection). Pass **configuration** instead: the manager's `worker_env` (and any
|
|
190
205
|
per-call `env`) is injected as environment variables, so a worker reads, say,
|
|
@@ -41,9 +41,11 @@ pip install .
|
|
|
41
41
|
|
|
42
42
|
```python
|
|
43
43
|
# workers/my_worker.py
|
|
44
|
-
import
|
|
44
|
+
import time
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
from crazy_workers import parse_params
|
|
47
|
+
|
|
48
|
+
params = parse_params()
|
|
47
49
|
duration = params.get('duration', 60)
|
|
48
50
|
|
|
49
51
|
for _ in range(duration):
|
|
@@ -141,15 +143,28 @@ Closes the database connection and clears internal process references. Does **no
|
|
|
141
143
|
|
|
142
144
|
## Worker Script Contract
|
|
143
145
|
|
|
144
|
-
A worker receives its parameters as a JSON string in `sys.argv[1]
|
|
146
|
+
A worker receives its parameters as a JSON string in `sys.argv[1]`. Use
|
|
147
|
+
`parse_params` instead of decoding it by hand:
|
|
145
148
|
|
|
146
149
|
```python
|
|
147
|
-
import
|
|
150
|
+
from crazy_workers import parse_params
|
|
148
151
|
|
|
149
|
-
params =
|
|
152
|
+
params = parse_params()
|
|
150
153
|
# ... do work ...
|
|
151
154
|
```
|
|
152
155
|
|
|
156
|
+
Without arguments it is lenient: a worker launched with no parameters gets
|
|
157
|
+
`{}`. Pass `required=` to abort before any real work when a parameter the
|
|
158
|
+
worker cannot run without is missing or empty — the worker exits with code 1
|
|
159
|
+
and a message on stderr:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
params = parse_params(required=('device_id', 'output_dir'))
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`parse_params(argv=...)` accepts an explicit argv list, which makes the
|
|
166
|
+
parsing trivially testable without patching `sys.argv`.
|
|
167
|
+
|
|
153
168
|
A worker is a separate process, so it cannot be handed a live object (e.g. a DB
|
|
154
169
|
connection). Pass **configuration** instead: the manager's `worker_env` (and any
|
|
155
170
|
per-call `env`) is injected as environment variables, so a worker reads, say,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
__all__ = ['WorkerManager', 'WorkerClient', 'WorkerStatus', 'DesiredStatus', 'parse_params']
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# Resolve exports lazily (PEP 562) so a worker doing `from crazy_workers import
|
|
5
|
+
# parse_params` only imports the lightweight params module — not the control
|
|
6
|
+
# plane (WorkerClient/WorkerManager) and its heavy SQLAlchemy dependencies.
|
|
7
|
+
def __getattr__(name):
|
|
8
|
+
if name == 'parse_params':
|
|
9
|
+
from .params import parse_params
|
|
10
|
+
|
|
11
|
+
return parse_params
|
|
12
|
+
if name == 'WorkerManager':
|
|
13
|
+
from .core.manager import WorkerManager
|
|
14
|
+
|
|
15
|
+
return WorkerManager
|
|
16
|
+
if name == 'WorkerClient':
|
|
17
|
+
from .client import WorkerClient
|
|
18
|
+
|
|
19
|
+
return WorkerClient
|
|
20
|
+
if name in ('DesiredStatus', 'WorkerStatus'):
|
|
21
|
+
from .database import schema
|
|
22
|
+
|
|
23
|
+
return getattr(schema, name)
|
|
24
|
+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|
|
@@ -6,12 +6,13 @@ from datetime import datetime
|
|
|
6
6
|
from rich.panel import Panel
|
|
7
7
|
from rich.table import Table
|
|
8
8
|
|
|
9
|
+
from ...core.engine import resolve_system_pid
|
|
9
10
|
from ..ui import console
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def show_status(client, workers_dir, json_mode=False):
|
|
13
14
|
"""Observability hub: the target state store plus the worker table (desired vs actual)."""
|
|
14
|
-
workers = _merge_with_filesystem(client.list(), workers_dir)
|
|
15
|
+
workers = _with_system_pids(_merge_with_filesystem(client.list(), workers_dir))
|
|
15
16
|
|
|
16
17
|
if json_mode:
|
|
17
18
|
sys.stdout.write(json.dumps({'workers': workers}) + '\n')
|
|
@@ -70,6 +71,25 @@ def _merge_with_filesystem(db_workers, workers_dir):
|
|
|
70
71
|
return results
|
|
71
72
|
|
|
72
73
|
|
|
74
|
+
def _with_system_pids(workers):
|
|
75
|
+
results = []
|
|
76
|
+
for worker in workers:
|
|
77
|
+
enriched = dict(worker)
|
|
78
|
+
enriched['system_pid'] = resolve_system_pid(enriched.get('pid'), worker_key=enriched.get('worker_key'))
|
|
79
|
+
results.append(enriched)
|
|
80
|
+
return results
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _format_pid(worker):
|
|
84
|
+
pid = worker.get('pid')
|
|
85
|
+
system_pid = worker.get('system_pid')
|
|
86
|
+
if not pid:
|
|
87
|
+
return '-'
|
|
88
|
+
if system_pid and system_pid != pid:
|
|
89
|
+
return f'{system_pid} [dim](ns {pid})[/dim]'
|
|
90
|
+
return str(pid)
|
|
91
|
+
|
|
92
|
+
|
|
73
93
|
def _build_table(workers):
|
|
74
94
|
table = Table(
|
|
75
95
|
title='[bold cyan]Workers — desired vs actual[/bold cyan]', border_style='cyan', header_style='bold magenta'
|
|
@@ -114,7 +134,7 @@ def _build_table(workers):
|
|
|
114
134
|
w['worker_type'],
|
|
115
135
|
f'[{desired_style}]{desired}[/{desired_style}]',
|
|
116
136
|
f'[{status_style}]{status}[/{status_style}]',
|
|
117
|
-
|
|
137
|
+
_format_pid(w),
|
|
118
138
|
last_action,
|
|
119
139
|
params_str,
|
|
120
140
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import os
|
|
2
3
|
import psutil
|
|
3
4
|
import subprocess
|
|
4
5
|
|
|
@@ -64,6 +65,81 @@ def is_process_running(pid):
|
|
|
64
65
|
return False
|
|
65
66
|
|
|
66
67
|
|
|
68
|
+
def resolve_system_pid(pid, worker_key=None):
|
|
69
|
+
"""Return the most-native PID visible for ``pid``.
|
|
70
|
+
|
|
71
|
+
``pid`` remains the control PID used by the current process namespace. On
|
|
72
|
+
Linux, ``CRAZY_WORKERS_HOST_PROC`` can point at a read-only host procfs mount
|
|
73
|
+
(for example /host/proc in Docker) so status can show the host PID. Without
|
|
74
|
+
that mount, /proc exposes NSpid when PID namespaces are visible; its first
|
|
75
|
+
value is the PID in the outermost namespace visible to this procfs mount. On
|
|
76
|
+
Windows and ordinary Linux hosts this is just ``pid``.
|
|
77
|
+
"""
|
|
78
|
+
if pid is None:
|
|
79
|
+
return None
|
|
80
|
+
if os.name != 'posix':
|
|
81
|
+
return pid
|
|
82
|
+
|
|
83
|
+
host_pid = _resolve_from_host_proc(pid, worker_key=worker_key)
|
|
84
|
+
if host_pid is not None:
|
|
85
|
+
return host_pid
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
with open(f'/proc/{pid}/status', encoding='utf-8') as f:
|
|
89
|
+
for line in f:
|
|
90
|
+
if line.startswith('NSpid:'):
|
|
91
|
+
values = [int(value) for value in line.split()[1:]]
|
|
92
|
+
return values[0] if values else pid
|
|
93
|
+
except (OSError, ValueError):
|
|
94
|
+
return pid
|
|
95
|
+
|
|
96
|
+
return pid
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _resolve_from_host_proc(pid, worker_key=None):
|
|
100
|
+
host_proc = os.environ.get('CRAZY_WORKERS_HOST_PROC')
|
|
101
|
+
if not host_proc:
|
|
102
|
+
return None
|
|
103
|
+
if not os.path.isdir(host_proc):
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
for entry in os.listdir(host_proc):
|
|
107
|
+
if not entry.isdigit():
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
status_path = os.path.join(host_proc, entry, 'status')
|
|
111
|
+
try:
|
|
112
|
+
with open(status_path, encoding='utf-8') as f:
|
|
113
|
+
nspid = _read_nspid(f)
|
|
114
|
+
except (OSError, ValueError):
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
if not nspid or nspid[-1] != pid:
|
|
118
|
+
continue
|
|
119
|
+
if worker_key and not _host_proc_cmdline_matches(host_proc, entry, worker_key):
|
|
120
|
+
continue
|
|
121
|
+
return nspid[0]
|
|
122
|
+
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _read_nspid(lines):
|
|
127
|
+
for line in lines:
|
|
128
|
+
if line.startswith('NSpid:'):
|
|
129
|
+
return [int(value) for value in line.split()[1:]]
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _host_proc_cmdline_matches(host_proc, host_pid, worker_key):
|
|
134
|
+
try:
|
|
135
|
+
with open(os.path.join(host_proc, host_pid, 'cmdline'), 'rb') as f:
|
|
136
|
+
raw = f.read()
|
|
137
|
+
except OSError:
|
|
138
|
+
return False
|
|
139
|
+
parts = [part.decode(errors='ignore') for part in raw.split(b'\0') if part]
|
|
140
|
+
return worker_key_token(worker_key) in parts
|
|
141
|
+
|
|
142
|
+
|
|
67
143
|
def terminate_process(pid, timeout=5, popen_process=None, exclude_pids=None):
|
|
68
144
|
"""Gracefully terminates a process and its non-managed children.
|
|
69
145
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Worker-side parsing of the JSON parameters the daemon passes on argv.
|
|
2
|
+
|
|
3
|
+
The manager spawns every worker as ``python -m crazy_workers._bootstrap
|
|
4
|
+
<worker_path> <json_params>`` and the bootstrap restores ``sys.argv`` to
|
|
5
|
+
``[worker_path, json_params]``. This module is the worker-side counterpart of
|
|
6
|
+
that contract: call :func:`parse_params` at the top of a worker's ``main()``
|
|
7
|
+
instead of re-implementing the argv/JSON boilerplate in every worker script.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_params(argv=None, required=()):
|
|
15
|
+
"""Return the parameters dict the daemon passed to this worker.
|
|
16
|
+
|
|
17
|
+
Without ``required`` a worker launched with no parameters gets ``{}`` — the
|
|
18
|
+
lenient mode for workers whose parameters are all optional. With ``required``
|
|
19
|
+
a missing argv, malformed JSON, or a missing/empty required key aborts the
|
|
20
|
+
worker with a message on stderr (``SystemExit``, exit code 1) before it does
|
|
21
|
+
any real work. A key counts as missing when it is absent, ``None``, or an
|
|
22
|
+
empty string; a falsy-but-present value like ``0`` or ``False`` is kept.
|
|
23
|
+
|
|
24
|
+
``argv`` defaults to ``sys.argv``; pass a list explicitly in tests.
|
|
25
|
+
"""
|
|
26
|
+
argv = sys.argv if argv is None else argv
|
|
27
|
+
|
|
28
|
+
if len(argv) < 2:
|
|
29
|
+
if required:
|
|
30
|
+
raise SystemExit(f'Missing required parameters: {", ".join(required)}')
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
params = json.loads(argv[1])
|
|
35
|
+
except json.JSONDecodeError as error:
|
|
36
|
+
raise SystemExit(f'Invalid JSON parameters: {error}')
|
|
37
|
+
|
|
38
|
+
if not isinstance(params, dict):
|
|
39
|
+
raise SystemExit('Invalid JSON parameters: expected a JSON object')
|
|
40
|
+
|
|
41
|
+
missing = [key for key in required if key not in params or params[key] in (None, '')]
|
|
42
|
+
if missing:
|
|
43
|
+
raise SystemExit(f'Missing required parameters: {", ".join(missing)}')
|
|
44
|
+
|
|
45
|
+
return params
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crazy-workers
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: A Python library for managing background worker processes with persistent state, automatic recovery, and a CLI.
|
|
5
5
|
Author: GioVanni Colasanto
|
|
6
6
|
License: MIT
|
|
@@ -76,9 +76,11 @@ pip install .
|
|
|
76
76
|
|
|
77
77
|
```python
|
|
78
78
|
# workers/my_worker.py
|
|
79
|
-
import
|
|
79
|
+
import time
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
from crazy_workers import parse_params
|
|
82
|
+
|
|
83
|
+
params = parse_params()
|
|
82
84
|
duration = params.get('duration', 60)
|
|
83
85
|
|
|
84
86
|
for _ in range(duration):
|
|
@@ -176,15 +178,28 @@ Closes the database connection and clears internal process references. Does **no
|
|
|
176
178
|
|
|
177
179
|
## Worker Script Contract
|
|
178
180
|
|
|
179
|
-
A worker receives its parameters as a JSON string in `sys.argv[1]
|
|
181
|
+
A worker receives its parameters as a JSON string in `sys.argv[1]`. Use
|
|
182
|
+
`parse_params` instead of decoding it by hand:
|
|
180
183
|
|
|
181
184
|
```python
|
|
182
|
-
import
|
|
185
|
+
from crazy_workers import parse_params
|
|
183
186
|
|
|
184
|
-
params =
|
|
187
|
+
params = parse_params()
|
|
185
188
|
# ... do work ...
|
|
186
189
|
```
|
|
187
190
|
|
|
191
|
+
Without arguments it is lenient: a worker launched with no parameters gets
|
|
192
|
+
`{}`. Pass `required=` to abort before any real work when a parameter the
|
|
193
|
+
worker cannot run without is missing or empty — the worker exits with code 1
|
|
194
|
+
and a message on stderr:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
params = parse_params(required=('device_id', 'output_dir'))
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`parse_params(argv=...)` accepts an explicit argv list, which makes the
|
|
201
|
+
parsing trivially testable without patching `sys.argv`.
|
|
202
|
+
|
|
188
203
|
A worker is a separate process, so it cannot be handed a live object (e.g. a DB
|
|
189
204
|
connection). Pass **configuration** instead: the manager's `worker_env` (and any
|
|
190
205
|
per-call `env`) is injected as environment variables, so a worker reads, say,
|
|
@@ -4,6 +4,7 @@ pyproject.toml
|
|
|
4
4
|
crazy_workers/__init__.py
|
|
5
5
|
crazy_workers/_bootstrap.py
|
|
6
6
|
crazy_workers/client.py
|
|
7
|
+
crazy_workers/params.py
|
|
7
8
|
crazy_workers.egg-info/PKG-INFO
|
|
8
9
|
crazy_workers.egg-info/SOURCES.txt
|
|
9
10
|
crazy_workers.egg-info/dependency_links.txt
|
|
@@ -45,4 +46,5 @@ crazy_workers/database/schema.py
|
|
|
45
46
|
crazy_workers/database/storage.py
|
|
46
47
|
crazy_workers/testing/__init__.py
|
|
47
48
|
crazy_workers/testing/backends.py
|
|
48
|
-
crazy_workers/testing/polling.py
|
|
49
|
+
crazy_workers/testing/polling.py
|
|
50
|
+
tests/test_params.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "crazy-workers"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.5.0"
|
|
4
4
|
description = "A Python library for managing background worker processes with persistent state, automatic recovery, and a CLI."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "GioVanni Colasanto" }]
|
|
@@ -63,7 +63,7 @@ omit = ["*/venv/*", "*/build/*", "tests/*"]
|
|
|
63
63
|
[tool.coverage.report]
|
|
64
64
|
show_missing = true
|
|
65
65
|
skip_covered = false
|
|
66
|
-
fail_under =
|
|
66
|
+
fail_under = 90
|
|
67
67
|
|
|
68
68
|
[tool.ruff]
|
|
69
69
|
line-length = 120
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
from crazy_workers import parse_params
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestParseParams(unittest.TestCase):
|
|
8
|
+
def test_no_argv_returns_empty_dict(self):
|
|
9
|
+
self.assertEqual(parse_params(['worker.py']), {})
|
|
10
|
+
|
|
11
|
+
def test_no_argv_with_required_exits(self):
|
|
12
|
+
with self.assertRaises(SystemExit) as ctx:
|
|
13
|
+
parse_params(['worker.py'], required=('device_id', 'output_dir'))
|
|
14
|
+
self.assertEqual(ctx.exception.code, 'Missing required parameters: device_id, output_dir')
|
|
15
|
+
|
|
16
|
+
def test_invalid_json_exits(self):
|
|
17
|
+
with self.assertRaises(SystemExit) as ctx:
|
|
18
|
+
parse_params(['worker.py', 'not json'])
|
|
19
|
+
self.assertIn('Invalid JSON parameters', str(ctx.exception.code))
|
|
20
|
+
|
|
21
|
+
def test_non_object_json_exits(self):
|
|
22
|
+
with self.assertRaises(SystemExit) as ctx:
|
|
23
|
+
parse_params(['worker.py', '[1, 2, 3]'])
|
|
24
|
+
self.assertEqual(ctx.exception.code, 'Invalid JSON parameters: expected a JSON object')
|
|
25
|
+
|
|
26
|
+
def test_returns_parsed_params(self):
|
|
27
|
+
self.assertEqual(parse_params(['worker.py', '{"device_id": "7"}']), {'device_id': '7'})
|
|
28
|
+
|
|
29
|
+
def test_required_present_returns_params(self):
|
|
30
|
+
argv = ['worker.py', '{"device_id": "7", "output_dir": "/out"}']
|
|
31
|
+
params = parse_params(argv, required=('device_id', 'output_dir'))
|
|
32
|
+
self.assertEqual(params, {'device_id': '7', 'output_dir': '/out'})
|
|
33
|
+
|
|
34
|
+
def test_required_missing_exits_naming_keys(self):
|
|
35
|
+
with self.assertRaises(SystemExit) as ctx:
|
|
36
|
+
parse_params(['worker.py', '{"device_id": "7"}'], required=('device_id', 'output_dir'))
|
|
37
|
+
self.assertEqual(ctx.exception.code, 'Missing required parameters: output_dir')
|
|
38
|
+
|
|
39
|
+
def test_required_empty_value_counts_as_missing(self):
|
|
40
|
+
with self.assertRaises(SystemExit) as ctx:
|
|
41
|
+
parse_params(['worker.py', '{"device_id": ""}'], required=('device_id',))
|
|
42
|
+
self.assertEqual(ctx.exception.code, 'Missing required parameters: device_id')
|
|
43
|
+
|
|
44
|
+
def test_required_falsy_but_present_value_is_kept(self):
|
|
45
|
+
argv = ['worker.py', '{"retries": 0, "verbose": false}']
|
|
46
|
+
params = parse_params(argv, required=('retries', 'verbose'))
|
|
47
|
+
self.assertEqual(params, {'retries': 0, 'verbose': False})
|
|
48
|
+
|
|
49
|
+
def test_defaults_to_sys_argv(self):
|
|
50
|
+
with patch('sys.argv', ['worker.py', '{"interval": 5}']):
|
|
51
|
+
self.assertEqual(parse_params(), {'interval': 5})
|
|
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
|