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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-dmon
3
- Version: 0.2.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
+ ![dmon-demo-gif](https://github.com/user-attachments/assets/9bae2f46-5ef4-4784-aced-18d573204efc)
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 Task Configuration](#example-task-configuration) for more configuration options.
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 Task Configuration
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
+ ![dmon-demo-gif](https://github.com/user-attachments/assets/9bae2f46-5ef4-4784-aced-18d573204efc)
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 Task Configuration](#example-task-configuration) for more configuration options.
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 Task Configuration
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.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 (YAML or TOML) (default: search from current directory upwards)",
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(f"Config file '{path}' does not exist.")
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 for config files
25
- # starting from the current working directory upwards
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 integer"
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 integer"
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=int,
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=int,
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: int = 5
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: int = 5
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"""