File indexing completed on 2026-08-12 08:24:57
0001 """ExecutionEngine for DAG-based workflow execution.
0002 ExecutionEngines are the smallest executable units in a workflow. They encapsulate
0003 the logic to execute a specific job type (bash command, Python function,
0004 Docker container, etc). Each execution engine runs within a JobContext and can
0005 exchange data via XCom.
0006
0007 Inspired by Apache Airflow's operator model, execution engines provide:
0008 - Job execution logic
0009 - Parameter handling and templating
0010 - Context-aware execution (logs, XCom, artifacts)
0011 - Failure handling and retries
0012
0013 Supported execution engines:
0014 BaseExecutionEngine: Abstract base for all execution engines
0015 BashExecutionEngine: Execute shell commands
0016 PythonExecutionEngine: Execute Python functions
0017 ContainerExecutionEngine: Execute Docker containers
0018
0019 Context hierarchy:
0020 JobContext: Execution context for a single job
0021 StageContext: Parameters shared across all jobs in a stage
0022 BranchContext: Parameters shared across all stages in a branch
0023
0024 Project: AID2E v0.0.0 - AI assisted Detector Design for EIC
0025 Homepage: https://aid2e.github.io/aid2e-framework
0026 Repository: https://github.com/aid2e/AID2E-framework.git
0027 """
0028
0029 from abc import ABC, abstractmethod
0030 from ast import literal_eval
0031 from typing import Any, Callable, Dict, List, Optional
0032 from dataclasses import dataclass, field
0033 from functools import reduce
0034 import subprocess
0035 import json
0036 import os
0037 import re
0038 from pathlib import Path
0039
0040 from aid2e.utilities.configurations.problem_config import (
0041 ProblemConfiguration,
0042 )
0043 from aid2e.utilities.configurations.experimental_stack_config import (
0044 StackLayerConfig,
0045 )
0046 from aid2e.utilities.configurations.stack_registry import StackRegistry
0047
0048 from .experimental_stack import ExperimentStack
0049
0050
0051 @dataclass
0052 class WorkflowSharedContext:
0053 """Context shared across all jobs of one workflow execution.
0054
0055 Attributes:
0056 workflow_id: Unique workflow identifier.
0057 parameters: Parameters available to all branches, stages, and
0058 jobs in this workflow.
0059 """
0060 workflow_id: str
0061 parameters: Dict[str, Any] = field(default_factory=dict)
0062
0063
0064 @dataclass
0065 class BranchContext:
0066 """Context for parameters shared across all stages in a branch.
0067
0068 Attributes:
0069 branch_id: Unique branch identifier.
0070 parameters: Parameters available to all stages in this branch.
0071 """
0072 branch_id: str
0073 parameters: Dict[str, Any] = field(default_factory=dict)
0074
0075
0076 @dataclass
0077 class StageContext:
0078 """Context for parameters shared across all jobs in a stage.
0079
0080 Attributes:
0081 stage_id: Unique stage identifier.
0082 parameters: Parameters available to all jobs in this stage.
0083 branch_context: Parent branch context (optional).
0084 """
0085 stage_id: str
0086 parameters: Dict[str, Any] = field(default_factory=dict)
0087 branch_context: Optional[BranchContext] = None
0088
0089
0090 @dataclass
0091 class JobContext:
0092 """Execution context for a single job (XCom-like data passing).
0093
0094 Holds job metadata, input/output data, logs, and artifacts.
0095 Enables data flow between jobs in a workflow.
0096
0097 Attributes:
0098 task_id: Key encoding stage, job ID. Used in XCom,
0099 formatted as {stage_id}:{job_id}
0100 job_id: Unique job identifier.
0101 stage_id: Parent stage identifier.
0102 workflow_id: Root workflow identifier.
0103 design_point: Input design point (optimizer output).
0104 xcom: Dict of data from upstream jobs (job_id:key → value).
0105 artifacts: Dict of output artifact paths produced by this job.
0106 logs: Execution logs (stdout/stderr).
0107 execution_dir: Working directory for job execution.
0108 stage_context: Parent stage context (optional).
0109 problem_config: Problem configuration for accessing stack-
0110 dependent design space
0111 workflow_context: Shared workflow context (optional).
0112 """
0113 task_id: str
0114 job_id: str
0115 stage_id: str
0116 workflow_id: str
0117 design_point: Dict[str, Any] = field(default_factory=dict)
0118 xcom: Dict[str, Any] = field(default_factory=dict)
0119 artifacts: Dict[str, str] = field(default_factory=dict)
0120 logs: List[str] = field(default_factory=list)
0121 execution_dir: Optional[str] = None
0122 output_dir: Optional[str] = None
0123 stage_context: Optional[StageContext] = None
0124 problem_config: Optional[ProblemConfiguration] = None
0125 workflow_context: Optional[WorkflowSharedContext] = None
0126
0127 def xcom_key(self, key: str, task_id: str) -> str:
0128 """Get xcom key for a given job, stage
0129
0130 Args:
0131 key: XCom key (e.g. 'return_value', 'metrics').
0132 task_id: Unique ID of job + stage (e.g. 'sim_stage:sim_job')
0133
0134 Returns:
0135 Key formatted as task_id:key
0136 """
0137 return f"{task_id}:{key}"
0138
0139 def xcom_push(self, key: str, value: Any) -> None:
0140 """Push data to XCom for downstream jobs.
0141
0142 Data stored in a dictionary with the format:
0143
0144 {'task_id:key': data}
0145
0146 Args:
0147 key: XCom data key (e.g., 'return_value', 'metrics').
0148 value: Data to push (any serializable type).
0149
0150 Example:
0151 >>> context.xcom_push('objectives', {'f1': 0.5, 'f2': 0.3})
0152 """
0153 xcom_key = self.xcom_key(key, self.task_id)
0154 self.xcom[xcom_key] = value
0155
0156 def xcom_pull(self, task_id: str, key: str = 'return_value') -> Any:
0157 """Pull data from upstream job's XCom.
0158
0159 Args:
0160 task_id: Upstream job key
0161 key: XCom data key (optional, default is 'return_value').
0162
0163 Returns:
0164 Data pushed by upstream job, or None if not found.
0165
0166 Example:
0167 >>> params = context.xcom_pull('prepare_params', key='params')
0168 """
0169 xcom_key = self.xcom_key(key, task_id)
0170 return self.xcom.get(xcom_key)
0171
0172 def add_log(self, message: str) -> None:
0173 """Add a log message.
0174
0175 Args:
0176 message: Log message.
0177 """
0178 self.logs.append(message)
0179
0180 def save_artifact(self, artifact_key: str, artifact_path: str) -> None:
0181 """Register an output artifact.
0182
0183 Args:
0184 artifact_key: Logical name for artifact (e.g., 'objectives').
0185 artifact_path: File path to artifact.
0186
0187 Example:
0188 >>> context.save_artifact('objectives', '/work/objectives.json')
0189 """
0190 self.artifacts[artifact_key] = artifact_path
0191
0192
0193 class Template:
0194 """class for common template substitutions
0195
0196 Supports:
0197 - {{design_point.key}} → Value for design parameter with name `key`
0198 - {{job_id}} → Name of current job
0199 - {{stage_id}} → Name of current stage
0200 - {{branch_id}} → Name of current branch
0201 - {{workflow_id}} → Name of workflow
0202 - {{execution_dir}} → Current working directory
0203 - {{output_dir}} → Current output directory
0204 - {{geometry_dir}} → Geometry directory to use
0205 - {{artifacts[key]}} → Artifact path ID'd by key
0206 - {{xcom[key]}} → Scalar XCom data ID'd by key
0207 - {{xcom[key](acc)}} → Non-scalar XCom data ID'd by key,
0208 accessed with acc
0209 - {{inputs[key](acc)}} → Stack layer input acc, ID'd by key
0210 - {{outputs[key](acc}} → Stack layer output acc, ID'd by key
0211 - {{arguments[key](acc}} → Stack layer argument acc, ID'd by key
0212
0213 Attributes:
0214 _substitutions: Dictionary of template variables onto lambdas
0215 to replace them. Format is {'pattern': 'rule'}.
0216 """
0217 _substitutions = {
0218 "{{design_point.key}}":
0219 (lambda text, context: reduce(lambda result, key: result.replace(f"{{{{design_point.{key[0]}}}}}", str(key[1])), context.design_point.items(), text)),
0220 "{{job_id}}":
0221 (lambda text, context: text.replace("{{job_id}}", str(context.job_id))),
0222 "{{stage_id}}":
0223 (lambda text, context: text.replace("{{stage_id}}", str(context.stage_id))),
0224 "{{branch_id}}":
0225 (lambda text, context: text.replace("{{branch_id}}", str(context.stage_context.branch_context.branch_id))
0226 if context.stage_context is not None and context.stage_context.branch_context is not None
0227 else text.replace("{{branch_id}}", "NotAvailable")),
0228 "{{workflow_id}}":
0229 (lambda text, context: text.replace("{{workflow_id}}", str(context.workflow_id))),
0230 "{{execution_dir}}":
0231 (lambda text, context: text.replace("{{execution_dir}}", str(context.execution_dir))),
0232 "{{output_dir}}":
0233 (lambda text, context: text.replace("{{output_dir}}", str(context.output_dir))),
0234 "{{geometry_dir}}":
0235 (lambda text, context: text.replace("{{geometry_dir}}", str(context.workflow_context.parameters["prepared_geometry_dir"]))
0236 if context.workflow_context is not None and "prepared_geometry_dir" in context.workflow_context.parameters
0237 else text.replace("{{geometry_dir}}", "NotAvailable")),
0238 "{{artifacts[key]}}":
0239 (lambda text, context:
0240 re.sub(r"{{artifacts\[(.*?)\]}}", lambda match: str(context.artifacts[match.group(1)]), text)),
0241 "{{xcom[key]}}":
0242 (lambda text, context:
0243 re.sub(r"{{xcom\[(.*?)\]}}", lambda match: str(context.xcom[match.group(1)]), text)),
0244 "{{xcom[key](acc)}}":
0245 (lambda text, context: re.sub(r"{{xcom\[(.*?)\]\((.*?)\)}}", lambda match: str(context.xcom[match.group(1)][literal_eval(match.group(2))]), text)),
0246 "{{inputs[key](acc)}}":
0247 (lambda text, context: re.sub(r"{{inputs\[(.*?)\]\((.*?)\)}}", lambda match: str(context.xcom[match.group(1) + ':inputs'][literal_eval(match.group(2))]), text)),
0248 "{{outputs[key](acc)}}":
0249 (lambda text, context: re.sub(r"{{outputs\[(.*?)\]\((.*?)\)}}", lambda match: str(context.xcom[match.group(1) + ':outputs'][literal_eval(match.group(2))]), text)),
0250 "{{arguments[key](acc)}}":
0251 (lambda text, context: re.sub(r"{{arguments\[(.*?)\]\((.*?)\)}}", lambda match: str(context.xcom[match.group(1) + ':arguments'][literal_eval(match.group(2))]), text)),
0252 }
0253
0254 @classmethod
0255 def substitute(cls, text: str, context: JobContext) -> str:
0256 """Apply template substitutions
0257
0258 Args:
0259 text: The text to apply substitution to
0260 context: JobContext holding job, stage, branch, and workflow info
0261 """
0262 result = text
0263 for template, substitution in cls._substitutions.items():
0264 result = substitution(result, context)
0265 return result
0266
0267
0268 class BaseExecutionEngine(ABC):
0269 """Base class for all execution engines.
0270
0271 ExecutionEngines are reusable task implementations that can be combined
0272 in workflows. Each execution engine encapsulates the logic to execute
0273 a specific type of work (shell command, Python function, container, etc).
0274
0275 Attributes:
0276 engine_id: Unique identifier.
0277 params: Task parameters (executor-dependent).
0278 """
0279 _template = Template
0280
0281 def __init__(self, engine_id: str, **kwargs):
0282 """Initialize execution engine.
0283
0284 Args:
0285 engine_id: Unique identifier for this task
0286 **kwargs: Execution engine-specific parameters.
0287 """
0288 self.engine_id = engine_id
0289 self.params = kwargs
0290
0291 @abstractmethod
0292 def execute(self, context: JobContext) -> Any:
0293 """Execute the engine.
0294
0295 Must be implemented by subclasses. Execution engines should:
0296 1. Use context.xcom_pull() to get inputs from upstream tasks
0297 2. Perform the actual work
0298 3. Use context.xcom_push() to return results
0299 4. Use context.add_log() for logging
0300 5. Use context.save_artifact() to register outputs
0301
0302 Args:
0303 context: Operation execution context (XCom, logs, artifacts).
0304
0305 Returns:
0306 Result of execution (any serializable type).
0307 """
0308 raise NotImplementedError
0309
0310 def __repr__(self) -> str:
0311 """String representation of execution engine."""
0312 return f"{self.__class__.__name__}(engine_id='{self.engine_id}')"
0313
0314
0315 class BashExecutionEngine(BaseExecutionEngine):
0316 """Execute a bash shell command.
0317
0318 Executes arbitrary shell commands, capturing stdout/stderr.
0319 Supports template variable substitution in bash_command and env.
0320
0321 Attributes:
0322 bash_command: Bash command to execute.
0323 env: Environment variables (optional).
0324
0325 Example:
0326 >>> engine = BashExecutionEngine(
0327 ... engine_id='run_sim',
0328 ... bash_command='python scripts/simulate.py --input {input_file}',
0329 ... env={'PYTHONUNBUFFERED': '1'}
0330 ... )
0331 >>> result = engine.execute(context)
0332 """
0333
0334 def __init__(self, engine_id: str, bash_command: str, env: Optional[Dict[str, str]] = None, **kwargs):
0335 """Initialize BashExecutionEngine.
0336
0337 Args:
0338 engine_id: Task identifier.
0339 bash_command: Command to execute (supports template variables).
0340 env: Environment variables.
0341 **kwargs: Additional parameters.
0342 """
0343 super().__init__(engine_id, **kwargs)
0344 self.bash_command = bash_command
0345 self.env = env or {}
0346
0347 def execute(self, context: JobContext) -> Dict[str, Any]:
0348 """Execute bash command.
0349
0350 Args:
0351 context: Task context.
0352
0353 Returns:
0354 Dict with 'stdout', 'stderr', 'returncode'.
0355
0356 Raises:
0357 RuntimeError: If command fails (returncode != 0).
0358 """
0359 try:
0360
0361 command = self._template.substitute(self.bash_command, context)
0362
0363 context.add_log(f"Executing bash command: {command}")
0364
0365
0366 result = subprocess.run(
0367 command,
0368 shell=True,
0369 capture_output=True,
0370 text=True,
0371 env={**os.environ, **self.env} if self.env else None,
0372 cwd=context.execution_dir
0373 )
0374
0375
0376 if result.stdout:
0377 context.add_log(f"STDOUT:\n{result.stdout}")
0378 if result.stderr:
0379 context.add_log(f"STDERR:\n{result.stderr}")
0380
0381
0382 output = {
0383 'stdout': result.stdout,
0384 'stderr': result.stderr,
0385 'returncode': result.returncode
0386 }
0387 context.xcom_push('return_value', output)
0388
0389 if result.returncode != 0:
0390 raise RuntimeError(f"Command failed with code {result.returncode}")
0391
0392 return output
0393
0394 except Exception as e:
0395 context.add_log(f"ERROR: {str(e)}")
0396 raise
0397
0398
0399 class PythonExecutionEngine(BaseExecutionEngine):
0400 """Execute a Python callable (function).
0401
0402 Executes a Python function with optional arguments.
0403 The function receives the JobContext as first argument.
0404
0405 Attributes:
0406 python_callable: Function to execute.
0407 op_args: Positional arguments to function.
0408 op_kwargs: Keyword arguments to function.
0409
0410 Example:
0411 >>> def compute_metrics(context, threshold=0.5):
0412 ... data = context.xcom_pull('upstream_task', 'data')
0413 ... result = apply_threshold(data, threshold)
0414 ... context.xcom_push('metrics', result)
0415 ... return result
0416 >>>
0417 >>> engine = PythonExecutionEngine(
0418 ... engine_id='compute',
0419 ... python_callable=compute_metrics,
0420 ... op_kwargs={'threshold': 0.7}
0421 ... )
0422 """
0423
0424 def __init__(
0425 self,
0426 engine_id: str,
0427 python_callable: Callable,
0428 op_args: Optional[tuple] = None,
0429 op_kwargs: Optional[Dict[str, Any]] = None,
0430 **kwargs
0431 ):
0432 """Initialize PythonExecutionEngine.
0433
0434 Args:
0435 job_id: Task identifier.
0436 python_callable: Function to execute.
0437 op_args: Positional arguments (after context).
0438 op_kwargs: Keyword arguments.
0439 **kwargs: Additional parameters.
0440 """
0441 super().__init__(engine_id, **kwargs)
0442 self.python_callable = python_callable
0443 self.op_args = op_args or ()
0444 self.op_kwargs = op_kwargs or {}
0445
0446 def execute(self, context: JobContext) -> Any:
0447 """Execute Python function.
0448
0449 Args:
0450 context: Task context (passed as first argument to callable).
0451
0452 Returns:
0453 Function return value.
0454
0455 Raises:
0456 Exception: Any exception raised by the function.
0457 """
0458 try:
0459 context.add_log(f"Executing Python callable: {self.python_callable.__name__}")
0460
0461
0462 result = self.python_callable(context, *self.op_args, **self.op_kwargs)
0463
0464 context.add_log(f"Function returned: {result}")
0465
0466
0467 context.xcom_push('return_value', result)
0468
0469 return result
0470
0471 except Exception as e:
0472 context.add_log(f"ERROR: {str(e)}")
0473 raise
0474
0475
0476 class ContainerExecutionEngine(BaseExecutionEngine):
0477 """Execute a Docker container.
0478
0479 Runs a Docker image with specified parameters. Supports:
0480 - Environment variables
0481 - Volume mounts
0482 - Resource limits
0483 - Container command override
0484
0485 Attributes:
0486 image: Docker image URI (e.g., 'python:3.10', 'ghcr.io/user/sim:latest').
0487 command: Container command override (optional).
0488 environment: Environment variables to pass into container.
0489 volumes: Volume mounts ({host_path: container_path}).
0490 resources: Resource constraints (memory, cpus, etc).
0491
0492 Example:
0493 >>> engine = ContainerExecutionEngine(
0494 ... engine_id='run_simulation',
0495 ... image='physics-sim:1.0',
0496 ... command=['/app/run_sim.sh'],
0497 ... environment={
0498 ... 'INPUT_FILE': '/data/input.json',
0499 ... 'OUTPUT_DIR': '/output'
0500 ... },
0501 ... volumes={
0502 ... '/host/data': '/data',
0503 ... '/host/output': '/output'
0504 ... },
0505 ... resources={
0506 ... 'memory': '4g',
0507 ... 'cpus': '2'
0508 ... }
0509 ... )
0510 """
0511
0512 def __init__(
0513 self,
0514 engine_id: str,
0515 image: str,
0516 command: Optional[List[str]] = None,
0517 environment: Optional[Dict[str, str]] = None,
0518 volumes: Optional[Dict[str, str]] = None,
0519 resources: Optional[Dict[str, str]] = None,
0520 **kwargs
0521 ):
0522 """Initialize ContainerExecutionEngine.
0523
0524 Args:
0525 engine_id: Task identifier.
0526 image: Docker image URI.
0527 command: Container command (overrides ENTRYPOINT).
0528 environment: Environment variables in container.
0529 volumes: Volume mounts (host_path: container_path).
0530 resources: Resource constraints.
0531 **kwargs: Additional parameters.
0532 """
0533 super().__init__(engine_id, **kwargs)
0534 self.image = image
0535 self.command = command
0536 self.environment = environment or {}
0537 self.volumes = volumes or {}
0538 self.resources = resources or {}
0539
0540 def execute(self, context: JobContext) -> Dict[str, Any]:
0541 """Execute Docker container.
0542
0543 Args:
0544 context: Task context.
0545
0546 Returns:
0547 Dict with 'container_id', 'stdout', 'stderr', 'returncode'.
0548
0549 Raises:
0550 RuntimeError: If docker run fails.
0551 """
0552 try:
0553 context.add_log(f"Running Docker container: {self.image}")
0554
0555
0556 docker_cmd = self._build_docker_command(context)
0557
0558 context.add_log(f"Docker command: {docker_cmd}")
0559
0560
0561 result = subprocess.run(
0562 docker_cmd,
0563 shell=True,
0564 capture_output=True,
0565 text=True,
0566 cwd=context.execution_dir
0567 )
0568
0569
0570 if result.stdout:
0571 context.add_log(f"STDOUT:\n{result.stdout}")
0572 if result.stderr:
0573 context.add_log(f"STDERR:\n{result.stderr}")
0574
0575
0576 output = {
0577 'stdout': result.stdout,
0578 'stderr': result.stderr,
0579 'returncode': result.returncode,
0580 'image': self.image
0581 }
0582 context.xcom_push('return_value', output)
0583
0584 if result.returncode != 0:
0585 raise RuntimeError(f"Docker container failed with code {result.returncode}")
0586
0587 return output
0588
0589 except Exception as e:
0590 context.add_log(f"ERROR: {str(e)}")
0591 raise
0592
0593 def _build_docker_command(self, context: JobContext) -> str:
0594 """Build docker run command.
0595
0596 Args:
0597 context: Task context.
0598
0599 Returns:
0600 Docker run command string.
0601 """
0602 cmd_parts = ['docker', 'run', '--rm']
0603
0604
0605 for key, value in self.environment.items():
0606
0607 value_resolved = self._template.substitute(value, context)
0608 cmd_parts.append(f'-e {key}={value_resolved}')
0609
0610
0611 for host_path, container_path in self.volumes.items():
0612 cmd_parts.append(f'-v {host_path}:{container_path}')
0613
0614
0615 if 'memory' in self.resources:
0616 cmd_parts.append(f'-m {self.resources["memory"]}')
0617 if 'cpus' in self.resources:
0618 cmd_parts.append(f'--cpus {self.resources["cpus"]}')
0619
0620
0621 cmd_parts.append(self.image)
0622
0623
0624 if self.command:
0625 cmd_parts.extend(self.command)
0626
0627 return ' '.join(cmd_parts)
0628
0629
0630 class StackExecutionEngine(BaseExecutionEngine):
0631 """Execute layers of an experimental software stack.
0632
0633 Runs a sequence of layers of a generic experimental
0634 software stack.
0635
0636 Example:
0637 >>> engine = StackExecutionEngine(
0638 ... engine_id='run_simulation',
0639 ... stack_type='EpicStack',
0640 ... layers=[
0641 ... StackLayerConfig(
0642 ... name='sim',
0643 ... inputs='in.hepmc3.tree.root',
0644 ... outputs='out.edm4hep.root',
0645 ... ],
0646 ... ]
0647 ... )
0648 """
0649
0650 def __init__(
0651 self,
0652 engine_id: str,
0653 stack_type: str,
0654 layers: List[StackLayerConfig],
0655 **kwargs
0656 ):
0657 """Initialize StackExecutionEngine
0658
0659 Args:
0660 engine_id: Task identifier
0661 stack_type: Which type of stack to use (e.g. 'EpicStack')
0662 layers: List of layers to run
0663 """
0664 super().__init__(engine_id, **kwargs)
0665 self.layers = layers
0666 self.stack_type = stack_type
0667 self.stack_class = StackRegistry.get_experimental_stack(self.stack_type)
0668 if not self.stack_class:
0669 raise ValueError(f"Unknown stack type: {stack_type}")
0670
0671 def execute(self, context: JobContext) -> Dict[str, Any]:
0672 """Execute experimental stack
0673
0674 Args:
0675 context: Task context.
0676
0677 Returns:
0678 Dict with 'stdout', 'stderr', 'returncode'
0679
0680 Raises:
0681 RuntimeError: If execution fails
0682
0683 Note:
0684 Layer inputs, outputs, and arguments are pushed
0685 to XCom for retrieval downstream.
0686 """
0687 stack = self.stack_class()
0688
0689
0690 preparations = stack.prepare_for_execution(context = context)
0691
0692
0693
0694 for layer in self.layers:
0695 self._apply_template_substitution(layer, context)
0696 context.xcom_push(f'{layer.name}:inputs', layer.inputs)
0697 context.xcom_push(f'{layer.name}:outputs', layer.outputs)
0698 context.xcom_push(f'{layer.name}:arguments', layer.arguments)
0699
0700
0701 driver = f"{context.execution_dir}/{self.engine_id}_driver.sh"
0702 command = stack.make_driver_command(driver)
0703 stack.make_driver_script(
0704 script=driver,
0705 configs=self.layers,
0706 preparations=preparations,
0707 context=context,
0708 )
0709
0710
0711 context.add_log(f"Driver script: {driver}")
0712 context.add_log(f"Driver command: {command}")
0713
0714
0715 try:
0716 result = subprocess.run(
0717 command,
0718 shell=True,
0719 capture_output=True,
0720 text=True,
0721 )
0722
0723
0724 if result.stdout:
0725 context.add_log(f"STDOUT:\n{result.stdout}")
0726 if result.stderr:
0727 context.add_log(f"STDERR:\n{result.stderr}")
0728
0729
0730 output = {
0731 'stdout': result.stdout,
0732 'stderr': result.stderr,
0733 'returncode': result.returncode,
0734 }
0735 context.xcom_push('return_value', output)
0736
0737 if result.returncode != 0:
0738 raise RuntimeError(f"{self.stack_type} execution failed with code {result.returncode}")
0739 return output
0740
0741
0742
0743 except Exception as e:
0744 context.add_log(f"ERROR: {str(e)}")
0745 raise
0746
0747 def _apply_template_substitution(self, layer: StackLayerConfig, context: JobContext) -> None:
0748 """
0749 Apply template substitutions to a layer config
0750
0751 Args:
0752 layer: The layer config to apply substitutions to
0753 context: Context for the current job
0754 """
0755 resolved_inputs = list()
0756 for layer_input in layer.inputs:
0757 layer_input = self._template.substitute(layer_input, context)
0758 resolved_inputs.append(layer_input)
0759 layer.inputs = resolved_inputs
0760
0761 resolved_outputs = list()
0762 for layer_output in layer.outputs:
0763 layer_output = self._template.substitute(layer_output, context)
0764 resolved_outputs.append(layer_output)
0765 layer.outputs = resolved_outputs
0766
0767 if layer.arguments is not None:
0768 resolved_arguments = list()
0769 for layer_argument in layer.arguments:
0770 layer_argument = self._template.substitute(layer_argument, context)
0771 resolved_arguments.append(layer_argument)
0772 layer.arguments = resolved_arguments
0773
0774
0775 __all__ = [
0776 'BranchContext',
0777 'StageContext',
0778 'JobContext',
0779 'Template',
0780 'BaseExecutionEngine',
0781 'BashExecutionEngine',
0782 'PythonExecutionEngine',
0783 'ContainerExecutionEngine',
0784 'StackExecutionEngine'
0785 ]