tasktree 0.0.21__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/cli.py +91 -31
- tasktree/docker.py +24 -17
- tasktree/executor.py +263 -211
- tasktree/graph.py +15 -10
- tasktree/hasher.py +13 -6
- tasktree/parser.py +220 -121
- tasktree/state.py +7 -8
- tasktree/substitution.py +27 -15
- tasktree/types.py +29 -12
- {tasktree-0.0.21.dist-info → tasktree-0.0.22.dist-info}/METADATA +13 -15
- tasktree-0.0.22.dist-info/RECORD +14 -0
- tasktree-0.0.21.dist-info/RECORD +0 -14
- {tasktree-0.0.21.dist-info → tasktree-0.0.22.dist-info}/WHEEL +0 -0
- {tasktree-0.0.21.dist-info → tasktree-0.0.22.dist-info}/entry_points.txt +0 -0
tasktree/graph.py
CHANGED
|
@@ -4,7 +4,6 @@ Dependency resolution using topological sorting.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from graphlib import TopologicalSorter
|
|
7
|
-
from pathlib import Path
|
|
8
7
|
from typing import Any
|
|
9
8
|
|
|
10
9
|
from tasktree.hasher import hash_args
|
|
@@ -13,7 +12,6 @@ from tasktree.parser import (
|
|
|
13
12
|
Task,
|
|
14
13
|
DependencyInvocation,
|
|
15
14
|
parse_dependency_spec,
|
|
16
|
-
parse_arg_spec,
|
|
17
15
|
)
|
|
18
16
|
from tasktree.substitution import substitute_dependency_args
|
|
19
17
|
|
|
@@ -58,7 +56,7 @@ def resolve_dependency_invocation(
|
|
|
58
56
|
parent_task_name: str,
|
|
59
57
|
parent_args: dict[str, Any],
|
|
60
58
|
parent_exported_args: set[str],
|
|
61
|
-
recipe: Recipe
|
|
59
|
+
recipe: Recipe,
|
|
62
60
|
) -> DependencyInvocation:
|
|
63
61
|
"""
|
|
64
62
|
Parse dependency specification and substitute parent argument templates.
|
|
@@ -246,9 +244,7 @@ class TaskNode:
|
|
|
246
244
|
|
|
247
245
|
|
|
248
246
|
def resolve_execution_order(
|
|
249
|
-
recipe: Recipe,
|
|
250
|
-
target_task: str,
|
|
251
|
-
target_args: dict[str, Any] | None = None
|
|
247
|
+
recipe: Recipe, target_task: str, target_args: dict[str, Any] | None = None
|
|
252
248
|
) -> list[tuple[str, dict[str, Any]]]:
|
|
253
249
|
"""
|
|
254
250
|
Resolve execution order for a task and its dependencies.
|
|
@@ -273,7 +269,9 @@ def resolve_execution_order(
|
|
|
273
269
|
graph: dict[TaskNode, set[TaskNode]] = {}
|
|
274
270
|
|
|
275
271
|
# Track seen nodes to detect duplicates
|
|
276
|
-
seen_invocations: dict[
|
|
272
|
+
seen_invocations: dict[
|
|
273
|
+
tuple[str, str], TaskNode
|
|
274
|
+
] = {} # (task_name, args_hash) -> node
|
|
277
275
|
|
|
278
276
|
def get_or_create_node(task_name: str, args: dict[str, Any] | None) -> TaskNode:
|
|
279
277
|
"""Get existing node or create new one for this invocation."""
|
|
@@ -307,7 +305,7 @@ def resolve_execution_order(
|
|
|
307
305
|
parent_task_name=node.task_name,
|
|
308
306
|
parent_args=node.args or {},
|
|
309
307
|
parent_exported_args=parent_exported_args,
|
|
310
|
-
recipe=recipe
|
|
308
|
+
recipe=recipe,
|
|
311
309
|
)
|
|
312
310
|
|
|
313
311
|
# Create or get node for this dependency invocation
|
|
@@ -592,7 +590,9 @@ def get_implicit_inputs(recipe: Recipe, task: Task) -> list[str]:
|
|
|
592
590
|
return implicit_inputs
|
|
593
591
|
|
|
594
592
|
|
|
595
|
-
def build_dependency_tree(
|
|
593
|
+
def build_dependency_tree(
|
|
594
|
+
recipe: Recipe, target_task: str, target_args: dict[str, Any] | None = None
|
|
595
|
+
) -> dict:
|
|
596
596
|
"""
|
|
597
597
|
Build a tree structure representing dependencies for visualization.
|
|
598
598
|
|
|
@@ -622,12 +622,17 @@ def build_dependency_tree(recipe: Recipe, target_task: str, target_args: dict[st
|
|
|
622
622
|
|
|
623
623
|
# Create node identifier for cycle detection
|
|
624
624
|
from tasktree.hasher import hash_args
|
|
625
|
+
|
|
625
626
|
args_dict = args or {}
|
|
626
627
|
node_id = (task_name, hash_args(args_dict) if args_dict else "")
|
|
627
628
|
|
|
628
629
|
# Detect cycles in current recursion path
|
|
629
630
|
if node_id in current_path:
|
|
630
|
-
display_name =
|
|
631
|
+
display_name = (
|
|
632
|
+
task_name
|
|
633
|
+
if not args_dict
|
|
634
|
+
else f"{task_name}({', '.join(f'{k}={v}' for k, v in sorted(args_dict.items()))})"
|
|
635
|
+
)
|
|
631
636
|
return {"name": display_name, "deps": [], "cycle": True}
|
|
632
637
|
|
|
633
638
|
current_path.add(node_id)
|
tasktree/hasher.py
CHANGED
|
@@ -28,7 +28,9 @@ def _arg_sort_key(arg: str | dict[str, Any]) -> str:
|
|
|
28
28
|
return arg
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def _normalize_choices_lists(
|
|
31
|
+
def _normalize_choices_lists(
|
|
32
|
+
args: list[str | dict[str, Any]],
|
|
33
|
+
) -> list[str | dict[str, Any]]:
|
|
32
34
|
"""
|
|
33
35
|
Normalize argument choices lists by sorting them for deterministic hashing.
|
|
34
36
|
|
|
@@ -46,8 +48,11 @@ def _normalize_choices_lists(args: list[str | dict[str, Any]]) -> list[str | di
|
|
|
46
48
|
# Deep copy and sort choices if present
|
|
47
49
|
normalized = {}
|
|
48
50
|
for key, value in arg.items():
|
|
49
|
-
if isinstance(value, dict) and
|
|
50
|
-
normalized[key] = {
|
|
51
|
+
if isinstance(value, dict) and "choices" in value:
|
|
52
|
+
normalized[key] = {
|
|
53
|
+
**value,
|
|
54
|
+
"choices": sorted(value["choices"], key=str),
|
|
55
|
+
}
|
|
51
56
|
else:
|
|
52
57
|
normalized[key] = value
|
|
53
58
|
normalized_args.append(normalized)
|
|
@@ -94,7 +99,7 @@ def hash_task(
|
|
|
94
99
|
working_dir: str,
|
|
95
100
|
args: list[str | dict[str, Any]],
|
|
96
101
|
env: str = "",
|
|
97
|
-
deps: list[str | dict[str, Any]] | None = None
|
|
102
|
+
deps: list[str | dict[str, Any]] | None = None,
|
|
98
103
|
) -> str:
|
|
99
104
|
"""
|
|
100
105
|
Hash task definition including dependencies.
|
|
@@ -134,7 +139,10 @@ def hash_task(
|
|
|
134
139
|
else:
|
|
135
140
|
normalized_deps.append(dep)
|
|
136
141
|
# Sort using JSON serialization for consistent ordering
|
|
137
|
-
data["deps"] = sorted(
|
|
142
|
+
data["deps"] = sorted(
|
|
143
|
+
normalized_deps,
|
|
144
|
+
key=lambda x: json.dumps(x, sort_keys=True) if isinstance(x, dict) else x,
|
|
145
|
+
)
|
|
138
146
|
|
|
139
147
|
serialized = json.dumps(data, sort_keys=True, separators=(",", ":"))
|
|
140
148
|
return hashlib.sha256(serialized.encode()).hexdigest()[:8]
|
|
@@ -168,7 +176,6 @@ def hash_environment_definition(env) -> str:
|
|
|
168
176
|
@athena: 2de34f1a0b4a
|
|
169
177
|
"""
|
|
170
178
|
# Import inside function to avoid circular dependency
|
|
171
|
-
from tasktree.parser import Environment
|
|
172
179
|
|
|
173
180
|
# Handle args - can be list (shell args) or dict (docker build args)
|
|
174
181
|
args_value = env.args
|