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