python-dmon 0.2.1__py3-none-any.whl → 0.2.3__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/cli.py CHANGED
@@ -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()
dmon/config.py CHANGED
@@ -10,79 +10,60 @@ else:
10
10
  from .types import CmdType, DmonTaskConfig
11
11
 
12
12
 
13
- def find_pyproject_toml(start: Optional[Path] = None):
13
+ def search_config(start_dir: Path, recursive: bool) -> Optional[Path]:
14
14
  """
15
- Search for pyproject.toml from the current directory (or specified directory) upwards.
16
- Return a Path object if found.
15
+ Search for dmon.yaml, dmon.yml, or pyproject.toml from the given directory upwards.
16
+ Return the path if found, None otherwise.
17
17
  """
18
- start = start or Path.cwd()
19
- current = start.resolve()
20
-
21
- for parent in [current, *current.parents]:
22
- candidate = parent / "pyproject.toml"
23
- if candidate.is_file():
24
- return candidate
25
-
26
- # raise FileNotFoundError(f"No pyproject.toml found from {start} upwards.")
27
-
28
-
29
- def find_dmon_yaml(start: Optional[Path] = None):
30
- """
31
- Search for dmon.yaml / dmon.yml from the current directory (or specified directory) upwards.
32
- Return a Path object if found.
33
- """
34
- start = start or Path.cwd()
35
- current = start.resolve()
36
-
37
- for parent in [current, *current.parents]:
38
- for filename in ["dmon.yaml", "dmon.yml"]:
39
- candidate = parent / filename
40
- if candidate.is_file():
41
- return candidate
42
-
43
- # raise FileNotFoundError(f"No dmon.yaml or dmon.yml found from {start} upwards.")
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
44
26
 
45
27
 
46
28
  def load_config(cfg_path: Optional[str] = None):
47
29
  """
48
- Load configuration from the given path, or search for dmon.yaml or pyproject.toml.
30
+ Load configuration from the given path, or search it from the current working directory upwards.
49
31
  """
50
32
 
51
- # Load configuration from the given path
52
33
  if cfg_path:
34
+ # Load configuration from the given path
53
35
  path = Path(cfg_path).resolve()
54
36
  if not path.exists():
55
- raise FileNotFoundError(f"Config file '{path}' does not exist.")
56
- if path.suffix in [".yaml", ".yml"]:
57
- import yaml
58
-
59
- with path.open("r", encoding="utf-8") as f:
60
- cfg = yaml.safe_load(f)
61
- elif path.suffix == ".toml":
62
- with path.open("rb") as f:
63
- cfg = tomllib.load(f)
64
- cfg = cfg.get("tool", {}).get("dmon", {})
65
- else:
66
- raise ValueError("Config file must be YAML (.yaml/.yml) or TOML (.toml)")
67
- return cfg, path
68
-
69
- # No path provided, search for dmon.yaml first, then pyproject.toml
70
- path = find_dmon_yaml()
71
- if path:
72
- import yaml
73
-
74
- with path.open("r", encoding="utf-8") as f:
75
- cfg = yaml.safe_load(f)
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
76
48
  else:
77
- path = find_pyproject_toml()
49
+ # No path provided, search from the current working directory upwards
50
+ path = search_config(Path.cwd(), recursive=True)
78
51
  if not path:
79
52
  raise FileNotFoundError(
80
53
  "No dmon.yaml or pyproject.toml found in current or any parent directory."
81
54
  )
82
55
 
56
+ if path.suffix in [".yaml", ".yml"]:
57
+ import yaml
58
+
59
+ with path.open("r", encoding="utf-8") as f:
60
+ cfg = yaml.safe_load(f)
61
+ elif path.suffix == ".toml":
83
62
  with path.open("rb") as f:
84
63
  cfg = tomllib.load(f)
85
64
  cfg = cfg.get("tool", {}).get("dmon", {})
65
+ else:
66
+ raise ValueError("Config file must be YAML (.yaml/.yml) or TOML (.toml)")
86
67
  return cfg, path
87
68
 
88
69
 
@@ -188,6 +169,8 @@ def get_task_config(name: Optional[str], cfg_path: Optional[str]):
188
169
  if not isinstance(tasks, dict):
189
170
  raise TypeError("'tasks' must be a table")
190
171
 
172
+ name = name or cfg.get("default_task", None)
173
+
191
174
  if not name:
192
175
  if len(tasks) == 0:
193
176
  raise ValueError(f"No task found in {path}")
dmon/control.py CHANGED
@@ -525,14 +525,14 @@ def print_process_table(metas: List[DmonMeta], full_width: bool = False):
525
525
 
526
526
  def list_processes(dir: PathType, full_width: bool):
527
527
  target_dmon_dir = Path(dir).resolve()
528
- metas = []
528
+ metas: List[DmonMeta] = []
529
529
  if target_dmon_dir.exists() and target_dmon_dir.is_dir():
530
530
  for meta_file in target_dmon_dir.glob(f"*{META_SUFFIX}"):
531
531
  meta = DmonMeta.load(meta_file)
532
532
  if meta is not None:
533
533
  metas.append(meta)
534
534
  # sort by name (case-insensitive)
535
- metas.sort(key=lambda m: m.name.lower())
535
+ metas.sort(key=lambda m: m.task.lower())
536
536
  n_task = len(metas)
537
537
  processes = print_process_table(metas, full_width)
538
538
  n_proc = len(processes)
@@ -557,6 +557,9 @@ def execute(cfg: DmonTaskConfig):
557
557
  env = {**os.environ, **cfg.env}
558
558
 
559
559
  shell = isinstance(cfg.cmd, str)
560
+ if ON_WINDOWS and isinstance(cfg.cmd, list) and len(cfg.cmd) > 0:
561
+ # On Windows, use full path for the executable when shell=False
562
+ cfg.cmd[0] = shutil.which(cfg.cmd[0]) or cfg.cmd[0]
560
563
 
561
564
  def signal_handler(signum: int, frame) -> None:
562
565
  if ON_WINDOWS and signum == signal.SIGINT:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-dmon
3
- Version: 0.2.1
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
@@ -86,14 +111,38 @@ dmon stop app
86
111
 
87
112
  # Check task status
88
113
  dmon status app
114
+
115
+ # Execute a task in the foreground (useful for debugging)
116
+ dmon exec app
89
117
  ```
90
118
 
91
- 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:
92
120
 
93
121
  ```sh
94
122
  dmon start
95
123
  dmon stop
96
124
  dmon status
125
+ dmon exec
126
+ ```
127
+
128
+ You can use `--config` to specify a custom config file or the directory containing it:
129
+
130
+ ```sh
131
+ dmon start --config /path/to/dmon.yaml app # YAML
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`
134
+ ```
135
+
136
+ And yes, you can use `dmon` to run in a nested manner:
137
+
138
+ ```yaml
139
+ tasks:
140
+ app: ["python", "-u", "server.py"]
141
+ nested: pwd && dmon exec app # nest `dmon exec`
142
+ subdir_task1:
143
+ cwd: /path/to/dir
144
+ cmd: ["dmon", "exec", "app"] # run task defined in another folder
145
+ subdir_task2: dmon exec app --config /path/to/dir/dmon.yaml # like above
97
146
  ```
98
147
 
99
148
 
@@ -121,7 +170,7 @@ dmon list
121
170
  ```
122
171
 
123
172
 
124
- ## Example Task Configuration
173
+ ## Example Configuration
125
174
 
126
175
  A task can be a **string**, **list**, or **dictionary**.
127
176
 
@@ -142,8 +191,23 @@ tasks:
142
191
  rotate_log_path: "logs/<task>.rotate.log" # path to rotation log
143
192
  rotate_log_max_size: 5 # max rotation log file size in MB
144
193
  meta_path: ".dmon/<task>.meta.json" # path to meta file
194
+ default_task: your_task_name # the default task name
145
195
  ```
146
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
+
147
211
  ## Under the Hood
148
212
 
149
213
  Each task is associated with a meta file (e.g. `.dmon/<task>.meta.json`) stored in the current working directory.
@@ -1,14 +1,14 @@
1
1
  dmon/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  dmon/__main__.py,sha256=wu5N2wk8mvBgyvr2ghmQf4prezAe0_i-p123VVreyYc,62
3
- dmon/cli.py,sha256=Dqu43_hcsx8iCJXAL1YXqBpp1jFt0Y7qBEr9mYzkAn8,8764
4
- dmon/config.py,sha256=a7YM4U_iMB1wQ03NpWGAjbX-N-8Rdt9n6XTzKzeGZq4,7566
3
+ dmon/cli.py,sha256=CdlgGTWWbTbJ-yz1Cct1EoJtaNGI3zs_3puoMRtrqbg,8780
4
+ dmon/config.py,sha256=RXcQEgyXSmx_sIXzcrMnc_XkyV0uc20MEv5DTNxFHrw,7204
5
5
  dmon/constants.py,sha256=uJPI66IJvT6vhamqEDeqHjZQS12RxCPe3DD5yCU0UR8,407
6
- dmon/control.py,sha256=u1sz2MtxHHDqsaFyc7QaG7HFPLnAkser_8riTYxFK1A,17276
6
+ dmon/control.py,sha256=KpGNPPsuAsDNX3FTJxnGNnt9f4Q18omvG42zO1CwTy4,17494
7
7
  dmon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  dmon/runner.py,sha256=iyotNIrbO6I_u02EcqwO7R7N3BEBP6b_8eL8yb3zeJQ,6082
9
9
  dmon/types.py,sha256=tTVoJzWk-Z8XiZCUt0VE8Z7-URpXMoS54HfVYXhW-0Q,1795
10
10
  dmon/utils.py,sha256=zVQC9YNhMQu4hrAp9k2ds2egnbWZZv7S5w8i2oVIPJg,808
11
- python_dmon-0.2.1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
12
- python_dmon-0.2.1.dist-info/entry_points.txt,sha256=rad5ugz4K_IyNXS_IrFPu0tS-UXlBCSJRhJv-OWd_yk,45
13
- python_dmon-0.2.1.dist-info/METADATA,sha256=gIMrWHILmOEnjtU-gZAoYdz8JAOxFYibkos4DYs_ba0,4496
14
- python_dmon-0.2.1.dist-info/RECORD,,
11
+ python_dmon-0.2.3.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
12
+ python_dmon-0.2.3.dist-info/entry_points.txt,sha256=1XmEpyjShANXEHMIMa4p3WXpcllIZfGt5DQyTFuSI74,78
13
+ python_dmon-0.2.3.dist-info/METADATA,sha256=7k6z_ZfbmzpHEAiUKY_O0MQVwkEuWE53PLPz1Th66wA,5984
14
+ python_dmon-0.2.3.dist-info/RECORD,,
@@ -1,3 +1,4 @@
1
1
  [console_scripts]
2
2
  dmon = dmon.__main__:main
3
+ python-dmon = dmon.__main__:main
3
4