tasktree 0.0.20__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 +4 -1
- tasktree/cli.py +107 -29
- tasktree/docker.py +87 -53
- tasktree/executor.py +194 -129
- tasktree/graph.py +123 -72
- tasktree/hasher.py +68 -19
- tasktree/parser.py +340 -229
- tasktree/state.py +46 -17
- tasktree/substitution.py +162 -103
- tasktree/types.py +50 -12
- {tasktree-0.0.20.dist-info → tasktree-0.0.21.dist-info}/METADATA +135 -7
- tasktree-0.0.21.dist-info/RECORD +14 -0
- tasktree-0.0.20.dist-info/RECORD +0 -14
- {tasktree-0.0.20.dist-info → tasktree-0.0.21.dist-info}/WHEEL +0 -0
- {tasktree-0.0.20.dist-info → tasktree-0.0.21.dist-info}/entry_points.txt +0 -0
tasktree/executor.py
CHANGED
|
@@ -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 = {}
|
|
@@ -485,14 +516,16 @@ class Executor:
|
|
|
485
516
|
return statuses
|
|
486
517
|
|
|
487
518
|
def _run_task(self, task: Task, args_dict: dict[str, Any]) -> None:
|
|
488
|
-
"""
|
|
519
|
+
"""
|
|
520
|
+
Execute a single task.
|
|
489
521
|
|
|
490
522
|
Args:
|
|
491
|
-
|
|
492
|
-
|
|
523
|
+
task: Task to execute
|
|
524
|
+
args_dict: Arguments to substitute in command
|
|
493
525
|
|
|
494
526
|
Raises:
|
|
495
|
-
|
|
527
|
+
ExecutionError: If task execution fails
|
|
528
|
+
@athena: 885c66658550
|
|
496
529
|
"""
|
|
497
530
|
# Capture timestamp at task start for consistency (in UTC)
|
|
498
531
|
task_start_time = datetime.now(timezone.utc)
|
|
@@ -566,18 +599,20 @@ class Executor:
|
|
|
566
599
|
self, cmd: str, working_dir: Path, task_name: str, shell: str, shell_args: list[str],
|
|
567
600
|
exported_env_vars: dict[str, str] | None = None
|
|
568
601
|
) -> None:
|
|
569
|
-
"""
|
|
602
|
+
"""
|
|
603
|
+
Execute a single-line command via shell.
|
|
570
604
|
|
|
571
605
|
Args:
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
|
578
612
|
|
|
579
613
|
Raises:
|
|
580
|
-
|
|
614
|
+
ExecutionError: If command execution fails
|
|
615
|
+
@athena: 46849e6a0bbb
|
|
581
616
|
"""
|
|
582
617
|
# Prepare environment with exported args
|
|
583
618
|
env = self._prepare_env_with_exports(exported_env_vars)
|
|
@@ -601,18 +636,20 @@ class Executor:
|
|
|
601
636
|
self, cmd: str, working_dir: Path, task_name: str, shell: str, preamble: str,
|
|
602
637
|
exported_env_vars: dict[str, str] | None = None
|
|
603
638
|
) -> None:
|
|
604
|
-
"""
|
|
639
|
+
"""
|
|
640
|
+
Execute a multi-line command via temporary script file.
|
|
605
641
|
|
|
606
642
|
Args:
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
|
613
649
|
|
|
614
650
|
Raises:
|
|
615
|
-
|
|
651
|
+
ExecutionError: If command execution fails
|
|
652
|
+
@athena: 825892b6db05
|
|
616
653
|
"""
|
|
617
654
|
# Prepare environment with exported args
|
|
618
655
|
env = self._prepare_env_with_exports(exported_env_vars)
|
|
@@ -671,17 +708,19 @@ class Executor:
|
|
|
671
708
|
pass # Ignore cleanup errors
|
|
672
709
|
|
|
673
710
|
def _substitute_builtin_in_environment(self, env: Environment, builtin_vars: dict[str, str]) -> Environment:
|
|
674
|
-
"""
|
|
711
|
+
"""
|
|
712
|
+
Substitute builtin and environment variables in environment fields.
|
|
675
713
|
|
|
676
714
|
Args:
|
|
677
|
-
|
|
678
|
-
|
|
715
|
+
env: Environment to process
|
|
716
|
+
builtin_vars: Built-in variable values
|
|
679
717
|
|
|
680
718
|
Returns:
|
|
681
|
-
|
|
719
|
+
New Environment with builtin and environment variables substituted
|
|
682
720
|
|
|
683
721
|
Raises:
|
|
684
|
-
|
|
722
|
+
ValueError: If builtin variable or environment variable is not defined
|
|
723
|
+
@athena: 21e2ccd27dbb
|
|
685
724
|
"""
|
|
686
725
|
from dataclasses import replace
|
|
687
726
|
|
|
@@ -728,17 +767,19 @@ class Executor:
|
|
|
728
767
|
self, task: Task, env: Any, cmd: str, working_dir: Path,
|
|
729
768
|
exported_env_vars: dict[str, str] | None = None
|
|
730
769
|
) -> None:
|
|
731
|
-
"""
|
|
770
|
+
"""
|
|
771
|
+
Execute task inside Docker container.
|
|
732
772
|
|
|
733
773
|
Args:
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
|
739
779
|
|
|
740
780
|
Raises:
|
|
741
|
-
|
|
781
|
+
ExecutionError: If Docker execution fails
|
|
782
|
+
@athena: fe972e4c97a3
|
|
742
783
|
"""
|
|
743
784
|
# Get builtin variables for substitution in environment fields
|
|
744
785
|
task_start_time = datetime.now(timezone.utc)
|
|
@@ -780,15 +821,17 @@ class Executor:
|
|
|
780
821
|
raise ExecutionError(str(e)) from e
|
|
781
822
|
|
|
782
823
|
def _validate_no_working_dir_circular_ref(self, text: str) -> None:
|
|
783
|
-
"""
|
|
824
|
+
"""
|
|
825
|
+
Validate that working_dir field does not contain {{ tt.working_dir }}.
|
|
784
826
|
|
|
785
827
|
Using {{ tt.working_dir }} in the working_dir field creates a circular dependency.
|
|
786
828
|
|
|
787
829
|
Args:
|
|
788
|
-
|
|
830
|
+
text: The working_dir field value to validate
|
|
789
831
|
|
|
790
832
|
Raises:
|
|
791
|
-
|
|
833
|
+
ExecutionError: If {{ tt.working_dir }} placeholder is found
|
|
834
|
+
@athena: 5dc6ee41d403
|
|
792
835
|
"""
|
|
793
836
|
import re
|
|
794
837
|
# Pattern to match {{ tt.working_dir }} specifically
|
|
@@ -802,68 +845,76 @@ class Executor:
|
|
|
802
845
|
)
|
|
803
846
|
|
|
804
847
|
def _substitute_builtin(self, text: str, builtin_vars: dict[str, str]) -> str:
|
|
805
|
-
"""
|
|
848
|
+
"""
|
|
849
|
+
Substitute {{ tt.name }} placeholders in text.
|
|
806
850
|
|
|
807
851
|
Built-in variables are resolved at execution time.
|
|
808
852
|
|
|
809
853
|
Args:
|
|
810
|
-
|
|
811
|
-
|
|
854
|
+
text: Text with {{ tt.name }} placeholders
|
|
855
|
+
builtin_vars: Built-in variable values
|
|
812
856
|
|
|
813
857
|
Returns:
|
|
814
|
-
|
|
858
|
+
Text with built-in variables substituted
|
|
815
859
|
|
|
816
860
|
Raises:
|
|
817
|
-
|
|
861
|
+
ValueError: If built-in variable is not defined
|
|
862
|
+
@athena: 463600a203f4
|
|
818
863
|
"""
|
|
819
864
|
from tasktree.substitution import substitute_builtin_variables
|
|
820
865
|
return substitute_builtin_variables(text, builtin_vars)
|
|
821
866
|
|
|
822
867
|
def _substitute_args(self, cmd: str, args_dict: dict[str, Any], exported_args: set[str] | None = None) -> str:
|
|
823
|
-
"""
|
|
868
|
+
"""
|
|
869
|
+
Substitute {{ arg.name }} placeholders in command string.
|
|
824
870
|
|
|
825
871
|
Variables are already substituted at parse time by the parser.
|
|
826
872
|
This only handles runtime argument substitution.
|
|
827
873
|
|
|
828
874
|
Args:
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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)
|
|
832
878
|
|
|
833
879
|
Returns:
|
|
834
|
-
|
|
880
|
+
Command with arguments substituted
|
|
835
881
|
|
|
836
882
|
Raises:
|
|
837
|
-
|
|
883
|
+
ValueError: If an exported argument is used in template substitution
|
|
884
|
+
@athena: 4261a91c6a98
|
|
838
885
|
"""
|
|
839
886
|
from tasktree.substitution import substitute_arguments
|
|
840
887
|
return substitute_arguments(cmd, args_dict, exported_args)
|
|
841
888
|
|
|
842
889
|
def _substitute_env(self, text: str) -> str:
|
|
843
|
-
"""
|
|
890
|
+
"""
|
|
891
|
+
Substitute {{ env.NAME }} placeholders in text.
|
|
844
892
|
|
|
845
893
|
Environment variables are resolved at execution time from os.environ.
|
|
846
894
|
|
|
847
895
|
Args:
|
|
848
|
-
|
|
896
|
+
text: Text with {{ env.NAME }} placeholders
|
|
849
897
|
|
|
850
898
|
Returns:
|
|
851
|
-
|
|
899
|
+
Text with environment variables substituted
|
|
852
900
|
|
|
853
901
|
Raises:
|
|
854
|
-
|
|
902
|
+
ValueError: If environment variable is not set
|
|
903
|
+
@athena: 63becab531cd
|
|
855
904
|
"""
|
|
856
905
|
from tasktree.substitution import substitute_environment
|
|
857
906
|
return substitute_environment(text)
|
|
858
907
|
|
|
859
908
|
def _get_all_inputs(self, task: Task) -> list[str]:
|
|
860
|
-
"""
|
|
909
|
+
"""
|
|
910
|
+
Get all inputs for a task (explicit + implicit from dependencies).
|
|
861
911
|
|
|
862
912
|
Args:
|
|
863
|
-
|
|
913
|
+
task: Task to get inputs for
|
|
864
914
|
|
|
865
915
|
Returns:
|
|
866
|
-
|
|
916
|
+
List of input glob patterns
|
|
917
|
+
@athena: ca7ed7a6682f
|
|
867
918
|
"""
|
|
868
919
|
# Extract paths from inputs (handle both anonymous strings and named dicts)
|
|
869
920
|
all_inputs = []
|
|
@@ -881,18 +932,20 @@ class Executor:
|
|
|
881
932
|
def _check_environment_changed(
|
|
882
933
|
self, task: Task, cached_state: TaskState, env_name: str
|
|
883
934
|
) -> bool:
|
|
884
|
-
"""
|
|
935
|
+
"""
|
|
936
|
+
Check if environment definition has changed since last run.
|
|
885
937
|
|
|
886
938
|
For shell environments: checks YAML definition hash
|
|
887
939
|
For Docker environments: checks YAML hash AND Docker image ID
|
|
888
940
|
|
|
889
941
|
Args:
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
942
|
+
task: Task to check
|
|
943
|
+
cached_state: Cached state from previous run
|
|
944
|
+
env_name: Effective environment name (from _get_effective_env_name)
|
|
893
945
|
|
|
894
946
|
Returns:
|
|
895
|
-
|
|
947
|
+
True if environment definition changed, False otherwise
|
|
948
|
+
@athena: 052561b75455
|
|
896
949
|
"""
|
|
897
950
|
# If using platform default (no environment), no definition to track
|
|
898
951
|
if not env_name:
|
|
@@ -931,18 +984,20 @@ class Executor:
|
|
|
931
984
|
def _check_docker_image_changed(
|
|
932
985
|
self, env: Environment, cached_state: TaskState, env_name: str
|
|
933
986
|
) -> bool:
|
|
934
|
-
"""
|
|
987
|
+
"""
|
|
988
|
+
Check if Docker image ID has changed.
|
|
935
989
|
|
|
936
990
|
Builds the image and compares the resulting image ID with the cached ID.
|
|
937
991
|
This detects changes from unpinned base images, network-dependent builds, etc.
|
|
938
992
|
|
|
939
993
|
Args:
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
994
|
+
env: Docker environment definition
|
|
995
|
+
cached_state: Cached state from previous run
|
|
996
|
+
env_name: Environment name
|
|
943
997
|
|
|
944
998
|
Returns:
|
|
945
|
-
|
|
999
|
+
True if image ID changed, False otherwise
|
|
1000
|
+
@athena: 8af77cb1be44
|
|
946
1001
|
"""
|
|
947
1002
|
# Build/ensure image is built and get its ID
|
|
948
1003
|
try:
|
|
@@ -965,7 +1020,8 @@ class Executor:
|
|
|
965
1020
|
def _check_inputs_changed(
|
|
966
1021
|
self, task: Task, cached_state: TaskState, all_inputs: list[str]
|
|
967
1022
|
) -> list[str]:
|
|
968
|
-
"""
|
|
1023
|
+
"""
|
|
1024
|
+
Check if any input files have changed since last run.
|
|
969
1025
|
|
|
970
1026
|
Handles both regular file inputs and Docker-specific inputs:
|
|
971
1027
|
- Regular files: checked via mtime
|
|
@@ -973,12 +1029,13 @@ class Executor:
|
|
|
973
1029
|
- Dockerfile digests: checked via parsing and comparison
|
|
974
1030
|
|
|
975
1031
|
Args:
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1032
|
+
task: Task to check
|
|
1033
|
+
cached_state: Cached state from previous run
|
|
1034
|
+
all_inputs: All input glob patterns
|
|
979
1035
|
|
|
980
1036
|
Returns:
|
|
981
|
-
|
|
1037
|
+
List of changed file paths
|
|
1038
|
+
@athena: 15b13fd181bf
|
|
982
1039
|
"""
|
|
983
1040
|
changed_files = []
|
|
984
1041
|
|
|
@@ -1059,13 +1116,15 @@ class Executor:
|
|
|
1059
1116
|
return changed_files
|
|
1060
1117
|
|
|
1061
1118
|
def _expand_output_paths(self, task: Task) -> list[str]:
|
|
1062
|
-
"""
|
|
1119
|
+
"""
|
|
1120
|
+
Extract all output paths from task outputs (both named and anonymous).
|
|
1063
1121
|
|
|
1064
1122
|
Args:
|
|
1065
|
-
|
|
1123
|
+
task: Task with outputs to extract
|
|
1066
1124
|
|
|
1067
1125
|
Returns:
|
|
1068
|
-
|
|
1126
|
+
List of output path patterns (glob patterns as strings)
|
|
1127
|
+
@athena: 848a28564b14
|
|
1069
1128
|
"""
|
|
1070
1129
|
paths = []
|
|
1071
1130
|
for output in task.outputs:
|
|
@@ -1078,13 +1137,15 @@ class Executor:
|
|
|
1078
1137
|
return paths
|
|
1079
1138
|
|
|
1080
1139
|
def _check_outputs_missing(self, task: Task) -> list[str]:
|
|
1081
|
-
"""
|
|
1140
|
+
"""
|
|
1141
|
+
Check if any declared outputs are missing.
|
|
1082
1142
|
|
|
1083
1143
|
Args:
|
|
1084
|
-
|
|
1144
|
+
task: Task to check
|
|
1085
1145
|
|
|
1086
1146
|
Returns:
|
|
1087
|
-
|
|
1147
|
+
List of output patterns that have no matching files
|
|
1148
|
+
@athena: 9ceac49b4e68
|
|
1088
1149
|
"""
|
|
1089
1150
|
if not task.outputs:
|
|
1090
1151
|
return []
|
|
@@ -1104,14 +1165,16 @@ class Executor:
|
|
|
1104
1165
|
return missing_patterns
|
|
1105
1166
|
|
|
1106
1167
|
def _expand_globs(self, patterns: list[str], working_dir: str) -> list[str]:
|
|
1107
|
-
"""
|
|
1168
|
+
"""
|
|
1169
|
+
Expand glob patterns to actual file paths.
|
|
1108
1170
|
|
|
1109
1171
|
Args:
|
|
1110
|
-
|
|
1111
|
-
|
|
1172
|
+
patterns: List of glob patterns
|
|
1173
|
+
working_dir: Working directory to resolve patterns from
|
|
1112
1174
|
|
|
1113
1175
|
Returns:
|
|
1114
|
-
|
|
1176
|
+
List of file paths (relative to working_dir)
|
|
1177
|
+
@athena: 5ba093866558
|
|
1115
1178
|
"""
|
|
1116
1179
|
files = []
|
|
1117
1180
|
base_path = self.recipe.project_root / working_dir
|
|
@@ -1128,11 +1191,13 @@ class Executor:
|
|
|
1128
1191
|
return files
|
|
1129
1192
|
|
|
1130
1193
|
def _update_state(self, task: Task, args_dict: dict[str, Any]) -> None:
|
|
1131
|
-
"""
|
|
1194
|
+
"""
|
|
1195
|
+
Update state after task execution.
|
|
1132
1196
|
|
|
1133
1197
|
Args:
|
|
1134
|
-
|
|
1135
|
-
|
|
1198
|
+
task: Task that was executed
|
|
1199
|
+
args_dict: Arguments used for execution
|
|
1200
|
+
@athena: 1fcfdfcb9be9
|
|
1136
1201
|
"""
|
|
1137
1202
|
# Compute hashes (include effective environment and dependencies)
|
|
1138
1203
|
effective_env = self._get_effective_env_name(task)
|