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.
Files changed (53) hide show
  1. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/PKG-INFO +21 -6
  2. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/README.md +20 -5
  3. crazy_workers-1.5.0/crazy_workers/__init__.py +24 -0
  4. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/commands/status.py +22 -2
  5. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/engine.py +76 -0
  6. crazy_workers-1.5.0/crazy_workers/params.py +45 -0
  7. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers.egg-info/PKG-INFO +21 -6
  8. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers.egg-info/SOURCES.txt +3 -1
  9. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/pyproject.toml +2 -2
  10. crazy_workers-1.5.0/tests/test_params.py +51 -0
  11. crazy_workers-1.4.2/crazy_workers/__init__.py +0 -6
  12. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/LICENSE +0 -0
  13. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/_bootstrap.py +0 -0
  14. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/__init__.py +0 -0
  15. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/__main__.py +0 -0
  16. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/base.py +0 -0
  17. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/detect.py +0 -0
  18. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/entry.py +0 -0
  19. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/orchestrator.py +0 -0
  20. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/systemd.py +0 -0
  21. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/boot/windows.py +0 -0
  22. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/__init__.py +0 -0
  23. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/commands/__init__.py +0 -0
  24. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/commands/params.py +0 -0
  25. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/commands/starter.py +0 -0
  26. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/commands/stopper.py +0 -0
  27. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/discovery.py +0 -0
  28. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/main.py +0 -0
  29. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/cli/ui.py +0 -0
  30. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/client.py +0 -0
  31. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/__init__.py +0 -0
  32. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/backend.py +0 -0
  33. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/manager/__init__.py +0 -0
  34. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/manager/lister.py +0 -0
  35. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/manager/recoverer.py +0 -0
  36. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/manager/starter.py +0 -0
  37. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/manager/stopper.py +0 -0
  38. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/core/recovery.py +0 -0
  39. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/daemon/__init__.py +0 -0
  40. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/daemon/__main__.py +0 -0
  41. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/daemon/reconciler.py +0 -0
  42. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/daemon/runner.py +0 -0
  43. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/database/__init__.py +0 -0
  44. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/database/schema.py +0 -0
  45. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/database/storage.py +0 -0
  46. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/testing/__init__.py +0 -0
  47. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/testing/backends.py +0 -0
  48. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers/testing/polling.py +0 -0
  49. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers.egg-info/dependency_links.txt +0 -0
  50. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers.egg-info/entry_points.txt +0 -0
  51. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers.egg-info/requires.txt +0 -0
  52. {crazy_workers-1.4.2 → crazy_workers-1.5.0}/crazy_workers.egg-info/top_level.txt +0 -0
  53. {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.4.2
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 json, sys, time
79
+ import time
80
80
 
81
- params = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
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 json, sys
185
+ from crazy_workers import parse_params
183
186
 
184
- params = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
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 json, sys, time
44
+ import time
45
45
 
46
- params = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
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 json, sys
150
+ from crazy_workers import parse_params
148
151
 
149
- params = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
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
- str(w['pid']) if w['pid'] else '-',
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.4.2
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 json, sys, time
79
+ import time
80
80
 
81
- params = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
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 json, sys
185
+ from crazy_workers import parse_params
183
186
 
184
- params = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
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.4.2"
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 = 95
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})
@@ -1,6 +0,0 @@
1
- from .client import WorkerClient
2
- from .core.manager import WorkerManager
3
- from .database.schema import DesiredStatus, WorkerStatus
4
-
5
-
6
- __all__ = ['WorkerManager', 'WorkerClient', 'WorkerStatus', 'DesiredStatus']
File without changes
File without changes