tasktree 0.0.5__py3-none-any.whl → 0.0.6__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/cli.py +66 -31
- tasktree/parser.py +44 -7
- {tasktree-0.0.5.dist-info → tasktree-0.0.6.dist-info}/METADATA +32 -1
- {tasktree-0.0.5.dist-info → tasktree-0.0.6.dist-info}/RECORD +6 -6
- {tasktree-0.0.5.dist-info → tasktree-0.0.6.dist-info}/WHEEL +0 -0
- {tasktree-0.0.5.dist-info → tasktree-0.0.6.dist-info}/entry_points.txt +0 -0
tasktree/cli.py
CHANGED
|
@@ -26,11 +26,11 @@ app = typer.Typer(
|
|
|
26
26
|
console = Console()
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def _list_tasks():
|
|
29
|
+
def _list_tasks(tasks_file: Optional[str] = None):
|
|
30
30
|
"""List all available tasks with descriptions."""
|
|
31
|
-
recipe = _get_recipe()
|
|
31
|
+
recipe = _get_recipe(tasks_file)
|
|
32
32
|
if recipe is None:
|
|
33
|
-
console.print("[red]No recipe file found (tasktree.yaml
|
|
33
|
+
console.print("[red]No recipe file found (tasktree.yaml, tasktree.yml, tt.yaml, or *.tasks)[/red]")
|
|
34
34
|
raise typer.Exit(1)
|
|
35
35
|
|
|
36
36
|
table = Table(title="Available Tasks")
|
|
@@ -45,11 +45,11 @@ def _list_tasks():
|
|
|
45
45
|
console.print(table)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
def _show_task(task_name: str):
|
|
48
|
+
def _show_task(task_name: str, tasks_file: Optional[str] = None):
|
|
49
49
|
"""Show task definition with syntax highlighting."""
|
|
50
|
-
recipe = _get_recipe()
|
|
50
|
+
recipe = _get_recipe(tasks_file)
|
|
51
51
|
if recipe is None:
|
|
52
|
-
console.print("[red]No recipe file found (tasktree.yaml
|
|
52
|
+
console.print("[red]No recipe file found (tasktree.yaml, tasktree.yml, tt.yaml, or *.tasks)[/red]")
|
|
53
53
|
raise typer.Exit(1)
|
|
54
54
|
|
|
55
55
|
task = recipe.get_task(task_name)
|
|
@@ -79,17 +79,26 @@ def _show_task(task_name: str):
|
|
|
79
79
|
task_dict = task_yaml[task_name]
|
|
80
80
|
task_yaml[task_name] = {k: v for k, v in task_dict.items() if v}
|
|
81
81
|
|
|
82
|
+
# Configure YAML dumper to use literal block style for multiline strings
|
|
83
|
+
def literal_presenter(dumper, data):
|
|
84
|
+
"""Use literal block style (|) for strings containing newlines."""
|
|
85
|
+
if '\n' in data:
|
|
86
|
+
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|')
|
|
87
|
+
return dumper.represent_scalar('tag:yaml.org,2002:str', data)
|
|
88
|
+
|
|
89
|
+
yaml.add_representer(str, literal_presenter)
|
|
90
|
+
|
|
82
91
|
# Format and highlight using Rich
|
|
83
92
|
yaml_str = yaml.dump(task_yaml, default_flow_style=False, sort_keys=False)
|
|
84
93
|
syntax = Syntax(yaml_str, "yaml", theme="ansi_light", line_numbers=False)
|
|
85
94
|
console.print(syntax)
|
|
86
95
|
|
|
87
96
|
|
|
88
|
-
def _show_tree(task_name: str):
|
|
97
|
+
def _show_tree(task_name: str, tasks_file: Optional[str] = None):
|
|
89
98
|
"""Show dependency tree structure."""
|
|
90
|
-
recipe = _get_recipe()
|
|
99
|
+
recipe = _get_recipe(tasks_file)
|
|
91
100
|
if recipe is None:
|
|
92
|
-
console.print("[red]No recipe file found (tasktree.yaml
|
|
101
|
+
console.print("[red]No recipe file found (tasktree.yaml, tasktree.yml, tt.yaml, or *.tasks)[/red]")
|
|
93
102
|
raise typer.Exit(1)
|
|
94
103
|
|
|
95
104
|
task = recipe.get_task(task_name)
|
|
@@ -169,6 +178,7 @@ def main(
|
|
|
169
178
|
list_opt: Optional[bool] = typer.Option(None, "--list", "-l", help="List all available tasks"),
|
|
170
179
|
show: Optional[str] = typer.Option(None, "--show", "-s", help="Show task definition"),
|
|
171
180
|
tree: Optional[str] = typer.Option(None, "--tree", "-t", help="Show dependency tree"),
|
|
181
|
+
tasks_file: Optional[str] = typer.Option(None, "--tasks", "-T", help="Path to recipe file (tasktree.yaml, *.tasks, etc.)"),
|
|
172
182
|
init: Optional[bool] = typer.Option(
|
|
173
183
|
None, "--init", "-i", help="Create a blank tasktree.yaml"
|
|
174
184
|
),
|
|
@@ -208,15 +218,15 @@ def main(
|
|
|
208
218
|
"""
|
|
209
219
|
|
|
210
220
|
if list_opt:
|
|
211
|
-
_list_tasks()
|
|
221
|
+
_list_tasks(tasks_file)
|
|
212
222
|
raise typer.Exit()
|
|
213
223
|
|
|
214
224
|
if show:
|
|
215
|
-
_show_task(show)
|
|
225
|
+
_show_task(show, tasks_file)
|
|
216
226
|
raise typer.Exit()
|
|
217
227
|
|
|
218
228
|
if tree:
|
|
219
|
-
_show_tree(tree)
|
|
229
|
+
_show_tree(tree, tasks_file)
|
|
220
230
|
raise typer.Exit()
|
|
221
231
|
|
|
222
232
|
if init:
|
|
@@ -224,17 +234,17 @@ def main(
|
|
|
224
234
|
raise typer.Exit()
|
|
225
235
|
|
|
226
236
|
if clean or clean_state or reset:
|
|
227
|
-
_clean_state()
|
|
237
|
+
_clean_state(tasks_file)
|
|
228
238
|
raise typer.Exit()
|
|
229
239
|
|
|
230
240
|
if task_args:
|
|
231
241
|
# --only implies --force
|
|
232
242
|
force_execution = force or only or False
|
|
233
|
-
_execute_dynamic_task(task_args, force=force_execution, only=only or False, env=env)
|
|
243
|
+
_execute_dynamic_task(task_args, force=force_execution, only=only or False, env=env, tasks_file=tasks_file)
|
|
234
244
|
else:
|
|
235
|
-
recipe = _get_recipe()
|
|
245
|
+
recipe = _get_recipe(tasks_file)
|
|
236
246
|
if recipe is None:
|
|
237
|
-
console.print("[red]No recipe file found (tasktree.yaml
|
|
247
|
+
console.print("[red]No recipe file found (tasktree.yaml, tasktree.yml, tt.yaml, or *.tasks)[/red]")
|
|
238
248
|
console.print("Run [cyan]tt --init[/cyan] to create a blank recipe file")
|
|
239
249
|
raise typer.Exit(1)
|
|
240
250
|
|
|
@@ -245,13 +255,19 @@ def main(
|
|
|
245
255
|
console.print("Use [cyan]tt <task-name>[/cyan] to run a task")
|
|
246
256
|
|
|
247
257
|
|
|
248
|
-
def _clean_state() -> None:
|
|
258
|
+
def _clean_state(tasks_file: Optional[str] = None) -> None:
|
|
249
259
|
"""Remove the .tasktree-state file to reset task execution state."""
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
260
|
+
if tasks_file:
|
|
261
|
+
recipe_path = Path(tasks_file)
|
|
262
|
+
if not recipe_path.exists():
|
|
263
|
+
console.print(f"[red]Recipe file not found: {tasks_file}[/red]")
|
|
264
|
+
raise typer.Exit(1)
|
|
265
|
+
else:
|
|
266
|
+
recipe_path = find_recipe_file()
|
|
267
|
+
if recipe_path is None:
|
|
268
|
+
console.print("[yellow]No recipe file found[/yellow]")
|
|
269
|
+
console.print("State file location depends on recipe file location")
|
|
270
|
+
raise typer.Exit(1)
|
|
255
271
|
|
|
256
272
|
project_root = recipe_path.parent
|
|
257
273
|
state_path = project_root / ".tasktree-state"
|
|
@@ -264,29 +280,48 @@ def _clean_state() -> None:
|
|
|
264
280
|
console.print(f"[yellow]No state file found at {state_path}[/yellow]")
|
|
265
281
|
|
|
266
282
|
|
|
267
|
-
def _get_recipe() -> Optional[Recipe]:
|
|
268
|
-
"""Get parsed recipe or None if not found.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
283
|
+
def _get_recipe(recipe_file: Optional[str] = None) -> Optional[Recipe]:
|
|
284
|
+
"""Get parsed recipe or None if not found.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
recipe_file: Optional path to recipe file. If not provided, searches for recipe file.
|
|
288
|
+
"""
|
|
289
|
+
if recipe_file:
|
|
290
|
+
recipe_path = Path(recipe_file)
|
|
291
|
+
if not recipe_path.exists():
|
|
292
|
+
console.print(f"[red]Recipe file not found: {recipe_file}[/red]")
|
|
293
|
+
raise typer.Exit(1)
|
|
294
|
+
# When explicitly specified, project root is current working directory
|
|
295
|
+
project_root = Path.cwd()
|
|
296
|
+
else:
|
|
297
|
+
try:
|
|
298
|
+
recipe_path = find_recipe_file()
|
|
299
|
+
if recipe_path is None:
|
|
300
|
+
return None
|
|
301
|
+
except ValueError as e:
|
|
302
|
+
# Multiple recipe files found
|
|
303
|
+
console.print(f"[red]{e}[/red]")
|
|
304
|
+
raise typer.Exit(1)
|
|
305
|
+
# When auto-discovered, project root is recipe file's parent
|
|
306
|
+
project_root = None
|
|
272
307
|
|
|
273
308
|
try:
|
|
274
|
-
return parse_recipe(recipe_path)
|
|
309
|
+
return parse_recipe(recipe_path, project_root)
|
|
275
310
|
except Exception as e:
|
|
276
311
|
console.print(f"[red]Error parsing recipe: {e}[/red]")
|
|
277
312
|
raise typer.Exit(1)
|
|
278
313
|
|
|
279
314
|
|
|
280
|
-
def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = False, env: Optional[str] = None) -> None:
|
|
315
|
+
def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = False, env: Optional[str] = None, tasks_file: Optional[str] = None) -> None:
|
|
281
316
|
if not args:
|
|
282
317
|
return
|
|
283
318
|
|
|
284
319
|
task_name = args[0]
|
|
285
320
|
task_args = args[1:]
|
|
286
321
|
|
|
287
|
-
recipe = _get_recipe()
|
|
322
|
+
recipe = _get_recipe(tasks_file)
|
|
288
323
|
if recipe is None:
|
|
289
|
-
console.print("[red]No recipe file found (tasktree.yaml
|
|
324
|
+
console.print("[red]No recipe file found (tasktree.yaml, tasktree.yml, tt.yaml, or *.tasks)[/red]")
|
|
290
325
|
raise typer.Exit(1)
|
|
291
326
|
|
|
292
327
|
# Apply global environment override if provided
|
tasktree/parser.py
CHANGED
|
@@ -94,13 +94,25 @@ class Recipe:
|
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
def find_recipe_file(start_dir: Path | None = None) -> Path | None:
|
|
97
|
-
"""Find recipe file
|
|
97
|
+
"""Find recipe file in current or parent directories.
|
|
98
|
+
|
|
99
|
+
Looks for recipe files matching these patterns (in order of preference):
|
|
100
|
+
- tasktree.yaml
|
|
101
|
+
- tasktree.yml
|
|
102
|
+
- tt.yaml
|
|
103
|
+
- *.tasks
|
|
104
|
+
|
|
105
|
+
If multiple recipe files are found in the same directory, raises ValueError
|
|
106
|
+
with instructions to use --tasks option.
|
|
98
107
|
|
|
99
108
|
Args:
|
|
100
109
|
start_dir: Directory to start searching from (defaults to cwd)
|
|
101
110
|
|
|
102
111
|
Returns:
|
|
103
112
|
Path to recipe file if found, None otherwise
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ValueError: If multiple recipe files found in the same directory
|
|
104
116
|
"""
|
|
105
117
|
if start_dir is None:
|
|
106
118
|
start_dir = Path.cwd()
|
|
@@ -109,10 +121,30 @@ def find_recipe_file(start_dir: Path | None = None) -> Path | None:
|
|
|
109
121
|
|
|
110
122
|
# Search up the directory tree
|
|
111
123
|
while True:
|
|
112
|
-
|
|
124
|
+
candidates = []
|
|
125
|
+
|
|
126
|
+
# Check for exact filenames first
|
|
127
|
+
for filename in ["tasktree.yaml", "tasktree.yml", "tt.yaml"]:
|
|
113
128
|
recipe_path = current / filename
|
|
114
129
|
if recipe_path.exists():
|
|
115
|
-
|
|
130
|
+
candidates.append(recipe_path)
|
|
131
|
+
|
|
132
|
+
# Check for *.tasks files
|
|
133
|
+
for tasks_file in current.glob("*.tasks"):
|
|
134
|
+
if tasks_file.is_file():
|
|
135
|
+
candidates.append(tasks_file)
|
|
136
|
+
|
|
137
|
+
if len(candidates) > 1:
|
|
138
|
+
# Multiple recipe files found - ambiguous
|
|
139
|
+
filenames = [c.name for c in candidates]
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Multiple recipe files found in {current}:\n"
|
|
142
|
+
f" {', '.join(filenames)}\n\n"
|
|
143
|
+
f"Please specify which file to use with --tasks (-T):\n"
|
|
144
|
+
f" tt --tasks {filenames[0]} <task-name>"
|
|
145
|
+
)
|
|
146
|
+
elif len(candidates) == 1:
|
|
147
|
+
return candidates[0]
|
|
116
148
|
|
|
117
149
|
# Move to parent directory
|
|
118
150
|
parent = current.parent
|
|
@@ -186,11 +218,13 @@ def _parse_file_with_env(
|
|
|
186
218
|
return tasks, environments, default_env
|
|
187
219
|
|
|
188
220
|
|
|
189
|
-
def parse_recipe(recipe_path: Path) -> Recipe:
|
|
221
|
+
def parse_recipe(recipe_path: Path, project_root: Path | None = None) -> Recipe:
|
|
190
222
|
"""Parse a recipe file and handle imports recursively.
|
|
191
223
|
|
|
192
224
|
Args:
|
|
193
225
|
recipe_path: Path to the main recipe file
|
|
226
|
+
project_root: Optional project root directory. If not provided, uses recipe file's parent directory.
|
|
227
|
+
When using --tasks option, this should be the current working directory.
|
|
194
228
|
|
|
195
229
|
Returns:
|
|
196
230
|
Recipe object with all tasks (including recursively imported tasks)
|
|
@@ -204,7 +238,9 @@ def parse_recipe(recipe_path: Path) -> Recipe:
|
|
|
204
238
|
if not recipe_path.exists():
|
|
205
239
|
raise FileNotFoundError(f"Recipe file not found: {recipe_path}")
|
|
206
240
|
|
|
207
|
-
|
|
241
|
+
# Default project root to recipe file's parent if not specified
|
|
242
|
+
if project_root is None:
|
|
243
|
+
project_root = recipe_path.parent
|
|
208
244
|
|
|
209
245
|
# Parse main file - it will recursively handle all imports
|
|
210
246
|
tasks, environments, default_env = _parse_file_with_env(
|
|
@@ -263,8 +299,9 @@ def _parse_file(
|
|
|
263
299
|
tasks: dict[str, Task] = {}
|
|
264
300
|
file_dir = file_path.parent
|
|
265
301
|
|
|
266
|
-
# Default working directory is the
|
|
267
|
-
|
|
302
|
+
# Default working directory is the project root (where tt is invoked)
|
|
303
|
+
# NOT the directory where the tasks file is located
|
|
304
|
+
default_working_dir = "."
|
|
268
305
|
|
|
269
306
|
# Track local import namespaces for dependency rewriting
|
|
270
307
|
local_import_namespaces: set[str] = set()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tasktree
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: A task automation tool with incremental execution
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: click>=8.1.0
|
|
@@ -188,6 +188,37 @@ cd tasktree
|
|
|
188
188
|
pipx install .
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
+
## Editor Support
|
|
192
|
+
|
|
193
|
+
Task Tree includes a [JSON Schema](schema/tasktree-schema.json) that provides autocomplete, validation, and documentation in modern editors.
|
|
194
|
+
|
|
195
|
+
### VS Code
|
|
196
|
+
|
|
197
|
+
Install the [YAML extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml), then add to your workspace `.vscode/settings.json`:
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"yaml.schemas": {
|
|
202
|
+
"https://raw.githubusercontent.com/kevinchannon/tasktree/main/schema/tasktree-schema.json": [
|
|
203
|
+
"tasktree.yaml",
|
|
204
|
+
"tt.yaml"
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Or add a comment at the top of your `tasktree.yaml`:
|
|
211
|
+
|
|
212
|
+
```yaml
|
|
213
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/kevinchannon/tasktree/main/schema/tasktree-schema.json
|
|
214
|
+
|
|
215
|
+
tasks:
|
|
216
|
+
build:
|
|
217
|
+
cmd: cargo build
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
See [schema/README.md](schema/README.md) for IntelliJ/PyCharm and command-line validation.
|
|
221
|
+
|
|
191
222
|
## Quick Start
|
|
192
223
|
|
|
193
224
|
Create a `tasktree.yaml` (or `tt.yaml`) in your project:
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
tasktree/__init__.py,sha256=MVmdvKb3JdqLlo0x2_TPGMfgFC0HsDnP79HAzGnFnjI,1081
|
|
2
|
-
tasktree/cli.py,sha256=
|
|
2
|
+
tasktree/cli.py,sha256=0xusNitT1AtLgR3guUsupnHSXJ0_C749Dx7dfYCENJA,15233
|
|
3
3
|
tasktree/executor.py,sha256=_E37tShHuiOj0Mvx2GbS9y3GIozC3hpzAVhAjbvYJqg,18638
|
|
4
4
|
tasktree/graph.py,sha256=9ngfg93y7EkOIN_lUQa0u-JhnwiMN1UdQQvIFw8RYCE,4181
|
|
5
5
|
tasktree/hasher.py,sha256=puJey9wF_p37k_xqjhYr_6ICsbAfrTBWHec6MqKV4BU,814
|
|
6
|
-
tasktree/parser.py,sha256=
|
|
6
|
+
tasktree/parser.py,sha256=SzWn-V4KMgjxNZrN0ERApb5dd39LPJTfkA2Ih2nYWcs,15580
|
|
7
7
|
tasktree/state.py,sha256=rxKtS3SbsPtAuraHbN807RGWfoYYkQ3pe8CxUstwo2k,3535
|
|
8
8
|
tasktree/tasks.py,sha256=2QdQZtJAX2rSGbyXKG1z9VF_siz1DUzdvzCgPkykxtU,173
|
|
9
9
|
tasktree/types.py,sha256=w--sKjRTc8mGYkU5eAduqV86SolDqOYspAPuVKIuSQQ,3797
|
|
10
|
-
tasktree-0.0.
|
|
11
|
-
tasktree-0.0.
|
|
12
|
-
tasktree-0.0.
|
|
13
|
-
tasktree-0.0.
|
|
10
|
+
tasktree-0.0.6.dist-info/METADATA,sha256=GQZHcFVOIXfWmgPbb9pAt7zB0iH22hyGToc0YEwUv34,18287
|
|
11
|
+
tasktree-0.0.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
tasktree-0.0.6.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
|
|
13
|
+
tasktree-0.0.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|