python-dmon 0.3.0__tar.gz → 0.3.1__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.3.0 → python_dmon-0.3.1}/PKG-INFO +30 -4
- {python_dmon-0.3.0 → python_dmon-0.3.1}/README.md +29 -3
- {python_dmon-0.3.0 → python_dmon-0.3.1}/pyproject.toml +1 -1
- {python_dmon-0.3.0 → python_dmon-0.3.1}/src/dmon/cli.py +44 -19
- {python_dmon-0.3.0 → python_dmon-0.3.1}/src/dmon/config.py +28 -3
- {python_dmon-0.3.0 → python_dmon-0.3.1}/src/dmon/control.py +344 -190
- {python_dmon-0.3.0 → python_dmon-0.3.1}/src/dmon/runner.py +74 -27
- {python_dmon-0.3.0 → python_dmon-0.3.1}/src/dmon/types.py +40 -3
- {python_dmon-0.3.0 → python_dmon-0.3.1}/src/dmon/__init__.py +0 -0
- {python_dmon-0.3.0 → python_dmon-0.3.1}/src/dmon/__main__.py +0 -0
- {python_dmon-0.3.0 → python_dmon-0.3.1}/src/dmon/constants.py +0 -0
- {python_dmon-0.3.0 → python_dmon-0.3.1}/src/dmon/py.typed +0 -0
- {python_dmon-0.3.0 → python_dmon-0.3.1}/src/dmon/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: python-dmon
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -37,7 +37,7 @@ It is a Python-based and more powerful successor to the [handy-backend shell scr
|
|
|
37
37
|
- 🖥️ **Cross-platform:** Works on Linux, macOS, and Windows.
|
|
38
38
|
- ⚡ **Lightweight:** Pure Python, no Docker or external dependencies needed.
|
|
39
39
|
- 🧩 **Flexible tasks:** Tasks can be configured in `pyproject.toml` or `dmon.yaml`; or run ad-hoc commands directly.
|
|
40
|
-
- 🪵 **Logging & log rotation:**
|
|
40
|
+
- 🪵 **Logging & log rotation:** Keep active log files manageable, with optional archive retention limits.
|
|
41
41
|
|
|
42
42
|

|
|
43
43
|
|
|
@@ -126,6 +126,11 @@ dmon exec app
|
|
|
126
126
|
|
|
127
127
|
You can specify multiple tasks at once, e.g.: `dmon start app1 app2 app3`, except for `dmon exec` which only accepts one task.
|
|
128
128
|
|
|
129
|
+
Multi-task `start` is best-effort: dmon attempts every requested task and leaves
|
|
130
|
+
successful tasks running if another task cannot start. The command returns a
|
|
131
|
+
non-zero status and prints a summary naming the failed tasks. This is useful for
|
|
132
|
+
independent background services and does not provide atomic stack semantics.
|
|
133
|
+
|
|
129
134
|
Or use `--all` to operate on all tasks:
|
|
130
135
|
|
|
131
136
|
```sh
|
|
@@ -145,12 +150,12 @@ dmon status
|
|
|
145
150
|
dmon exec
|
|
146
151
|
```
|
|
147
152
|
|
|
148
|
-
You can use `--config` to specify a custom config file or the directory containing it:
|
|
153
|
+
You can use `-c` / `--config` to specify a custom config file or the directory containing it:
|
|
149
154
|
|
|
150
155
|
```sh
|
|
151
156
|
dmon start --config /path/to/dmon.yaml app # YAML
|
|
152
157
|
dmon start --config /path/to/pyproject.toml app # or TOML
|
|
153
|
-
dmon start
|
|
158
|
+
dmon start -c /path/to/dir app # shorter, dir with `dmon.y(a)ml` or `pyproject.toml`
|
|
154
159
|
```
|
|
155
160
|
|
|
156
161
|
And yes, you can use `dmon` to run in a nested manner:
|
|
@@ -172,6 +177,9 @@ tasks:
|
|
|
172
177
|
# Run a command with arguments in the background
|
|
173
178
|
dmon run --name myserver python -u server.py
|
|
174
179
|
|
|
180
|
+
# Optionally use -- to make the child-command boundary explicit
|
|
181
|
+
dmon run --name timer -- python -c 'import time; time.sleep(30)'
|
|
182
|
+
|
|
175
183
|
# Run a shell command in the background
|
|
176
184
|
dmon run --shell echo "Hello World"
|
|
177
185
|
|
|
@@ -194,6 +202,16 @@ dmon list
|
|
|
194
202
|
|
|
195
203
|
A task can be a **string**, **list**, or **dictionary**.
|
|
196
204
|
|
|
205
|
+
When rotation is enabled, dmon keeps timestamped archives such as
|
|
206
|
+
`app.log.20260807-142106`. Both task and runner logs use this cross-platform
|
|
207
|
+
format; a same-second collision adds `.1`, `.2`, and so on. Archives are never
|
|
208
|
+
deleted by default. Set a backup count explicitly to enable retention cleanup.
|
|
209
|
+
`log_path` contains task output; `rotate_log_path` contains diagnostics from the
|
|
210
|
+
dmon process that captures and rotates that output. They are independent log
|
|
211
|
+
streams and use independent retention settings.
|
|
212
|
+
The size limit is checked at line boundaries, so a single long line may exceed
|
|
213
|
+
the configured limit.
|
|
214
|
+
|
|
197
215
|
Here is a more complete example with default values:
|
|
198
216
|
|
|
199
217
|
```yaml
|
|
@@ -208,8 +226,10 @@ tasks:
|
|
|
208
226
|
log_path: "logs/<task>.log" # path to log file
|
|
209
227
|
log_rotate: false # enable log rotation
|
|
210
228
|
log_max_size: 5 # max log file size before rotation in MB
|
|
229
|
+
# log_backup_count: 10 # optional; omit to retain all task log archives
|
|
211
230
|
rotate_log_path: "logs/<task>.rotate.log" # path to rotation log
|
|
212
231
|
rotate_log_max_size: 5 # max rotation log file size in MB
|
|
232
|
+
# rotate_log_backup_count: 10 # optional; omit to retain all runner log archives
|
|
213
233
|
meta_path: ".dmon/<task>.meta.json" # path to meta file
|
|
214
234
|
default_task: your_task_name # the default task name
|
|
215
235
|
```
|
|
@@ -227,6 +247,8 @@ another_task = "cd subdir && ls && bash start.sh"
|
|
|
227
247
|
default_task = "your_task_name"
|
|
228
248
|
```
|
|
229
249
|
|
|
250
|
+
All paths can be absolute or relative to the **config file location**.
|
|
251
|
+
|
|
230
252
|
|
|
231
253
|
## Under the Hood
|
|
232
254
|
|
|
@@ -234,6 +256,10 @@ Each task is associated with a meta file (e.g. `.dmon/<task>.meta.json`) stored
|
|
|
234
256
|
The file contains details such as the command, PID, log path, and more.
|
|
235
257
|
**Do not** modify or delete these files manually.
|
|
236
258
|
|
|
259
|
+
`dmon status` returns a non-zero status if a recorded task has exited. Starting
|
|
260
|
+
that task again removes its stale metadata automatically. `dmon stop` terminates
|
|
261
|
+
the complete process tree and also cleans stale metadata left by an exited task.
|
|
262
|
+
|
|
237
263
|
|
|
238
264
|
## License
|
|
239
265
|
|
|
@@ -19,7 +19,7 @@ It is a Python-based and more powerful successor to the [handy-backend shell scr
|
|
|
19
19
|
- 🖥️ **Cross-platform:** Works on Linux, macOS, and Windows.
|
|
20
20
|
- ⚡ **Lightweight:** Pure Python, no Docker or external dependencies needed.
|
|
21
21
|
- 🧩 **Flexible tasks:** Tasks can be configured in `pyproject.toml` or `dmon.yaml`; or run ad-hoc commands directly.
|
|
22
|
-
- 🪵 **Logging & log rotation:**
|
|
22
|
+
- 🪵 **Logging & log rotation:** Keep active log files manageable, with optional archive retention limits.
|
|
23
23
|
|
|
24
24
|

|
|
25
25
|
|
|
@@ -108,6 +108,11 @@ dmon exec app
|
|
|
108
108
|
|
|
109
109
|
You can specify multiple tasks at once, e.g.: `dmon start app1 app2 app3`, except for `dmon exec` which only accepts one task.
|
|
110
110
|
|
|
111
|
+
Multi-task `start` is best-effort: dmon attempts every requested task and leaves
|
|
112
|
+
successful tasks running if another task cannot start. The command returns a
|
|
113
|
+
non-zero status and prints a summary naming the failed tasks. This is useful for
|
|
114
|
+
independent background services and does not provide atomic stack semantics.
|
|
115
|
+
|
|
111
116
|
Or use `--all` to operate on all tasks:
|
|
112
117
|
|
|
113
118
|
```sh
|
|
@@ -127,12 +132,12 @@ dmon status
|
|
|
127
132
|
dmon exec
|
|
128
133
|
```
|
|
129
134
|
|
|
130
|
-
You can use `--config` to specify a custom config file or the directory containing it:
|
|
135
|
+
You can use `-c` / `--config` to specify a custom config file or the directory containing it:
|
|
131
136
|
|
|
132
137
|
```sh
|
|
133
138
|
dmon start --config /path/to/dmon.yaml app # YAML
|
|
134
139
|
dmon start --config /path/to/pyproject.toml app # or TOML
|
|
135
|
-
dmon start
|
|
140
|
+
dmon start -c /path/to/dir app # shorter, dir with `dmon.y(a)ml` or `pyproject.toml`
|
|
136
141
|
```
|
|
137
142
|
|
|
138
143
|
And yes, you can use `dmon` to run in a nested manner:
|
|
@@ -154,6 +159,9 @@ tasks:
|
|
|
154
159
|
# Run a command with arguments in the background
|
|
155
160
|
dmon run --name myserver python -u server.py
|
|
156
161
|
|
|
162
|
+
# Optionally use -- to make the child-command boundary explicit
|
|
163
|
+
dmon run --name timer -- python -c 'import time; time.sleep(30)'
|
|
164
|
+
|
|
157
165
|
# Run a shell command in the background
|
|
158
166
|
dmon run --shell echo "Hello World"
|
|
159
167
|
|
|
@@ -176,6 +184,16 @@ dmon list
|
|
|
176
184
|
|
|
177
185
|
A task can be a **string**, **list**, or **dictionary**.
|
|
178
186
|
|
|
187
|
+
When rotation is enabled, dmon keeps timestamped archives such as
|
|
188
|
+
`app.log.20260807-142106`. Both task and runner logs use this cross-platform
|
|
189
|
+
format; a same-second collision adds `.1`, `.2`, and so on. Archives are never
|
|
190
|
+
deleted by default. Set a backup count explicitly to enable retention cleanup.
|
|
191
|
+
`log_path` contains task output; `rotate_log_path` contains diagnostics from the
|
|
192
|
+
dmon process that captures and rotates that output. They are independent log
|
|
193
|
+
streams and use independent retention settings.
|
|
194
|
+
The size limit is checked at line boundaries, so a single long line may exceed
|
|
195
|
+
the configured limit.
|
|
196
|
+
|
|
179
197
|
Here is a more complete example with default values:
|
|
180
198
|
|
|
181
199
|
```yaml
|
|
@@ -190,8 +208,10 @@ tasks:
|
|
|
190
208
|
log_path: "logs/<task>.log" # path to log file
|
|
191
209
|
log_rotate: false # enable log rotation
|
|
192
210
|
log_max_size: 5 # max log file size before rotation in MB
|
|
211
|
+
# log_backup_count: 10 # optional; omit to retain all task log archives
|
|
193
212
|
rotate_log_path: "logs/<task>.rotate.log" # path to rotation log
|
|
194
213
|
rotate_log_max_size: 5 # max rotation log file size in MB
|
|
214
|
+
# rotate_log_backup_count: 10 # optional; omit to retain all runner log archives
|
|
195
215
|
meta_path: ".dmon/<task>.meta.json" # path to meta file
|
|
196
216
|
default_task: your_task_name # the default task name
|
|
197
217
|
```
|
|
@@ -209,6 +229,8 @@ another_task = "cd subdir && ls && bash start.sh"
|
|
|
209
229
|
default_task = "your_task_name"
|
|
210
230
|
```
|
|
211
231
|
|
|
232
|
+
All paths can be absolute or relative to the **config file location**.
|
|
233
|
+
|
|
212
234
|
|
|
213
235
|
## Under the Hood
|
|
214
236
|
|
|
@@ -216,6 +238,10 @@ Each task is associated with a meta file (e.g. `.dmon/<task>.meta.json`) stored
|
|
|
216
238
|
The file contains details such as the command, PID, log path, and more.
|
|
217
239
|
**Do not** modify or delete these files manually.
|
|
218
240
|
|
|
241
|
+
`dmon status` returns a non-zero status if a recorded task has exited. Starting
|
|
242
|
+
that task again removes its stale metadata automatically. `dmon stop` terminates
|
|
243
|
+
the complete process tree and also cleans stale metadata left by an exited task.
|
|
244
|
+
|
|
219
245
|
|
|
220
246
|
## License
|
|
221
247
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import os
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
import shlex
|
|
4
|
-
import sys
|
|
5
5
|
|
|
6
6
|
from colorama import just_fix_windows_console
|
|
7
7
|
|
|
8
|
-
from .config import check_name_in_config, get_task_config
|
|
8
|
+
from .config import check_name_in_config, get_task_config, load_config
|
|
9
9
|
from .control import (
|
|
10
10
|
execute,
|
|
11
11
|
get_meta_paths,
|
|
@@ -193,9 +193,9 @@ def main():
|
|
|
193
193
|
)
|
|
194
194
|
sp_run.add_argument(
|
|
195
195
|
"command_list",
|
|
196
|
-
metavar="
|
|
197
|
-
nargs=argparse.
|
|
198
|
-
help="Command
|
|
196
|
+
metavar="COMMAND",
|
|
197
|
+
nargs=argparse.REMAINDER,
|
|
198
|
+
help="Command and arguments to run; '--' is an optional separator",
|
|
199
199
|
)
|
|
200
200
|
|
|
201
201
|
sp_exec = subparsers.add_parser(
|
|
@@ -212,7 +212,9 @@ def main():
|
|
|
212
212
|
# add custom config file option
|
|
213
213
|
for sp in [sp_start, sp_stop, sp_restart, sp_status, sp_exec]:
|
|
214
214
|
sp.add_argument(
|
|
215
|
+
"-c",
|
|
215
216
|
"--config",
|
|
217
|
+
metavar="PATH",
|
|
216
218
|
help="Path to config file or the directory containing it (default: search from current directory upwards)",
|
|
217
219
|
)
|
|
218
220
|
|
|
@@ -221,7 +223,10 @@ def main():
|
|
|
221
223
|
if args.command in ["start", "restart"]:
|
|
222
224
|
sp = sp_start if args.command == "start" else sp_restart
|
|
223
225
|
try:
|
|
224
|
-
tasks, task_cfgs = get_task_config(
|
|
226
|
+
tasks, task_cfgs, cfg_path = get_task_config(
|
|
227
|
+
args.task, args.config, args.all
|
|
228
|
+
)
|
|
229
|
+
os.chdir(cfg_path.parent)
|
|
225
230
|
except Exception as e:
|
|
226
231
|
sp.error(str(e))
|
|
227
232
|
|
|
@@ -245,19 +250,34 @@ def main():
|
|
|
245
250
|
task_cfg.rotate_log_path or ROTATE_LOG_PATH_TEMPLATE.format(task=task)
|
|
246
251
|
)
|
|
247
252
|
if args.command == "start":
|
|
248
|
-
|
|
253
|
+
sp.exit(start(task_cfgs))
|
|
249
254
|
else:
|
|
250
|
-
|
|
255
|
+
sp.exit(restart(task_cfgs))
|
|
251
256
|
elif args.command == "exec":
|
|
252
257
|
try:
|
|
253
|
-
_, task_cfgs = 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)
|
|
254
260
|
except Exception as e:
|
|
255
261
|
sp_exec.error(str(e))
|
|
256
|
-
|
|
262
|
+
sp_exec.exit(execute(task_cfgs[0]))
|
|
257
263
|
elif args.command in ["stop", "status"]:
|
|
258
264
|
sp = sp_stop if args.command == "stop" else sp_status
|
|
259
265
|
meta_paths = []
|
|
260
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
|
+
|
|
261
281
|
# Collect meta paths from --all
|
|
262
282
|
if args.all:
|
|
263
283
|
meta_paths.extend(get_meta_paths(DEFAULT_META_DIR))
|
|
@@ -267,14 +287,14 @@ def main():
|
|
|
267
287
|
meta_paths.append(args.meta_file)
|
|
268
288
|
|
|
269
289
|
# Collect meta paths from task names
|
|
270
|
-
if len(
|
|
271
|
-
tasks = args.task
|
|
290
|
+
if len(tasks) > 0:
|
|
272
291
|
meta_paths.extend([META_PATH_TEMPLATE.format(task=task) for task in tasks])
|
|
273
292
|
|
|
274
293
|
# If no meta paths collected, use default task
|
|
275
294
|
if len(meta_paths) == 0:
|
|
276
295
|
try:
|
|
277
|
-
tasks, _ = get_task_config(args.task, args.config)
|
|
296
|
+
tasks, _, cfg_path = get_task_config(args.task, args.config)
|
|
297
|
+
os.chdir(cfg_path.parent)
|
|
278
298
|
except Exception as e:
|
|
279
299
|
sp.error(str(e))
|
|
280
300
|
meta_paths.extend([META_PATH_TEMPLATE.format(task=task) for task in tasks])
|
|
@@ -283,13 +303,18 @@ def main():
|
|
|
283
303
|
unique_meta_paths = sorted(set(Path(p).resolve() for p in meta_paths))
|
|
284
304
|
|
|
285
305
|
if args.command == "stop":
|
|
286
|
-
|
|
306
|
+
sp.exit(stop(unique_meta_paths))
|
|
287
307
|
else:
|
|
288
|
-
|
|
308
|
+
sp.exit(status(unique_meta_paths))
|
|
289
309
|
elif args.command == "list":
|
|
290
310
|
dir = args.dir or DEFAULT_META_DIR
|
|
291
|
-
|
|
311
|
+
sp_list.exit(list_processes(dir, args.full))
|
|
292
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.")
|
|
293
318
|
if not args.name:
|
|
294
319
|
sp_run.error("Please provide a non-empty name for the task.")
|
|
295
320
|
elif check_name_in_config(args.name):
|
|
@@ -299,7 +324,7 @@ def main():
|
|
|
299
324
|
|
|
300
325
|
task_cfg = DmonTaskConfig(
|
|
301
326
|
task=args.name,
|
|
302
|
-
cmd=shlex.join(
|
|
327
|
+
cmd=shlex.join(command_list) if args.shell else command_list,
|
|
303
328
|
cwd=args.cwd,
|
|
304
329
|
meta_path=args.meta_file or META_PATH_TEMPLATE.format(task=args.name),
|
|
305
330
|
log_path=args.log_file or LOG_PATH_TEMPLATE.format(task=args.name),
|
|
@@ -307,10 +332,10 @@ def main():
|
|
|
307
332
|
rotate_log_path=args.rotate_log_path
|
|
308
333
|
or ROTATE_LOG_PATH_TEMPLATE.format(task=args.name),
|
|
309
334
|
)
|
|
310
|
-
|
|
335
|
+
sp_run.exit(start([task_cfg]))
|
|
311
336
|
else:
|
|
312
337
|
parser.print_help()
|
|
313
|
-
|
|
338
|
+
parser.exit(1)
|
|
314
339
|
|
|
315
340
|
|
|
316
341
|
if __name__ == "__main__":
|
|
@@ -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
|
|
|
@@ -130,6 +134,14 @@ def validate_task(task, name: str) -> DmonTaskConfig:
|
|
|
130
134
|
)
|
|
131
135
|
ret.log_max_size = task["log_max_size"]
|
|
132
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
|
+
|
|
133
145
|
if "rotate_log_path" in task:
|
|
134
146
|
if not isinstance(task["rotate_log_path"], str):
|
|
135
147
|
raise TypeError(
|
|
@@ -147,6 +159,14 @@ def validate_task(task, name: str) -> DmonTaskConfig:
|
|
|
147
159
|
)
|
|
148
160
|
ret.rotate_log_max_size = task["rotate_log_max_size"]
|
|
149
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
|
+
|
|
150
170
|
if "meta_path" in task:
|
|
151
171
|
if not isinstance(task["meta_path"], str):
|
|
152
172
|
raise TypeError(f"Task '{name}' 'meta_path' field must be a string")
|
|
@@ -160,7 +180,7 @@ def validate_task(task, name: str) -> DmonTaskConfig:
|
|
|
160
180
|
|
|
161
181
|
def get_task_config(
|
|
162
182
|
names: Union[Sequence[str], str, None], cfg_path: Optional[str], all: bool = False
|
|
163
|
-
) -> Tuple[Sequence[str], List[DmonTaskConfig]]:
|
|
183
|
+
) -> Tuple[Sequence[str], List[DmonTaskConfig], Path]:
|
|
164
184
|
"""
|
|
165
185
|
Get the validated task configurations for the given task names.
|
|
166
186
|
If 'all' is True, return all tasks.
|
|
@@ -195,6 +215,7 @@ def get_task_config(
|
|
|
195
215
|
else:
|
|
196
216
|
raise ValueError(f"Multiple tasks found in {path}; please specify one.")
|
|
197
217
|
|
|
218
|
+
ret_names = []
|
|
198
219
|
ret_tasks = []
|
|
199
220
|
for name in names:
|
|
200
221
|
name = name.lower()
|
|
@@ -202,8 +223,9 @@ def get_task_config(
|
|
|
202
223
|
raise ValueError(f"Task '{name}' not found in {path}")
|
|
203
224
|
|
|
204
225
|
task = validate_task(tasks[name], name)
|
|
226
|
+
ret_names.append(name)
|
|
205
227
|
ret_tasks.append(task)
|
|
206
|
-
return
|
|
228
|
+
return ret_names, ret_tasks, path
|
|
207
229
|
|
|
208
230
|
|
|
209
231
|
def check_name_in_config(name: str) -> bool:
|
|
@@ -211,7 +233,10 @@ def check_name_in_config(name: str) -> bool:
|
|
|
211
233
|
Check if the given task name exists in the tasks.
|
|
212
234
|
Return True if found, False otherwise.
|
|
213
235
|
"""
|
|
214
|
-
|
|
236
|
+
try:
|
|
237
|
+
cfg, _ = load_config()
|
|
238
|
+
except FileNotFoundError:
|
|
239
|
+
return False
|
|
215
240
|
tasks = cfg.get("tasks", {})
|
|
216
241
|
|
|
217
242
|
if not isinstance(tasks, dict):
|