tasktree 0.0.22__py3-none-any.whl → 0.0.23__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 +1 -1
- tasktree/cli.py +145 -112
- tasktree/console_logger.py +66 -0
- tasktree/docker.py +14 -8
- tasktree/executor.py +161 -41
- tasktree/graph.py +3 -3
- tasktree/hasher.py +5 -5
- tasktree/logging.py +112 -0
- tasktree/parser.py +20 -17
- tasktree/process_runner.py +411 -0
- tasktree/substitution.py +2 -2
- tasktree/types.py +3 -3
- {tasktree-0.0.22.dist-info → tasktree-0.0.23.dist-info}/METADATA +201 -4
- tasktree-0.0.23.dist-info/RECORD +17 -0
- tasktree-0.0.22.dist-info/RECORD +0 -14
- {tasktree-0.0.22.dist-info → tasktree-0.0.23.dist-info}/WHEEL +0 -0
- {tasktree-0.0.22.dist-info → tasktree-0.0.23.dist-info}/entry_points.txt +0 -0
tasktree/docker.py
CHANGED
|
@@ -21,6 +21,7 @@ except ImportError:
|
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
23
|
from tasktree.parser import Environment
|
|
24
|
+
from tasktree.process_runner import ProcessRunner
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class DockerError(Exception):
|
|
@@ -35,7 +36,7 @@ class DockerError(Exception):
|
|
|
35
36
|
class DockerManager:
|
|
36
37
|
"""
|
|
37
38
|
Manages Docker image building and container execution.
|
|
38
|
-
@athena:
|
|
39
|
+
@athena: f8f5c2693d84
|
|
39
40
|
"""
|
|
40
41
|
|
|
41
42
|
def __init__(self, project_root: Path):
|
|
@@ -70,12 +71,15 @@ class DockerManager:
|
|
|
70
71
|
# Check if os.getuid() and os.getgid() are available (Linux/macOS)
|
|
71
72
|
return hasattr(os, "getuid") and hasattr(os, "getgid")
|
|
72
73
|
|
|
73
|
-
def ensure_image_built(
|
|
74
|
+
def ensure_image_built(
|
|
75
|
+
self, env: Environment, process_runner: ProcessRunner
|
|
76
|
+
) -> tuple[str, str]:
|
|
74
77
|
"""
|
|
75
78
|
Build Docker image if not already built this invocation.
|
|
76
79
|
|
|
77
80
|
Args:
|
|
78
81
|
env: Environment definition with dockerfile and context
|
|
82
|
+
process_runner: ProcessRunner instance for subprocess execution
|
|
79
83
|
|
|
80
84
|
Returns:
|
|
81
85
|
Tuple of (image_tag, image_id)
|
|
@@ -84,7 +88,7 @@ class DockerManager:
|
|
|
84
88
|
|
|
85
89
|
Raises:
|
|
86
90
|
DockerError: If docker command not available or build fails
|
|
87
|
-
@athena:
|
|
91
|
+
@athena: 42b53d2685e0
|
|
88
92
|
"""
|
|
89
93
|
# Check if already built this invocation
|
|
90
94
|
if env.name in self._built_images:
|
|
@@ -119,7 +123,7 @@ class DockerManager:
|
|
|
119
123
|
|
|
120
124
|
docker_build_cmd.append(str(context_path))
|
|
121
125
|
|
|
122
|
-
|
|
126
|
+
process_runner.run(
|
|
123
127
|
docker_build_cmd,
|
|
124
128
|
check=True,
|
|
125
129
|
capture_output=False, # Show build output to user
|
|
@@ -147,6 +151,7 @@ class DockerManager:
|
|
|
147
151
|
cmd: str,
|
|
148
152
|
working_dir: Path,
|
|
149
153
|
container_working_dir: str,
|
|
154
|
+
process_runner: ProcessRunner,
|
|
150
155
|
) -> subprocess.CompletedProcess:
|
|
151
156
|
"""
|
|
152
157
|
Execute command inside Docker container.
|
|
@@ -156,16 +161,17 @@ class DockerManager:
|
|
|
156
161
|
cmd: Command to execute
|
|
157
162
|
working_dir: Host working directory (for resolving relative volume paths)
|
|
158
163
|
container_working_dir: Working directory inside container
|
|
164
|
+
process_runner: ProcessRunner instance to use for subprocess execution
|
|
159
165
|
|
|
160
166
|
Returns:
|
|
161
167
|
CompletedProcess from subprocess.run
|
|
162
168
|
|
|
163
169
|
Raises:
|
|
164
170
|
DockerError: If docker run fails
|
|
165
|
-
@athena:
|
|
171
|
+
@athena: ef024ea2c182
|
|
166
172
|
"""
|
|
167
173
|
# Ensure image is built (returns tag and ID)
|
|
168
|
-
image_tag, image_id = self.ensure_image_built(env)
|
|
174
|
+
image_tag, image_id = self.ensure_image_built(env, process_runner)
|
|
169
175
|
|
|
170
176
|
# Build docker run command
|
|
171
177
|
docker_cmd = ["docker", "run", "--rm"]
|
|
@@ -208,7 +214,7 @@ class DockerManager:
|
|
|
208
214
|
|
|
209
215
|
# Execute
|
|
210
216
|
try:
|
|
211
|
-
result =
|
|
217
|
+
result = process_runner.run(
|
|
212
218
|
docker_cmd,
|
|
213
219
|
cwd=working_dir,
|
|
214
220
|
check=True,
|
|
@@ -361,7 +367,7 @@ def parse_dockerignore(dockerignore_path: Path) -> PathSpec | None:
|
|
|
361
367
|
|
|
362
368
|
Returns:
|
|
363
369
|
PathSpec object for matching, or None if file doesn't exist or pathspec not available
|
|
364
|
-
@athena:
|
|
370
|
+
@athena: 13fca9ee5a73
|
|
365
371
|
"""
|
|
366
372
|
if pathspec is None:
|
|
367
373
|
# pathspec library not available - can't parse .dockerignore
|
tasktree/executor.py
CHANGED
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import io
|
|
5
6
|
import os
|
|
6
7
|
import platform
|
|
7
8
|
import stat
|
|
8
9
|
import subprocess
|
|
10
|
+
import sys
|
|
9
11
|
import tempfile
|
|
10
12
|
import time
|
|
11
13
|
from dataclasses import dataclass, field
|
|
12
14
|
from datetime import datetime, timezone
|
|
13
15
|
from pathlib import Path
|
|
14
|
-
from typing import Any
|
|
16
|
+
from typing import Any, Callable
|
|
15
17
|
|
|
16
18
|
from tasktree import docker as docker_module
|
|
17
19
|
from tasktree.graph import (
|
|
@@ -21,7 +23,9 @@ from tasktree.graph import (
|
|
|
21
23
|
resolve_self_references,
|
|
22
24
|
)
|
|
23
25
|
from tasktree.hasher import hash_args, hash_task, make_cache_key
|
|
26
|
+
from tasktree.logging import Logger, LogLevel
|
|
24
27
|
from tasktree.parser import Recipe, Task, Environment
|
|
28
|
+
from tasktree.process_runner import ProcessRunner, TaskOutputTypes
|
|
25
29
|
from tasktree.state import StateManager, TaskState
|
|
26
30
|
from tasktree.hasher import hash_environment_definition
|
|
27
31
|
|
|
@@ -53,7 +57,7 @@ class ExecutionError(Exception):
|
|
|
53
57
|
class Executor:
|
|
54
58
|
"""
|
|
55
59
|
Executes tasks with incremental execution logic.
|
|
56
|
-
@athena:
|
|
60
|
+
@athena: 779b12944194
|
|
57
61
|
"""
|
|
58
62
|
|
|
59
63
|
# Protected environment variables that cannot be overridden by exported args
|
|
@@ -68,17 +72,27 @@ class Executor:
|
|
|
68
72
|
"LOGNAME",
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
def __init__(
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
recipe: Recipe,
|
|
78
|
+
state_manager: StateManager,
|
|
79
|
+
logger: Logger,
|
|
80
|
+
process_runner_factory: Callable[[TaskOutputTypes, Logger], ProcessRunner]
|
|
81
|
+
):
|
|
72
82
|
"""
|
|
73
83
|
Initialize executor.
|
|
74
84
|
|
|
75
85
|
Args:
|
|
76
86
|
recipe: Parsed recipe containing all tasks
|
|
77
87
|
state_manager: State manager for tracking task execution
|
|
78
|
-
|
|
88
|
+
logger_fn: Logger function for output (matches Console.print signature)
|
|
89
|
+
process_runner_factory: Factory function for creating ProcessRunner instances
|
|
90
|
+
@athena: d09e6a537c99
|
|
79
91
|
"""
|
|
80
92
|
self.recipe = recipe
|
|
81
93
|
self.state = state_manager
|
|
94
|
+
self.logger = logger
|
|
95
|
+
self._process_runner_factory = process_runner_factory
|
|
82
96
|
self.docker_manager = docker_module.DockerManager(recipe.project_root)
|
|
83
97
|
|
|
84
98
|
@staticmethod
|
|
@@ -91,7 +105,7 @@ class Executor:
|
|
|
91
105
|
|
|
92
106
|
Returns:
|
|
93
107
|
True if task has at least one regular (non-exported) argument, False otherwise
|
|
94
|
-
@athena:
|
|
108
|
+
@athena: c529cda63cce
|
|
95
109
|
"""
|
|
96
110
|
if not task.args:
|
|
97
111
|
return False
|
|
@@ -123,7 +137,7 @@ class Executor:
|
|
|
123
137
|
|
|
124
138
|
Returns:
|
|
125
139
|
Dictionary containing only regular (non-exported) arguments
|
|
126
|
-
@athena:
|
|
140
|
+
@athena: 1ae863406335
|
|
127
141
|
"""
|
|
128
142
|
if not task.args or not task_args:
|
|
129
143
|
return {}
|
|
@@ -160,7 +174,7 @@ class Executor:
|
|
|
160
174
|
|
|
161
175
|
Raises:
|
|
162
176
|
ExecutionError: If any built-in variable fails to resolve
|
|
163
|
-
@athena:
|
|
177
|
+
@athena: a0c1316fd713
|
|
164
178
|
"""
|
|
165
179
|
import os
|
|
166
180
|
|
|
@@ -214,7 +228,7 @@ class Executor:
|
|
|
214
228
|
|
|
215
229
|
Raises:
|
|
216
230
|
ExecutionError: If any built-in variable fails to resolve
|
|
217
|
-
@athena:
|
|
231
|
+
@athena: 7f6203e8d617
|
|
218
232
|
"""
|
|
219
233
|
# Get early builtin vars (those that don't depend on working_dir)
|
|
220
234
|
builtin_vars = self._collect_early_builtin_variables(task, timestamp)
|
|
@@ -343,6 +357,7 @@ class Executor:
|
|
|
343
357
|
self,
|
|
344
358
|
task: Task,
|
|
345
359
|
args_dict: dict[str, Any],
|
|
360
|
+
process_runner: ProcessRunner,
|
|
346
361
|
force: bool = False,
|
|
347
362
|
) -> TaskStatus:
|
|
348
363
|
"""
|
|
@@ -361,11 +376,12 @@ class Executor:
|
|
|
361
376
|
Args:
|
|
362
377
|
task: Task to check
|
|
363
378
|
args_dict: Arguments for this task execution
|
|
379
|
+
process_runner: ProcessRunner instance for subprocess execution
|
|
364
380
|
force: If True, ignore freshness and force execution
|
|
365
381
|
|
|
366
382
|
Returns:
|
|
367
383
|
TaskStatus indicating whether task will run and why
|
|
368
|
-
@athena:
|
|
384
|
+
@athena: 03922de1bd23
|
|
369
385
|
"""
|
|
370
386
|
# If force flag is set, always run
|
|
371
387
|
if force:
|
|
@@ -406,8 +422,9 @@ class Executor:
|
|
|
406
422
|
reason="never_run",
|
|
407
423
|
)
|
|
408
424
|
|
|
409
|
-
|
|
410
|
-
|
|
425
|
+
env_changed = self._check_environment_changed(
|
|
426
|
+
task, cached_state, effective_env, process_runner
|
|
427
|
+
)
|
|
411
428
|
if env_changed:
|
|
412
429
|
return TaskStatus(
|
|
413
430
|
task_name=task.name,
|
|
@@ -446,9 +463,21 @@ class Executor:
|
|
|
446
463
|
last_run=datetime.fromtimestamp(cached_state.last_run),
|
|
447
464
|
)
|
|
448
465
|
|
|
466
|
+
@staticmethod
|
|
467
|
+
def _get_task_output_type(user_inputted_value: TaskOutputTypes | None, task: Task) -> TaskOutputTypes:
|
|
468
|
+
if user_inputted_value is None:
|
|
469
|
+
if task.task_output is not None:
|
|
470
|
+
return task.task_output
|
|
471
|
+
|
|
472
|
+
return TaskOutputTypes.ALL
|
|
473
|
+
|
|
474
|
+
return user_inputted_value
|
|
475
|
+
|
|
476
|
+
|
|
449
477
|
def execute_task(
|
|
450
478
|
self,
|
|
451
479
|
task_name: str,
|
|
480
|
+
user_inputted_task_output_types: TaskOutputTypes | None,
|
|
452
481
|
args_dict: dict[str, Any] | None = None,
|
|
453
482
|
force: bool = False,
|
|
454
483
|
only: bool = False,
|
|
@@ -458,6 +487,7 @@ class Executor:
|
|
|
458
487
|
|
|
459
488
|
Args:
|
|
460
489
|
task_name: Name of task to execute
|
|
490
|
+
task_output_type: TaskOutputTypes enum value for controlling subprocess output
|
|
461
491
|
args_dict: Arguments to pass to the task
|
|
462
492
|
force: If True, ignore freshness and re-run all tasks
|
|
463
493
|
only: If True, run only the specified task without dependencies (implies force=True)
|
|
@@ -467,7 +497,7 @@ class Executor:
|
|
|
467
497
|
|
|
468
498
|
Raises:
|
|
469
499
|
ExecutionError: If task execution fails
|
|
470
|
-
@athena:
|
|
500
|
+
@athena: 4773fc590d9a
|
|
471
501
|
"""
|
|
472
502
|
if args_dict is None:
|
|
473
503
|
args_dict = {}
|
|
@@ -500,8 +530,12 @@ class Executor:
|
|
|
500
530
|
# Convert None to {} for internal use (None is used to distinguish simple deps in graph)
|
|
501
531
|
args_dict_for_execution = task_args if task_args is not None else {}
|
|
502
532
|
|
|
533
|
+
process_runner = self._process_runner_factory(self._get_task_output_type(user_inputted_task_output_types, task), self.logger)
|
|
534
|
+
|
|
503
535
|
# Check if task needs to run (based on CURRENT filesystem state)
|
|
504
|
-
status = self.check_task_status(
|
|
536
|
+
status = self.check_task_status(
|
|
537
|
+
task, args_dict_for_execution, process_runner, force=force
|
|
538
|
+
)
|
|
505
539
|
|
|
506
540
|
# Use a key that includes args for status tracking
|
|
507
541
|
# Only include regular (non-exported) args in status key for parameterized dependencies
|
|
@@ -532,28 +566,29 @@ class Executor:
|
|
|
532
566
|
if status.will_run:
|
|
533
567
|
# Warn if re-running due to missing outputs
|
|
534
568
|
if status.reason == "outputs_missing":
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
print(
|
|
569
|
+
self.logger.log(
|
|
570
|
+
LogLevel.WARN,
|
|
538
571
|
f"Warning: Re-running task '{name}' because declared outputs are missing",
|
|
539
|
-
file=sys.stderr,
|
|
540
572
|
)
|
|
541
573
|
|
|
542
|
-
self._run_task(task, args_dict_for_execution)
|
|
574
|
+
self._run_task(task, args_dict_for_execution, process_runner)
|
|
543
575
|
|
|
544
576
|
return statuses
|
|
545
577
|
|
|
546
|
-
def _run_task(
|
|
578
|
+
def _run_task(
|
|
579
|
+
self, task: Task, args_dict: dict[str, Any], process_runner: ProcessRunner
|
|
580
|
+
) -> None:
|
|
547
581
|
"""
|
|
548
582
|
Execute a single task.
|
|
549
583
|
|
|
550
584
|
Args:
|
|
551
585
|
task: Task to execute
|
|
552
586
|
args_dict: Arguments to substitute in command
|
|
587
|
+
process_runner: ProcessRunner instance for subprocess execution
|
|
553
588
|
|
|
554
589
|
Raises:
|
|
555
590
|
ExecutionError: If task execution fails
|
|
556
|
-
@athena:
|
|
591
|
+
@athena: b5abffeef10a
|
|
557
592
|
"""
|
|
558
593
|
# Capture timestamp at task start for consistency (in UTC)
|
|
559
594
|
task_start_time = datetime.now(timezone.utc)
|
|
@@ -611,17 +646,30 @@ class Executor:
|
|
|
611
646
|
env = self.recipe.get_environment(env_name)
|
|
612
647
|
|
|
613
648
|
# Execute command
|
|
614
|
-
|
|
649
|
+
self.logger.log(LogLevel.INFO, f"Running: {task.name}")
|
|
615
650
|
|
|
616
651
|
# Route to Docker execution or regular execution
|
|
617
652
|
if env and env.dockerfile:
|
|
618
653
|
# Docker execution path
|
|
619
|
-
self._run_task_in_docker(
|
|
654
|
+
self._run_task_in_docker(
|
|
655
|
+
task,
|
|
656
|
+
env,
|
|
657
|
+
cmd,
|
|
658
|
+
working_dir,
|
|
659
|
+
process_runner,
|
|
660
|
+
exported_env_vars,
|
|
661
|
+
)
|
|
620
662
|
else:
|
|
621
663
|
# Regular execution path - use unified script-based execution
|
|
622
664
|
shell, preamble = self._resolve_environment(task)
|
|
623
665
|
self._run_command_as_script(
|
|
624
|
-
cmd,
|
|
666
|
+
cmd,
|
|
667
|
+
working_dir,
|
|
668
|
+
task.name,
|
|
669
|
+
shell,
|
|
670
|
+
preamble,
|
|
671
|
+
process_runner,
|
|
672
|
+
exported_env_vars,
|
|
625
673
|
)
|
|
626
674
|
|
|
627
675
|
# Update state
|
|
@@ -634,6 +682,7 @@ class Executor:
|
|
|
634
682
|
task_name: str,
|
|
635
683
|
shell: str,
|
|
636
684
|
preamble: str,
|
|
685
|
+
process_runner: ProcessRunner,
|
|
637
686
|
exported_env_vars: dict[str, str] | None = None,
|
|
638
687
|
) -> None:
|
|
639
688
|
"""
|
|
@@ -649,12 +698,13 @@ class Executor:
|
|
|
649
698
|
task_name: Task name (for error messages)
|
|
650
699
|
shell: Shell to use for script execution
|
|
651
700
|
preamble: Preamble text to prepend to script
|
|
701
|
+
process_runner: ProcessRunner instance to use for subprocess execution
|
|
652
702
|
exported_env_vars: Exported arguments to set as environment variables
|
|
653
703
|
|
|
654
704
|
Raises:
|
|
655
705
|
ExecutionError: If command execution fails
|
|
656
706
|
@athena: TBD
|
|
657
|
-
@athena:
|
|
707
|
+
@athena: 228cc00e7665
|
|
658
708
|
"""
|
|
659
709
|
# Prepare environment with exported args
|
|
660
710
|
env = self._prepare_env_with_exports(exported_env_vars)
|
|
@@ -694,13 +744,56 @@ class Executor:
|
|
|
694
744
|
|
|
695
745
|
# Execute script file
|
|
696
746
|
try:
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
747
|
+
# Check if stdout/stderr support fileno() (real file descriptors)
|
|
748
|
+
# CliRunner uses StringIO which has fileno() method but raises when called
|
|
749
|
+
def supports_fileno(stream):
|
|
750
|
+
"""Check if a stream has a working fileno() method."""
|
|
751
|
+
try:
|
|
752
|
+
stream.fileno()
|
|
753
|
+
return True
|
|
754
|
+
except (AttributeError, OSError, io.UnsupportedOperation):
|
|
755
|
+
return False
|
|
756
|
+
|
|
757
|
+
# Determine output targets based on task_output mode
|
|
758
|
+
# For "all" mode: show everything
|
|
759
|
+
# Future modes: use subprocess.DEVNULL for suppression
|
|
760
|
+
should_suppress = False # Will be: self.task_output == "none", etc.
|
|
761
|
+
|
|
762
|
+
if should_suppress:
|
|
763
|
+
stdout_target = subprocess.DEVNULL
|
|
764
|
+
stderr_target = subprocess.DEVNULL
|
|
765
|
+
else:
|
|
766
|
+
stdout_target = sys.stdout
|
|
767
|
+
stderr_target = sys.stderr
|
|
768
|
+
|
|
769
|
+
# If streams support fileno, pass target streams directly (most efficient)
|
|
770
|
+
# Otherwise capture and manually write (CliRunner compatibility)
|
|
771
|
+
if not should_suppress and not (
|
|
772
|
+
supports_fileno(sys.stdout) and supports_fileno(sys.stderr)
|
|
773
|
+
):
|
|
774
|
+
# CliRunner path: capture and write manually
|
|
775
|
+
result = process_runner.run(
|
|
776
|
+
[script_path],
|
|
777
|
+
cwd=working_dir,
|
|
778
|
+
check=True,
|
|
779
|
+
capture_output=True,
|
|
780
|
+
text=True,
|
|
781
|
+
env=env,
|
|
782
|
+
)
|
|
783
|
+
if result.stdout:
|
|
784
|
+
sys.stdout.write(result.stdout)
|
|
785
|
+
if result.stderr:
|
|
786
|
+
sys.stderr.write(result.stderr)
|
|
787
|
+
else:
|
|
788
|
+
# Normal execution path: use target streams (including DEVNULL when suppressing)
|
|
789
|
+
process_runner.run(
|
|
790
|
+
[script_path],
|
|
791
|
+
cwd=working_dir,
|
|
792
|
+
check=True,
|
|
793
|
+
stdout=stdout_target,
|
|
794
|
+
stderr=stderr_target,
|
|
795
|
+
env=env,
|
|
796
|
+
)
|
|
704
797
|
except subprocess.CalledProcessError as e:
|
|
705
798
|
raise ExecutionError(
|
|
706
799
|
f"Task '{task_name}' failed with exit code {e.returncode}"
|
|
@@ -727,7 +820,7 @@ class Executor:
|
|
|
727
820
|
|
|
728
821
|
Raises:
|
|
729
822
|
ValueError: If builtin variable or environment variable is not defined
|
|
730
|
-
@athena:
|
|
823
|
+
@athena: eba6e3d62062
|
|
731
824
|
"""
|
|
732
825
|
from dataclasses import replace
|
|
733
826
|
|
|
@@ -798,6 +891,7 @@ class Executor:
|
|
|
798
891
|
env: Any,
|
|
799
892
|
cmd: str,
|
|
800
893
|
working_dir: Path,
|
|
894
|
+
process_runner: ProcessRunner,
|
|
801
895
|
exported_env_vars: dict[str, str] | None = None,
|
|
802
896
|
) -> None:
|
|
803
897
|
"""
|
|
@@ -808,11 +902,13 @@ class Executor:
|
|
|
808
902
|
env: Docker environment configuration
|
|
809
903
|
cmd: Command to execute
|
|
810
904
|
working_dir: Host working directory
|
|
905
|
+
process_runner: ProcessRunner instance to use for subprocess execution
|
|
811
906
|
exported_env_vars: Exported arguments to set as environment variables
|
|
907
|
+
task_output: Control task subprocess output (all, out, err, on-err, none)
|
|
812
908
|
|
|
813
909
|
Raises:
|
|
814
910
|
ExecutionError: If Docker execution fails
|
|
815
|
-
@athena:
|
|
911
|
+
@athena: 61725a57e304
|
|
816
912
|
"""
|
|
817
913
|
# Get builtin variables for substitution in environment fields
|
|
818
914
|
task_start_time = datetime.now(timezone.utc)
|
|
@@ -852,6 +948,7 @@ class Executor:
|
|
|
852
948
|
cmd=cmd,
|
|
853
949
|
working_dir=working_dir,
|
|
854
950
|
container_working_dir=container_working_dir,
|
|
951
|
+
process_runner=process_runner,
|
|
855
952
|
)
|
|
856
953
|
except docker_module.DockerError as e:
|
|
857
954
|
raise ExecutionError(str(e)) from e
|
|
@@ -868,7 +965,7 @@ class Executor:
|
|
|
868
965
|
|
|
869
966
|
Raises:
|
|
870
967
|
ExecutionError: If {{ tt.working_dir }} placeholder is found
|
|
871
|
-
@athena:
|
|
968
|
+
@athena: 82822f02716a
|
|
872
969
|
"""
|
|
873
970
|
import re
|
|
874
971
|
|
|
@@ -977,7 +1074,11 @@ class Executor:
|
|
|
977
1074
|
|
|
978
1075
|
# TODO: Understand why task isn't used
|
|
979
1076
|
def _check_environment_changed(
|
|
980
|
-
self,
|
|
1077
|
+
self,
|
|
1078
|
+
task: Task,
|
|
1079
|
+
cached_state: TaskState,
|
|
1080
|
+
env_name: str,
|
|
1081
|
+
process_runner: ProcessRunner,
|
|
981
1082
|
) -> bool:
|
|
982
1083
|
"""
|
|
983
1084
|
Check if environment definition has changed since last run.
|
|
@@ -989,10 +1090,11 @@ class Executor:
|
|
|
989
1090
|
task: Task to check
|
|
990
1091
|
cached_state: Cached state from previous run
|
|
991
1092
|
env_name: Effective environment name (from _get_effective_env_name)
|
|
1093
|
+
process_runner: ProcessRunner instance for subprocess execution
|
|
992
1094
|
|
|
993
1095
|
Returns:
|
|
994
1096
|
True if environment definition changed, False otherwise
|
|
995
|
-
@athena:
|
|
1097
|
+
@athena: e206e104150a
|
|
996
1098
|
"""
|
|
997
1099
|
# If using platform default (no environment), no definition to track
|
|
998
1100
|
if not env_name:
|
|
@@ -1023,13 +1125,19 @@ class Executor:
|
|
|
1023
1125
|
|
|
1024
1126
|
# For Docker environments, also check if image ID changed
|
|
1025
1127
|
if env.dockerfile:
|
|
1026
|
-
return self._check_docker_image_changed(
|
|
1128
|
+
return self._check_docker_image_changed(
|
|
1129
|
+
env, cached_state, env_name, process_runner
|
|
1130
|
+
)
|
|
1027
1131
|
|
|
1028
1132
|
# Shell environment with unchanged hash
|
|
1029
1133
|
return False
|
|
1030
1134
|
|
|
1031
1135
|
def _check_docker_image_changed(
|
|
1032
|
-
self,
|
|
1136
|
+
self,
|
|
1137
|
+
env: Environment,
|
|
1138
|
+
cached_state: TaskState,
|
|
1139
|
+
env_name: str,
|
|
1140
|
+
process_runner: ProcessRunner,
|
|
1033
1141
|
) -> bool:
|
|
1034
1142
|
"""
|
|
1035
1143
|
Check if Docker image ID has changed.
|
|
@@ -1041,14 +1149,17 @@ class Executor:
|
|
|
1041
1149
|
env: Docker environment definition
|
|
1042
1150
|
cached_state: Cached state from previous run
|
|
1043
1151
|
env_name: Environment name
|
|
1152
|
+
process_runner: ProcessRunner instance for subprocess execution
|
|
1044
1153
|
|
|
1045
1154
|
Returns:
|
|
1046
1155
|
True if image ID changed, False otherwise
|
|
1047
|
-
@athena:
|
|
1156
|
+
@athena: bc954288e4ad
|
|
1048
1157
|
"""
|
|
1049
1158
|
# Build/ensure image is built and get its ID
|
|
1050
1159
|
try:
|
|
1051
|
-
image_tag, current_image_id = self.docker_manager.ensure_image_built(
|
|
1160
|
+
image_tag, current_image_id = self.docker_manager.ensure_image_built(
|
|
1161
|
+
env, process_runner
|
|
1162
|
+
)
|
|
1052
1163
|
except Exception:
|
|
1053
1164
|
# If we can't build, treat as changed (will fail later with better error)
|
|
1054
1165
|
return True
|
|
@@ -1243,7 +1354,7 @@ class Executor:
|
|
|
1243
1354
|
def _update_state(self, task: Task, args_dict: dict[str, Any]) -> None:
|
|
1244
1355
|
"""
|
|
1245
1356
|
Update state after task execution.
|
|
1246
|
-
@athena:
|
|
1357
|
+
@athena: f4d3efdaac7c
|
|
1247
1358
|
"""
|
|
1248
1359
|
cache_key = self._cache_key(task, args_dict)
|
|
1249
1360
|
input_state = self._input_files_to_modified_times(task)
|
|
@@ -1261,6 +1372,9 @@ class Executor:
|
|
|
1261
1372
|
self.state.save()
|
|
1262
1373
|
|
|
1263
1374
|
def _cache_key(self, task: Task, args_dict: dict[str, Any]) -> str:
|
|
1375
|
+
"""
|
|
1376
|
+
@athena: d20ce4090741
|
|
1377
|
+
"""
|
|
1264
1378
|
effective_env = self._get_effective_env_name(task)
|
|
1265
1379
|
task_hash = hash_task(
|
|
1266
1380
|
task.cmd,
|
|
@@ -1274,6 +1388,9 @@ class Executor:
|
|
|
1274
1388
|
return make_cache_key(task_hash, args_hash)
|
|
1275
1389
|
|
|
1276
1390
|
def _input_files_to_modified_times(self, task: Task) -> dict[str, float]:
|
|
1391
|
+
"""
|
|
1392
|
+
@athena: 7e5ba779a41f
|
|
1393
|
+
"""
|
|
1277
1394
|
input_files = self._expand_globs(self._get_all_inputs(task), task.working_dir)
|
|
1278
1395
|
|
|
1279
1396
|
input_state = {}
|
|
@@ -1291,6 +1408,9 @@ class Executor:
|
|
|
1291
1408
|
def _docker_inputs_to_modified_times(
|
|
1292
1409
|
self, env_name: str, env: Environment
|
|
1293
1410
|
) -> dict[str, float]:
|
|
1411
|
+
"""
|
|
1412
|
+
@athena: bfe53b0d56cd
|
|
1413
|
+
"""
|
|
1294
1414
|
input_state = dict()
|
|
1295
1415
|
# Record Dockerfile mtime
|
|
1296
1416
|
dockerfile_path = self.recipe.project_root / env.dockerfile
|
tasktree/graph.py
CHANGED
|
@@ -98,7 +98,7 @@ def resolve_dependency_invocation(
|
|
|
98
98
|
... recipe
|
|
99
99
|
... )
|
|
100
100
|
DependencyInvocation("build", {"mode": "production"})
|
|
101
|
-
@athena:
|
|
101
|
+
@athena: dd9a84821159
|
|
102
102
|
"""
|
|
103
103
|
# Simple string case - no args to substitute
|
|
104
104
|
if isinstance(dep_spec, str):
|
|
@@ -260,7 +260,7 @@ def resolve_execution_order(
|
|
|
260
260
|
Raises:
|
|
261
261
|
TaskNotFoundError: If target task or any dependency doesn't exist
|
|
262
262
|
CycleError: If a dependency cycle is detected
|
|
263
|
-
@athena:
|
|
263
|
+
@athena: 687f627efc75
|
|
264
264
|
"""
|
|
265
265
|
if target_task not in recipe.tasks:
|
|
266
266
|
raise TaskNotFoundError(f"Task not found: {target_task}")
|
|
@@ -607,7 +607,7 @@ def build_dependency_tree(
|
|
|
607
607
|
|
|
608
608
|
Returns:
|
|
609
609
|
Nested dictionary representing the dependency tree
|
|
610
|
-
@athena:
|
|
610
|
+
@athena: 8c853b393bdb
|
|
611
611
|
"""
|
|
612
612
|
if target_task not in recipe.tasks:
|
|
613
613
|
raise TaskNotFoundError(f"Task not found: {target_task}")
|
tasktree/hasher.py
CHANGED
|
@@ -35,12 +35,12 @@ def _normalize_choices_lists(
|
|
|
35
35
|
Normalize argument choices lists by sorting them for deterministic hashing.
|
|
36
36
|
|
|
37
37
|
Args:
|
|
38
|
-
|
|
38
|
+
args: List of argument specifications (strings or dicts)
|
|
39
39
|
|
|
40
40
|
Returns:
|
|
41
|
-
|
|
41
|
+
List of argument specs with sorted choices lists
|
|
42
42
|
|
|
43
|
-
@athena:
|
|
43
|
+
@athena: dc34b6fb09a6
|
|
44
44
|
"""
|
|
45
45
|
normalized_args = []
|
|
46
46
|
for arg in args:
|
|
@@ -114,7 +114,7 @@ def hash_task(
|
|
|
114
114
|
|
|
115
115
|
Returns:
|
|
116
116
|
8-character hash of task definition
|
|
117
|
-
@athena:
|
|
117
|
+
@athena: 815a47cf1af0
|
|
118
118
|
"""
|
|
119
119
|
data = {
|
|
120
120
|
"cmd": cmd,
|
|
@@ -173,7 +173,7 @@ def hash_environment_definition(env) -> str:
|
|
|
173
173
|
|
|
174
174
|
Returns:
|
|
175
175
|
16-character hash of environment definition
|
|
176
|
-
@athena:
|
|
176
|
+
@athena: a54f6c171ba9
|
|
177
177
|
"""
|
|
178
178
|
# Import inside function to avoid circular dependency
|
|
179
179
|
|