apache-hamilton 1.90.0.dev0__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.
- apache_hamilton-1.90.0.dev0.dist-info/METADATA +407 -0
- apache_hamilton-1.90.0.dev0.dist-info/RECORD +151 -0
- apache_hamilton-1.90.0.dev0.dist-info/WHEEL +4 -0
- apache_hamilton-1.90.0.dev0.dist-info/entry_points.txt +9 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/DISCLAIMER +10 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/LICENSE +228 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/NOTICE +5 -0
- hamilton/__init__.py +24 -0
- hamilton/ad_hoc_utils.py +132 -0
- hamilton/async_driver.py +465 -0
- hamilton/base.py +466 -0
- hamilton/caching/__init__.py +16 -0
- hamilton/caching/adapter.py +1475 -0
- hamilton/caching/cache_key.py +70 -0
- hamilton/caching/fingerprinting.py +287 -0
- hamilton/caching/stores/__init__.py +16 -0
- hamilton/caching/stores/base.py +242 -0
- hamilton/caching/stores/file.py +140 -0
- hamilton/caching/stores/memory.py +297 -0
- hamilton/caching/stores/sqlite.py +282 -0
- hamilton/caching/stores/utils.py +40 -0
- hamilton/cli/__init__.py +16 -0
- hamilton/cli/__main__.py +328 -0
- hamilton/cli/commands.py +126 -0
- hamilton/cli/logic.py +338 -0
- hamilton/common/__init__.py +76 -0
- hamilton/contrib/__init__.py +41 -0
- hamilton/data_quality/__init__.py +16 -0
- hamilton/data_quality/base.py +198 -0
- hamilton/data_quality/default_validators.py +560 -0
- hamilton/data_quality/pandera_validators.py +121 -0
- hamilton/dataflows/__init__.py +726 -0
- hamilton/dataflows/template/README.md +25 -0
- hamilton/dataflows/template/__init__.py +54 -0
- hamilton/dataflows/template/author.md +28 -0
- hamilton/dataflows/template/requirements.txt +0 -0
- hamilton/dataflows/template/tags.json +7 -0
- hamilton/dataflows/template/valid_configs.jsonl +1 -0
- hamilton/dev_utils/__init__.py +16 -0
- hamilton/dev_utils/deprecation.py +204 -0
- hamilton/driver.py +2112 -0
- hamilton/execution/__init__.py +16 -0
- hamilton/execution/debugging_utils.py +56 -0
- hamilton/execution/executors.py +502 -0
- hamilton/execution/graph_functions.py +421 -0
- hamilton/execution/grouping.py +430 -0
- hamilton/execution/state.py +539 -0
- hamilton/experimental/__init__.py +27 -0
- hamilton/experimental/databackend.py +61 -0
- hamilton/experimental/decorators/__init__.py +16 -0
- hamilton/experimental/decorators/parameterize_frame.py +233 -0
- hamilton/experimental/h_async.py +29 -0
- hamilton/experimental/h_cache.py +413 -0
- hamilton/experimental/h_dask.py +28 -0
- hamilton/experimental/h_databackends.py +174 -0
- hamilton/experimental/h_ray.py +28 -0
- hamilton/experimental/h_spark.py +32 -0
- hamilton/function_modifiers/README +40 -0
- hamilton/function_modifiers/__init__.py +121 -0
- hamilton/function_modifiers/adapters.py +900 -0
- hamilton/function_modifiers/base.py +859 -0
- hamilton/function_modifiers/configuration.py +310 -0
- hamilton/function_modifiers/delayed.py +202 -0
- hamilton/function_modifiers/dependencies.py +246 -0
- hamilton/function_modifiers/expanders.py +1230 -0
- hamilton/function_modifiers/macros.py +1634 -0
- hamilton/function_modifiers/metadata.py +434 -0
- hamilton/function_modifiers/recursive.py +908 -0
- hamilton/function_modifiers/validation.py +289 -0
- hamilton/function_modifiers_base.py +31 -0
- hamilton/graph.py +1153 -0
- hamilton/graph_types.py +264 -0
- hamilton/graph_utils.py +41 -0
- hamilton/htypes.py +450 -0
- hamilton/io/__init__.py +32 -0
- hamilton/io/data_adapters.py +216 -0
- hamilton/io/default_data_loaders.py +224 -0
- hamilton/io/materialization.py +500 -0
- hamilton/io/utils.py +158 -0
- hamilton/lifecycle/__init__.py +67 -0
- hamilton/lifecycle/api.py +833 -0
- hamilton/lifecycle/base.py +1130 -0
- hamilton/lifecycle/default.py +802 -0
- hamilton/log_setup.py +47 -0
- hamilton/models.py +92 -0
- hamilton/node.py +449 -0
- hamilton/plugins/README.md +48 -0
- hamilton/plugins/__init__.py +16 -0
- hamilton/plugins/dask_extensions.py +47 -0
- hamilton/plugins/dlt_extensions.py +161 -0
- hamilton/plugins/geopandas_extensions.py +49 -0
- hamilton/plugins/h_dask.py +331 -0
- hamilton/plugins/h_ddog.py +522 -0
- hamilton/plugins/h_diskcache.py +163 -0
- hamilton/plugins/h_experiments/__init__.py +22 -0
- hamilton/plugins/h_experiments/__main__.py +62 -0
- hamilton/plugins/h_experiments/cache.py +39 -0
- hamilton/plugins/h_experiments/data_model.py +68 -0
- hamilton/plugins/h_experiments/hook.py +219 -0
- hamilton/plugins/h_experiments/server.py +375 -0
- hamilton/plugins/h_kedro.py +152 -0
- hamilton/plugins/h_logging.py +454 -0
- hamilton/plugins/h_mcp/__init__.py +28 -0
- hamilton/plugins/h_mcp/__main__.py +33 -0
- hamilton/plugins/h_mcp/_helpers.py +129 -0
- hamilton/plugins/h_mcp/_templates.py +417 -0
- hamilton/plugins/h_mcp/server.py +328 -0
- hamilton/plugins/h_mlflow.py +335 -0
- hamilton/plugins/h_narwhals.py +134 -0
- hamilton/plugins/h_openlineage.py +400 -0
- hamilton/plugins/h_opentelemetry.py +167 -0
- hamilton/plugins/h_pandas.py +257 -0
- hamilton/plugins/h_pandera.py +117 -0
- hamilton/plugins/h_polars.py +304 -0
- hamilton/plugins/h_polars_lazyframe.py +282 -0
- hamilton/plugins/h_pyarrow.py +56 -0
- hamilton/plugins/h_pydantic.py +127 -0
- hamilton/plugins/h_ray.py +242 -0
- hamilton/plugins/h_rich.py +142 -0
- hamilton/plugins/h_schema.py +493 -0
- hamilton/plugins/h_slack.py +100 -0
- hamilton/plugins/h_spark.py +1380 -0
- hamilton/plugins/h_threadpool.py +125 -0
- hamilton/plugins/h_tqdm.py +122 -0
- hamilton/plugins/h_vaex.py +129 -0
- hamilton/plugins/huggingface_extensions.py +236 -0
- hamilton/plugins/ibis_extensions.py +93 -0
- hamilton/plugins/jupyter_magic.py +622 -0
- hamilton/plugins/kedro_extensions.py +117 -0
- hamilton/plugins/lightgbm_extensions.py +99 -0
- hamilton/plugins/matplotlib_extensions.py +108 -0
- hamilton/plugins/mlflow_extensions.py +216 -0
- hamilton/plugins/numpy_extensions.py +105 -0
- hamilton/plugins/pandas_extensions.py +1763 -0
- hamilton/plugins/plotly_extensions.py +150 -0
- hamilton/plugins/polars_extensions.py +71 -0
- hamilton/plugins/polars_implementations.py +25 -0
- hamilton/plugins/polars_lazyframe_extensions.py +302 -0
- hamilton/plugins/polars_post_1_0_0_extensions.py +877 -0
- hamilton/plugins/polars_pre_1_0_0_extension.py +836 -0
- hamilton/plugins/pydantic_extensions.py +98 -0
- hamilton/plugins/pyspark_pandas_extensions.py +47 -0
- hamilton/plugins/sklearn_plot_extensions.py +129 -0
- hamilton/plugins/spark_extensions.py +105 -0
- hamilton/plugins/vaex_extensions.py +51 -0
- hamilton/plugins/xgboost_extensions.py +91 -0
- hamilton/plugins/yaml_extensions.py +89 -0
- hamilton/registry.py +254 -0
- hamilton/settings.py +18 -0
- hamilton/telemetry.py +50 -0
- hamilton/version.py +18 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import pprint
|
|
20
|
+
from collections import deque
|
|
21
|
+
from collections.abc import Collection
|
|
22
|
+
from functools import partial
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from hamilton import node
|
|
26
|
+
from hamilton.lifecycle.base import LifecycleAdapterSet
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
"""A set of utility functions for managing/traversing DAGs. Note these all operate on nodes.
|
|
31
|
+
We will likely want to genericize them so we're dealing with anything, not just node.Nodes.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def topologically_sort_nodes(nodes: list[node.Node]) -> list[node.Node]:
|
|
36
|
+
"""Topologically sorts a list of nodes based on their dependencies.
|
|
37
|
+
Note that we bypass utilizing the preset dependencies/depended_on_by attributes of the node,
|
|
38
|
+
as we may want to use this before these nodes get put in a function graph.
|
|
39
|
+
|
|
40
|
+
Thus we compute our own dependency map...
|
|
41
|
+
Note that this assumes that the nodes are continuous -- if there is a hidden dependency that
|
|
42
|
+
connects them, this has no way of knowing about it.
|
|
43
|
+
|
|
44
|
+
TODO -- use python graphlib when we no longer have to support 3.8.
|
|
45
|
+
|
|
46
|
+
https://docs.python.org/3/library/graphlib.html
|
|
47
|
+
|
|
48
|
+
:param nodes: Nodes to sort
|
|
49
|
+
:return: Nodes in sorted order
|
|
50
|
+
"""
|
|
51
|
+
node_name_map = {node_.name: node_ for node_ in nodes}
|
|
52
|
+
depended_on_by_map = {}
|
|
53
|
+
dependency_map = {}
|
|
54
|
+
for node_ in nodes:
|
|
55
|
+
dependency_map[node_.name] = []
|
|
56
|
+
for dep in node_.input_types:
|
|
57
|
+
# if the dependency is not here, we don't want to count it
|
|
58
|
+
# that means it depends on something outside the set of nodes we're sorting
|
|
59
|
+
if dep not in node_name_map:
|
|
60
|
+
continue
|
|
61
|
+
dependency_map[node_.name].append(dep)
|
|
62
|
+
if dep not in depended_on_by_map:
|
|
63
|
+
depended_on_by_map[dep] = []
|
|
64
|
+
depended_on_by_map[dep].append(node_)
|
|
65
|
+
|
|
66
|
+
in_degrees = {node_.name: len(dependency_map.get(node_.name, [])) for node_ in nodes}
|
|
67
|
+
# TODO -- determine what happens if nodes have dependencies that aren't present
|
|
68
|
+
sources = [node_ for node_ in nodes if in_degrees[node_.name] == 0]
|
|
69
|
+
queue = deque()
|
|
70
|
+
for source in sources:
|
|
71
|
+
queue.append(source)
|
|
72
|
+
sorted_nodes = []
|
|
73
|
+
while len(queue) > 0:
|
|
74
|
+
node_ = queue.popleft()
|
|
75
|
+
sorted_nodes.append(node_)
|
|
76
|
+
for next_node in depended_on_by_map.get(node_.name, []):
|
|
77
|
+
if next_node.name in in_degrees:
|
|
78
|
+
in_degrees[next_node.name] -= 1
|
|
79
|
+
if in_degrees[next_node.name] == 0:
|
|
80
|
+
queue.append(next_node)
|
|
81
|
+
return sorted_nodes
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_node_levels(topologically_sorted_nodes: list[node.Node]) -> dict[str, int]:
|
|
85
|
+
"""Gets the levels for a group of topologically sorted nodes.
|
|
86
|
+
This only works if its topologically sorted, of course...
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
:param topologically_sorted_nodes:
|
|
90
|
+
:return: A dictionary of node name -> level
|
|
91
|
+
"""
|
|
92
|
+
node_levels = {}
|
|
93
|
+
node_set = {node_.name for node_ in topologically_sorted_nodes}
|
|
94
|
+
for node_ in topologically_sorted_nodes:
|
|
95
|
+
dependencies_in_set = {n.name for n in node_.dependencies}.intersection(node_set)
|
|
96
|
+
if len(dependencies_in_set) == 0:
|
|
97
|
+
node_levels[node_.name] = 0
|
|
98
|
+
else:
|
|
99
|
+
node_levels[node_.name] = max([node_levels[n] for n in dependencies_in_set]) + 1
|
|
100
|
+
return node_levels
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def combine_config_and_inputs(config: dict[str, Any], inputs: dict[str, Any]) -> dict[str, Any]:
|
|
104
|
+
"""Validates and combines config and inputs, ensuring that they're mutually disjoint.
|
|
105
|
+
:param config: Config to construct, run the DAG with.
|
|
106
|
+
:param inputs: Inputs to run the DAG on at runtime
|
|
107
|
+
:return: The combined set of inputs to the DAG.
|
|
108
|
+
:raises ValueError: if they are not disjoint
|
|
109
|
+
"""
|
|
110
|
+
duplicated_inputs = [key for key in inputs if key in config]
|
|
111
|
+
if len(duplicated_inputs) > 0:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"The following inputs are present in both config and inputs. They must be "
|
|
114
|
+
f"mutually disjoint. {duplicated_inputs} "
|
|
115
|
+
)
|
|
116
|
+
return {**config, **inputs}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def create_input_string(kwargs: dict) -> str:
|
|
120
|
+
"""This is a utility function to create a string representation of the inputs to a function.
|
|
121
|
+
|
|
122
|
+
This is useful for debugging, as it can be printed out to see what the inputs were.
|
|
123
|
+
|
|
124
|
+
:param kwargs: The inputs to the function that errored.
|
|
125
|
+
:return: The string representation of the inputs, truncated appropriately.
|
|
126
|
+
"""
|
|
127
|
+
pp = pprint.PrettyPrinter(width=80)
|
|
128
|
+
inputs = {}
|
|
129
|
+
for k, v in kwargs.items():
|
|
130
|
+
item_repr = repr(v)
|
|
131
|
+
if len(item_repr) > 50:
|
|
132
|
+
item_repr = item_repr[:50] + "..."
|
|
133
|
+
else:
|
|
134
|
+
item_repr = v
|
|
135
|
+
inputs[k] = item_repr
|
|
136
|
+
input_string = pp.pformat(inputs)
|
|
137
|
+
if len(input_string) > 1000:
|
|
138
|
+
input_string = input_string[:1000] + "..."
|
|
139
|
+
return input_string
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def create_error_message(kwargs: dict, node_: node.Node, step: str) -> str:
|
|
143
|
+
"""Creates an error message for a node that errored."""
|
|
144
|
+
# This code is coupled to how @config resolution works. Ideally it shouldn't be,
|
|
145
|
+
# so when @config resolvers are changed to return Nodes, then fn.__name__ should
|
|
146
|
+
# just work.
|
|
147
|
+
original_func_name = "unknown"
|
|
148
|
+
if node_.originating_functions:
|
|
149
|
+
if hasattr(node_.originating_functions[0], "__original_name__"):
|
|
150
|
+
original_func_name = node_.originating_functions[0].__original_name__
|
|
151
|
+
else:
|
|
152
|
+
original_func_name = node_.originating_functions[0].__name__
|
|
153
|
+
module = (
|
|
154
|
+
node_.originating_functions[0].__module__
|
|
155
|
+
if node_.originating_functions and hasattr(node_.originating_functions[0], "__module__")
|
|
156
|
+
else "unknown_module"
|
|
157
|
+
)
|
|
158
|
+
message = f">{step} {node_.name} [{module}.{original_func_name}()] encountered an error"
|
|
159
|
+
padding = " " * (80 - min(len(message), 79) - 1)
|
|
160
|
+
message += padding + "<"
|
|
161
|
+
input_string = create_input_string(kwargs)
|
|
162
|
+
message += "\n> Node inputs:\n" + input_string
|
|
163
|
+
border = "*" * 80
|
|
164
|
+
message = "\n" + border + "\n" + message + "\n" + border
|
|
165
|
+
return message
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def execute_subdag(
|
|
169
|
+
nodes: Collection[node.Node],
|
|
170
|
+
inputs: dict[str, Any],
|
|
171
|
+
adapter: LifecycleAdapterSet = None,
|
|
172
|
+
computed: dict[str, Any] = None,
|
|
173
|
+
overrides: dict[str, Any] = None,
|
|
174
|
+
run_id: str = None,
|
|
175
|
+
task_id: str = None,
|
|
176
|
+
) -> dict[str, Any]:
|
|
177
|
+
"""Base function to execute a subdag. This conducts a depth first traversal of the graph.
|
|
178
|
+
|
|
179
|
+
:param nodes: Nodes to compute
|
|
180
|
+
:param inputs: Inputs, external
|
|
181
|
+
:param adapter: Adapter to use to compute
|
|
182
|
+
:param computed: Already computed nodes
|
|
183
|
+
:param overrides: Overrides to use, will short-circuit computation
|
|
184
|
+
:param run_id: Run ID to use
|
|
185
|
+
:param task_id: Task ID to use -- this is optional for the purpose of the task-based execution...
|
|
186
|
+
:return: The results
|
|
187
|
+
"""
|
|
188
|
+
if overrides is None:
|
|
189
|
+
overrides = {}
|
|
190
|
+
if computed is None:
|
|
191
|
+
computed = {}
|
|
192
|
+
nodes_to_compute = {node_.name for node_ in nodes}
|
|
193
|
+
|
|
194
|
+
if adapter is None:
|
|
195
|
+
adapter = LifecycleAdapterSet()
|
|
196
|
+
|
|
197
|
+
def dfs_traverse(
|
|
198
|
+
node_: node.Node, dependency_type: node.DependencyType = node.DependencyType.REQUIRED
|
|
199
|
+
):
|
|
200
|
+
if node_.name in computed:
|
|
201
|
+
return
|
|
202
|
+
if node_.name in overrides:
|
|
203
|
+
computed[node_.name] = overrides[node_.name]
|
|
204
|
+
return
|
|
205
|
+
for n in node_.dependencies:
|
|
206
|
+
if n.name not in computed:
|
|
207
|
+
_, node_dependency_type = node_.input_types[n.name]
|
|
208
|
+
dfs_traverse(n, node_dependency_type)
|
|
209
|
+
|
|
210
|
+
logger.debug(f"Computing {node_.name}.")
|
|
211
|
+
if node_.user_defined:
|
|
212
|
+
if node_.name not in inputs:
|
|
213
|
+
if dependency_type != node.DependencyType.OPTIONAL:
|
|
214
|
+
raise NotImplementedError(
|
|
215
|
+
f"{node_.name} was expected to be passed in but was not."
|
|
216
|
+
)
|
|
217
|
+
return
|
|
218
|
+
result = inputs[node_.name]
|
|
219
|
+
else:
|
|
220
|
+
kwargs = {} # construct signature
|
|
221
|
+
for dependency in node_.dependencies:
|
|
222
|
+
if dependency.name in computed:
|
|
223
|
+
kwargs[dependency.name] = computed[dependency.name]
|
|
224
|
+
|
|
225
|
+
execute_lifecycle_for_node_partial = partial(
|
|
226
|
+
execute_lifecycle_for_node,
|
|
227
|
+
__node_=node_,
|
|
228
|
+
__adapter=adapter,
|
|
229
|
+
__run_id=run_id,
|
|
230
|
+
__task_id=task_id,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if adapter.does_method("do_remote_execute", is_async=False):
|
|
234
|
+
result = adapter.call_lifecycle_method_sync(
|
|
235
|
+
"do_remote_execute",
|
|
236
|
+
node=node_,
|
|
237
|
+
execute_lifecycle_for_node=execute_lifecycle_for_node_partial,
|
|
238
|
+
**kwargs,
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
result = execute_lifecycle_for_node_partial(**kwargs)
|
|
242
|
+
|
|
243
|
+
computed[node_.name] = result
|
|
244
|
+
# > pruning the graph
|
|
245
|
+
# This doesn't narrow it down to the entire space of the graph
|
|
246
|
+
# E.G. if something is not needed by this current execution due to
|
|
247
|
+
# the selection of nodes to run it might not prune everything.
|
|
248
|
+
# to do this we'd need to first determine all nodes on the path, then prune
|
|
249
|
+
# We may also want to use a reference counter for slightly cleaner/more efficient memory management
|
|
250
|
+
|
|
251
|
+
for dep in node_.dependencies:
|
|
252
|
+
if dep.name in computed and dep.name not in nodes_to_compute:
|
|
253
|
+
for downstream_node in dep.depended_on_by:
|
|
254
|
+
# if it isn't computed, and it isn't required, we can't prune
|
|
255
|
+
if (
|
|
256
|
+
downstream_node.name not in computed
|
|
257
|
+
or downstream_node.name in nodes_to_compute
|
|
258
|
+
):
|
|
259
|
+
break
|
|
260
|
+
# If the result of this node is no longer needed, we can prune it/save the memory
|
|
261
|
+
else:
|
|
262
|
+
del computed[dep.name]
|
|
263
|
+
|
|
264
|
+
for final_var_node in nodes:
|
|
265
|
+
dep_type = node.DependencyType.REQUIRED
|
|
266
|
+
if final_var_node.user_defined:
|
|
267
|
+
# from the top level, we don't know if this UserInput is required. So mark as optional.
|
|
268
|
+
dep_type = node.DependencyType.OPTIONAL
|
|
269
|
+
dfs_traverse(final_var_node, dep_type)
|
|
270
|
+
return computed
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# TODO: better function name
|
|
274
|
+
def execute_lifecycle_for_node(
|
|
275
|
+
__node_: node.Node,
|
|
276
|
+
__adapter: LifecycleAdapterSet,
|
|
277
|
+
__run_id: str,
|
|
278
|
+
__task_id: str,
|
|
279
|
+
**__kwargs: dict[str, Any],
|
|
280
|
+
):
|
|
281
|
+
"""Helper function to properly execute node lifecycle.
|
|
282
|
+
|
|
283
|
+
Firstly, we execute the pre-node-execute hooks if supplied adapters have any, then we execute the node function, and lastly, we execute the post-node-execute hooks if present in the adapters.
|
|
284
|
+
|
|
285
|
+
For local runtime gets execute directy. Otherwise, serves as a sandwich function that guarantees the pre_node and post_node lifecycle hooks are executed in the remote environment.
|
|
286
|
+
|
|
287
|
+
:param __node_: Node that is being executed
|
|
288
|
+
:param __adapter: Adapter to use to compute
|
|
289
|
+
:param __run_id: ID of the run, unique in scope of the driver.
|
|
290
|
+
:param __task_id: ID of the task, defaults to None if not in a task setting
|
|
291
|
+
:param ___kwargs: Keyword arguments that are being passed into the node
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
error = None
|
|
295
|
+
result = None
|
|
296
|
+
success = True
|
|
297
|
+
pre_node_execute_errored = False
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
if __adapter.does_hook("pre_node_execute", is_async=False):
|
|
301
|
+
try:
|
|
302
|
+
__adapter.call_all_lifecycle_hooks_sync(
|
|
303
|
+
"pre_node_execute",
|
|
304
|
+
run_id=__run_id,
|
|
305
|
+
node_=__node_,
|
|
306
|
+
kwargs=__kwargs,
|
|
307
|
+
task_id=__task_id,
|
|
308
|
+
)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
pre_node_execute_errored = True
|
|
311
|
+
raise e
|
|
312
|
+
if __adapter.does_method("do_node_execute", is_async=False):
|
|
313
|
+
result = __adapter.call_lifecycle_method_sync(
|
|
314
|
+
"do_node_execute",
|
|
315
|
+
run_id=__run_id,
|
|
316
|
+
node_=__node_,
|
|
317
|
+
kwargs=__kwargs,
|
|
318
|
+
task_id=__task_id,
|
|
319
|
+
)
|
|
320
|
+
else:
|
|
321
|
+
result = __node_(**__kwargs)
|
|
322
|
+
|
|
323
|
+
return result
|
|
324
|
+
|
|
325
|
+
except Exception as e:
|
|
326
|
+
success = False
|
|
327
|
+
error = e
|
|
328
|
+
step = "[pre-node-execute]" if pre_node_execute_errored else ""
|
|
329
|
+
message = create_error_message(__kwargs, __node_, step)
|
|
330
|
+
logger.exception(message)
|
|
331
|
+
raise
|
|
332
|
+
finally:
|
|
333
|
+
if not pre_node_execute_errored and __adapter.does_hook(
|
|
334
|
+
"post_node_execute", is_async=False
|
|
335
|
+
):
|
|
336
|
+
try:
|
|
337
|
+
__adapter.call_all_lifecycle_hooks_sync(
|
|
338
|
+
"post_node_execute",
|
|
339
|
+
run_id=__run_id,
|
|
340
|
+
node_=__node_,
|
|
341
|
+
kwargs=__kwargs,
|
|
342
|
+
success=success,
|
|
343
|
+
error=error,
|
|
344
|
+
result=result,
|
|
345
|
+
task_id=__task_id,
|
|
346
|
+
)
|
|
347
|
+
except Exception:
|
|
348
|
+
message = create_error_message(__kwargs, __node_, "[post-node-execute]")
|
|
349
|
+
logger.exception(message)
|
|
350
|
+
raise
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def nodes_between(
|
|
354
|
+
end_node: node.Node,
|
|
355
|
+
search_condition: lambda node_: bool,
|
|
356
|
+
) -> tuple[node.Node | None, list[node.Node]]:
|
|
357
|
+
"""Utility function to search backwards from an end node to a start node.
|
|
358
|
+
This returns all nodes for which both of the following conditions are met:
|
|
359
|
+
|
|
360
|
+
1. It contains a node that matches the start_condition as an ancestor
|
|
361
|
+
2. It contains a node that matches the end node as a dependent
|
|
362
|
+
|
|
363
|
+
Note that currently it is assumed that only one node will
|
|
364
|
+
match search_condition.
|
|
365
|
+
|
|
366
|
+
This just grabs the search node when it finds it -- the nonlocal is a bit hacky but more fun
|
|
367
|
+
than passing a ton of data back and forth (who the parent is, etc...).
|
|
368
|
+
|
|
369
|
+
:param end_node: Node to trace back from
|
|
370
|
+
:param search_condition: Condition to stop the search for ancestors
|
|
371
|
+
:return: A tuple of [start_node, between], where start_node is None
|
|
372
|
+
if there is no path (and between will be empty).
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
out = set()
|
|
376
|
+
visited = set()
|
|
377
|
+
search_node = None
|
|
378
|
+
|
|
379
|
+
def dfs_traverse(node_: node.Node):
|
|
380
|
+
if search_condition(node_):
|
|
381
|
+
# if we hit the end, we want to include all others in it
|
|
382
|
+
nonlocal search_node
|
|
383
|
+
search_node = node_
|
|
384
|
+
return True
|
|
385
|
+
if node_ in visited:
|
|
386
|
+
# if we've already seen it, we want to include it
|
|
387
|
+
return node_ in out
|
|
388
|
+
# now we mark that we've seen it
|
|
389
|
+
visited.add(node_)
|
|
390
|
+
|
|
391
|
+
any_deps_included = False
|
|
392
|
+
for n in node_.dependencies:
|
|
393
|
+
any_deps_included |= dfs_traverse(n)
|
|
394
|
+
if any_deps_included:
|
|
395
|
+
out.add(node_)
|
|
396
|
+
return any_deps_included
|
|
397
|
+
|
|
398
|
+
for dep in end_node.dependencies:
|
|
399
|
+
dfs_traverse(dep)
|
|
400
|
+
|
|
401
|
+
return search_node, list(out)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def node_is_required_by_anything(node_: node.Node, node_set: set[node.Node]) -> bool:
|
|
405
|
+
"""Checks dependencies on this node and determines if at least one requires it.
|
|
406
|
+
|
|
407
|
+
Nodes can be optionally depended upon, i.e. the function parameter has a default value. We want to check that
|
|
408
|
+
of the nodes the depend on this one, at least one of them requires it, i.e. the parameter is not optional.
|
|
409
|
+
|
|
410
|
+
:param node_: node in question
|
|
411
|
+
:param node_set: checks that we traverse only nodes in the provided set.
|
|
412
|
+
:return: True if it is required by any downstream node, false otherwise
|
|
413
|
+
"""
|
|
414
|
+
required = False
|
|
415
|
+
for downstream_node in node_.depended_on_by:
|
|
416
|
+
if downstream_node not in node_set:
|
|
417
|
+
continue
|
|
418
|
+
_, dep_type = downstream_node.input_types[node_.name]
|
|
419
|
+
if dep_type == node.DependencyType.REQUIRED:
|
|
420
|
+
return True
|
|
421
|
+
return required
|