tasktree 0.0.20__py3-none-any.whl → 0.0.22__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.
- tasktree/__init__.py +4 -1
- tasktree/cli.py +198 -60
- tasktree/docker.py +105 -64
- tasktree/executor.py +427 -310
- tasktree/graph.py +138 -82
- tasktree/hasher.py +81 -25
- tasktree/parser.py +554 -344
- tasktree/state.py +50 -22
- tasktree/substitution.py +188 -117
- tasktree/types.py +80 -25
- {tasktree-0.0.20.dist-info → tasktree-0.0.22.dist-info}/METADATA +147 -21
- tasktree-0.0.22.dist-info/RECORD +14 -0
- tasktree-0.0.20.dist-info/RECORD +0 -14
- {tasktree-0.0.20.dist-info → tasktree-0.0.22.dist-info}/WHEEL +0 -0
- {tasktree-0.0.20.dist-info → tasktree-0.0.22.dist-info}/entry_points.txt +0 -0
tasktree/__init__.py
CHANGED
tasktree/cli.py
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
"""Command-line interface for Task Tree.
|
|
2
|
+
|
|
3
|
+
Provides a Typer-based CLI with commands for listing, showing, executing,
|
|
4
|
+
and managing task definitions. Supports task execution with incremental builds,
|
|
5
|
+
dependency resolution, and rich terminal output via the Rich library.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
from __future__ import annotations
|
|
2
9
|
|
|
3
10
|
import os
|
|
@@ -14,7 +21,12 @@ from rich.tree import Tree
|
|
|
14
21
|
|
|
15
22
|
from tasktree import __version__
|
|
16
23
|
from tasktree.executor import Executor
|
|
17
|
-
from tasktree.graph import
|
|
24
|
+
from tasktree.graph import (
|
|
25
|
+
build_dependency_tree,
|
|
26
|
+
resolve_execution_order,
|
|
27
|
+
resolve_dependency_output_references,
|
|
28
|
+
resolve_self_references,
|
|
29
|
+
)
|
|
18
30
|
from tasktree.hasher import hash_task, hash_args
|
|
19
31
|
from tasktree.parser import Recipe, find_recipe_file, parse_arg_spec, parse_recipe
|
|
20
32
|
from tasktree.state import StateManager
|
|
@@ -29,10 +41,12 @@ console = Console()
|
|
|
29
41
|
|
|
30
42
|
|
|
31
43
|
def _supports_unicode() -> bool:
|
|
32
|
-
"""
|
|
44
|
+
"""
|
|
45
|
+
Check if the terminal supports Unicode characters.
|
|
33
46
|
|
|
34
47
|
Returns:
|
|
35
|
-
|
|
48
|
+
True if terminal supports UTF-8, False otherwise
|
|
49
|
+
@athena: 68f62a942a95
|
|
36
50
|
"""
|
|
37
51
|
# Hard stop: classic Windows console (conhost)
|
|
38
52
|
if os.name == "nt" and "WT_SESSION" not in os.environ:
|
|
@@ -51,37 +65,43 @@ def _supports_unicode() -> bool:
|
|
|
51
65
|
|
|
52
66
|
|
|
53
67
|
def get_action_success_string() -> str:
|
|
54
|
-
"""
|
|
68
|
+
"""
|
|
69
|
+
Get the appropriate success symbol based on terminal capabilities.
|
|
55
70
|
|
|
56
71
|
Returns:
|
|
57
|
-
|
|
72
|
+
Unicode tick symbol (✓) if terminal supports UTF-8, otherwise "[ OK ]"
|
|
73
|
+
@athena: 39d9966ee6c8
|
|
58
74
|
"""
|
|
59
75
|
return "✓" if _supports_unicode() else "[ OK ]"
|
|
60
76
|
|
|
61
77
|
|
|
62
78
|
def get_action_failure_string() -> str:
|
|
63
|
-
"""
|
|
79
|
+
"""
|
|
80
|
+
Get the appropriate failure symbol based on terminal capabilities.
|
|
64
81
|
|
|
65
82
|
Returns:
|
|
66
|
-
|
|
83
|
+
Unicode cross symbol (✗) if terminal supports UTF-8, otherwise "[ FAIL ]"
|
|
84
|
+
@athena: 5dd1111f8d74
|
|
67
85
|
"""
|
|
68
86
|
return "✗" if _supports_unicode() else "[ FAIL ]"
|
|
69
87
|
|
|
70
88
|
|
|
71
89
|
def _format_task_arguments(arg_specs: list[str | dict]) -> str:
|
|
72
|
-
"""
|
|
90
|
+
"""
|
|
91
|
+
Format task arguments for display in list output.
|
|
73
92
|
|
|
74
93
|
Args:
|
|
75
|
-
|
|
94
|
+
arg_specs: List of argument specifications from task definition (strings or dicts)
|
|
76
95
|
|
|
77
96
|
Returns:
|
|
78
|
-
|
|
97
|
+
Formatted string showing arguments with types and defaults
|
|
79
98
|
|
|
80
99
|
Examples:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
100
|
+
["mode", "target"] -> "mode:str target:str"
|
|
101
|
+
["mode=debug", "target=x86_64"] -> "mode:str [=debug] target:str [=x86_64]"
|
|
102
|
+
["port:int", "debug:bool=false"] -> "port:int debug:bool [=false]"
|
|
103
|
+
[{"timeout": {"type": "int", "default": 30}}] -> "timeout:int [=30]"
|
|
104
|
+
@athena: fc3d6da90aeb
|
|
85
105
|
"""
|
|
86
106
|
if not arg_specs:
|
|
87
107
|
return ""
|
|
@@ -104,10 +124,15 @@ def _format_task_arguments(arg_specs: list[str | dict]) -> str:
|
|
|
104
124
|
|
|
105
125
|
|
|
106
126
|
def _list_tasks(tasks_file: Optional[str] = None):
|
|
107
|
-
"""
|
|
127
|
+
"""
|
|
128
|
+
List all available tasks with descriptions.
|
|
129
|
+
@athena: 778f231737a1
|
|
130
|
+
"""
|
|
108
131
|
recipe = _get_recipe(tasks_file)
|
|
109
132
|
if recipe is None:
|
|
110
|
-
console.print(
|
|
133
|
+
console.print(
|
|
134
|
+
"[red]No recipe file found (tasktree.yaml, tasktree.yml, tt.yaml, or *.tasks)[/red]"
|
|
135
|
+
)
|
|
111
136
|
raise typer.Exit(1)
|
|
112
137
|
|
|
113
138
|
# Calculate maximum task name length for fixed-width column (only visible tasks)
|
|
@@ -116,13 +141,17 @@ def _list_tasks(tasks_file: Optional[str] = None):
|
|
|
116
141
|
task = recipe.get_task(name)
|
|
117
142
|
if task and not task.private:
|
|
118
143
|
visible_task_names.append(name)
|
|
119
|
-
max_task_name_len =
|
|
144
|
+
max_task_name_len = (
|
|
145
|
+
max(len(name) for name in visible_task_names) if visible_task_names else 0
|
|
146
|
+
)
|
|
120
147
|
|
|
121
148
|
# Create borderless table with three columns
|
|
122
149
|
table = Table(show_edge=False, show_header=False, box=None, padding=(0, 2))
|
|
123
150
|
|
|
124
|
-
# Command column: fixed width to accommodate longest task name
|
|
125
|
-
table.add_column(
|
|
151
|
+
# Command column: fixed width to accommodate the longest task name
|
|
152
|
+
table.add_column(
|
|
153
|
+
"Command", style="bold cyan", no_wrap=True, width=max_task_name_len
|
|
154
|
+
)
|
|
126
155
|
|
|
127
156
|
# Arguments column: allow wrapping with sensible max width
|
|
128
157
|
table.add_column("Arguments", style="white", max_width=60)
|
|
@@ -144,11 +173,16 @@ def _list_tasks(tasks_file: Optional[str] = None):
|
|
|
144
173
|
|
|
145
174
|
|
|
146
175
|
def _show_task(task_name: str, tasks_file: Optional[str] = None):
|
|
147
|
-
"""
|
|
176
|
+
"""
|
|
177
|
+
Show task definition with syntax highlighting.
|
|
178
|
+
@athena: 79ae3e330662
|
|
179
|
+
"""
|
|
148
180
|
# Pass task_name as root_task for lazy variable evaluation
|
|
149
181
|
recipe = _get_recipe(tasks_file, root_task=task_name)
|
|
150
182
|
if recipe is None:
|
|
151
|
-
console.print(
|
|
183
|
+
console.print(
|
|
184
|
+
"[red]No recipe file found (tasktree.yaml, tasktree.yml, tt.yaml, or *.tasks)[/red]"
|
|
185
|
+
)
|
|
152
186
|
raise typer.Exit(1)
|
|
153
187
|
|
|
154
188
|
task = recipe.get_task(task_name)
|
|
@@ -181,9 +215,9 @@ def _show_task(task_name: str, tasks_file: Optional[str] = None):
|
|
|
181
215
|
# Configure YAML dumper to use literal block style for multiline strings
|
|
182
216
|
def literal_presenter(dumper, data):
|
|
183
217
|
"""Use literal block style (|) for strings containing newlines."""
|
|
184
|
-
if
|
|
185
|
-
return dumper.represent_scalar(
|
|
186
|
-
return dumper.represent_scalar(
|
|
218
|
+
if "\n" in data:
|
|
219
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
|
|
220
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", data)
|
|
187
221
|
|
|
188
222
|
yaml.add_representer(str, literal_presenter)
|
|
189
223
|
|
|
@@ -194,11 +228,16 @@ def _show_task(task_name: str, tasks_file: Optional[str] = None):
|
|
|
194
228
|
|
|
195
229
|
|
|
196
230
|
def _show_tree(task_name: str, tasks_file: Optional[str] = None):
|
|
197
|
-
"""
|
|
231
|
+
"""
|
|
232
|
+
Show dependency tree structure.
|
|
233
|
+
@athena: a906cef99324
|
|
234
|
+
"""
|
|
198
235
|
# Pass task_name as root_task for lazy variable evaluation
|
|
199
236
|
recipe = _get_recipe(tasks_file, root_task=task_name)
|
|
200
237
|
if recipe is None:
|
|
201
|
-
console.print(
|
|
238
|
+
console.print(
|
|
239
|
+
"[red]No recipe file found (tasktree.yaml, tasktree.yml, tt.yaml, or *.tasks)[/red]"
|
|
240
|
+
)
|
|
202
241
|
raise typer.Exit(1)
|
|
203
242
|
|
|
204
243
|
task = recipe.get_task(task_name)
|
|
@@ -219,7 +258,10 @@ def _show_tree(task_name: str, tasks_file: Optional[str] = None):
|
|
|
219
258
|
|
|
220
259
|
|
|
221
260
|
def _init_recipe():
|
|
222
|
-
"""
|
|
261
|
+
"""
|
|
262
|
+
Create a blank recipe file with commented examples.
|
|
263
|
+
@athena: 189726c9b6c0
|
|
264
|
+
"""
|
|
223
265
|
recipe_path = Path("tasktree.yaml")
|
|
224
266
|
if recipe_path.exists():
|
|
225
267
|
console.print("[red]tasktree.yaml already exists[/red]")
|
|
@@ -260,7 +302,10 @@ tasks:
|
|
|
260
302
|
|
|
261
303
|
|
|
262
304
|
def _version_callback(value: bool):
|
|
263
|
-
"""
|
|
305
|
+
"""
|
|
306
|
+
Show version and exit.
|
|
307
|
+
@athena: abaed96ac23b
|
|
308
|
+
"""
|
|
264
309
|
if value:
|
|
265
310
|
console.print(f"task-tree version {__version__}")
|
|
266
311
|
raise typer.Exit()
|
|
@@ -277,10 +322,18 @@ def main(
|
|
|
277
322
|
is_eager=True,
|
|
278
323
|
help="Show version and exit",
|
|
279
324
|
),
|
|
280
|
-
list_opt: Optional[bool] = typer.Option(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
325
|
+
list_opt: Optional[bool] = typer.Option(
|
|
326
|
+
None, "--list", "-l", help="List all available tasks"
|
|
327
|
+
),
|
|
328
|
+
show: Optional[str] = typer.Option(
|
|
329
|
+
None, "--show", "-s", help="Show task definition"
|
|
330
|
+
),
|
|
331
|
+
tree: Optional[str] = typer.Option(
|
|
332
|
+
None, "--tree", "-t", help="Show dependency tree"
|
|
333
|
+
),
|
|
334
|
+
tasks_file: Optional[str] = typer.Option(
|
|
335
|
+
None, "--tasks", "-T", help="Path to recipe file (tasktree.yaml, *.tasks, etc.)"
|
|
336
|
+
),
|
|
284
337
|
init: Optional[bool] = typer.Option(
|
|
285
338
|
None, "--init", "-i", help="Create a blank tasktree.yaml"
|
|
286
339
|
),
|
|
@@ -297,7 +350,10 @@ def main(
|
|
|
297
350
|
None, "--force", "-f", help="Force re-run all tasks (ignore freshness)"
|
|
298
351
|
),
|
|
299
352
|
only: Optional[bool] = typer.Option(
|
|
300
|
-
None,
|
|
353
|
+
None,
|
|
354
|
+
"--only",
|
|
355
|
+
"-o",
|
|
356
|
+
help="Run only the specified task, skip dependencies (implies --force)",
|
|
301
357
|
),
|
|
302
358
|
env: Optional[str] = typer.Option(
|
|
303
359
|
None, "--env", "-e", help="Override environment for all tasks"
|
|
@@ -306,17 +362,19 @@ def main(
|
|
|
306
362
|
None, help="Task name and arguments"
|
|
307
363
|
),
|
|
308
364
|
):
|
|
309
|
-
"""
|
|
365
|
+
"""
|
|
366
|
+
Task Tree - A task automation tool with incremental execution.
|
|
310
367
|
|
|
311
368
|
Run tasks defined in tasktree.yaml with dependency tracking
|
|
312
369
|
and incremental execution.
|
|
313
370
|
|
|
314
371
|
Examples:
|
|
315
372
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
373
|
+
tt build # Run the 'build' task
|
|
374
|
+
tt deploy prod region=us-1 # Run 'deploy' with arguments
|
|
375
|
+
tt --list # List all tasks
|
|
376
|
+
tt --tree test # Show dependency tree for 'test'
|
|
377
|
+
@athena: f76c75c12d10
|
|
320
378
|
"""
|
|
321
379
|
|
|
322
380
|
if list_opt:
|
|
@@ -342,11 +400,19 @@ def main(
|
|
|
342
400
|
if task_args:
|
|
343
401
|
# --only implies --force
|
|
344
402
|
force_execution = force or only or False
|
|
345
|
-
_execute_dynamic_task(
|
|
403
|
+
_execute_dynamic_task(
|
|
404
|
+
task_args,
|
|
405
|
+
force=force_execution,
|
|
406
|
+
only=only or False,
|
|
407
|
+
env=env,
|
|
408
|
+
tasks_file=tasks_file,
|
|
409
|
+
)
|
|
346
410
|
else:
|
|
347
411
|
recipe = _get_recipe(tasks_file)
|
|
348
412
|
if recipe is None:
|
|
349
|
-
console.print(
|
|
413
|
+
console.print(
|
|
414
|
+
"[red]No recipe file found (tasktree.yaml, tasktree.yml, tt.yaml, or *.tasks)[/red]"
|
|
415
|
+
)
|
|
350
416
|
console.print("Run [cyan]tt --init[/cyan] to create a blank recipe file")
|
|
351
417
|
raise typer.Exit(1)
|
|
352
418
|
|
|
@@ -360,7 +426,10 @@ def main(
|
|
|
360
426
|
|
|
361
427
|
|
|
362
428
|
def _clean_state(tasks_file: Optional[str] = None) -> None:
|
|
363
|
-
"""
|
|
429
|
+
"""
|
|
430
|
+
Remove the .tasktree-state file to reset task execution state.
|
|
431
|
+
@athena: a0ddf4b333d4
|
|
432
|
+
"""
|
|
364
433
|
if tasks_file:
|
|
365
434
|
recipe_path = Path(tasks_file)
|
|
366
435
|
if not recipe_path.exists():
|
|
@@ -378,19 +447,25 @@ def _clean_state(tasks_file: Optional[str] = None) -> None:
|
|
|
378
447
|
|
|
379
448
|
if state_path.exists():
|
|
380
449
|
state_path.unlink()
|
|
381
|
-
console.print(
|
|
450
|
+
console.print(
|
|
451
|
+
f"[green]{get_action_success_string()} Removed {state_path}[/green]"
|
|
452
|
+
)
|
|
382
453
|
console.print("All tasks will run fresh on next execution")
|
|
383
454
|
else:
|
|
384
455
|
console.print(f"[yellow]No state file found at {state_path}[/yellow]")
|
|
385
456
|
|
|
386
457
|
|
|
387
|
-
def _get_recipe(
|
|
388
|
-
|
|
458
|
+
def _get_recipe(
|
|
459
|
+
recipe_file: Optional[str] = None, root_task: Optional[str] = None
|
|
460
|
+
) -> Optional[Recipe]:
|
|
461
|
+
"""
|
|
462
|
+
Get parsed recipe or None if not found.
|
|
389
463
|
|
|
390
464
|
Args:
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
465
|
+
recipe_file: Optional path to recipe file. If not provided, searches for recipe file.
|
|
466
|
+
root_task: Optional root task for lazy variable evaluation. If provided, only variables
|
|
467
|
+
reachable from this task will be evaluated (performance optimization).
|
|
468
|
+
@athena: 0ee00c67df25
|
|
394
469
|
"""
|
|
395
470
|
if recipe_file:
|
|
396
471
|
recipe_path = Path(recipe_file)
|
|
@@ -418,7 +493,25 @@ def _get_recipe(recipe_file: Optional[str] = None, root_task: Optional[str] = No
|
|
|
418
493
|
raise typer.Exit(1)
|
|
419
494
|
|
|
420
495
|
|
|
421
|
-
def _execute_dynamic_task(
|
|
496
|
+
def _execute_dynamic_task(
|
|
497
|
+
args: list[str],
|
|
498
|
+
force: bool = False,
|
|
499
|
+
only: bool = False,
|
|
500
|
+
env: Optional[str] = None,
|
|
501
|
+
tasks_file: Optional[str] = None,
|
|
502
|
+
) -> None:
|
|
503
|
+
"""
|
|
504
|
+
Execute a task with its dependencies and handle argument parsing.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
args: Task name followed by optional task arguments
|
|
508
|
+
force: Force re-execution even if task is up-to-date
|
|
509
|
+
only: Execute only the specified task, skip dependencies
|
|
510
|
+
env: Override environment for task execution
|
|
511
|
+
tasks_file: Path to recipe file (optional)
|
|
512
|
+
|
|
513
|
+
@athena: 207f7635a60d
|
|
514
|
+
"""
|
|
422
515
|
if not args:
|
|
423
516
|
return
|
|
424
517
|
|
|
@@ -428,7 +521,9 @@ def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = Fal
|
|
|
428
521
|
# Pass task_name as root_task for lazy variable evaluation
|
|
429
522
|
recipe = _get_recipe(tasks_file, root_task=task_name)
|
|
430
523
|
if recipe is None:
|
|
431
|
-
console.print(
|
|
524
|
+
console.print(
|
|
525
|
+
"[red]No recipe file found (tasktree.yaml, tasktree.yml, tt.yaml, or *.tasks)[/red]"
|
|
526
|
+
)
|
|
432
527
|
raise typer.Exit(1)
|
|
433
528
|
|
|
434
529
|
# Apply global environment override if provided
|
|
@@ -488,7 +583,7 @@ def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = Fal
|
|
|
488
583
|
task.working_dir,
|
|
489
584
|
task.args,
|
|
490
585
|
executor._get_effective_env_name(task),
|
|
491
|
-
task.deps
|
|
586
|
+
task.deps,
|
|
492
587
|
)
|
|
493
588
|
|
|
494
589
|
# If task has arguments, append args hash to create unique cache key
|
|
@@ -504,16 +599,35 @@ def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = Fal
|
|
|
504
599
|
state.save()
|
|
505
600
|
try:
|
|
506
601
|
executor.execute_task(task_name, args_dict, force=force, only=only)
|
|
507
|
-
console.print(
|
|
602
|
+
console.print(
|
|
603
|
+
f"[green]{get_action_success_string()} Task '{task_name}' completed successfully[/green]"
|
|
604
|
+
)
|
|
508
605
|
except Exception as e:
|
|
509
|
-
console.print(
|
|
606
|
+
console.print(
|
|
607
|
+
f"[red]{get_action_failure_string()} Task '{task_name}' failed: {e}[/red]"
|
|
608
|
+
)
|
|
510
609
|
raise typer.Exit(1)
|
|
511
610
|
|
|
512
611
|
|
|
513
612
|
def _parse_task_args(arg_specs: list[str], arg_values: list[str]) -> dict[str, Any]:
|
|
613
|
+
"""
|
|
614
|
+
Parse and validate task arguments from command line values.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
arg_specs: Task argument specifications with types and defaults
|
|
618
|
+
arg_values: Raw argument values from command line (positional or named)
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
Dictionary mapping argument names to typed, validated values
|
|
622
|
+
|
|
623
|
+
Raises:
|
|
624
|
+
typer.Exit: If arguments are invalid, missing, or unknown
|
|
625
|
+
|
|
626
|
+
@athena: 2072a35f9d11
|
|
627
|
+
"""
|
|
514
628
|
if not arg_specs:
|
|
515
629
|
if arg_values:
|
|
516
|
-
console.print(
|
|
630
|
+
console.print("[red]Task does not accept arguments[/red]")
|
|
517
631
|
raise typer.Exit(1)
|
|
518
632
|
return {}
|
|
519
633
|
|
|
@@ -537,7 +651,7 @@ def _parse_task_args(arg_specs: list[str], arg_values: list[str]) -> dict[str, A
|
|
|
537
651
|
else:
|
|
538
652
|
# Positional argument
|
|
539
653
|
if positional_index >= len(parsed_specs):
|
|
540
|
-
console.print(
|
|
654
|
+
console.print("[red]Too many arguments[/red]")
|
|
541
655
|
raise typer.Exit(1)
|
|
542
656
|
spec = parsed_specs[positional_index]
|
|
543
657
|
arg_value = value_str
|
|
@@ -545,13 +659,19 @@ def _parse_task_args(arg_specs: list[str], arg_values: list[str]) -> dict[str, A
|
|
|
545
659
|
|
|
546
660
|
# Convert value to appropriate type (exported args are always strings)
|
|
547
661
|
try:
|
|
548
|
-
click_type = get_click_type(
|
|
662
|
+
click_type = get_click_type(
|
|
663
|
+
spec.arg_type, min_val=spec.min_val, max_val=spec.max_val
|
|
664
|
+
)
|
|
549
665
|
converted_value = click_type.convert(arg_value, None, None)
|
|
550
666
|
|
|
551
667
|
# Validate choices after type conversion
|
|
552
668
|
if spec.choices is not None and converted_value not in spec.choices:
|
|
553
|
-
console.print(
|
|
554
|
-
|
|
669
|
+
console.print(
|
|
670
|
+
f"[red]Invalid value for {spec.name}: {converted_value!r}[/red]"
|
|
671
|
+
)
|
|
672
|
+
console.print(
|
|
673
|
+
f"Valid choices: {', '.join(repr(c) for c in spec.choices)}"
|
|
674
|
+
)
|
|
555
675
|
raise typer.Exit(1)
|
|
556
676
|
|
|
557
677
|
args_dict[spec.name] = converted_value
|
|
@@ -566,10 +686,14 @@ def _parse_task_args(arg_specs: list[str], arg_values: list[str]) -> dict[str, A
|
|
|
566
686
|
if spec.name not in args_dict:
|
|
567
687
|
if spec.default is not None:
|
|
568
688
|
try:
|
|
569
|
-
click_type = get_click_type(
|
|
689
|
+
click_type = get_click_type(
|
|
690
|
+
spec.arg_type, min_val=spec.min_val, max_val=spec.max_val
|
|
691
|
+
)
|
|
570
692
|
args_dict[spec.name] = click_type.convert(spec.default, None, None)
|
|
571
693
|
except Exception as e:
|
|
572
|
-
console.print(
|
|
694
|
+
console.print(
|
|
695
|
+
f"[red]Invalid default value for {spec.name}: {e}[/red]"
|
|
696
|
+
)
|
|
573
697
|
raise typer.Exit(1)
|
|
574
698
|
else:
|
|
575
699
|
console.print(f"[red]Missing required argument: {spec.name}[/red]")
|
|
@@ -579,6 +703,17 @@ def _parse_task_args(arg_specs: list[str], arg_values: list[str]) -> dict[str, A
|
|
|
579
703
|
|
|
580
704
|
|
|
581
705
|
def _build_rich_tree(dep_tree: dict) -> Tree:
|
|
706
|
+
"""
|
|
707
|
+
Build a Rich Tree visualization from a dependency tree structure.
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
dep_tree: Nested dictionary representing task dependencies
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
Rich Tree object for terminal display
|
|
714
|
+
|
|
715
|
+
@athena: 62472c8ca729
|
|
716
|
+
"""
|
|
582
717
|
task_name = dep_tree["name"]
|
|
583
718
|
tree = Tree(task_name)
|
|
584
719
|
|
|
@@ -591,7 +726,10 @@ def _build_rich_tree(dep_tree: dict) -> Tree:
|
|
|
591
726
|
|
|
592
727
|
|
|
593
728
|
def cli():
|
|
594
|
-
"""
|
|
729
|
+
"""
|
|
730
|
+
Entry point for the CLI.
|
|
731
|
+
@athena: 3b3cccd1ff6f
|
|
732
|
+
"""
|
|
595
733
|
app()
|
|
596
734
|
|
|
597
735
|
|