python-dmon 0.2.2__tar.gz → 0.2.3__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.3
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
@@ -41,10 +41,35 @@ It is a Python-based and more powerful successor to the [handy-backend shell scr
41
41
 
42
42
  ## Installation
43
43
 
44
+ `python-dmon` is available on [PyPI](https://pypi.org/project/python-dmon/):
45
+
44
46
  ```sh
45
47
  pip install python-dmon
46
48
  ```
47
49
 
50
+ We recommend installing into an isolated environment, e.g., with `uv` / `pipx`:
51
+
52
+ ```sh
53
+ # Install globally with uv tool
54
+ uv tool install python-dmon
55
+
56
+ # Or with pipx
57
+ pipx install python-dmon
58
+
59
+ # Add as a dev dependency in your project
60
+ uv add --dev python-dmon
61
+ ```
62
+
63
+ You can also invoke without installing:
64
+
65
+ ```sh
66
+ # With uvx (uv tool run)
67
+ uvx python-dmon
68
+
69
+ # Or with pipx
70
+ pipx run python-dmon
71
+ ```
72
+
48
73
  To get the latest features, install from source:
49
74
 
50
75
  ```sh
@@ -70,7 +95,7 @@ app = ["python", "-u", "server.py"]
70
95
  ```
71
96
 
72
97
  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.
98
+ See [Example Configuration](#example-configuration) for more configuration options.
74
99
 
75
100
 
76
101
  ### Run tasks
@@ -91,7 +116,7 @@ dmon status app
91
116
  dmon exec app
92
117
  ```
93
118
 
94
- If only one task is defined in the config file, you can omit the task name:
119
+ If you have defined `default_task`, or only one task is defined in the config file, you can omit the task name:
95
120
 
96
121
  ```sh
97
122
  dmon start
@@ -100,11 +125,12 @@ dmon status
100
125
  dmon exec
101
126
  ```
102
127
 
103
- You can use `--config` to specify a custom config file:
128
+ You can use `--config` to specify a custom config file or the directory containing it:
104
129
 
105
130
  ```sh
106
131
  dmon start --config /path/to/dmon.yaml app # YAML
107
132
  dmon start --config /path/to/pyproject.toml app # or TOML
133
+ dmon start --config /path/to/dir app # dir with `dmon.y(a)ml` or `pyproject.toml`
108
134
  ```
109
135
 
110
136
  And yes, you can use `dmon` to run in a nested manner:
@@ -144,7 +170,7 @@ dmon list
144
170
  ```
145
171
 
146
172
 
147
- ## Example Task Configuration
173
+ ## Example Configuration
148
174
 
149
175
  A task can be a **string**, **list**, or **dictionary**.
150
176
 
@@ -165,8 +191,23 @@ tasks:
165
191
  rotate_log_path: "logs/<task>.rotate.log" # path to rotation log
166
192
  rotate_log_max_size: 5 # max rotation log file size in MB
167
193
  meta_path: ".dmon/<task>.meta.json" # path to meta file
194
+ default_task: your_task_name # the default task name
168
195
  ```
169
196
 
197
+ In TOML, write like this:
198
+
199
+ ```toml
200
+ [tool.dmon.tasks]
201
+ your_task_name = { cmd = [
202
+ "python", "-u", "server.py"
203
+ ], ... }
204
+ another_task = "cd subdir && ls && bash start.sh"
205
+
206
+ [tool.dmon]
207
+ default_task = "your_task_name"
208
+ ```
209
+
210
+
170
211
  ## Under the Hood
171
212
 
172
213
  Each task is associated with a meta file (e.g. `.dmon/<task>.meta.json`) stored in the current working directory.
@@ -23,10 +23,35 @@ It is a Python-based and more powerful successor to the [handy-backend shell scr
23
23
 
24
24
  ## Installation
25
25
 
26
+ `python-dmon` is available on [PyPI](https://pypi.org/project/python-dmon/):
27
+
26
28
  ```sh
27
29
  pip install python-dmon
28
30
  ```
29
31
 
32
+ We recommend installing into an isolated environment, e.g., with `uv` / `pipx`:
33
+
34
+ ```sh
35
+ # Install globally with uv tool
36
+ uv tool install python-dmon
37
+
38
+ # Or with pipx
39
+ pipx install python-dmon
40
+
41
+ # Add as a dev dependency in your project
42
+ uv add --dev python-dmon
43
+ ```
44
+
45
+ You can also invoke without installing:
46
+
47
+ ```sh
48
+ # With uvx (uv tool run)
49
+ uvx python-dmon
50
+
51
+ # Or with pipx
52
+ pipx run python-dmon
53
+ ```
54
+
30
55
  To get the latest features, install from source:
31
56
 
32
57
  ```sh
@@ -52,7 +77,7 @@ app = ["python", "-u", "server.py"]
52
77
  ```
53
78
 
54
79
  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.
80
+ See [Example Configuration](#example-configuration) for more configuration options.
56
81
 
57
82
 
58
83
  ### Run tasks
@@ -73,7 +98,7 @@ dmon status app
73
98
  dmon exec app
74
99
  ```
75
100
 
76
- If only one task is defined in the config file, you can omit the task name:
101
+ If you have defined `default_task`, or only one task is defined in the config file, you can omit the task name:
77
102
 
78
103
  ```sh
79
104
  dmon start
@@ -82,11 +107,12 @@ dmon status
82
107
  dmon exec
83
108
  ```
84
109
 
85
- You can use `--config` to specify a custom config file:
110
+ You can use `--config` to specify a custom config file or the directory containing it:
86
111
 
87
112
  ```sh
88
113
  dmon start --config /path/to/dmon.yaml app # YAML
89
114
  dmon start --config /path/to/pyproject.toml app # or TOML
115
+ dmon start --config /path/to/dir app # dir with `dmon.y(a)ml` or `pyproject.toml`
90
116
  ```
91
117
 
92
118
  And yes, you can use `dmon` to run in a nested manner:
@@ -126,7 +152,7 @@ dmon list
126
152
  ```
127
153
 
128
154
 
129
- ## Example Task Configuration
155
+ ## Example Configuration
130
156
 
131
157
  A task can be a **string**, **list**, or **dictionary**.
132
158
 
@@ -147,8 +173,23 @@ tasks:
147
173
  rotate_log_path: "logs/<task>.rotate.log" # path to rotation log
148
174
  rotate_log_max_size: 5 # max rotation log file size in MB
149
175
  meta_path: ".dmon/<task>.meta.json" # path to meta file
176
+ default_task: your_task_name # the default task name
150
177
  ```
151
178
 
179
+ In TOML, write like this:
180
+
181
+ ```toml
182
+ [tool.dmon.tasks]
183
+ your_task_name = { cmd = [
184
+ "python", "-u", "server.py"
185
+ ], ... }
186
+ another_task = "cd subdir && ls && bash start.sh"
187
+
188
+ [tool.dmon]
189
+ default_task = "your_task_name"
190
+ ```
191
+
192
+
152
193
  ## Under the Hood
153
194
 
154
195
  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.3"
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."
@@ -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}")