python-dmon 0.2.4__py3-none-any.whl → 0.3.1__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
@@ -1,11 +1,20 @@
1
1
  import argparse
2
+ import os
3
+ from pathlib import Path
2
4
  import shlex
3
- import sys
4
5
 
5
6
  from colorama import just_fix_windows_console
6
7
 
7
- from .config import check_name_in_config, get_task_config
8
- from .control import execute, list_processes, restart, start, stop, status
8
+ from .config import check_name_in_config, get_task_config, load_config
9
+ from .control import (
10
+ execute,
11
+ get_meta_paths,
12
+ list_processes,
13
+ restart,
14
+ start,
15
+ stop,
16
+ status,
17
+ )
9
18
  from .constants import (
10
19
  DEFAULT_META_DIR,
11
20
  DEFAULT_RUN_NAME,
@@ -50,7 +59,7 @@ def main():
50
59
  sp_start.add_argument(
51
60
  "task",
52
61
  help="Configured task name (default: the only task if there's just one)",
53
- nargs="?",
62
+ nargs="*",
54
63
  )
55
64
  sp_start.add_argument(
56
65
  "--meta-file",
@@ -60,20 +69,26 @@ def main():
60
69
  "--log-file",
61
70
  help=f"Path to log file (default: task configured or {LOG_PATH_TEMPLATE})",
62
71
  )
72
+ sp_start.add_argument("--all", action="store_true", help="Start all processes")
63
73
 
64
74
  # stop subcommand
65
75
  sp_stop = subparsers.add_parser(
66
76
  "stop",
67
- help="Stop a background process",
68
- description="Stop a background process given name or meta file",
77
+ help="Stop background process(es)",
78
+ description="Stop background process(es) given name or meta file",
69
79
  # formatter_class=argparse.ArgumentDefaultsHelpFormatter,
70
80
  )
71
81
  sp_stop.add_argument(
72
82
  "task",
73
83
  help="Configured task name (default: the only task if there's just one)",
74
- nargs="?",
84
+ nargs="*",
75
85
  )
76
86
  sp_stop.add_argument("--meta-file", help="Path to meta file")
87
+ sp_stop.add_argument(
88
+ "--all",
89
+ action="store_true",
90
+ help=f"Stop all processes in meta dir ({DEFAULT_META_DIR})",
91
+ )
77
92
 
78
93
  # restart subcommand
79
94
  sp_restart = subparsers.add_parser(
@@ -85,7 +100,7 @@ def main():
85
100
  sp_restart.add_argument(
86
101
  "task",
87
102
  help="Configured task name (default: the only task if there's just one)",
88
- nargs="?",
103
+ nargs="*",
89
104
  )
90
105
  sp_restart.add_argument(
91
106
  "--meta-file",
@@ -95,23 +110,30 @@ def main():
95
110
  "--log-file",
96
111
  help=f"Path to log file (default: task configured or {LOG_PATH_TEMPLATE})",
97
112
  )
113
+ sp_restart.add_argument("--all", action="store_true", help="Restart all processes")
98
114
 
99
115
  # status subcommand
100
116
  sp_status = subparsers.add_parser(
101
117
  "status",
102
- help="Check process status",
103
- description="Check status of a background process given name or meta file",
118
+ help="Check status of background process(es)",
119
+ description="Check status of background process(es) given name or meta file",
104
120
  # formatter_class=argparse.ArgumentDefaultsHelpFormatter,
105
121
  )
106
122
  sp_status.add_argument(
107
123
  "task",
108
124
  help="Configured task name (default: the only task if there's just one)",
109
- nargs="?",
125
+ nargs="*",
110
126
  )
111
127
  sp_status.add_argument(
112
128
  "--meta-file",
113
129
  help=f"Path to meta file (default: {META_PATH_TEMPLATE})",
114
130
  )
131
+ sp_status.add_argument(
132
+ "-a",
133
+ "--all",
134
+ action="store_true",
135
+ help=f"Check status of all processes in meta dir ({DEFAULT_META_DIR})",
136
+ )
115
137
 
116
138
  # list subcommand
117
139
  sp_list = subparsers.add_parser(
@@ -171,9 +193,9 @@ def main():
171
193
  )
172
194
  sp_run.add_argument(
173
195
  "command_list",
174
- metavar="command",
175
- nargs=argparse.ONE_OR_MORE,
176
- help="Command (with args) to run",
196
+ metavar="COMMAND",
197
+ nargs=argparse.REMAINDER,
198
+ help="Command and arguments to run; '--' is an optional separator",
177
199
  )
178
200
 
179
201
  sp_exec = subparsers.add_parser(
@@ -190,7 +212,9 @@ def main():
190
212
  # add custom config file option
191
213
  for sp in [sp_start, sp_stop, sp_restart, sp_status, sp_exec]:
192
214
  sp.add_argument(
215
+ "-c",
193
216
  "--config",
217
+ metavar="PATH",
194
218
  help="Path to config file or the directory containing it (default: search from current directory upwards)",
195
219
  )
196
220
 
@@ -199,50 +223,98 @@ def main():
199
223
  if args.command in ["start", "restart"]:
200
224
  sp = sp_start if args.command == "start" else sp_restart
201
225
  try:
202
- task, task_cfg = get_task_config(args.task, args.config)
226
+ tasks, task_cfgs, cfg_path = get_task_config(
227
+ args.task, args.config, args.all
228
+ )
229
+ os.chdir(cfg_path.parent)
203
230
  except Exception as e:
204
231
  sp.error(str(e))
205
232
 
206
- task_cfg.meta_path = (
207
- args.meta_file or task_cfg.meta_path or META_PATH_TEMPLATE.format(task=task)
208
- )
209
- task_cfg.log_path = (
210
- args.log_file or task_cfg.log_path or LOG_PATH_TEMPLATE.format(task=task)
211
- )
212
- task_cfg.rotate_log_path = (
213
- task_cfg.rotate_log_path or ROTATE_LOG_PATH_TEMPLATE.format(task=task)
214
- )
233
+ # check if meta_file or log_file path is provided;
234
+ # if so, only one task should be specified
235
+ if args.meta_file or args.log_file:
236
+ if len(tasks) == 1:
237
+ task_cfgs[0].meta_path = args.meta_file or task_cfgs[0].meta_path
238
+ task_cfgs[0].log_path = args.log_file or task_cfgs[0].log_path
239
+ else:
240
+ sp.error(
241
+ f"'--meta-file' and '--log-file' can only be specified when {args.command}ing a single task"
242
+ )
243
+ # fill in default values if not provided
244
+ for task, task_cfg in zip(tasks, task_cfgs):
245
+ task_cfg.meta_path = task_cfg.meta_path or META_PATH_TEMPLATE.format(
246
+ task=task
247
+ )
248
+ task_cfg.log_path = task_cfg.log_path or LOG_PATH_TEMPLATE.format(task=task)
249
+ task_cfg.rotate_log_path = (
250
+ task_cfg.rotate_log_path or ROTATE_LOG_PATH_TEMPLATE.format(task=task)
251
+ )
215
252
  if args.command == "start":
216
- sys.exit(start(task_cfg))
253
+ sp.exit(start(task_cfgs))
217
254
  else:
218
- sys.exit(restart(task_cfg))
255
+ sp.exit(restart(task_cfgs))
219
256
  elif args.command == "exec":
220
257
  try:
221
- task, task_cfg = get_task_config(args.task, args.config)
258
+ _, task_cfgs, cfg_path = get_task_config(args.task, args.config)
259
+ os.chdir(cfg_path.parent)
222
260
  except Exception as e:
223
261
  sp_exec.error(str(e))
224
- sys.exit(execute(task_cfg))
262
+ sp_exec.exit(execute(task_cfgs[0]))
225
263
  elif args.command in ["stop", "status"]:
226
264
  sp = sp_stop if args.command == "stop" else sp_status
265
+ meta_paths = []
266
+
267
+ tasks = args.task
268
+ if args.task:
269
+ try:
270
+ tasks, _, cfg_path = get_task_config(args.task, args.config)
271
+ os.chdir(cfg_path.parent)
272
+ except Exception as e:
273
+ sp.error(str(e))
274
+ elif args.config:
275
+ try:
276
+ _, cfg_path = load_config(args.config)
277
+ os.chdir(cfg_path.parent)
278
+ except Exception as e:
279
+ sp.error(str(e))
280
+
281
+ # Collect meta paths from --all
282
+ if args.all:
283
+ meta_paths.extend(get_meta_paths(DEFAULT_META_DIR))
284
+
285
+ # Collect meta paths from --meta-file
227
286
  if args.meta_file:
228
- meta_path = args.meta_file
229
- else:
230
- if args.task:
231
- task = args.task
232
- else:
233
- try:
234
- task, _ = get_task_config(args.task, args.config)
235
- except Exception as e:
236
- sp.error(str(e))
237
- meta_path = META_PATH_TEMPLATE.format(task=task)
287
+ meta_paths.append(args.meta_file)
288
+
289
+ # Collect meta paths from task names
290
+ if len(tasks) > 0:
291
+ meta_paths.extend([META_PATH_TEMPLATE.format(task=task) for task in tasks])
292
+
293
+ # If no meta paths collected, use default task
294
+ if len(meta_paths) == 0:
295
+ try:
296
+ tasks, _, cfg_path = get_task_config(args.task, args.config)
297
+ os.chdir(cfg_path.parent)
298
+ except Exception as e:
299
+ sp.error(str(e))
300
+ meta_paths.extend([META_PATH_TEMPLATE.format(task=task) for task in tasks])
301
+
302
+ # Remove duplicates
303
+ unique_meta_paths = sorted(set(Path(p).resolve() for p in meta_paths))
304
+
238
305
  if args.command == "stop":
239
- sys.exit(stop(meta_path))
306
+ sp.exit(stop(unique_meta_paths))
240
307
  else:
241
- sys.exit(status(meta_path))
308
+ sp.exit(status(unique_meta_paths))
242
309
  elif args.command == "list":
243
310
  dir = args.dir or DEFAULT_META_DIR
244
- sys.exit(list_processes(dir, args.full))
311
+ sp_list.exit(list_processes(dir, args.full))
245
312
  elif args.command == "run":
313
+ command_list = args.command_list
314
+ if command_list and command_list[0] == "--":
315
+ command_list = command_list[1:]
316
+ if not command_list:
317
+ sp_run.error("Please provide a command to run.")
246
318
  if not args.name:
247
319
  sp_run.error("Please provide a non-empty name for the task.")
248
320
  elif check_name_in_config(args.name):
@@ -252,7 +324,7 @@ def main():
252
324
 
253
325
  task_cfg = DmonTaskConfig(
254
326
  task=args.name,
255
- cmd=shlex.join(args.command_list) if args.shell else args.command_list,
327
+ cmd=shlex.join(command_list) if args.shell else command_list,
256
328
  cwd=args.cwd,
257
329
  meta_path=args.meta_file or META_PATH_TEMPLATE.format(task=args.name),
258
330
  log_path=args.log_file or LOG_PATH_TEMPLATE.format(task=args.name),
@@ -260,10 +332,10 @@ def main():
260
332
  rotate_log_path=args.rotate_log_path
261
333
  or ROTATE_LOG_PATH_TEMPLATE.format(task=args.name),
262
334
  )
263
- sys.exit(start(task_cfg))
335
+ sp_run.exit(start([task_cfg]))
264
336
  else:
265
337
  parser.print_help()
266
- sys.exit(1)
338
+ parser.exit(1)
267
339
 
268
340
 
269
341
  if __name__ == "__main__":
dmon/config.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import sys
2
2
  from pathlib import Path
3
- from typing import Dict, Optional, cast
3
+ from typing import Dict, List, Optional, Sequence, Tuple, Union, cast
4
4
 
5
5
  if sys.version_info >= (3, 11):
6
6
  import tomllib
@@ -64,6 +64,10 @@ def load_config(cfg_path: Optional[str] = None):
64
64
  cfg = cfg.get("tool", {}).get("dmon", {})
65
65
  else:
66
66
  raise ValueError("Config file must be YAML (.yaml/.yml) or TOML (.toml)")
67
+ if cfg is None:
68
+ cfg = {}
69
+ if not isinstance(cfg, dict):
70
+ raise TypeError(f"Config in '{path}' must be a table")
67
71
  return cfg, path
68
72
 
69
73
 
@@ -121,12 +125,23 @@ def validate_task(task, name: str) -> DmonTaskConfig:
121
125
  ret.log_rotate = task["log_rotate"]
122
126
 
123
127
  if "log_max_size" in task:
124
- if not isinstance(task["log_max_size"], (int, float)) or task["log_max_size"] <= 0:
128
+ if (
129
+ not isinstance(task["log_max_size"], (int, float))
130
+ or task["log_max_size"] <= 0
131
+ ):
125
132
  raise TypeError(
126
133
  f"Task '{name}' 'log_max_size' field must be a positive number"
127
134
  )
128
135
  ret.log_max_size = task["log_max_size"]
129
136
 
137
+ if "log_backup_count" in task:
138
+ value = task["log_backup_count"]
139
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
140
+ raise TypeError(
141
+ f"Task '{name}' 'log_backup_count' field must be a positive integer"
142
+ )
143
+ ret.log_backup_count = value
144
+
130
145
  if "rotate_log_path" in task:
131
146
  if not isinstance(task["rotate_log_path"], str):
132
147
  raise TypeError(
@@ -144,6 +159,14 @@ def validate_task(task, name: str) -> DmonTaskConfig:
144
159
  )
145
160
  ret.rotate_log_max_size = task["rotate_log_max_size"]
146
161
 
162
+ if "rotate_log_backup_count" in task:
163
+ value = task["rotate_log_backup_count"]
164
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
165
+ raise TypeError(
166
+ f"Task '{name}' 'rotate_log_backup_count' field must be a positive integer"
167
+ )
168
+ ret.rotate_log_backup_count = value
169
+
147
170
  if "meta_path" in task:
148
171
  if not isinstance(task["meta_path"], str):
149
172
  raise TypeError(f"Task '{name}' 'meta_path' field must be a string")
@@ -155,11 +178,14 @@ def validate_task(task, name: str) -> DmonTaskConfig:
155
178
  return ret
156
179
 
157
180
 
158
- def get_task_config(name: Optional[str], cfg_path: Optional[str]):
181
+ def get_task_config(
182
+ names: Union[Sequence[str], str, None], cfg_path: Optional[str], all: bool = False
183
+ ) -> Tuple[Sequence[str], List[DmonTaskConfig], Path]:
159
184
  """
160
- Get the validated task configuration for the given task name.
161
- If name is None or empty, and there is only one task, return that task; otherwise, raise ValueError.
162
- If the task is not found, or required fields are missing, raise TypeError or ValueError.
185
+ Get the validated task configurations for the given task names.
186
+ If 'all' is True, return all tasks.
187
+ If no name specified, and there is only one task, return that task; otherwise, raise ValueError.
188
+ If any task is not found, or required fields are missing, raise TypeError or ValueError.
163
189
 
164
190
  The config is loaded from the given path, or searched for dmon.yaml or pyproject.toml.
165
191
  """
@@ -169,23 +195,37 @@ def get_task_config(name: Optional[str], cfg_path: Optional[str]):
169
195
  if not isinstance(tasks, dict):
170
196
  raise TypeError("'tasks' must be a table")
171
197
 
172
- name = name or cfg.get("default_task", None)
173
-
174
- if not name:
175
- if len(tasks) == 0:
176
- raise ValueError(f"No task found in {path}")
177
- elif len(tasks) == 1:
178
- name = next(iter(tasks))
198
+ if all:
199
+ names = list(tasks.keys())
200
+ elif isinstance(names, str):
201
+ names = [names]
202
+ elif names is None or len(names) == 0:
203
+ default_task_name = cfg.get("default_task", None)
204
+ if default_task_name:
205
+ if not isinstance(default_task_name, str):
206
+ raise TypeError("'default_task' must be a string")
207
+ names = [default_task_name]
179
208
  else:
180
- raise ValueError(f"Multiple tasks found in {path}; please specify one.")
181
- else:
209
+ if len(tasks) == 0:
210
+ raise ValueError(f"No task found in {path}")
211
+ elif len(tasks) == 1:
212
+ name = next(iter(tasks))
213
+ assert isinstance(name, str)
214
+ names = [name]
215
+ else:
216
+ raise ValueError(f"Multiple tasks found in {path}; please specify one.")
217
+
218
+ ret_names = []
219
+ ret_tasks = []
220
+ for name in names:
182
221
  name = name.lower()
183
222
  if name not in tasks:
184
223
  raise ValueError(f"Task '{name}' not found in {path}")
185
224
 
186
- assert isinstance(name, str)
187
- task = validate_task(tasks[name], name)
188
- return name, task
225
+ task = validate_task(tasks[name], name)
226
+ ret_names.append(name)
227
+ ret_tasks.append(task)
228
+ return ret_names, ret_tasks, path
189
229
 
190
230
 
191
231
  def check_name_in_config(name: str) -> bool:
@@ -193,7 +233,10 @@ def check_name_in_config(name: str) -> bool:
193
233
  Check if the given task name exists in the tasks.
194
234
  Return True if found, False otherwise.
195
235
  """
196
- cfg, _ = load_config()
236
+ try:
237
+ cfg, _ = load_config()
238
+ except FileNotFoundError:
239
+ return False
197
240
  tasks = cfg.get("tasks", {})
198
241
 
199
242
  if not isinstance(tasks, dict):