python-dmon 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
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.
- dmon/__init__.py +23 -0
- dmon/api.py +264 -0
- dmon/cli.py +508 -31
- dmon/config.py +234 -3
- dmon/constants.py +3 -0
- dmon/control.py +509 -232
- dmon/inspection.py +36 -0
- dmon/logs.py +240 -0
- dmon/readiness.py +148 -0
- dmon/results.py +111 -0
- dmon/runner.py +74 -27
- dmon/serialization.py +28 -0
- dmon/stack_runner.py +18 -0
- dmon/supervisor.py +951 -0
- dmon/types.py +129 -3
- python_dmon-0.4.0.dist-info/METADATA +481 -0
- python_dmon-0.4.0.dist-info/RECORD +22 -0
- python_dmon-0.3.0.dist-info/METADATA +0 -240
- python_dmon-0.3.0.dist-info/RECORD +0 -14
- {python_dmon-0.3.0.dist-info → python_dmon-0.4.0.dist-info}/WHEEL +0 -0
- {python_dmon-0.3.0.dist-info → python_dmon-0.4.0.dist-info}/entry_points.txt +0 -0
dmon/__init__.py
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .api import Dmon, DmonConfigError, DmonError
|
|
2
|
+
from .results import (
|
|
3
|
+
ActionResult,
|
|
4
|
+
BatchResult,
|
|
5
|
+
StackResult,
|
|
6
|
+
StackSnapshot,
|
|
7
|
+
TaskResult,
|
|
8
|
+
TaskSnapshot,
|
|
9
|
+
WaitResult,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ActionResult",
|
|
14
|
+
"BatchResult",
|
|
15
|
+
"Dmon",
|
|
16
|
+
"DmonConfigError",
|
|
17
|
+
"DmonError",
|
|
18
|
+
"StackResult",
|
|
19
|
+
"StackSnapshot",
|
|
20
|
+
"TaskResult",
|
|
21
|
+
"TaskSnapshot",
|
|
22
|
+
"WaitResult",
|
|
23
|
+
]
|
dmon/api.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from io import StringIO
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Iterator, Optional, Sequence, Tuple, Union
|
|
9
|
+
|
|
10
|
+
from .config import fill_default_paths, get_task_config, load_config
|
|
11
|
+
from .constants import DEFAULT_META_DIR, STACK_META_SUFFIX
|
|
12
|
+
from .control import (
|
|
13
|
+
check_running,
|
|
14
|
+
diagnostic_output,
|
|
15
|
+
restart,
|
|
16
|
+
start_single_result,
|
|
17
|
+
stop_single,
|
|
18
|
+
task_snapshot,
|
|
19
|
+
task_environment,
|
|
20
|
+
)
|
|
21
|
+
from .results import (
|
|
22
|
+
ActionResult,
|
|
23
|
+
BatchResult,
|
|
24
|
+
StackResult,
|
|
25
|
+
TaskResult,
|
|
26
|
+
WaitResult,
|
|
27
|
+
)
|
|
28
|
+
from .inspection import inspect_stack, inspect_task
|
|
29
|
+
from .readiness import ready_spec, wait_for_readiness
|
|
30
|
+
from .types import DmonMeta
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DmonError(Exception):
|
|
34
|
+
"""Base exception for invalid Python API requests."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DmonConfigError(DmonError):
|
|
38
|
+
"""Configuration could not be loaded or validated."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Dmon:
|
|
42
|
+
def __init__(self, config: Optional[Union[str, os.PathLike]] = None) -> None:
|
|
43
|
+
self.config = str(config) if config is not None else None
|
|
44
|
+
|
|
45
|
+
def start(self, *tasks: str) -> BatchResult:
|
|
46
|
+
names, configs, _ = self._tasks(tasks)
|
|
47
|
+
self._validate_environments(configs)
|
|
48
|
+
results = []
|
|
49
|
+
with self._operation():
|
|
50
|
+
for name, config in zip(names, configs):
|
|
51
|
+
result = start_single_result(config)
|
|
52
|
+
snapshot = (
|
|
53
|
+
task_snapshot(result.meta) if result.meta is not None else None
|
|
54
|
+
)
|
|
55
|
+
results.append(
|
|
56
|
+
ActionResult(
|
|
57
|
+
action="start",
|
|
58
|
+
name=name,
|
|
59
|
+
ok=result.exit_code == 0,
|
|
60
|
+
exit_code=result.exit_code,
|
|
61
|
+
snapshot=snapshot,
|
|
62
|
+
error="" if result.exit_code == 0 else "task did not start",
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
return BatchResult("start", tuple(results))
|
|
66
|
+
|
|
67
|
+
def stop(self, *tasks: str) -> BatchResult:
|
|
68
|
+
names, configs, _ = self._tasks(tasks)
|
|
69
|
+
results = []
|
|
70
|
+
with self._operation():
|
|
71
|
+
for name, config in zip(names, configs):
|
|
72
|
+
exit_code = stop_single(config.meta_path)
|
|
73
|
+
results.append(
|
|
74
|
+
ActionResult(
|
|
75
|
+
action="stop",
|
|
76
|
+
name=name,
|
|
77
|
+
ok=exit_code == 0,
|
|
78
|
+
exit_code=exit_code,
|
|
79
|
+
error="" if exit_code == 0 else "task did not stop",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
return BatchResult("stop", tuple(results))
|
|
83
|
+
|
|
84
|
+
def restart(self, *tasks: str) -> BatchResult:
|
|
85
|
+
names, configs, _ = self._tasks(tasks)
|
|
86
|
+
self._validate_environments(configs)
|
|
87
|
+
results = []
|
|
88
|
+
with self._operation():
|
|
89
|
+
for name, config in zip(names, configs):
|
|
90
|
+
exit_code = restart([config])
|
|
91
|
+
meta = self._load_task_meta(Path(config.meta_path))
|
|
92
|
+
snapshot = task_snapshot(meta) if meta is not None else None
|
|
93
|
+
results.append(
|
|
94
|
+
ActionResult(
|
|
95
|
+
action="restart",
|
|
96
|
+
name=name,
|
|
97
|
+
ok=exit_code == 0,
|
|
98
|
+
exit_code=exit_code,
|
|
99
|
+
snapshot=snapshot,
|
|
100
|
+
error="" if exit_code == 0 else "task did not restart",
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
return BatchResult("restart", tuple(results))
|
|
104
|
+
|
|
105
|
+
def status(self, task: Optional[str] = None) -> TaskResult:
|
|
106
|
+
names, configs, _ = self._tasks(() if task is None else (task,))
|
|
107
|
+
if len(names) != 1:
|
|
108
|
+
raise DmonConfigError("status requires exactly one task")
|
|
109
|
+
with self._operation():
|
|
110
|
+
return self._task_result(names[0], Path(configs[0].meta_path))
|
|
111
|
+
|
|
112
|
+
def list_tasks(self) -> Tuple[TaskResult, ...]:
|
|
113
|
+
project = self._project()
|
|
114
|
+
results = []
|
|
115
|
+
with self._operation():
|
|
116
|
+
meta_dir = project / DEFAULT_META_DIR
|
|
117
|
+
if meta_dir.is_dir():
|
|
118
|
+
for path in sorted(meta_dir.glob("*.meta.json")):
|
|
119
|
+
results.append(
|
|
120
|
+
inspect_task(
|
|
121
|
+
path.name[: -len(".meta.json")],
|
|
122
|
+
path,
|
|
123
|
+
require_running=False,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
return tuple(results)
|
|
127
|
+
|
|
128
|
+
def stack_status(self, stack: str) -> StackResult:
|
|
129
|
+
if not isinstance(stack, str) or not stack:
|
|
130
|
+
raise DmonConfigError("stack name must not be empty")
|
|
131
|
+
project = self._project()
|
|
132
|
+
name = stack.lower()
|
|
133
|
+
with self._operation():
|
|
134
|
+
return inspect_stack(
|
|
135
|
+
name,
|
|
136
|
+
project / DEFAULT_META_DIR / f"{name}{STACK_META_SUFFIX}",
|
|
137
|
+
require_running=True,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def list_stacks(self) -> Tuple[StackResult, ...]:
|
|
141
|
+
project = self._project()
|
|
142
|
+
results = []
|
|
143
|
+
with self._operation():
|
|
144
|
+
meta_dir = project / DEFAULT_META_DIR
|
|
145
|
+
if meta_dir.is_dir():
|
|
146
|
+
for path in sorted(meta_dir.glob(f"*{STACK_META_SUFFIX}")):
|
|
147
|
+
name = path.name[: -len(STACK_META_SUFFIX)]
|
|
148
|
+
results.append(inspect_stack(name, path, require_running=False))
|
|
149
|
+
return tuple(results)
|
|
150
|
+
|
|
151
|
+
def wait(
|
|
152
|
+
self,
|
|
153
|
+
*tasks: str,
|
|
154
|
+
timeout: Optional[float] = None,
|
|
155
|
+
interval: Optional[float] = None,
|
|
156
|
+
) -> Tuple[WaitResult, ...]:
|
|
157
|
+
for name, value in (("timeout", timeout), ("interval", interval)):
|
|
158
|
+
if value is not None and (
|
|
159
|
+
not isinstance(value, (int, float))
|
|
160
|
+
or isinstance(value, bool)
|
|
161
|
+
or not math.isfinite(value)
|
|
162
|
+
or value <= 0
|
|
163
|
+
):
|
|
164
|
+
raise DmonConfigError(f"{name} must be finite and greater than zero")
|
|
165
|
+
names, configs, _ = self._tasks(tasks)
|
|
166
|
+
self._validate_environments(configs)
|
|
167
|
+
results = []
|
|
168
|
+
with self._operation():
|
|
169
|
+
for name, config in zip(names, configs):
|
|
170
|
+
if not config.ready:
|
|
171
|
+
results.append(
|
|
172
|
+
WaitResult(
|
|
173
|
+
name,
|
|
174
|
+
False,
|
|
175
|
+
"invalid",
|
|
176
|
+
0.0,
|
|
177
|
+
0,
|
|
178
|
+
"task has no readiness probe",
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
continue
|
|
182
|
+
try:
|
|
183
|
+
meta = DmonMeta.load(config.meta_path)
|
|
184
|
+
except (OSError, ValueError, TypeError) as error:
|
|
185
|
+
results.append(
|
|
186
|
+
WaitResult(name, False, "metadata-error", 0.0, 0, str(error))
|
|
187
|
+
)
|
|
188
|
+
continue
|
|
189
|
+
if meta is None:
|
|
190
|
+
results.append(
|
|
191
|
+
WaitResult(
|
|
192
|
+
name,
|
|
193
|
+
False,
|
|
194
|
+
"not-running",
|
|
195
|
+
0.0,
|
|
196
|
+
0,
|
|
197
|
+
"task metadata not found",
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
continue
|
|
201
|
+
spec = ready_spec(config.ready, timeout=timeout, interval=interval)
|
|
202
|
+
results.append(
|
|
203
|
+
wait_for_readiness(
|
|
204
|
+
name,
|
|
205
|
+
spec,
|
|
206
|
+
cwd=config.cwd,
|
|
207
|
+
env=task_environment(config),
|
|
208
|
+
process_running=lambda meta=meta: check_running(
|
|
209
|
+
meta.pid, meta.create_time
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
return tuple(results)
|
|
214
|
+
|
|
215
|
+
def _tasks(self, tasks: Sequence[str]):
|
|
216
|
+
if any(not isinstance(task, str) or not task for task in tasks):
|
|
217
|
+
raise DmonConfigError("task names must be non-empty strings")
|
|
218
|
+
try:
|
|
219
|
+
names, configs, path = get_task_config(tasks, self.config)
|
|
220
|
+
fill_default_paths(configs)
|
|
221
|
+
except (OSError, ValueError, TypeError) as error:
|
|
222
|
+
raise DmonConfigError(str(error)) from error
|
|
223
|
+
project = path.parent.resolve()
|
|
224
|
+
for config in configs:
|
|
225
|
+
config.cwd = self._resolve_path(project, config.cwd or ".")
|
|
226
|
+
config.meta_path = self._resolve_path(project, config.meta_path)
|
|
227
|
+
config.log_path = self._resolve_path(project, config.log_path)
|
|
228
|
+
config.rotate_log_path = self._resolve_path(project, config.rotate_log_path)
|
|
229
|
+
config.env_files = [
|
|
230
|
+
self._resolve_path(project, value) for value in config.env_files
|
|
231
|
+
]
|
|
232
|
+
return names, configs, project
|
|
233
|
+
|
|
234
|
+
def _project(self) -> Path:
|
|
235
|
+
try:
|
|
236
|
+
_, path = load_config(self.config)
|
|
237
|
+
except (OSError, ValueError, TypeError) as error:
|
|
238
|
+
raise DmonConfigError(str(error)) from error
|
|
239
|
+
return path.parent
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def _validate_environments(configs) -> None:
|
|
243
|
+
try:
|
|
244
|
+
for config in configs:
|
|
245
|
+
task_environment(config)
|
|
246
|
+
except (OSError, ValueError) as error:
|
|
247
|
+
raise DmonConfigError(str(error)) from error
|
|
248
|
+
|
|
249
|
+
@contextmanager
|
|
250
|
+
def _operation(self) -> Iterator[None]:
|
|
251
|
+
with diagnostic_output(StringIO()):
|
|
252
|
+
yield
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def _resolve_path(project: Path, value: str) -> str:
|
|
256
|
+
path = Path(value)
|
|
257
|
+
return str(path.resolve() if path.is_absolute() else (project / path).resolve())
|
|
258
|
+
|
|
259
|
+
def _task_result(self, name: str, path: Path) -> TaskResult:
|
|
260
|
+
return inspect_task(name, path, require_running=True)
|
|
261
|
+
|
|
262
|
+
@staticmethod
|
|
263
|
+
def _load_task_meta(path: Path) -> Optional[DmonMeta]:
|
|
264
|
+
return DmonMeta.load(path)
|