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/executor.py
CHANGED
|
@@ -14,7 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
16
|
from tasktree import docker as docker_module
|
|
17
|
-
from tasktree.graph import get_implicit_inputs, resolve_execution_order, resolve_dependency_output_references
|
|
17
|
+
from tasktree.graph import get_implicit_inputs, resolve_execution_order, resolve_dependency_output_references, resolve_self_references
|
|
18
18
|
from tasktree.hasher import hash_args, hash_task, make_cache_key
|
|
19
19
|
from tasktree.parser import Recipe, Task, Environment
|
|
20
20
|
from tasktree.state import StateManager, TaskState
|
|
@@ -22,7 +22,10 @@ from tasktree.state import StateManager, TaskState
|
|
|
22
22
|
|
|
23
23
|
@dataclass
|
|
24
24
|
class TaskStatus:
|
|
25
|
-
"""
|
|
25
|
+
"""
|
|
26
|
+
Status of a task for execution planning.
|
|
27
|
+
@athena: a718e784981d
|
|
28
|
+
"""
|
|
26
29
|
|
|
27
30
|
task_name: str
|
|
28
31
|
will_run: bool
|
|
@@ -33,13 +36,19 @@ class TaskStatus:
|
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
class ExecutionError(Exception):
|
|
36
|
-
"""
|
|
39
|
+
"""
|
|
40
|
+
Raised when task execution fails.
|
|
41
|
+
@athena: f22d72903ee4
|
|
42
|
+
"""
|
|
37
43
|
|
|
38
44
|
pass
|
|
39
45
|
|
|
40
46
|
|
|
41
47
|
class Executor:
|
|
42
|
-
"""
|
|
48
|
+
"""
|
|
49
|
+
Executes tasks with incremental execution logic.
|
|
50
|
+
@athena: ac1e2fc7b82b
|
|
51
|
+
"""
|
|
43
52
|
|
|
44
53
|
# Protected environment variables that cannot be overridden by exported args
|
|
45
54
|
PROTECTED_ENV_VARS = {
|
|
@@ -54,24 +63,28 @@ class Executor:
|
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
def __init__(self, recipe: Recipe, state_manager: StateManager):
|
|
57
|
-
"""
|
|
66
|
+
"""
|
|
67
|
+
Initialize executor.
|
|
58
68
|
|
|
59
69
|
Args:
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
recipe: Parsed recipe containing all tasks
|
|
71
|
+
state_manager: State manager for tracking task execution
|
|
72
|
+
@athena: 21b65db48bca
|
|
62
73
|
"""
|
|
63
74
|
self.recipe = recipe
|
|
64
75
|
self.state = state_manager
|
|
65
76
|
self.docker_manager = docker_module.DockerManager(recipe.project_root)
|
|
66
77
|
|
|
67
78
|
def _has_regular_args(self, task: Task) -> bool:
|
|
68
|
-
"""
|
|
79
|
+
"""
|
|
80
|
+
Check if a task has any regular (non-exported) arguments.
|
|
69
81
|
|
|
70
82
|
Args:
|
|
71
|
-
|
|
83
|
+
task: Task to check
|
|
72
84
|
|
|
73
85
|
Returns:
|
|
74
|
-
|
|
86
|
+
True if task has at least one regular (non-exported) argument, False otherwise
|
|
87
|
+
@athena: 0fc46146eed3
|
|
75
88
|
"""
|
|
76
89
|
if not task.args:
|
|
77
90
|
return False
|
|
@@ -93,14 +106,16 @@ class Executor:
|
|
|
93
106
|
return False
|
|
94
107
|
|
|
95
108
|
def _filter_regular_args(self, task: Task, task_args: dict[str, Any]) -> dict[str, Any]:
|
|
96
|
-
"""
|
|
109
|
+
"""
|
|
110
|
+
Filter task_args to only include regular (non-exported) arguments.
|
|
97
111
|
|
|
98
112
|
Args:
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
task: Task definition
|
|
114
|
+
task_args: Dictionary of all task arguments
|
|
101
115
|
|
|
102
116
|
Returns:
|
|
103
|
-
|
|
117
|
+
Dictionary containing only regular (non-exported) arguments
|
|
118
|
+
@athena: 811abd3a56f9
|
|
104
119
|
"""
|
|
105
120
|
if not task.args or not task_args:
|
|
106
121
|
return {}
|
|
@@ -121,19 +136,21 @@ class Executor:
|
|
|
121
136
|
return {k: v for k, v in task_args.items() if k not in exported_names}
|
|
122
137
|
|
|
123
138
|
def _collect_early_builtin_variables(self, task: Task, timestamp: datetime) -> dict[str, str]:
|
|
124
|
-
"""
|
|
139
|
+
"""
|
|
140
|
+
Collect built-in variables that don't depend on working_dir.
|
|
125
141
|
|
|
126
142
|
These variables can be used in the working_dir field itself.
|
|
127
143
|
|
|
128
144
|
Args:
|
|
129
|
-
|
|
130
|
-
|
|
145
|
+
task: Task being executed
|
|
146
|
+
timestamp: Timestamp when task started execution
|
|
131
147
|
|
|
132
148
|
Returns:
|
|
133
|
-
|
|
149
|
+
Dictionary mapping built-in variable names to their string values
|
|
134
150
|
|
|
135
151
|
Raises:
|
|
136
|
-
|
|
152
|
+
ExecutionError: If any built-in variable fails to resolve
|
|
153
|
+
@athena: 0b348e67ce4c
|
|
137
154
|
"""
|
|
138
155
|
import os
|
|
139
156
|
|
|
@@ -174,18 +191,20 @@ class Executor:
|
|
|
174
191
|
return builtin_vars
|
|
175
192
|
|
|
176
193
|
def _collect_builtin_variables(self, task: Task, working_dir: Path, timestamp: datetime) -> dict[str, str]:
|
|
177
|
-
"""
|
|
194
|
+
"""
|
|
195
|
+
Collect built-in variables for task execution.
|
|
178
196
|
|
|
179
197
|
Args:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
198
|
+
task: Task being executed
|
|
199
|
+
working_dir: Resolved working directory for the task
|
|
200
|
+
timestamp: Timestamp when task started execution
|
|
183
201
|
|
|
184
202
|
Returns:
|
|
185
|
-
|
|
203
|
+
Dictionary mapping built-in variable names to their string values
|
|
186
204
|
|
|
187
205
|
Raises:
|
|
188
|
-
|
|
206
|
+
ExecutionError: If any built-in variable fails to resolve
|
|
207
|
+
@athena: bb8c385cb0a5
|
|
189
208
|
"""
|
|
190
209
|
# Get early builtin vars (those that don't depend on working_dir)
|
|
191
210
|
builtin_vars = self._collect_early_builtin_variables(task, timestamp)
|
|
@@ -197,16 +216,18 @@ class Executor:
|
|
|
197
216
|
return builtin_vars
|
|
198
217
|
|
|
199
218
|
def _prepare_env_with_exports(self, exported_env_vars: dict[str, str] | None = None) -> dict[str, str]:
|
|
200
|
-
"""
|
|
219
|
+
"""
|
|
220
|
+
Prepare environment with exported arguments.
|
|
201
221
|
|
|
202
222
|
Args:
|
|
203
|
-
|
|
223
|
+
exported_env_vars: Exported arguments to set as environment variables
|
|
204
224
|
|
|
205
225
|
Returns:
|
|
206
|
-
|
|
226
|
+
Environment dict with exported args merged
|
|
207
227
|
|
|
208
228
|
Raises:
|
|
209
|
-
|
|
229
|
+
ValueError: If an exported arg attempts to override a protected environment variable
|
|
230
|
+
@athena: 5340be771194
|
|
210
231
|
"""
|
|
211
232
|
env = os.environ.copy()
|
|
212
233
|
if exported_env_vars:
|
|
@@ -221,10 +242,12 @@ class Executor:
|
|
|
221
242
|
return env
|
|
222
243
|
|
|
223
244
|
def _get_platform_default_environment(self) -> tuple[str, list[str]]:
|
|
224
|
-
"""
|
|
245
|
+
"""
|
|
246
|
+
Get default shell and args for current platform.
|
|
225
247
|
|
|
226
248
|
Returns:
|
|
227
|
-
|
|
249
|
+
Tuple of (shell, args) for platform default
|
|
250
|
+
@athena: b67799671787
|
|
228
251
|
"""
|
|
229
252
|
is_windows = platform.system() == "Windows"
|
|
230
253
|
if is_windows:
|
|
@@ -233,7 +256,8 @@ class Executor:
|
|
|
233
256
|
return ("bash", ["-c"])
|
|
234
257
|
|
|
235
258
|
def _get_effective_env_name(self, task: Task) -> str:
|
|
236
|
-
"""
|
|
259
|
+
"""
|
|
260
|
+
Get the effective environment name for a task.
|
|
237
261
|
|
|
238
262
|
Resolution order:
|
|
239
263
|
1. Recipe's global_env_override (from CLI --env)
|
|
@@ -242,10 +266,11 @@ class Executor:
|
|
|
242
266
|
4. Empty string (for platform default)
|
|
243
267
|
|
|
244
268
|
Args:
|
|
245
|
-
|
|
269
|
+
task: Task to get environment name for
|
|
246
270
|
|
|
247
271
|
Returns:
|
|
248
|
-
|
|
272
|
+
Environment name (empty string if using platform default)
|
|
273
|
+
@athena: e5bface8a3a2
|
|
249
274
|
"""
|
|
250
275
|
# Check for global override first
|
|
251
276
|
if self.recipe.global_env_override:
|
|
@@ -263,7 +288,8 @@ class Executor:
|
|
|
263
288
|
return ""
|
|
264
289
|
|
|
265
290
|
def _resolve_environment(self, task: Task) -> tuple[str, list[str], str]:
|
|
266
|
-
"""
|
|
291
|
+
"""
|
|
292
|
+
Resolve which environment to use for a task.
|
|
267
293
|
|
|
268
294
|
Resolution order:
|
|
269
295
|
1. Recipe's global_env_override (from CLI --env)
|
|
@@ -272,10 +298,11 @@ class Executor:
|
|
|
272
298
|
4. Platform default (bash on Unix, cmd on Windows)
|
|
273
299
|
|
|
274
300
|
Args:
|
|
275
|
-
|
|
301
|
+
task: Task to resolve environment for
|
|
276
302
|
|
|
277
303
|
Returns:
|
|
278
|
-
|
|
304
|
+
Tuple of (shell, args, preamble)
|
|
305
|
+
@athena: b919568f73fc
|
|
279
306
|
"""
|
|
280
307
|
# Check for global override first
|
|
281
308
|
env_name = self.recipe.global_env_override
|
|
@@ -305,7 +332,8 @@ class Executor:
|
|
|
305
332
|
args_dict: dict[str, Any],
|
|
306
333
|
force: bool = False,
|
|
307
334
|
) -> TaskStatus:
|
|
308
|
-
"""
|
|
335
|
+
"""
|
|
336
|
+
Check if a task needs to run.
|
|
309
337
|
|
|
310
338
|
A task executes if ANY of these conditions are met:
|
|
311
339
|
1. Force flag is set (--force)
|
|
@@ -318,12 +346,13 @@ class Executor:
|
|
|
318
346
|
8. Different arguments than any cached execution
|
|
319
347
|
|
|
320
348
|
Args:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
349
|
+
task: Task to check
|
|
350
|
+
args_dict: Arguments for this task execution
|
|
351
|
+
force: If True, ignore freshness and force execution
|
|
324
352
|
|
|
325
353
|
Returns:
|
|
326
|
-
|
|
354
|
+
TaskStatus indicating whether task will run and why
|
|
355
|
+
@athena: 7252f5db8a4d
|
|
327
356
|
"""
|
|
328
357
|
# If force flag is set, always run
|
|
329
358
|
if force:
|
|
@@ -404,19 +433,21 @@ class Executor:
|
|
|
404
433
|
force: bool = False,
|
|
405
434
|
only: bool = False,
|
|
406
435
|
) -> dict[str, TaskStatus]:
|
|
407
|
-
"""
|
|
436
|
+
"""
|
|
437
|
+
Execute a task and its dependencies.
|
|
408
438
|
|
|
409
439
|
Args:
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
440
|
+
task_name: Name of task to execute
|
|
441
|
+
args_dict: Arguments to pass to the task
|
|
442
|
+
force: If True, ignore freshness and re-run all tasks
|
|
443
|
+
only: If True, run only the specified task without dependencies (implies force=True)
|
|
414
444
|
|
|
415
445
|
Returns:
|
|
416
|
-
|
|
446
|
+
Dictionary of task names to their execution status
|
|
417
447
|
|
|
418
448
|
Raises:
|
|
419
|
-
|
|
449
|
+
ExecutionError: If task execution fails
|
|
450
|
+
@athena: 1c293ee6a6fa
|
|
420
451
|
"""
|
|
421
452
|
if args_dict is None:
|
|
422
453
|
args_dict = {}
|
|
@@ -437,6 +468,10 @@ class Executor:
|
|
|
437
468
|
# This substitutes {{ dep.*.outputs.* }} templates before execution
|
|
438
469
|
resolve_dependency_output_references(self.recipe, execution_order)
|
|
439
470
|
|
|
471
|
+
# Resolve self-references in topological order
|
|
472
|
+
# This substitutes {{ self.inputs.* }} and {{ self.outputs.* }} templates
|
|
473
|
+
resolve_self_references(self.recipe, execution_order)
|
|
474
|
+
|
|
440
475
|
# Single phase: Check and execute incrementally
|
|
441
476
|
statuses: dict[str, TaskStatus] = {}
|
|
442
477
|
for name, task_args in execution_order:
|
|
@@ -481,14 +516,16 @@ class Executor:
|
|
|
481
516
|
return statuses
|
|
482
517
|
|
|
483
518
|
def _run_task(self, task: Task, args_dict: dict[str, Any]) -> None:
|
|
484
|
-
"""
|
|
519
|
+
"""
|
|
520
|
+
Execute a single task.
|
|
485
521
|
|
|
486
522
|
Args:
|
|
487
|
-
|
|
488
|
-
|
|
523
|
+
task: Task to execute
|
|
524
|
+
args_dict: Arguments to substitute in command
|
|
489
525
|
|
|
490
526
|
Raises:
|
|
491
|
-
|
|
527
|
+
ExecutionError: If task execution fails
|
|
528
|
+
@athena: 885c66658550
|
|
492
529
|
"""
|
|
493
530
|
# Capture timestamp at task start for consistency (in UTC)
|
|
494
531
|
task_start_time = datetime.now(timezone.utc)
|
|
@@ -562,18 +599,20 @@ class Executor:
|
|
|
562
599
|
self, cmd: str, working_dir: Path, task_name: str, shell: str, shell_args: list[str],
|
|
563
600
|
exported_env_vars: dict[str, str] | None = None
|
|
564
601
|
) -> None:
|
|
565
|
-
"""
|
|
602
|
+
"""
|
|
603
|
+
Execute a single-line command via shell.
|
|
566
604
|
|
|
567
605
|
Args:
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
606
|
+
cmd: Command string
|
|
607
|
+
working_dir: Working directory
|
|
608
|
+
task_name: Task name (for error messages)
|
|
609
|
+
shell: Shell executable to use
|
|
610
|
+
shell_args: Arguments to pass to shell
|
|
611
|
+
exported_env_vars: Exported arguments to set as environment variables
|
|
574
612
|
|
|
575
613
|
Raises:
|
|
576
|
-
|
|
614
|
+
ExecutionError: If command execution fails
|
|
615
|
+
@athena: 46849e6a0bbb
|
|
577
616
|
"""
|
|
578
617
|
# Prepare environment with exported args
|
|
579
618
|
env = self._prepare_env_with_exports(exported_env_vars)
|
|
@@ -597,18 +636,20 @@ class Executor:
|
|
|
597
636
|
self, cmd: str, working_dir: Path, task_name: str, shell: str, preamble: str,
|
|
598
637
|
exported_env_vars: dict[str, str] | None = None
|
|
599
638
|
) -> None:
|
|
600
|
-
"""
|
|
639
|
+
"""
|
|
640
|
+
Execute a multi-line command via temporary script file.
|
|
601
641
|
|
|
602
642
|
Args:
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
643
|
+
cmd: Multi-line command string
|
|
644
|
+
working_dir: Working directory
|
|
645
|
+
task_name: Task name (for error messages)
|
|
646
|
+
shell: Shell to use for script execution
|
|
647
|
+
preamble: Preamble text to prepend to script
|
|
648
|
+
exported_env_vars: Exported arguments to set as environment variables
|
|
609
649
|
|
|
610
650
|
Raises:
|
|
611
|
-
|
|
651
|
+
ExecutionError: If command execution fails
|
|
652
|
+
@athena: 825892b6db05
|
|
612
653
|
"""
|
|
613
654
|
# Prepare environment with exported args
|
|
614
655
|
env = self._prepare_env_with_exports(exported_env_vars)
|
|
@@ -667,17 +708,19 @@ class Executor:
|
|
|
667
708
|
pass # Ignore cleanup errors
|
|
668
709
|
|
|
669
710
|
def _substitute_builtin_in_environment(self, env: Environment, builtin_vars: dict[str, str]) -> Environment:
|
|
670
|
-
"""
|
|
711
|
+
"""
|
|
712
|
+
Substitute builtin and environment variables in environment fields.
|
|
671
713
|
|
|
672
714
|
Args:
|
|
673
|
-
|
|
674
|
-
|
|
715
|
+
env: Environment to process
|
|
716
|
+
builtin_vars: Built-in variable values
|
|
675
717
|
|
|
676
718
|
Returns:
|
|
677
|
-
|
|
719
|
+
New Environment with builtin and environment variables substituted
|
|
678
720
|
|
|
679
721
|
Raises:
|
|
680
|
-
|
|
722
|
+
ValueError: If builtin variable or environment variable is not defined
|
|
723
|
+
@athena: 21e2ccd27dbb
|
|
681
724
|
"""
|
|
682
725
|
from dataclasses import replace
|
|
683
726
|
|
|
@@ -724,17 +767,19 @@ class Executor:
|
|
|
724
767
|
self, task: Task, env: Any, cmd: str, working_dir: Path,
|
|
725
768
|
exported_env_vars: dict[str, str] | None = None
|
|
726
769
|
) -> None:
|
|
727
|
-
"""
|
|
770
|
+
"""
|
|
771
|
+
Execute task inside Docker container.
|
|
728
772
|
|
|
729
773
|
Args:
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
774
|
+
task: Task to execute
|
|
775
|
+
env: Docker environment configuration
|
|
776
|
+
cmd: Command to execute
|
|
777
|
+
working_dir: Host working directory
|
|
778
|
+
exported_env_vars: Exported arguments to set as environment variables
|
|
735
779
|
|
|
736
780
|
Raises:
|
|
737
|
-
|
|
781
|
+
ExecutionError: If Docker execution fails
|
|
782
|
+
@athena: fe972e4c97a3
|
|
738
783
|
"""
|
|
739
784
|
# Get builtin variables for substitution in environment fields
|
|
740
785
|
task_start_time = datetime.now(timezone.utc)
|
|
@@ -776,15 +821,17 @@ class Executor:
|
|
|
776
821
|
raise ExecutionError(str(e)) from e
|
|
777
822
|
|
|
778
823
|
def _validate_no_working_dir_circular_ref(self, text: str) -> None:
|
|
779
|
-
"""
|
|
824
|
+
"""
|
|
825
|
+
Validate that working_dir field does not contain {{ tt.working_dir }}.
|
|
780
826
|
|
|
781
827
|
Using {{ tt.working_dir }} in the working_dir field creates a circular dependency.
|
|
782
828
|
|
|
783
829
|
Args:
|
|
784
|
-
|
|
830
|
+
text: The working_dir field value to validate
|
|
785
831
|
|
|
786
832
|
Raises:
|
|
787
|
-
|
|
833
|
+
ExecutionError: If {{ tt.working_dir }} placeholder is found
|
|
834
|
+
@athena: 5dc6ee41d403
|
|
788
835
|
"""
|
|
789
836
|
import re
|
|
790
837
|
# Pattern to match {{ tt.working_dir }} specifically
|
|
@@ -798,70 +845,86 @@ class Executor:
|
|
|
798
845
|
)
|
|
799
846
|
|
|
800
847
|
def _substitute_builtin(self, text: str, builtin_vars: dict[str, str]) -> str:
|
|
801
|
-
"""
|
|
848
|
+
"""
|
|
849
|
+
Substitute {{ tt.name }} placeholders in text.
|
|
802
850
|
|
|
803
851
|
Built-in variables are resolved at execution time.
|
|
804
852
|
|
|
805
853
|
Args:
|
|
806
|
-
|
|
807
|
-
|
|
854
|
+
text: Text with {{ tt.name }} placeholders
|
|
855
|
+
builtin_vars: Built-in variable values
|
|
808
856
|
|
|
809
857
|
Returns:
|
|
810
|
-
|
|
858
|
+
Text with built-in variables substituted
|
|
811
859
|
|
|
812
860
|
Raises:
|
|
813
|
-
|
|
861
|
+
ValueError: If built-in variable is not defined
|
|
862
|
+
@athena: 463600a203f4
|
|
814
863
|
"""
|
|
815
864
|
from tasktree.substitution import substitute_builtin_variables
|
|
816
865
|
return substitute_builtin_variables(text, builtin_vars)
|
|
817
866
|
|
|
818
867
|
def _substitute_args(self, cmd: str, args_dict: dict[str, Any], exported_args: set[str] | None = None) -> str:
|
|
819
|
-
"""
|
|
868
|
+
"""
|
|
869
|
+
Substitute {{ arg.name }} placeholders in command string.
|
|
820
870
|
|
|
821
871
|
Variables are already substituted at parse time by the parser.
|
|
822
872
|
This only handles runtime argument substitution.
|
|
823
873
|
|
|
824
874
|
Args:
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
875
|
+
cmd: Command with {{ arg.name }} placeholders
|
|
876
|
+
args_dict: Argument values to substitute (only regular args)
|
|
877
|
+
exported_args: Set of argument names that are exported (not available for substitution)
|
|
828
878
|
|
|
829
879
|
Returns:
|
|
830
|
-
|
|
880
|
+
Command with arguments substituted
|
|
831
881
|
|
|
832
882
|
Raises:
|
|
833
|
-
|
|
883
|
+
ValueError: If an exported argument is used in template substitution
|
|
884
|
+
@athena: 4261a91c6a98
|
|
834
885
|
"""
|
|
835
886
|
from tasktree.substitution import substitute_arguments
|
|
836
887
|
return substitute_arguments(cmd, args_dict, exported_args)
|
|
837
888
|
|
|
838
889
|
def _substitute_env(self, text: str) -> str:
|
|
839
|
-
"""
|
|
890
|
+
"""
|
|
891
|
+
Substitute {{ env.NAME }} placeholders in text.
|
|
840
892
|
|
|
841
893
|
Environment variables are resolved at execution time from os.environ.
|
|
842
894
|
|
|
843
895
|
Args:
|
|
844
|
-
|
|
896
|
+
text: Text with {{ env.NAME }} placeholders
|
|
845
897
|
|
|
846
898
|
Returns:
|
|
847
|
-
|
|
899
|
+
Text with environment variables substituted
|
|
848
900
|
|
|
849
901
|
Raises:
|
|
850
|
-
|
|
902
|
+
ValueError: If environment variable is not set
|
|
903
|
+
@athena: 63becab531cd
|
|
851
904
|
"""
|
|
852
905
|
from tasktree.substitution import substitute_environment
|
|
853
906
|
return substitute_environment(text)
|
|
854
907
|
|
|
855
908
|
def _get_all_inputs(self, task: Task) -> list[str]:
|
|
856
|
-
"""
|
|
909
|
+
"""
|
|
910
|
+
Get all inputs for a task (explicit + implicit from dependencies).
|
|
857
911
|
|
|
858
912
|
Args:
|
|
859
|
-
|
|
913
|
+
task: Task to get inputs for
|
|
860
914
|
|
|
861
915
|
Returns:
|
|
862
|
-
|
|
916
|
+
List of input glob patterns
|
|
917
|
+
@athena: ca7ed7a6682f
|
|
863
918
|
"""
|
|
864
|
-
|
|
919
|
+
# Extract paths from inputs (handle both anonymous strings and named dicts)
|
|
920
|
+
all_inputs = []
|
|
921
|
+
for inp in task.inputs:
|
|
922
|
+
if isinstance(inp, str):
|
|
923
|
+
all_inputs.append(inp)
|
|
924
|
+
elif isinstance(inp, dict):
|
|
925
|
+
# Named input - extract the path value(s)
|
|
926
|
+
all_inputs.extend(inp.values())
|
|
927
|
+
|
|
865
928
|
implicit_inputs = get_implicit_inputs(self.recipe, task)
|
|
866
929
|
all_inputs.extend(implicit_inputs)
|
|
867
930
|
return all_inputs
|
|
@@ -869,18 +932,20 @@ class Executor:
|
|
|
869
932
|
def _check_environment_changed(
|
|
870
933
|
self, task: Task, cached_state: TaskState, env_name: str
|
|
871
934
|
) -> bool:
|
|
872
|
-
"""
|
|
935
|
+
"""
|
|
936
|
+
Check if environment definition has changed since last run.
|
|
873
937
|
|
|
874
938
|
For shell environments: checks YAML definition hash
|
|
875
939
|
For Docker environments: checks YAML hash AND Docker image ID
|
|
876
940
|
|
|
877
941
|
Args:
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
942
|
+
task: Task to check
|
|
943
|
+
cached_state: Cached state from previous run
|
|
944
|
+
env_name: Effective environment name (from _get_effective_env_name)
|
|
881
945
|
|
|
882
946
|
Returns:
|
|
883
|
-
|
|
947
|
+
True if environment definition changed, False otherwise
|
|
948
|
+
@athena: 052561b75455
|
|
884
949
|
"""
|
|
885
950
|
# If using platform default (no environment), no definition to track
|
|
886
951
|
if not env_name:
|
|
@@ -919,18 +984,20 @@ class Executor:
|
|
|
919
984
|
def _check_docker_image_changed(
|
|
920
985
|
self, env: Environment, cached_state: TaskState, env_name: str
|
|
921
986
|
) -> bool:
|
|
922
|
-
"""
|
|
987
|
+
"""
|
|
988
|
+
Check if Docker image ID has changed.
|
|
923
989
|
|
|
924
990
|
Builds the image and compares the resulting image ID with the cached ID.
|
|
925
991
|
This detects changes from unpinned base images, network-dependent builds, etc.
|
|
926
992
|
|
|
927
993
|
Args:
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
994
|
+
env: Docker environment definition
|
|
995
|
+
cached_state: Cached state from previous run
|
|
996
|
+
env_name: Environment name
|
|
931
997
|
|
|
932
998
|
Returns:
|
|
933
|
-
|
|
999
|
+
True if image ID changed, False otherwise
|
|
1000
|
+
@athena: 8af77cb1be44
|
|
934
1001
|
"""
|
|
935
1002
|
# Build/ensure image is built and get its ID
|
|
936
1003
|
try:
|
|
@@ -953,7 +1020,8 @@ class Executor:
|
|
|
953
1020
|
def _check_inputs_changed(
|
|
954
1021
|
self, task: Task, cached_state: TaskState, all_inputs: list[str]
|
|
955
1022
|
) -> list[str]:
|
|
956
|
-
"""
|
|
1023
|
+
"""
|
|
1024
|
+
Check if any input files have changed since last run.
|
|
957
1025
|
|
|
958
1026
|
Handles both regular file inputs and Docker-specific inputs:
|
|
959
1027
|
- Regular files: checked via mtime
|
|
@@ -961,12 +1029,13 @@ class Executor:
|
|
|
961
1029
|
- Dockerfile digests: checked via parsing and comparison
|
|
962
1030
|
|
|
963
1031
|
Args:
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1032
|
+
task: Task to check
|
|
1033
|
+
cached_state: Cached state from previous run
|
|
1034
|
+
all_inputs: All input glob patterns
|
|
967
1035
|
|
|
968
1036
|
Returns:
|
|
969
|
-
|
|
1037
|
+
List of changed file paths
|
|
1038
|
+
@athena: 15b13fd181bf
|
|
970
1039
|
"""
|
|
971
1040
|
changed_files = []
|
|
972
1041
|
|
|
@@ -1047,13 +1116,15 @@ class Executor:
|
|
|
1047
1116
|
return changed_files
|
|
1048
1117
|
|
|
1049
1118
|
def _expand_output_paths(self, task: Task) -> list[str]:
|
|
1050
|
-
"""
|
|
1119
|
+
"""
|
|
1120
|
+
Extract all output paths from task outputs (both named and anonymous).
|
|
1051
1121
|
|
|
1052
1122
|
Args:
|
|
1053
|
-
|
|
1123
|
+
task: Task with outputs to extract
|
|
1054
1124
|
|
|
1055
1125
|
Returns:
|
|
1056
|
-
|
|
1126
|
+
List of output path patterns (glob patterns as strings)
|
|
1127
|
+
@athena: 848a28564b14
|
|
1057
1128
|
"""
|
|
1058
1129
|
paths = []
|
|
1059
1130
|
for output in task.outputs:
|
|
@@ -1066,13 +1137,15 @@ class Executor:
|
|
|
1066
1137
|
return paths
|
|
1067
1138
|
|
|
1068
1139
|
def _check_outputs_missing(self, task: Task) -> list[str]:
|
|
1069
|
-
"""
|
|
1140
|
+
"""
|
|
1141
|
+
Check if any declared outputs are missing.
|
|
1070
1142
|
|
|
1071
1143
|
Args:
|
|
1072
|
-
|
|
1144
|
+
task: Task to check
|
|
1073
1145
|
|
|
1074
1146
|
Returns:
|
|
1075
|
-
|
|
1147
|
+
List of output patterns that have no matching files
|
|
1148
|
+
@athena: 9ceac49b4e68
|
|
1076
1149
|
"""
|
|
1077
1150
|
if not task.outputs:
|
|
1078
1151
|
return []
|
|
@@ -1092,14 +1165,16 @@ class Executor:
|
|
|
1092
1165
|
return missing_patterns
|
|
1093
1166
|
|
|
1094
1167
|
def _expand_globs(self, patterns: list[str], working_dir: str) -> list[str]:
|
|
1095
|
-
"""
|
|
1168
|
+
"""
|
|
1169
|
+
Expand glob patterns to actual file paths.
|
|
1096
1170
|
|
|
1097
1171
|
Args:
|
|
1098
|
-
|
|
1099
|
-
|
|
1172
|
+
patterns: List of glob patterns
|
|
1173
|
+
working_dir: Working directory to resolve patterns from
|
|
1100
1174
|
|
|
1101
1175
|
Returns:
|
|
1102
|
-
|
|
1176
|
+
List of file paths (relative to working_dir)
|
|
1177
|
+
@athena: 5ba093866558
|
|
1103
1178
|
"""
|
|
1104
1179
|
files = []
|
|
1105
1180
|
base_path = self.recipe.project_root / working_dir
|
|
@@ -1116,11 +1191,13 @@ class Executor:
|
|
|
1116
1191
|
return files
|
|
1117
1192
|
|
|
1118
1193
|
def _update_state(self, task: Task, args_dict: dict[str, Any]) -> None:
|
|
1119
|
-
"""
|
|
1194
|
+
"""
|
|
1195
|
+
Update state after task execution.
|
|
1120
1196
|
|
|
1121
1197
|
Args:
|
|
1122
|
-
|
|
1123
|
-
|
|
1198
|
+
task: Task that was executed
|
|
1199
|
+
args_dict: Arguments used for execution
|
|
1200
|
+
@athena: 1fcfdfcb9be9
|
|
1124
1201
|
"""
|
|
1125
1202
|
# Compute hashes (include effective environment and dependencies)
|
|
1126
1203
|
effective_env = self._get_effective_env_name(task)
|