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/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 hash_task(cmd: str, outputs: list[str], working_dir: str, args: list[str], env: str = "") -> str:
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}"