python-dmon 0.2.2__tar.gz → 0.2.4__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.
- {python_dmon-0.2.2 → python_dmon-0.2.4}/PKG-INFO +52 -7
- {python_dmon-0.2.2 → python_dmon-0.2.4}/README.md +51 -6
- {python_dmon-0.2.2 → python_dmon-0.2.4}/pyproject.toml +2 -1
- {python_dmon-0.2.2 → python_dmon-0.2.4}/src/dmon/cli.py +1 -1
- {python_dmon-0.2.2 → python_dmon-0.2.4}/src/dmon/config.py +34 -17
- {python_dmon-0.2.2 → python_dmon-0.2.4}/src/dmon/runner.py +4 -4
- {python_dmon-0.2.2 → python_dmon-0.2.4}/src/dmon/types.py +2 -2
- {python_dmon-0.2.2 → python_dmon-0.2.4}/src/dmon/__init__.py +0 -0
- {python_dmon-0.2.2 → python_dmon-0.2.4}/src/dmon/__main__.py +0 -0
- {python_dmon-0.2.2 → python_dmon-0.2.4}/src/dmon/constants.py +0 -0
- {python_dmon-0.2.2 → python_dmon-0.2.4}/src/dmon/control.py +0 -0
- {python_dmon-0.2.2 → python_dmon-0.2.4}/src/dmon/py.typed +0 -0
- {python_dmon-0.2.2 → python_dmon-0.2.4}/src/dmon/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: python-dmon
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: A lightweight, cross-platform daemon manager that runs any command as a background process.
|
|
5
5
|
Keywords: python-dmon,dmon,daemon,background,detach,process management
|
|
6
6
|
Author: Atomie CHEN
|
|
@@ -38,13 +38,40 @@ It is a Python-based and more powerful successor to the [handy-backend shell scr
|
|
|
38
38
|
- 🧩 **Flexible tasks:** Tasks can be configured in `pyproject.toml` or `dmon.yaml`; or run ad-hoc commands directly.
|
|
39
39
|
- 🪵 **Logging & log rotation:** Automatically manage log files to prevent uncontrolled growth.
|
|
40
40
|
|
|
41
|
+

|
|
42
|
+
|
|
41
43
|
|
|
42
44
|
## Installation
|
|
43
45
|
|
|
46
|
+
`python-dmon` is available on [PyPI](https://pypi.org/project/python-dmon/):
|
|
47
|
+
|
|
44
48
|
```sh
|
|
45
49
|
pip install python-dmon
|
|
46
50
|
```
|
|
47
51
|
|
|
52
|
+
We recommend installing into an isolated environment, e.g., with `uv` / `pipx`:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
# Install globally with uv tool
|
|
56
|
+
uv tool install python-dmon
|
|
57
|
+
|
|
58
|
+
# Or with pipx
|
|
59
|
+
pipx install python-dmon
|
|
60
|
+
|
|
61
|
+
# Add as a dev dependency in your project
|
|
62
|
+
uv add --dev python-dmon
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
You can also invoke without installing:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
# With uvx (uv tool run)
|
|
69
|
+
uvx python-dmon
|
|
70
|
+
|
|
71
|
+
# Or with pipx
|
|
72
|
+
pipx run python-dmon
|
|
73
|
+
```
|
|
74
|
+
|
|
48
75
|
To get the latest features, install from source:
|
|
49
76
|
|
|
50
77
|
```sh
|
|
@@ -59,18 +86,20 @@ Create a `dmon.yaml` file:
|
|
|
59
86
|
|
|
60
87
|
```yaml
|
|
61
88
|
tasks:
|
|
62
|
-
app: ["python", "-u", "server.py"]
|
|
89
|
+
app: ["python", "-u", "server.py"] # option 1: exec form
|
|
90
|
+
# app: "python -u server.py" # option 2: shell string
|
|
63
91
|
```
|
|
64
92
|
|
|
65
93
|
Or add to your `pyproject.toml`:
|
|
66
94
|
|
|
67
95
|
```toml
|
|
68
96
|
[tool.dmon.tasks]
|
|
69
|
-
app = ["python", "-u", "server.py"]
|
|
97
|
+
app = ["python", "-u", "server.py"] # option 1: exec form
|
|
98
|
+
# app = "python -u server.py" # option 2: shell string
|
|
70
99
|
```
|
|
71
100
|
|
|
72
101
|
Commands can be a single string (run in shell), or list of strings (exec form).
|
|
73
|
-
See [Example
|
|
102
|
+
See [Example Configuration](#example-configuration) for more configuration options.
|
|
74
103
|
|
|
75
104
|
|
|
76
105
|
### Run tasks
|
|
@@ -91,7 +120,7 @@ dmon status app
|
|
|
91
120
|
dmon exec app
|
|
92
121
|
```
|
|
93
122
|
|
|
94
|
-
If only one task is defined in the config file, you can omit the task name:
|
|
123
|
+
If you have defined `default_task`, or only one task is defined in the config file, you can omit the task name:
|
|
95
124
|
|
|
96
125
|
```sh
|
|
97
126
|
dmon start
|
|
@@ -100,11 +129,12 @@ dmon status
|
|
|
100
129
|
dmon exec
|
|
101
130
|
```
|
|
102
131
|
|
|
103
|
-
You can use `--config` to specify a custom config file:
|
|
132
|
+
You can use `--config` to specify a custom config file or the directory containing it:
|
|
104
133
|
|
|
105
134
|
```sh
|
|
106
135
|
dmon start --config /path/to/dmon.yaml app # YAML
|
|
107
136
|
dmon start --config /path/to/pyproject.toml app # or TOML
|
|
137
|
+
dmon start --config /path/to/dir app # dir with `dmon.y(a)ml` or `pyproject.toml`
|
|
108
138
|
```
|
|
109
139
|
|
|
110
140
|
And yes, you can use `dmon` to run in a nested manner:
|
|
@@ -144,7 +174,7 @@ dmon list
|
|
|
144
174
|
```
|
|
145
175
|
|
|
146
176
|
|
|
147
|
-
## Example
|
|
177
|
+
## Example Configuration
|
|
148
178
|
|
|
149
179
|
A task can be a **string**, **list**, or **dictionary**.
|
|
150
180
|
|
|
@@ -165,8 +195,23 @@ tasks:
|
|
|
165
195
|
rotate_log_path: "logs/<task>.rotate.log" # path to rotation log
|
|
166
196
|
rotate_log_max_size: 5 # max rotation log file size in MB
|
|
167
197
|
meta_path: ".dmon/<task>.meta.json" # path to meta file
|
|
198
|
+
default_task: your_task_name # the default task name
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
In TOML, write like this:
|
|
202
|
+
|
|
203
|
+
```toml
|
|
204
|
+
[tool.dmon.tasks]
|
|
205
|
+
your_task_name = { cmd = [
|
|
206
|
+
"python", "-u", "server.py"
|
|
207
|
+
], ... }
|
|
208
|
+
another_task = "cd subdir && ls && bash start.sh"
|
|
209
|
+
|
|
210
|
+
[tool.dmon]
|
|
211
|
+
default_task = "your_task_name"
|
|
168
212
|
```
|
|
169
213
|
|
|
214
|
+
|
|
170
215
|
## Under the Hood
|
|
171
216
|
|
|
172
217
|
Each task is associated with a meta file (e.g. `.dmon/<task>.meta.json`) stored in the current working directory.
|
|
@@ -20,13 +20,40 @@ It is a Python-based and more powerful successor to the [handy-backend shell scr
|
|
|
20
20
|
- 🧩 **Flexible tasks:** Tasks can be configured in `pyproject.toml` or `dmon.yaml`; or run ad-hoc commands directly.
|
|
21
21
|
- 🪵 **Logging & log rotation:** Automatically manage log files to prevent uncontrolled growth.
|
|
22
22
|
|
|
23
|
+

|
|
24
|
+
|
|
23
25
|
|
|
24
26
|
## Installation
|
|
25
27
|
|
|
28
|
+
`python-dmon` is available on [PyPI](https://pypi.org/project/python-dmon/):
|
|
29
|
+
|
|
26
30
|
```sh
|
|
27
31
|
pip install python-dmon
|
|
28
32
|
```
|
|
29
33
|
|
|
34
|
+
We recommend installing into an isolated environment, e.g., with `uv` / `pipx`:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
# Install globally with uv tool
|
|
38
|
+
uv tool install python-dmon
|
|
39
|
+
|
|
40
|
+
# Or with pipx
|
|
41
|
+
pipx install python-dmon
|
|
42
|
+
|
|
43
|
+
# Add as a dev dependency in your project
|
|
44
|
+
uv add --dev python-dmon
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
You can also invoke without installing:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
# With uvx (uv tool run)
|
|
51
|
+
uvx python-dmon
|
|
52
|
+
|
|
53
|
+
# Or with pipx
|
|
54
|
+
pipx run python-dmon
|
|
55
|
+
```
|
|
56
|
+
|
|
30
57
|
To get the latest features, install from source:
|
|
31
58
|
|
|
32
59
|
```sh
|
|
@@ -41,18 +68,20 @@ Create a `dmon.yaml` file:
|
|
|
41
68
|
|
|
42
69
|
```yaml
|
|
43
70
|
tasks:
|
|
44
|
-
app: ["python", "-u", "server.py"]
|
|
71
|
+
app: ["python", "-u", "server.py"] # option 1: exec form
|
|
72
|
+
# app: "python -u server.py" # option 2: shell string
|
|
45
73
|
```
|
|
46
74
|
|
|
47
75
|
Or add to your `pyproject.toml`:
|
|
48
76
|
|
|
49
77
|
```toml
|
|
50
78
|
[tool.dmon.tasks]
|
|
51
|
-
app = ["python", "-u", "server.py"]
|
|
79
|
+
app = ["python", "-u", "server.py"] # option 1: exec form
|
|
80
|
+
# app = "python -u server.py" # option 2: shell string
|
|
52
81
|
```
|
|
53
82
|
|
|
54
83
|
Commands can be a single string (run in shell), or list of strings (exec form).
|
|
55
|
-
See [Example
|
|
84
|
+
See [Example Configuration](#example-configuration) for more configuration options.
|
|
56
85
|
|
|
57
86
|
|
|
58
87
|
### Run tasks
|
|
@@ -73,7 +102,7 @@ dmon status app
|
|
|
73
102
|
dmon exec app
|
|
74
103
|
```
|
|
75
104
|
|
|
76
|
-
If only one task is defined in the config file, you can omit the task name:
|
|
105
|
+
If you have defined `default_task`, or only one task is defined in the config file, you can omit the task name:
|
|
77
106
|
|
|
78
107
|
```sh
|
|
79
108
|
dmon start
|
|
@@ -82,11 +111,12 @@ dmon status
|
|
|
82
111
|
dmon exec
|
|
83
112
|
```
|
|
84
113
|
|
|
85
|
-
You can use `--config` to specify a custom config file:
|
|
114
|
+
You can use `--config` to specify a custom config file or the directory containing it:
|
|
86
115
|
|
|
87
116
|
```sh
|
|
88
117
|
dmon start --config /path/to/dmon.yaml app # YAML
|
|
89
118
|
dmon start --config /path/to/pyproject.toml app # or TOML
|
|
119
|
+
dmon start --config /path/to/dir app # dir with `dmon.y(a)ml` or `pyproject.toml`
|
|
90
120
|
```
|
|
91
121
|
|
|
92
122
|
And yes, you can use `dmon` to run in a nested manner:
|
|
@@ -126,7 +156,7 @@ dmon list
|
|
|
126
156
|
```
|
|
127
157
|
|
|
128
158
|
|
|
129
|
-
## Example
|
|
159
|
+
## Example Configuration
|
|
130
160
|
|
|
131
161
|
A task can be a **string**, **list**, or **dictionary**.
|
|
132
162
|
|
|
@@ -147,8 +177,23 @@ tasks:
|
|
|
147
177
|
rotate_log_path: "logs/<task>.rotate.log" # path to rotation log
|
|
148
178
|
rotate_log_max_size: 5 # max rotation log file size in MB
|
|
149
179
|
meta_path: ".dmon/<task>.meta.json" # path to meta file
|
|
180
|
+
default_task: your_task_name # the default task name
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
In TOML, write like this:
|
|
184
|
+
|
|
185
|
+
```toml
|
|
186
|
+
[tool.dmon.tasks]
|
|
187
|
+
your_task_name = { cmd = [
|
|
188
|
+
"python", "-u", "server.py"
|
|
189
|
+
], ... }
|
|
190
|
+
another_task = "cd subdir && ls && bash start.sh"
|
|
191
|
+
|
|
192
|
+
[tool.dmon]
|
|
193
|
+
default_task = "your_task_name"
|
|
150
194
|
```
|
|
151
195
|
|
|
196
|
+
|
|
152
197
|
## Under the Hood
|
|
153
198
|
|
|
154
199
|
Each task is associated with a meta file (e.g. `.dmon/<task>.meta.json`) stored in the current working directory.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-dmon"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
description = "A lightweight, cross-platform daemon manager that runs any command as a background process."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -23,6 +23,7 @@ Changelog = "https://github.com/atomiechen/python-dmon/blob/main/CHANGELOG.md"
|
|
|
23
23
|
|
|
24
24
|
[project.scripts]
|
|
25
25
|
dmon = "dmon.__main__:main"
|
|
26
|
+
python-dmon = "dmon.__main__:main"
|
|
26
27
|
|
|
27
28
|
[build-system]
|
|
28
29
|
requires = ["uv_build>=0.8.4,<0.9.0"]
|
|
@@ -191,7 +191,7 @@ def main():
|
|
|
191
191
|
for sp in [sp_start, sp_stop, sp_restart, sp_status, sp_exec]:
|
|
192
192
|
sp.add_argument(
|
|
193
193
|
"--config",
|
|
194
|
-
help="Path to config file
|
|
194
|
+
help="Path to config file or the directory containing it (default: search from current directory upwards)",
|
|
195
195
|
)
|
|
196
196
|
|
|
197
197
|
args = parser.parse_args()
|
|
@@ -10,6 +10,21 @@ else:
|
|
|
10
10
|
from .types import CmdType, DmonTaskConfig
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def search_config(start_dir: Path, recursive: bool) -> Optional[Path]:
|
|
14
|
+
"""
|
|
15
|
+
Search for dmon.yaml, dmon.yml, or pyproject.toml from the given directory upwards.
|
|
16
|
+
Return the path if found, None otherwise.
|
|
17
|
+
"""
|
|
18
|
+
current = start_dir.resolve()
|
|
19
|
+
directories = [current] if not recursive else [current, *current.parents]
|
|
20
|
+
for parent in directories:
|
|
21
|
+
for filename in ["dmon.yaml", "dmon.yml", "pyproject.toml"]:
|
|
22
|
+
path = parent / filename
|
|
23
|
+
if path.is_file():
|
|
24
|
+
return path
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
13
28
|
def load_config(cfg_path: Optional[str] = None):
|
|
14
29
|
"""
|
|
15
30
|
Load configuration from the given path, or search it from the current working directory upwards.
|
|
@@ -19,20 +34,20 @@ def load_config(cfg_path: Optional[str] = None):
|
|
|
19
34
|
# Load configuration from the given path
|
|
20
35
|
path = Path(cfg_path).resolve()
|
|
21
36
|
if not path.exists():
|
|
22
|
-
raise FileNotFoundError(
|
|
37
|
+
raise FileNotFoundError(
|
|
38
|
+
f"Config file or directory '{path}' does not exist."
|
|
39
|
+
)
|
|
40
|
+
elif path.is_dir():
|
|
41
|
+
# If it's a directory, search for config files in it
|
|
42
|
+
result = search_config(path, recursive=False)
|
|
43
|
+
if not result:
|
|
44
|
+
raise FileNotFoundError(
|
|
45
|
+
f"No dmon.yaml or pyproject.toml found in directory '{path}'."
|
|
46
|
+
)
|
|
47
|
+
path = result
|
|
23
48
|
else:
|
|
24
|
-
# No path provided, search
|
|
25
|
-
|
|
26
|
-
current = Path.cwd().resolve()
|
|
27
|
-
path = None
|
|
28
|
-
for parent in [current, *current.parents]:
|
|
29
|
-
for filename in ["dmon.yaml", "dmon.yml", "pyproject.toml"]:
|
|
30
|
-
path = parent / filename
|
|
31
|
-
if path.is_file():
|
|
32
|
-
break
|
|
33
|
-
else:
|
|
34
|
-
continue # Only break out of the inner loop if a file was found
|
|
35
|
-
break # Break out of the outer loop if a file was found
|
|
49
|
+
# No path provided, search from the current working directory upwards
|
|
50
|
+
path = search_config(Path.cwd(), recursive=True)
|
|
36
51
|
if not path:
|
|
37
52
|
raise FileNotFoundError(
|
|
38
53
|
"No dmon.yaml or pyproject.toml found in current or any parent directory."
|
|
@@ -106,9 +121,9 @@ def validate_task(task, name: str) -> DmonTaskConfig:
|
|
|
106
121
|
ret.log_rotate = task["log_rotate"]
|
|
107
122
|
|
|
108
123
|
if "log_max_size" in task:
|
|
109
|
-
if not isinstance(task["log_max_size"], int) or task["log_max_size"] <= 0:
|
|
124
|
+
if not isinstance(task["log_max_size"], (int, float)) or task["log_max_size"] <= 0:
|
|
110
125
|
raise TypeError(
|
|
111
|
-
f"Task '{name}' 'log_max_size' field must be a positive
|
|
126
|
+
f"Task '{name}' 'log_max_size' field must be a positive number"
|
|
112
127
|
)
|
|
113
128
|
ret.log_max_size = task["log_max_size"]
|
|
114
129
|
|
|
@@ -121,11 +136,11 @@ def validate_task(task, name: str) -> DmonTaskConfig:
|
|
|
121
136
|
|
|
122
137
|
if "rotate_log_max_size" in task:
|
|
123
138
|
if (
|
|
124
|
-
not isinstance(task["rotate_log_max_size"], int)
|
|
139
|
+
not isinstance(task["rotate_log_max_size"], (int, float))
|
|
125
140
|
or task["rotate_log_max_size"] <= 0
|
|
126
141
|
):
|
|
127
142
|
raise TypeError(
|
|
128
|
-
f"Task '{name}' 'rotate_log_max_size' field must be a positive
|
|
143
|
+
f"Task '{name}' 'rotate_log_max_size' field must be a positive number"
|
|
129
144
|
)
|
|
130
145
|
ret.rotate_log_max_size = task["rotate_log_max_size"]
|
|
131
146
|
|
|
@@ -154,6 +169,8 @@ def get_task_config(name: Optional[str], cfg_path: Optional[str]):
|
|
|
154
169
|
if not isinstance(tasks, dict):
|
|
155
170
|
raise TypeError("'tasks' must be a table")
|
|
156
171
|
|
|
172
|
+
name = name or cfg.get("default_task", None)
|
|
173
|
+
|
|
157
174
|
if not name:
|
|
158
175
|
if len(tasks) == 0:
|
|
159
176
|
raise ValueError(f"No task found in {path}")
|
|
@@ -167,7 +167,7 @@ if __name__ == "__main__":
|
|
|
167
167
|
parser.add_argument(
|
|
168
168
|
"--max-log-size",
|
|
169
169
|
help="Max log file size (MB); 0 for no rotation",
|
|
170
|
-
type=
|
|
170
|
+
type=float,
|
|
171
171
|
default=5,
|
|
172
172
|
)
|
|
173
173
|
parser.add_argument(
|
|
@@ -178,15 +178,15 @@ if __name__ == "__main__":
|
|
|
178
178
|
parser.add_argument(
|
|
179
179
|
"--max-rotate-log-size",
|
|
180
180
|
help="Max file size (MB) for this rotation log; 0 for no rotation",
|
|
181
|
-
type=
|
|
181
|
+
type=float,
|
|
182
182
|
default=5,
|
|
183
183
|
)
|
|
184
184
|
args = parser.parse_args()
|
|
185
185
|
main(
|
|
186
186
|
" ".join(args.command) if args.shell else args.command,
|
|
187
187
|
args.log_path,
|
|
188
|
-
args.max_log_size * 1024 * 1024,
|
|
188
|
+
int(args.max_log_size * 1024 * 1024),
|
|
189
189
|
args.rotate_log_path,
|
|
190
|
-
args.max_rotate_log_size * 1024 * 1024,
|
|
190
|
+
int(args.max_rotate_log_size * 1024 * 1024),
|
|
191
191
|
)
|
|
192
192
|
logger.info("Process finished.")
|
|
@@ -31,11 +31,11 @@ class DmonTaskConfig:
|
|
|
31
31
|
"""Path to log file"""
|
|
32
32
|
log_rotate: bool = False
|
|
33
33
|
"""Whether to rotate log file"""
|
|
34
|
-
log_max_size:
|
|
34
|
+
log_max_size: float = 5
|
|
35
35
|
"""Size in MB to rotate log file"""
|
|
36
36
|
rotate_log_path: str = ""
|
|
37
37
|
"""Path to rotation log file"""
|
|
38
|
-
rotate_log_max_size:
|
|
38
|
+
rotate_log_max_size: float = 5
|
|
39
39
|
"""Size in MB to rotation log file"""
|
|
40
40
|
meta_path: str = ""
|
|
41
41
|
"""Path to meta file"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|