runnable 0.50.0__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.
Files changed (72) hide show
  1. extensions/README.md +0 -0
  2. extensions/__init__.py +0 -0
  3. extensions/catalog/README.md +0 -0
  4. extensions/catalog/any_path.py +214 -0
  5. extensions/catalog/file_system.py +52 -0
  6. extensions/catalog/minio.py +72 -0
  7. extensions/catalog/pyproject.toml +14 -0
  8. extensions/catalog/s3.py +11 -0
  9. extensions/job_executor/README.md +0 -0
  10. extensions/job_executor/__init__.py +236 -0
  11. extensions/job_executor/emulate.py +70 -0
  12. extensions/job_executor/k8s.py +553 -0
  13. extensions/job_executor/k8s_job_spec.yaml +37 -0
  14. extensions/job_executor/local.py +35 -0
  15. extensions/job_executor/local_container.py +161 -0
  16. extensions/job_executor/pyproject.toml +16 -0
  17. extensions/nodes/README.md +0 -0
  18. extensions/nodes/__init__.py +0 -0
  19. extensions/nodes/conditional.py +301 -0
  20. extensions/nodes/fail.py +78 -0
  21. extensions/nodes/loop.py +394 -0
  22. extensions/nodes/map.py +477 -0
  23. extensions/nodes/parallel.py +281 -0
  24. extensions/nodes/pyproject.toml +15 -0
  25. extensions/nodes/stub.py +93 -0
  26. extensions/nodes/success.py +78 -0
  27. extensions/nodes/task.py +156 -0
  28. extensions/pipeline_executor/README.md +0 -0
  29. extensions/pipeline_executor/__init__.py +871 -0
  30. extensions/pipeline_executor/argo.py +1266 -0
  31. extensions/pipeline_executor/emulate.py +119 -0
  32. extensions/pipeline_executor/local.py +226 -0
  33. extensions/pipeline_executor/local_container.py +369 -0
  34. extensions/pipeline_executor/mocked.py +159 -0
  35. extensions/pipeline_executor/pyproject.toml +16 -0
  36. extensions/run_log_store/README.md +0 -0
  37. extensions/run_log_store/__init__.py +0 -0
  38. extensions/run_log_store/any_path.py +100 -0
  39. extensions/run_log_store/chunked_fs.py +122 -0
  40. extensions/run_log_store/chunked_minio.py +141 -0
  41. extensions/run_log_store/file_system.py +91 -0
  42. extensions/run_log_store/generic_chunked.py +549 -0
  43. extensions/run_log_store/minio.py +114 -0
  44. extensions/run_log_store/pyproject.toml +15 -0
  45. extensions/secrets/README.md +0 -0
  46. extensions/secrets/dotenv.py +62 -0
  47. extensions/secrets/pyproject.toml +15 -0
  48. runnable/__init__.py +108 -0
  49. runnable/catalog.py +141 -0
  50. runnable/cli.py +484 -0
  51. runnable/context.py +730 -0
  52. runnable/datastore.py +1058 -0
  53. runnable/defaults.py +159 -0
  54. runnable/entrypoints.py +390 -0
  55. runnable/exceptions.py +137 -0
  56. runnable/executor.py +561 -0
  57. runnable/gantt.py +1646 -0
  58. runnable/graph.py +501 -0
  59. runnable/names.py +546 -0
  60. runnable/nodes.py +593 -0
  61. runnable/parameters.py +217 -0
  62. runnable/pickler.py +96 -0
  63. runnable/sdk.py +1277 -0
  64. runnable/secrets.py +92 -0
  65. runnable/tasks.py +1268 -0
  66. runnable/telemetry.py +142 -0
  67. runnable/utils.py +423 -0
  68. runnable-0.50.0.dist-info/METADATA +189 -0
  69. runnable-0.50.0.dist-info/RECORD +72 -0
  70. runnable-0.50.0.dist-info/WHEEL +4 -0
  71. runnable-0.50.0.dist-info/entry_points.txt +53 -0
  72. runnable-0.50.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,281 @@
1
+ import logging
2
+ from copy import deepcopy
3
+ from multiprocessing import Pool
4
+ from typing import Any, Dict, Optional, cast
5
+
6
+ from pydantic import Field, field_serializer
7
+
8
+ from runnable import console, defaults, exceptions
9
+ from runnable.defaults import IterableParameterModel
10
+ from runnable.graph import Graph, create_graph
11
+ from runnable.nodes import CompositeNode
12
+
13
+ logger = logging.getLogger(defaults.LOGGER_NAME)
14
+
15
+
16
+ class ParallelNode(CompositeNode):
17
+ """
18
+ A composite node containing many graph objects within itself.
19
+
20
+ The structure is generally:
21
+ ParallelNode:
22
+ Branch A:
23
+ Sub graph definition
24
+ Branch B:
25
+ Sub graph definition
26
+ . . .
27
+
28
+ """
29
+
30
+ node_type: str = Field(default="parallel", serialization_alias="type")
31
+ branches: Dict[str, Graph]
32
+
33
+ def get_summary(self) -> Dict[str, Any]:
34
+ summary = {
35
+ "name": self.name,
36
+ "type": self.node_type,
37
+ "branches": [branch.get_summary() for branch in self.branches.values()],
38
+ }
39
+
40
+ return summary
41
+
42
+ @field_serializer("branches")
43
+ def ser_branches(self, branches: Dict[str, Graph]) -> Dict[str, Graph]:
44
+ ret: Dict[str, Graph] = {}
45
+
46
+ for branch_name, branch in branches.items():
47
+ ret[branch_name.split(".")[-1]] = branch
48
+
49
+ return ret
50
+
51
+ @classmethod
52
+ def parse_from_config(cls, config: Dict[str, Any]) -> "ParallelNode":
53
+ internal_name = cast(str, config.get("internal_name"))
54
+
55
+ config_branches = config.pop("branches", {})
56
+ branches = {}
57
+ for branch_name, branch_config in config_branches.items():
58
+ sub_graph = create_graph(
59
+ deepcopy(branch_config),
60
+ internal_branch_name=internal_name + "." + branch_name,
61
+ )
62
+ branches[internal_name + "." + branch_name] = sub_graph
63
+
64
+ if not branches:
65
+ raise Exception("A parallel node should have branches")
66
+ return cls(branches=branches, **config)
67
+
68
+ def _get_branch_by_name(self, branch_name: str) -> Graph:
69
+ if branch_name in self.branches:
70
+ return self.branches[branch_name]
71
+
72
+ raise Exception(f"Branch {branch_name} does not exist")
73
+
74
+ def fan_out(
75
+ self,
76
+ iter_variable: Optional[IterableParameterModel] = None,
77
+ ):
78
+ """
79
+ The general fan out method for a node of type Parallel.
80
+ This method assumes that the step log has already been created.
81
+
82
+ 3rd party orchestrators should create the step log and use this method to create the branch logs.
83
+
84
+ Args:
85
+ executor (BaseExecutor): The executor class as defined by the config
86
+ iter_variable (dict, optional): If the node is part of a map node. Defaults to None.
87
+ """
88
+ # Prepare the branch logs
89
+ for internal_branch_name, _ in self.branches.items():
90
+ effective_branch_name = self._resolve_map_placeholders(
91
+ internal_branch_name, iter_variable=iter_variable
92
+ )
93
+
94
+ try:
95
+ branch_log = self._context.run_log_store.get_branch_log(
96
+ effective_branch_name, self._context.run_id
97
+ )
98
+ console.print(f"Branch log already exists for {effective_branch_name}")
99
+ except (exceptions.BranchLogNotFoundError, exceptions.EntityNotFoundError):
100
+ branch_log = self._context.run_log_store.create_branch_log(
101
+ effective_branch_name
102
+ )
103
+ console.print(f"Branch log created for {effective_branch_name}")
104
+
105
+ branch_log.status = defaults.PROCESSING
106
+ self._context.run_log_store.add_branch_log(branch_log, self._context.run_id)
107
+
108
+ def execute_as_graph(
109
+ self,
110
+ iter_variable: Optional[IterableParameterModel] = None,
111
+ ):
112
+ """
113
+ This function does the actual execution of the sub-branches of the parallel node.
114
+
115
+ From a design perspective, this function should not be called if the execution is 3rd party orchestrated.
116
+
117
+ The modes that render the job specifications, do not need to interact with this node at all as they have their
118
+ own internal mechanisms of handing parallel states.
119
+ If they do not, you can find a way using as-is nodes as hack nodes.
120
+
121
+ The execution of a dag, could result in
122
+ * The dag being completely executed with a definite (fail, success) state in case of
123
+ local or local-container execution
124
+ * The dag being in a processing state with PROCESSING status in case of local-aws-batch
125
+
126
+ Only fail state is considered failure during this phase of execution.
127
+
128
+ Args:
129
+ executor (Executor): The Executor as per the use config
130
+ **kwargs: Optional kwargs passed around
131
+ """
132
+ self.fan_out(iter_variable=iter_variable)
133
+
134
+ # Check if parallel execution is enabled and supported
135
+ enable_parallel = getattr(
136
+ self._context.pipeline_executor, "enable_parallel", False
137
+ )
138
+ supports_parallel_writes = getattr(
139
+ self._context.run_log_store, "supports_parallel_writes", False
140
+ )
141
+
142
+ # Check if we're using a local executor (local or local-container)
143
+ executor_service_name = getattr(
144
+ self._context.pipeline_executor, "service_name", ""
145
+ )
146
+ is_local_executor = executor_service_name in ["local", "local-container"]
147
+
148
+ if enable_parallel and is_local_executor:
149
+ if not supports_parallel_writes:
150
+ logger.warning(
151
+ "Parallel execution was requested but the run log store does not support parallel writes. "
152
+ "Falling back to sequential execution. Consider using a run log store with "
153
+ "supports_parallel_writes=True for parallel execution."
154
+ )
155
+ self._execute_sequentially(iter_variable)
156
+ else:
157
+ logger.info("Executing branches in parallel")
158
+ self._execute_in_parallel(iter_variable)
159
+ else:
160
+ self._execute_sequentially(iter_variable)
161
+
162
+ self.fan_in(iter_variable=iter_variable)
163
+
164
+ def _execute_sequentially(
165
+ self,
166
+ iter_variable: Optional[IterableParameterModel] = None,
167
+ ):
168
+ """Execute branches sequentially (original behavior)."""
169
+ for _, branch in self.branches.items():
170
+ self._context.pipeline_executor.execute_graph(
171
+ branch, iter_variable=iter_variable
172
+ )
173
+
174
+ def _execute_in_parallel(
175
+ self,
176
+ iter_variable: Optional[IterableParameterModel] = None,
177
+ ):
178
+ """Execute branches in parallel using multiprocessing."""
179
+ from runnable.entrypoints import execute_single_branch
180
+
181
+ # Prepare arguments for each branch
182
+ branch_args = []
183
+ for branch_name, branch in self.branches.items():
184
+ branch_args.append((branch_name, branch, self._context, iter_variable))
185
+
186
+ # Use multiprocessing Pool to execute branches in parallel
187
+ with Pool() as pool:
188
+ results = pool.starmap(execute_single_branch, branch_args)
189
+
190
+ # Check if any branch failed
191
+ if not all(results):
192
+ failed_branches = [
193
+ branch_name
194
+ for (branch_name, _, _, _), result in zip(branch_args, results)
195
+ if not result
196
+ ]
197
+ logger.error(f"The following branches failed: {failed_branches}")
198
+ # Note: The actual failure handling and status update will be done in fan_in()
199
+
200
+ def fan_in(
201
+ self,
202
+ iter_variable: Optional[IterableParameterModel] = None,
203
+ ):
204
+ """
205
+ The general fan in method for a node of type Parallel.
206
+
207
+ 3rd party orchestrators should use this method to find the status of the composite step.
208
+
209
+ Args:
210
+ executor (BaseExecutor): The executor class as defined by the config
211
+ iter_variable (dict, optional): If the node is part of a map. Defaults to None.
212
+ """
213
+ effective_internal_name = self._resolve_map_placeholders(
214
+ self.internal_name, iter_variable=iter_variable
215
+ )
216
+ step_success_bool = True
217
+ for internal_branch_name, _ in self.branches.items():
218
+ effective_branch_name = self._resolve_map_placeholders(
219
+ internal_branch_name, iter_variable=iter_variable
220
+ )
221
+ branch_log = self._context.run_log_store.get_branch_log(
222
+ effective_branch_name, self._context.run_id
223
+ )
224
+
225
+ if branch_log.status != defaults.SUCCESS:
226
+ step_success_bool = False
227
+
228
+ # Collate all the results and update the status of the step
229
+
230
+ step_log = self._context.run_log_store.get_step_log(
231
+ effective_internal_name, self._context.run_id
232
+ )
233
+
234
+ if step_success_bool: # If none failed
235
+ step_log.status = defaults.SUCCESS
236
+ else:
237
+ step_log.status = defaults.FAIL
238
+
239
+ self._context.run_log_store.add_step_log(step_log, self._context.run_id)
240
+
241
+ # If we failed, return without parameter rollback
242
+ if not step_log.status == defaults.SUCCESS:
243
+ return
244
+
245
+ # Roll back parameters from all branches to parent scope
246
+ parent_params = self._context.run_log_store.get_parameters(
247
+ self._context.run_id, internal_branch_name=self.internal_branch_name
248
+ )
249
+
250
+ for internal_branch_name, _ in self.branches.items():
251
+ effective_branch_name = self._resolve_map_placeholders(
252
+ internal_branch_name, iter_variable=iter_variable
253
+ )
254
+
255
+ branch_params = self._context.run_log_store.get_parameters(
256
+ self._context.run_id, internal_branch_name=effective_branch_name
257
+ )
258
+
259
+ # Merge branch parameters into parent (overwrite with branch values)
260
+ # If multiple branches set the same parameter, last one wins
261
+ parent_params.update(branch_params)
262
+
263
+ self._context.run_log_store.set_parameters(
264
+ parameters=parent_params,
265
+ run_id=self._context.run_id,
266
+ internal_branch_name=self.internal_branch_name,
267
+ )
268
+
269
+ async def execute_as_graph_async(
270
+ self,
271
+ iter_variable: Optional[IterableParameterModel] = None,
272
+ ):
273
+ """Async parallel execution."""
274
+ self.fan_out(iter_variable=iter_variable) # sync - just creates branch logs
275
+
276
+ for _, branch in self.branches.items():
277
+ await self._context.pipeline_executor.execute_graph_async(
278
+ branch, iter_variable=iter_variable
279
+ )
280
+
281
+ self.fan_in(iter_variable=iter_variable) # sync - just collates status
@@ -0,0 +1,15 @@
1
+ [project]
2
+ name = "nodes"
3
+ version = "0.0.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = []
8
+
9
+
10
+ [build-system]
11
+ requires = ["hatchling"]
12
+ build-backend = "hatchling.build"
13
+
14
+ [tool.hatch.build.targets.wheel]
15
+ packages = ["."]
@@ -0,0 +1,93 @@
1
+ import logging
2
+ from datetime import datetime
3
+ from typing import Any, Dict, Optional
4
+
5
+ from pydantic import ConfigDict, Field
6
+
7
+ from runnable import datastore, defaults
8
+ from runnable.datastore import StepLog
9
+ from runnable.defaults import IterableParameterModel
10
+ from runnable.nodes import ExecutableNode
11
+
12
+ logger = logging.getLogger(defaults.LOGGER_NAME)
13
+
14
+
15
+ class StubNode(ExecutableNode):
16
+ """
17
+ Stub is a convenience design node.
18
+ It always returns success in the attempt log and does nothing.
19
+
20
+ This node is very similar to pass state in Step functions.
21
+
22
+ This node type could be handy when designing the pipeline and stubbing functions
23
+ --8<-- [start:stub_reference]
24
+ An stub execution node of the pipeline.
25
+ Please refer to define pipeline/tasks/stub for more information.
26
+
27
+ As part of the dag definition, a stub task is defined as follows:
28
+
29
+ dag:
30
+ steps:
31
+ stub_task: # The name of the node
32
+ type: stub
33
+ on_failure: The name of the step to traverse in case of failure
34
+ next: The next node to execute after this task, use "success" to terminate the pipeline successfully
35
+ or "fail" to terminate the pipeline with an error.
36
+
37
+ It can take arbritary number of parameters, which is handy to temporarily silence a task node.
38
+ --8<-- [end:stub_reference]
39
+ """
40
+
41
+ node_type: str = Field(default="stub", serialization_alias="type")
42
+ model_config = ConfigDict(extra="ignore")
43
+
44
+ def get_summary(self) -> Dict[str, Any]:
45
+ summary = {
46
+ "name": self.name,
47
+ "type": self.node_type,
48
+ }
49
+
50
+ return summary
51
+
52
+ @classmethod
53
+ def parse_from_config(cls, config: Dict[str, Any]) -> "StubNode":
54
+ return cls(**config)
55
+
56
+ def execute(
57
+ self,
58
+ mock=False,
59
+ iter_variable: Optional[IterableParameterModel] = None,
60
+ attempt_number: int = 1,
61
+ ) -> StepLog:
62
+ """
63
+ Do Nothing node.
64
+ We just send an success attempt log back to the caller
65
+
66
+ Args:
67
+ executor ([type]): [description]
68
+ mock (bool, optional): [description]. Defaults to False.
69
+ iter_variable (str, optional): [description]. Defaults to ''.
70
+
71
+ Returns:
72
+ [type]: [description]
73
+ """
74
+ step_log = self._context.run_log_store.get_step_log(
75
+ self._get_step_log_name(iter_variable), self._context.run_id
76
+ )
77
+
78
+ attempt_log = datastore.StepAttempt(
79
+ status=defaults.SUCCESS,
80
+ start_time=str(datetime.now()),
81
+ end_time=str(datetime.now()),
82
+ attempt_number=attempt_number,
83
+ )
84
+
85
+ self._context.pipeline_executor.add_code_identities(
86
+ node=self, attempt_log=attempt_log
87
+ )
88
+
89
+ step_log.status = attempt_log.status
90
+
91
+ step_log.attempts.append(attempt_log)
92
+
93
+ return step_log
@@ -0,0 +1,78 @@
1
+ from datetime import datetime
2
+ from typing import Any, Dict, Optional, cast
3
+
4
+ from pydantic import Field
5
+
6
+ from runnable import datastore, defaults
7
+ from runnable.datastore import StepLog
8
+ from runnable.defaults import IterableParameterModel
9
+ from runnable.nodes import TerminalNode
10
+
11
+
12
+ class SuccessNode(TerminalNode):
13
+ """
14
+ A leaf node of the graph that represents a success node
15
+ """
16
+
17
+ node_type: str = Field(default="success", serialization_alias="type")
18
+
19
+ @classmethod
20
+ def parse_from_config(cls, config: Dict[str, Any]) -> "SuccessNode":
21
+ return cast("SuccessNode", super().parse_from_config(config))
22
+
23
+ def get_summary(self) -> Dict[str, Any]:
24
+ summary = {
25
+ "name": self.name,
26
+ "type": self.node_type,
27
+ }
28
+
29
+ return summary
30
+
31
+ def execute(
32
+ self,
33
+ mock=False,
34
+ iter_variable: Optional[IterableParameterModel] = None,
35
+ attempt_number: int = 1,
36
+ ) -> StepLog:
37
+ """
38
+ Execute the success node.
39
+ Set the run or branch log status to success.
40
+
41
+ Args:
42
+ executor (_type_): The executor class
43
+ mock (bool, optional): If we should just mock and not perform anything. Defaults to False.
44
+ iter_variable (dict, optional): If the node belongs to an internal branch. Defaults to None.
45
+
46
+ Returns:
47
+ StepAttempt: The step attempt object
48
+ """
49
+ step_log = self._context.run_log_store.get_step_log(
50
+ self._get_step_log_name(iter_variable), self._context.run_id
51
+ )
52
+
53
+ attempt_log = datastore.StepAttempt(
54
+ status=defaults.SUCCESS,
55
+ start_time=str(datetime.now()),
56
+ end_time=str(datetime.now()),
57
+ attempt_number=attempt_number,
58
+ retry_indicator=self._context.retry_indicator,
59
+ )
60
+
61
+ # Add code identities to the attempt
62
+ self._context.pipeline_executor.add_code_identities(
63
+ node=self, attempt_log=attempt_log
64
+ )
65
+
66
+ run_or_branch_log = self._context.run_log_store.get_branch_log(
67
+ self._get_branch_log_name(iter_variable), self._context.run_id
68
+ )
69
+ run_or_branch_log.status = defaults.SUCCESS
70
+ self._context.run_log_store.add_branch_log(
71
+ run_or_branch_log, self._context.run_id
72
+ )
73
+
74
+ step_log.status = attempt_log.status
75
+
76
+ step_log.attempts.append(attempt_log)
77
+
78
+ return step_log
@@ -0,0 +1,156 @@
1
+ import logging
2
+ from datetime import datetime
3
+ from typing import Any, Dict, Optional
4
+
5
+ from pydantic import ConfigDict, Field
6
+
7
+ from runnable import datastore, defaults
8
+ from runnable.datastore import StepLog
9
+ from runnable.defaults import IterableParameterModel
10
+ from runnable.nodes import ExecutableNode
11
+ from runnable.tasks import BaseTaskType, create_task
12
+
13
+ logger = logging.getLogger(defaults.LOGGER_NAME)
14
+
15
+
16
+ class TaskNode(ExecutableNode):
17
+ """
18
+ A node of type Task.
19
+
20
+ This node does the actual function execution of the graph in all cases.
21
+ """
22
+
23
+ executable: BaseTaskType = Field(exclude=True)
24
+ node_type: str = Field(default="task", serialization_alias="type")
25
+
26
+ # It is technically not allowed as parse_from_config filters them.
27
+ # This is just to get the task level configuration to be present during serialization.
28
+ model_config = ConfigDict(extra="allow")
29
+
30
+ @classmethod
31
+ def parse_from_config(cls, config: Dict[str, Any]) -> "TaskNode":
32
+ # separate task config from node config
33
+ task_config = {
34
+ k: v for k, v in config.items() if k not in TaskNode.model_fields.keys()
35
+ }
36
+ node_config = {
37
+ k: v for k, v in config.items() if k in TaskNode.model_fields.keys()
38
+ }
39
+
40
+ executable = create_task(task_config)
41
+ return cls(executable=executable, **node_config, **task_config)
42
+
43
+ def get_summary(self) -> Dict[str, Any]:
44
+ summary = {
45
+ "name": self.name,
46
+ "type": self.node_type,
47
+ "executable": self.executable.get_summary(),
48
+ "catalog": self._get_catalog_settings(),
49
+ }
50
+
51
+ return summary
52
+
53
+ def execute(
54
+ self,
55
+ mock=False,
56
+ iter_variable: Optional[IterableParameterModel] = None,
57
+ attempt_number: int = 1,
58
+ ) -> StepLog:
59
+ """
60
+ All that we do in runnable is to come to this point where we actually execute the command.
61
+
62
+ Args:
63
+ executor (_type_): The executor class
64
+ mock (bool, optional): If we should just mock and not execute. Defaults to False.
65
+ iter_variable: Optional iteration variable if the node is part of internal branch. Defaults to None.
66
+
67
+ Returns:
68
+ StepAttempt: The attempt object
69
+ """
70
+ step_log = self._context.run_log_store.get_step_log(
71
+ self._get_step_log_name(iter_variable), self._context.run_id
72
+ )
73
+
74
+ # Set the branch scope for parameter operations
75
+ self.executable.internal_branch_name = self._get_branch_log_name(iter_variable)
76
+
77
+ if not mock:
78
+ # Do not run if we are mocking the execution, could be useful for caching and dry runs
79
+ attempt_log = self.executable.execute_command(iter_variable=iter_variable)
80
+ attempt_log.attempt_number = attempt_number
81
+ attempt_log.retry_indicator = self._context.retry_indicator
82
+ else:
83
+ attempt_log = datastore.StepAttempt(
84
+ status=defaults.SUCCESS,
85
+ start_time=str(datetime.now()),
86
+ end_time=str(datetime.now()),
87
+ attempt_number=attempt_number,
88
+ retry_indicator=self._context.retry_indicator,
89
+ )
90
+
91
+ # Add code identities to the attempt
92
+ self._context.pipeline_executor.add_code_identities(
93
+ node=self, attempt_log=attempt_log
94
+ )
95
+
96
+ logger.info(f"attempt_log: {attempt_log}")
97
+ logger.info(f"Step {self.name} completed with status: {attempt_log.status}")
98
+
99
+ step_log.status = attempt_log.status
100
+ step_log.attempts.append(attempt_log)
101
+
102
+ return step_log
103
+
104
+ async def execute_async(
105
+ self,
106
+ iter_variable: Optional[IterableParameterModel] = None,
107
+ attempt_number: int = 1,
108
+ mock: bool = False,
109
+ ) -> StepLog:
110
+ """Async task execution with fallback to sync."""
111
+ step_log = self._context.run_log_store.get_step_log(
112
+ self._get_step_log_name(iter_variable), self._context.run_id
113
+ )
114
+
115
+ # Set the branch scope for parameter operations
116
+ self.executable.internal_branch_name = self._get_branch_log_name(iter_variable)
117
+
118
+ if not mock:
119
+ # Get event_callback from executor
120
+ event_callback = self._context.pipeline_executor._event_callback
121
+
122
+ # Try async first, fall back to sync
123
+ try:
124
+ attempt_log = await self.executable.execute_command_async(
125
+ iter_variable=iter_variable,
126
+ event_callback=event_callback,
127
+ )
128
+ except NotImplementedError:
129
+ # Task doesn't support async, fall back to sync
130
+ attempt_log = self.executable.execute_command(
131
+ iter_variable=iter_variable
132
+ )
133
+
134
+ attempt_log.attempt_number = attempt_number
135
+ attempt_log.retry_indicator = self._context.retry_indicator
136
+ else:
137
+ attempt_log = datastore.StepAttempt(
138
+ status=defaults.SUCCESS,
139
+ start_time=str(datetime.now()),
140
+ end_time=str(datetime.now()),
141
+ attempt_number=attempt_number,
142
+ retry_indicator=self._context.retry_indicator,
143
+ )
144
+
145
+ # Add code identities to the attempt
146
+ self._context.pipeline_executor.add_code_identities(
147
+ node=self, attempt_log=attempt_log
148
+ )
149
+
150
+ logger.info(f"attempt_log: {attempt_log}")
151
+ logger.info(f"Step {self.name} completed with status: {attempt_log.status}")
152
+
153
+ step_log.status = attempt_log.status
154
+ step_log.attempts.append(attempt_log)
155
+
156
+ return step_log
File without changes