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/graph.py CHANGED
@@ -1,7 +1,9 @@
1
- """Dependency resolution using topological sorting."""
1
+ """
2
+ Dependency resolution using topological sorting.
3
+ @athena: bc19c5dc0cca
4
+ """
2
5
 
3
6
  from graphlib import TopologicalSorter
4
- from pathlib import Path
5
7
  from typing import Any
6
8
 
7
9
  from tasktree.hasher import hash_args
@@ -10,25 +12,26 @@ from tasktree.parser import (
10
12
  Task,
11
13
  DependencyInvocation,
12
14
  parse_dependency_spec,
13
- parse_arg_spec,
14
15
  )
15
16
  from tasktree.substitution import substitute_dependency_args
16
17
 
17
18
 
18
19
  def _get_exported_arg_names(task: Task) -> set[str]:
19
- """Extract names of exported arguments from a task.
20
+ """
21
+ Extract names of exported arguments from a task.
20
22
 
21
23
  Exported arguments are identified by the '$' prefix in their definition.
22
24
 
23
25
  Args:
24
- task: Task to extract exported arg names from
26
+ task: Task to extract exported arg names from
25
27
 
26
28
  Returns:
27
- Set of exported argument names (without the '$' prefix)
29
+ Set of exported argument names (without the '$' prefix)
28
30
 
29
31
  Example:
30
- Task with args: ["$server", "port"]
31
- Returns: {"server"}
32
+ Task with args: ["$server", "port"]
33
+ Returns: {"server"}
34
+ @athena: 706576271735
32
35
  """
33
36
  if not task.args:
34
37
  return set()
@@ -53,9 +56,10 @@ def resolve_dependency_invocation(
53
56
  parent_task_name: str,
54
57
  parent_args: dict[str, Any],
55
58
  parent_exported_args: set[str],
56
- recipe: Recipe
59
+ recipe: Recipe,
57
60
  ) -> DependencyInvocation:
58
- """Parse dependency specification and substitute parent argument templates.
61
+ """
62
+ Parse dependency specification and substitute parent argument templates.
59
63
 
60
64
  This function handles template substitution in dependency arguments. It:
61
65
  1. Checks if dependency arguments contain {{ arg.* }} templates
@@ -63,37 +67,38 @@ def resolve_dependency_invocation(
63
67
  3. Delegates to parse_dependency_spec for type conversion and validation
64
68
 
65
69
  Args:
66
- dep_spec: Dependency specification (str or dict with task name and args)
67
- parent_task_name: Name of the parent task (for error messages)
68
- parent_args: Parent task's argument values (for template substitution)
69
- parent_exported_args: Set of parent's exported argument names
70
- recipe: Recipe containing task definitions
70
+ dep_spec: Dependency specification (str or dict with task name and args)
71
+ parent_task_name: Name of the parent task (for error messages)
72
+ parent_args: Parent task's argument values (for template substitution)
73
+ parent_exported_args: Set of parent's exported argument names
74
+ recipe: Recipe containing task definitions
71
75
 
72
76
  Returns:
73
- DependencyInvocation with typed, validated arguments
77
+ DependencyInvocation with typed, validated arguments
74
78
 
75
79
  Raises:
76
- ValueError: If template substitution fails, argument validation fails,
77
- or dependency task doesn't exist
80
+ ValueError: If template substitution fails, argument validation fails,
81
+ or dependency task doesn't exist
78
82
 
79
83
  Examples:
80
- Simple string (no templates):
81
- >>> resolve_dependency_invocation("build", "test", {}, set(), recipe)
82
- DependencyInvocation("build", None)
83
-
84
- Literal arguments (no templates):
85
- >>> resolve_dependency_invocation({"build": ["debug"]}, "test", {}, set(), recipe)
86
- DependencyInvocation("build", {"mode": "debug"})
87
-
88
- Template substitution:
89
- >>> resolve_dependency_invocation(
90
- ... {"build": ["{{ arg.env }}"]},
91
- ... "test",
92
- ... {"env": "production"},
93
- ... set(),
94
- ... recipe
95
- ... )
96
- DependencyInvocation("build", {"mode": "production"})
84
+ Simple string (no templates):
85
+ >>> resolve_dependency_invocation("build", "test", {}, set(), recipe)
86
+ DependencyInvocation("build", None)
87
+
88
+ Literal arguments (no templates):
89
+ >>> resolve_dependency_invocation({"build": ["debug"]}, "test", {}, set(), recipe)
90
+ DependencyInvocation("build", {"mode": "debug"})
91
+
92
+ Template substitution:
93
+ >>> resolve_dependency_invocation(
94
+ ... {"build": ["{{ arg.env }}"]},
95
+ ... "test",
96
+ ... {"env": "production"},
97
+ ... set(),
98
+ ... recipe
99
+ ... )
100
+ DependencyInvocation("build", {"mode": "production"})
101
+ @athena: 968bae796809
97
102
  """
98
103
  # Simple string case - no args to substitute
99
104
  if isinstance(dep_spec, str):
@@ -163,30 +168,44 @@ def resolve_dependency_invocation(
163
168
 
164
169
 
165
170
  class CycleError(Exception):
166
- """Raised when a dependency cycle is detected."""
171
+ """
172
+ Raised when a dependency cycle is detected.
173
+ @athena: 80f584af9b92
174
+ """
167
175
 
168
176
  pass
169
177
 
170
178
 
171
179
  class TaskNotFoundError(Exception):
172
- """Raised when a task dependency doesn't exist."""
180
+ """
181
+ Raised when a task dependency doesn't exist.
182
+ @athena: f838e39564ae
183
+ """
173
184
 
174
185
  pass
175
186
 
176
187
 
177
188
  class TaskNode:
178
- """Represents a node in the dependency graph (task + arguments).
189
+ """
190
+ Represents a node in the dependency graph (task + arguments).
179
191
 
180
192
  Each node represents a unique invocation of a task with specific arguments.
181
193
  Tasks invoked with different arguments are considered different nodes.
194
+ @athena: b5ff009e2f60
182
195
  """
183
196
 
184
197
  def __init__(self, task_name: str, args: dict[str, Any] | None = None):
198
+ """
199
+ @athena: 24d686697ce3
200
+ """
185
201
  self.task_name = task_name
186
202
  self.args = args # Keep None as None
187
203
 
188
204
  def __hash__(self):
189
- """Hash based on task name and sorted args."""
205
+ """
206
+ Hash based on task name and sorted args.
207
+ @athena: 16615e007076
208
+ """
190
209
  # Treat None and {} as equivalent for hashing
191
210
  if not self.args:
192
211
  return hash(self.task_name)
@@ -194,7 +213,10 @@ class TaskNode:
194
213
  return hash((self.task_name, args_hash))
195
214
 
196
215
  def __eq__(self, other):
197
- """Equality based on task name and args."""
216
+ """
217
+ Equality based on task name and args.
218
+ @athena: 139d5234448f
219
+ """
198
220
  if not isinstance(other, TaskNode):
199
221
  return False
200
222
  # Treat None and {} as equivalent
@@ -203,12 +225,18 @@ class TaskNode:
203
225
  return self.task_name == other.task_name and self_args == other_args
204
226
 
205
227
  def __repr__(self):
228
+ """
229
+ @athena: dd99b6a7835e
230
+ """
206
231
  if not self.args:
207
232
  return f"TaskNode({self.task_name})"
208
233
  args_str = ", ".join(f"{k}={v}" for k, v in sorted(self.args.items()))
209
234
  return f"TaskNode({self.task_name}, {{{args_str}}})"
210
235
 
211
236
  def __str__(self):
237
+ """
238
+ @athena: 2d82ca4b9d41
239
+ """
212
240
  if not self.args:
213
241
  return self.task_name
214
242
  args_str = ", ".join(f"{k}={v}" for k, v in sorted(self.args.items()))
@@ -216,23 +244,23 @@ class TaskNode:
216
244
 
217
245
 
218
246
  def resolve_execution_order(
219
- recipe: Recipe,
220
- target_task: str,
221
- target_args: dict[str, Any] | None = None
247
+ recipe: Recipe, target_task: str, target_args: dict[str, Any] | None = None
222
248
  ) -> list[tuple[str, dict[str, Any]]]:
223
- """Resolve execution order for a task and its dependencies.
249
+ """
250
+ Resolve execution order for a task and its dependencies.
224
251
 
225
252
  Args:
226
- recipe: Parsed recipe containing all tasks
227
- target_task: Name of the task to execute
228
- target_args: Arguments for the target task (optional)
253
+ recipe: Parsed recipe containing all tasks
254
+ target_task: Name of the task to execute
255
+ target_args: Arguments for the target task (optional)
229
256
 
230
257
  Returns:
231
- List of (task_name, args_dict) tuples in execution order (dependencies first)
258
+ List of (task_name, args_dict) tuples in execution order (dependencies first)
232
259
 
233
260
  Raises:
234
- TaskNotFoundError: If target task or any dependency doesn't exist
235
- CycleError: If a dependency cycle is detected
261
+ TaskNotFoundError: If target task or any dependency doesn't exist
262
+ CycleError: If a dependency cycle is detected
263
+ @athena: b4443e1cb45d
236
264
  """
237
265
  if target_task not in recipe.tasks:
238
266
  raise TaskNotFoundError(f"Task not found: {target_task}")
@@ -241,7 +269,9 @@ def resolve_execution_order(
241
269
  graph: dict[TaskNode, set[TaskNode]] = {}
242
270
 
243
271
  # Track seen nodes to detect duplicates
244
- seen_invocations: dict[tuple[str, str], TaskNode] = {} # (task_name, args_hash) -> node
272
+ seen_invocations: dict[
273
+ tuple[str, str], TaskNode
274
+ ] = {} # (task_name, args_hash) -> node
245
275
 
246
276
  def get_or_create_node(task_name: str, args: dict[str, Any] | None) -> TaskNode:
247
277
  """Get existing node or create new one for this invocation."""
@@ -275,7 +305,7 @@ def resolve_execution_order(
275
305
  parent_task_name=node.task_name,
276
306
  parent_args=node.args or {},
277
307
  parent_exported_args=parent_exported_args,
278
- recipe=recipe
308
+ recipe=recipe,
279
309
  )
280
310
 
281
311
  # Create or get node for this dependency invocation
@@ -310,24 +340,26 @@ def resolve_dependency_output_references(
310
340
  recipe: Recipe,
311
341
  ordered_tasks: list[tuple[str, dict[str, Any]]],
312
342
  ) -> None:
313
- """Resolve {{ dep.<task>.outputs.<name> }} references in topological order.
343
+ """
344
+ Resolve {{ dep.<task>.outputs.<name> }} references in topological order.
314
345
 
315
346
  This function walks through tasks in dependency order (dependencies first) and
316
347
  resolves any references to dependency outputs in task fields. Templates are
317
348
  resolved in place, modifying the Task objects in the recipe.
318
349
 
319
350
  Args:
320
- recipe: Recipe containing task definitions
321
- ordered_tasks: List of (task_name, args) tuples in topological order
351
+ recipe: Recipe containing task definitions
352
+ ordered_tasks: List of (task_name, args) tuples in topological order
322
353
 
323
354
  Raises:
324
- ValueError: If template references cannot be resolved (missing task,
325
- missing output, task not in dependencies, etc.)
355
+ ValueError: If template references cannot be resolved (missing task,
356
+ missing output, task not in dependencies, etc.)
326
357
 
327
358
  Example:
328
- Given tasks in topological order: [('build', {}), ('deploy', {})]
329
- If deploy.cmd contains "{{ dep.build.outputs.bundle }}", it will be
330
- resolved to the actual output path from the build task.
359
+ Given tasks in topological order: [('build', {}), ('deploy', {})]
360
+ If deploy.cmd contains "{{ dep.build.outputs.bundle }}", it will be
361
+ resolved to the actual output path from the build task.
362
+ @athena: 6dce9dbe6286
331
363
  """
332
364
  from tasktree.substitution import substitute_dependency_outputs
333
365
 
@@ -418,22 +450,29 @@ def resolve_self_references(
418
450
  recipe: Recipe,
419
451
  ordered_tasks: list[tuple[str, dict[str, Any]]],
420
452
  ) -> None:
421
- """Resolve {{ self.inputs.name }} and {{ self.outputs.name }} references.
453
+ """
454
+ Resolve {{ self.inputs.name }} and {{ self.inputs.0 }} style references.
422
455
 
423
456
  This function walks through tasks and resolves self-references to task's own
424
- inputs/outputs. Must be called AFTER resolve_dependency_output_references()
425
- so that dependency outputs are already resolved in output paths.
457
+ inputs/outputs. Supports both named access ({{ self.inputs.name }}) and
458
+ positional access ({{ self.inputs.0 }}, {{ self.inputs.1 }}, etc.).
459
+
460
+ Must be called AFTER resolve_dependency_output_references() so that
461
+ dependency outputs are already resolved in output paths.
426
462
 
427
463
  Args:
428
- recipe: Recipe containing task definitions
429
- ordered_tasks: List of (task_name, args) tuples in topological order
464
+ recipe: Recipe containing task definitions
465
+ ordered_tasks: List of (task_name, args) tuples in topological order
430
466
 
431
467
  Raises:
432
- ValueError: If self-reference cannot be resolved (missing name, etc.)
468
+ ValueError: If self-reference cannot be resolved (missing name, out of bounds index, etc.)
433
469
 
434
470
  Example:
435
- If task.cmd contains "{{ self.inputs.src }}" and task has input {src: "*.txt"},
436
- it will be resolved to "*.txt" (literal string, no glob expansion).
471
+ If task.cmd contains "{{ self.inputs.src }}" and task has input {src: "*.txt"},
472
+ it will be resolved to "*.txt" (literal string, no glob expansion).
473
+ If task.cmd contains "{{ self.inputs.0 }}" and task has inputs ["*.txt", "*.md"],
474
+ it will be resolved to "*.txt" (first input in YAML order).
475
+ @athena: 641e28567d1d
437
476
  """
438
477
  from tasktree.substitution import substitute_self_references
439
478
 
@@ -449,6 +488,8 @@ def resolve_self_references(
449
488
  task_name,
450
489
  task._input_map,
451
490
  task._output_map,
491
+ task._indexed_inputs,
492
+ task._indexed_outputs,
452
493
  )
453
494
 
454
495
  # Resolve self-references in working_dir
@@ -458,6 +499,8 @@ def resolve_self_references(
458
499
  task_name,
459
500
  task._input_map,
460
501
  task._output_map,
502
+ task._indexed_inputs,
503
+ task._indexed_outputs,
461
504
  )
462
505
 
463
506
  # Resolve self-references in argument defaults
@@ -472,26 +515,30 @@ def resolve_self_references(
472
515
  task_name,
473
516
  task._input_map,
474
517
  task._output_map,
518
+ task._indexed_inputs,
519
+ task._indexed_outputs,
475
520
  )
476
521
 
477
522
 
478
523
  def get_implicit_inputs(recipe: Recipe, task: Task) -> list[str]:
479
- """Get implicit inputs for a task based on its dependencies.
524
+ """
525
+ Get implicit inputs for a task based on its dependencies.
480
526
 
481
527
  Tasks automatically inherit inputs from dependencies:
482
528
  1. All outputs from dependency tasks become implicit inputs
483
529
  2. All inputs from dependency tasks that don't declare outputs are inherited
484
530
  3. If task uses a Docker environment, Docker artifacts become implicit inputs:
485
- - Dockerfile
486
- - .dockerignore (if present)
487
- - Special markers for context directory and base image digests
531
+ - Dockerfile
532
+ - .dockerignore (if present)
533
+ - Special markers for context directory and base image digests
488
534
 
489
535
  Args:
490
- recipe: Parsed recipe containing all tasks
491
- task: Task to get implicit inputs for
536
+ recipe: Parsed recipe containing all tasks
537
+ task: Task to get implicit inputs for
492
538
 
493
539
  Returns:
494
- List of glob patterns for implicit inputs, including Docker-specific markers
540
+ List of glob patterns for implicit inputs, including Docker-specific markers
541
+ @athena: da25e64fbd2b
495
542
  """
496
543
  implicit_inputs = []
497
544
 
@@ -543,20 +590,24 @@ def get_implicit_inputs(recipe: Recipe, task: Task) -> list[str]:
543
590
  return implicit_inputs
544
591
 
545
592
 
546
- def build_dependency_tree(recipe: Recipe, target_task: str, target_args: dict[str, Any] | None = None) -> dict:
547
- """Build a tree structure representing dependencies for visualization.
593
+ def build_dependency_tree(
594
+ recipe: Recipe, target_task: str, target_args: dict[str, Any] | None = None
595
+ ) -> dict:
596
+ """
597
+ Build a tree structure representing dependencies for visualization.
548
598
 
549
599
  Note: This builds a true tree representation where shared dependencies may
550
600
  appear multiple times. Each dependency is shown in the context of its parent,
551
601
  allowing the full dependency path to be visible from any node.
552
602
 
553
603
  Args:
554
- recipe: Parsed recipe containing all tasks
555
- target_task: Name of the task to build tree for
556
- target_args: Arguments for the target task (optional)
604
+ recipe: Parsed recipe containing all tasks
605
+ target_task: Name of the task to build tree for
606
+ target_args: Arguments for the target task (optional)
557
607
 
558
608
  Returns:
559
- Nested dictionary representing the dependency tree
609
+ Nested dictionary representing the dependency tree
610
+ @athena: 570e5c663887
560
611
  """
561
612
  if target_task not in recipe.tasks:
562
613
  raise TaskNotFoundError(f"Task not found: {target_task}")
@@ -571,12 +622,17 @@ def build_dependency_tree(recipe: Recipe, target_task: str, target_args: dict[st
571
622
 
572
623
  # Create node identifier for cycle detection
573
624
  from tasktree.hasher import hash_args
625
+
574
626
  args_dict = args or {}
575
627
  node_id = (task_name, hash_args(args_dict) if args_dict else "")
576
628
 
577
629
  # Detect cycles in current recursion path
578
630
  if node_id in current_path:
579
- display_name = task_name if not args_dict else f"{task_name}({', '.join(f'{k}={v}' for k, v in sorted(args_dict.items()))})"
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
+ )
580
636
  return {"name": display_name, "deps": [], "cycle": True}
581
637
 
582
638
  current_path.add(node_id)
tasktree/hasher.py CHANGED
@@ -1,16 +1,25 @@
1
+ """Task and argument hashing for incremental execution.
2
+
3
+ Provides functions to compute deterministic hashes of task definitions,
4
+ arguments, and environment configurations. These hashes are used to detect
5
+ changes that require task re-execution in the incremental build system.
6
+ """
7
+
1
8
  import hashlib
2
9
  import json
3
10
  from typing import Any, Optional
4
11
 
5
12
 
6
13
  def _arg_sort_key(arg: str | dict[str, Any]) -> str:
7
- """Extract the sort key from an arg for deterministic hashing.
14
+ """
15
+ Extract the sort key from an arg for deterministic hashing.
8
16
 
9
17
  Args:
10
- arg: Either a string arg or dict arg specification
18
+ arg: Either a string arg or dict arg specification
11
19
 
12
20
  Returns:
13
- The argument name to use as a sort key
21
+ The argument name to use as a sort key
22
+ @athena: c88bb2cf696b
14
23
  """
15
24
  if isinstance(arg, dict):
16
25
  # Dict args have exactly one key - the argument name
@@ -19,15 +28,31 @@ def _arg_sort_key(arg: str | dict[str, Any]) -> str:
19
28
  return arg
20
29
 
21
30
 
22
- def _normalize_choices_lists(args: list[str | dict[str, Any]]) -> list[str | dict[str, Any]]:
31
+ def _normalize_choices_lists(
32
+ args: list[str | dict[str, Any]],
33
+ ) -> list[str | dict[str, Any]]:
34
+ """
35
+ Normalize argument choices lists by sorting them for deterministic hashing.
36
+
37
+ Args:
38
+ args: List of argument specifications (strings or dicts)
39
+
40
+ Returns:
41
+ List of argument specs with sorted choices lists
42
+
43
+ @athena: 7512379275e3
44
+ """
23
45
  normalized_args = []
24
46
  for arg in args:
25
47
  if isinstance(arg, dict):
26
48
  # Deep copy and sort choices if present
27
49
  normalized = {}
28
50
  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)}
51
+ if isinstance(value, dict) and "choices" in value:
52
+ normalized[key] = {
53
+ **value,
54
+ "choices": sorted(value["choices"], key=str),
55
+ }
31
56
  else:
32
57
  normalized[key] = value
33
58
  normalized_args.append(normalized)
@@ -38,20 +63,22 @@ def _normalize_choices_lists(args: list[str | dict[str, Any]]) -> list[str | di
38
63
 
39
64
 
40
65
  def _serialize_outputs_for_hash(outputs: list[str | dict[str, str]]) -> list[str]:
41
- """Serialize outputs to consistent list of strings for hashing.
66
+ """
67
+ Serialize outputs to consistent list of strings for hashing.
42
68
 
43
69
  Converts both named outputs (dicts) and anonymous outputs (strings)
44
70
  into a consistent, sortable format.
45
71
 
46
72
  Args:
47
- outputs: List of output specifications (strings or dicts)
73
+ outputs: List of output specifications (strings or dicts)
48
74
 
49
75
  Returns:
50
- List of serialized output strings in sorted order
76
+ List of serialized output strings in sorted order
51
77
 
52
78
  Example:
53
- >>> _serialize_outputs_for_hash(["file.txt", {"bundle": "app.js"}])
54
- ['bundle:app.js', 'file.txt']
79
+ >>> _serialize_outputs_for_hash(["file.txt", {"bundle": "app.js"}])
80
+ ['bundle:app.js', 'file.txt']
81
+ @athena: 933995400691
55
82
  """
56
83
  serialized = []
57
84
  for output in outputs:
@@ -72,20 +99,22 @@ def hash_task(
72
99
  working_dir: str,
73
100
  args: list[str | dict[str, Any]],
74
101
  env: str = "",
75
- deps: list[str | dict[str, Any]] | None = None
102
+ deps: list[str | dict[str, Any]] | None = None,
76
103
  ) -> str:
77
- """Hash task definition including dependencies.
104
+ """
105
+ Hash task definition including dependencies.
78
106
 
79
107
  Args:
80
- cmd: Task command
81
- outputs: Task outputs (strings or named dicts)
82
- working_dir: Working directory
83
- args: Task argument specifications
84
- env: Environment name
85
- deps: Dependency specifications (optional, for dependency hash)
108
+ cmd: Task command
109
+ outputs: Task outputs (strings or named dicts)
110
+ working_dir: Working directory
111
+ args: Task argument specifications
112
+ env: Environment name
113
+ deps: Dependency specifications (optional, for dependency hash)
86
114
 
87
115
  Returns:
88
- 8-character hash of task definition
116
+ 8-character hash of task definition
117
+ @athena: 7a461d51a8bb
89
118
  """
90
119
  data = {
91
120
  "cmd": cmd,
@@ -110,28 +139,43 @@ def hash_task(
110
139
  else:
111
140
  normalized_deps.append(dep)
112
141
  # Sort using JSON serialization for consistent ordering
113
- data["deps"] = sorted(normalized_deps, key=lambda x: json.dumps(x, sort_keys=True) if isinstance(x, dict) else x)
142
+ data["deps"] = sorted(
143
+ normalized_deps,
144
+ key=lambda x: json.dumps(x, sort_keys=True) if isinstance(x, dict) else x,
145
+ )
114
146
 
115
147
  serialized = json.dumps(data, sort_keys=True, separators=(",", ":"))
116
148
  return hashlib.sha256(serialized.encode()).hexdigest()[:8]
117
149
 
118
150
 
119
151
  def hash_args(args_dict: dict[str, Any]) -> str:
152
+ """
153
+ Hash task argument values for cache key generation.
154
+
155
+ Args:
156
+ args_dict: Dictionary mapping argument names to their values
157
+
158
+ Returns:
159
+ 8-character hash of the argument dictionary
160
+
161
+ @athena: 768e562bff64
162
+ """
120
163
  serialized = json.dumps(args_dict, sort_keys=True, separators=(",", ":"))
121
164
  return hashlib.sha256(serialized.encode()).hexdigest()[:8]
122
165
 
123
166
 
124
167
  def hash_environment_definition(env) -> str:
125
- """Hash environment definition fields that affect task execution.
168
+ """
169
+ Hash environment definition fields that affect task execution.
126
170
 
127
171
  Args:
128
- env: Environment to hash
172
+ env: Environment to hash
129
173
 
130
174
  Returns:
131
- 16-character hash of environment definition
175
+ 16-character hash of environment definition
176
+ @athena: 2de34f1a0b4a
132
177
  """
133
178
  # Import inside function to avoid circular dependency
134
- from tasktree.parser import Environment
135
179
 
136
180
  # Handle args - can be list (shell args) or dict (docker build args)
137
181
  args_value = env.args
@@ -156,6 +200,18 @@ def hash_environment_definition(env) -> str:
156
200
 
157
201
 
158
202
  def make_cache_key(task_hash: str, args_hash: Optional[str] = None) -> str:
203
+ """
204
+ Combine task and argument hashes into a cache key.
205
+
206
+ Args:
207
+ task_hash: Hash of the task definition
208
+ args_hash: Optional hash of task arguments
209
+
210
+ Returns:
211
+ Combined cache key string in format "task_hash__args_hash" or just "task_hash"
212
+
213
+ @athena: c85093f485c7
214
+ """
159
215
  if args_hash:
160
216
  return f"{task_hash}__{args_hash}"
161
217
  return task_hash