tasktree 0.0.6__py3-none-any.whl → 0.0.8__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 +24 -16
- tasktree/docker.py +438 -0
- tasktree/executor.py +526 -50
- tasktree/graph.py +30 -1
- tasktree/hasher.py +63 -2
- tasktree/parser.py +1099 -32
- tasktree/state.py +1 -1
- tasktree/substitution.py +195 -0
- tasktree/types.py +11 -2
- tasktree-0.0.8.dist-info/METADATA +1149 -0
- tasktree-0.0.8.dist-info/RECORD +15 -0
- tasktree-0.0.6.dist-info/METADATA +0 -699
- tasktree-0.0.6.dist-info/RECORD +0 -13
- {tasktree-0.0.6.dist-info → tasktree-0.0.8.dist-info}/WHEEL +0 -0
- {tasktree-0.0.6.dist-info → tasktree-0.0.8.dist-info}/entry_points.txt +0 -0
tasktree/graph.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Dependency resolution using topological sorting."""
|
|
2
2
|
|
|
3
3
|
from graphlib import TopologicalSorter
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
from tasktree.parser import Recipe, Task
|
|
6
7
|
|
|
@@ -71,16 +72,21 @@ def get_implicit_inputs(recipe: Recipe, task: Task) -> list[str]:
|
|
|
71
72
|
Tasks automatically inherit inputs from dependencies:
|
|
72
73
|
1. All outputs from dependency tasks become implicit inputs
|
|
73
74
|
2. All inputs from dependency tasks that don't declare outputs are inherited
|
|
75
|
+
3. If task uses a Docker environment, Docker artifacts become implicit inputs:
|
|
76
|
+
- Dockerfile
|
|
77
|
+
- .dockerignore (if present)
|
|
78
|
+
- Special markers for context directory and base image digests
|
|
74
79
|
|
|
75
80
|
Args:
|
|
76
81
|
recipe: Parsed recipe containing all tasks
|
|
77
82
|
task: Task to get implicit inputs for
|
|
78
83
|
|
|
79
84
|
Returns:
|
|
80
|
-
List of glob patterns for implicit inputs
|
|
85
|
+
List of glob patterns for implicit inputs, including Docker-specific markers
|
|
81
86
|
"""
|
|
82
87
|
implicit_inputs = []
|
|
83
88
|
|
|
89
|
+
# Inherit from dependencies
|
|
84
90
|
for dep_name in task.deps:
|
|
85
91
|
dep_task = recipe.tasks.get(dep_name)
|
|
86
92
|
if dep_task is None:
|
|
@@ -93,6 +99,29 @@ def get_implicit_inputs(recipe: Recipe, task: Task) -> list[str]:
|
|
|
93
99
|
elif dep_task.inputs:
|
|
94
100
|
implicit_inputs.extend(dep_task.inputs)
|
|
95
101
|
|
|
102
|
+
# Add Docker-specific implicit inputs if task uses Docker environment
|
|
103
|
+
env_name = task.env or recipe.default_env
|
|
104
|
+
if env_name:
|
|
105
|
+
env = recipe.get_environment(env_name)
|
|
106
|
+
if env and env.dockerfile:
|
|
107
|
+
# Add Dockerfile as input
|
|
108
|
+
implicit_inputs.append(env.dockerfile)
|
|
109
|
+
|
|
110
|
+
# Add .dockerignore if it exists in context directory
|
|
111
|
+
context_path = recipe.project_root / env.context
|
|
112
|
+
dockerignore_path = context_path / ".dockerignore"
|
|
113
|
+
if dockerignore_path.exists():
|
|
114
|
+
relative_dockerignore = str(
|
|
115
|
+
dockerignore_path.relative_to(recipe.project_root)
|
|
116
|
+
)
|
|
117
|
+
implicit_inputs.append(relative_dockerignore)
|
|
118
|
+
|
|
119
|
+
# Add special markers for context directory and digest tracking
|
|
120
|
+
# These are tracked differently in state management (not file paths)
|
|
121
|
+
# The executor will handle these specially
|
|
122
|
+
implicit_inputs.append(f"_docker_context_{env.context}")
|
|
123
|
+
implicit_inputs.append(f"_docker_dockerfile_{env.dockerfile}")
|
|
124
|
+
|
|
96
125
|
return implicit_inputs
|
|
97
126
|
|
|
98
127
|
|
tasktree/hasher.py
CHANGED
|
@@ -3,12 +3,46 @@ import json
|
|
|
3
3
|
from typing import Any, Optional
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def
|
|
6
|
+
def _arg_sort_key(arg: str | dict[str, Any]) -> str:
|
|
7
|
+
"""Extract the sort key from an arg for deterministic hashing.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
arg: Either a string arg or dict arg specification
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
The argument name to use as a sort key
|
|
14
|
+
"""
|
|
15
|
+
if isinstance(arg, dict):
|
|
16
|
+
# Dict args have exactly one key - the argument name
|
|
17
|
+
# This is validated by parse_arg_spec in parser.py
|
|
18
|
+
return next(iter(arg.keys()))
|
|
19
|
+
return arg
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _normalize_choices_lists(args: list[str | dict[str, Any]]) -> list[str | dict[str, Any]]:
|
|
23
|
+
normalized_args = []
|
|
24
|
+
for arg in args:
|
|
25
|
+
if isinstance(arg, dict):
|
|
26
|
+
# Deep copy and sort choices if present
|
|
27
|
+
normalized = {}
|
|
28
|
+
for key, value in arg.items():
|
|
29
|
+
if isinstance(value, dict) and 'choices' in value:
|
|
30
|
+
normalized[key] = {**value, 'choices': sorted(value['choices'], key=str)}
|
|
31
|
+
else:
|
|
32
|
+
normalized[key] = value
|
|
33
|
+
normalized_args.append(normalized)
|
|
34
|
+
else:
|
|
35
|
+
normalized_args.append(arg)
|
|
36
|
+
|
|
37
|
+
return normalized_args
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def hash_task(cmd: str, outputs: list[str], working_dir: str, args: list[str | dict[str, Any]], env: str = "") -> str:
|
|
7
41
|
data = {
|
|
8
42
|
"cmd": cmd,
|
|
9
43
|
"outputs": sorted(outputs),
|
|
10
44
|
"working_dir": working_dir,
|
|
11
|
-
"args": sorted(args),
|
|
45
|
+
"args": sorted(_normalize_choices_lists(args), key=_arg_sort_key),
|
|
12
46
|
"env": env,
|
|
13
47
|
}
|
|
14
48
|
|
|
@@ -21,6 +55,33 @@ def hash_args(args_dict: dict[str, Any]) -> str:
|
|
|
21
55
|
return hashlib.sha256(serialized.encode()).hexdigest()[:8]
|
|
22
56
|
|
|
23
57
|
|
|
58
|
+
def hash_environment_definition(env) -> str:
|
|
59
|
+
"""Hash environment definition fields that affect task execution.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
env: Environment to hash
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
16-character hash of environment definition
|
|
66
|
+
"""
|
|
67
|
+
# Import inside function to avoid circular dependency
|
|
68
|
+
from tasktree.parser import Environment
|
|
69
|
+
|
|
70
|
+
data = {
|
|
71
|
+
"shell": env.shell,
|
|
72
|
+
"args": sorted(env.args), # Sort for determinism
|
|
73
|
+
"preamble": env.preamble,
|
|
74
|
+
"dockerfile": env.dockerfile,
|
|
75
|
+
"context": env.context,
|
|
76
|
+
"volumes": sorted(env.volumes),
|
|
77
|
+
"ports": sorted(env.ports),
|
|
78
|
+
"env_vars": dict(sorted(env.env_vars.items())),
|
|
79
|
+
"working_dir": env.working_dir,
|
|
80
|
+
}
|
|
81
|
+
serialized = json.dumps(data, sort_keys=True, separators=(",", ":"))
|
|
82
|
+
return hashlib.sha256(serialized.encode()).hexdigest()[:16]
|
|
83
|
+
|
|
84
|
+
|
|
24
85
|
def make_cache_key(task_hash: str, args_hash: Optional[str] = None) -> str:
|
|
25
86
|
if args_hash:
|
|
26
87
|
return f"{task_hash}__{args_hash}"
|