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,802 @@
|
|
|
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
|
+
"""A selection of default lifeycle hooks/methods that come with Hamilton. These carry no additional requirements"""
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import logging
|
|
22
|
+
import pdb
|
|
23
|
+
import pickle
|
|
24
|
+
import pprint
|
|
25
|
+
import random
|
|
26
|
+
import shelve
|
|
27
|
+
import time
|
|
28
|
+
from collections.abc import Callable
|
|
29
|
+
from functools import partial
|
|
30
|
+
from typing import Any, Union
|
|
31
|
+
|
|
32
|
+
from hamilton import graph_types, htypes
|
|
33
|
+
from hamilton.graph_types import HamiltonGraph
|
|
34
|
+
from hamilton.lifecycle import (
|
|
35
|
+
EdgeConnectionHook,
|
|
36
|
+
GraphExecutionHook,
|
|
37
|
+
NodeExecutionHook,
|
|
38
|
+
NodeExecutionMethod,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
NodeFilter = Union[
|
|
44
|
+
Callable[
|
|
45
|
+
[str, dict[str, Any]], bool
|
|
46
|
+
], # filter function for nodes, mapping node name to a boolean
|
|
47
|
+
list[str], # list of node names to run
|
|
48
|
+
str, # node name to run
|
|
49
|
+
None, # run all nodes
|
|
50
|
+
] # filter function for nodes, mapping node name and node tags to a boolean
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def should_run_node(node_name: str, node_tags: dict[str, Any], node_filter: NodeFilter) -> bool:
|
|
54
|
+
if node_filter is None:
|
|
55
|
+
return True
|
|
56
|
+
if isinstance(node_filter, str):
|
|
57
|
+
return node_name == node_filter
|
|
58
|
+
if isinstance(node_filter, list):
|
|
59
|
+
return node_name in node_filter
|
|
60
|
+
if callable(node_filter):
|
|
61
|
+
return node_filter(node_name, node_tags)
|
|
62
|
+
raise ValueError(f"Invalid node filter: {node_filter}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PrintLn(NodeExecutionHook):
|
|
66
|
+
"""Basic hook to print out information before/after node execution."""
|
|
67
|
+
|
|
68
|
+
NODE_TIME_STATE = "node_time"
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _format_time_delta(delta: float) -> str:
|
|
72
|
+
"""Formats a time delta into a human-readable string."""
|
|
73
|
+
# Determine the appropriate unit and format
|
|
74
|
+
if delta < 0.001: # Less than 1 millisecond
|
|
75
|
+
return f"{delta * 1e6:.3g}μs"
|
|
76
|
+
elif delta < 1: # Less than 1 second
|
|
77
|
+
return f"{delta * 1e3:.3g}ms"
|
|
78
|
+
return f"{delta:.3g}s"
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _validate_verbosity(verbosity: int):
|
|
82
|
+
"""Validates that the verbosity is one of [1, 2]"""
|
|
83
|
+
if verbosity not in [1, 2]:
|
|
84
|
+
raise ValueError(f"Verbosity must be one of [1, 2], got {verbosity}")
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _format_node_name(node_name: str, task_id: str | None) -> str:
|
|
88
|
+
"""Formats a node name and task id into a unique node name."""
|
|
89
|
+
if task_id is not None:
|
|
90
|
+
return f"{task_id}:{node_name}"
|
|
91
|
+
return node_name
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
verbosity: int = 1,
|
|
96
|
+
print_fn: Callable[[str], None] = print,
|
|
97
|
+
node_filter: NodeFilter = None,
|
|
98
|
+
):
|
|
99
|
+
"""Prints out information before/after node execution.
|
|
100
|
+
|
|
101
|
+
:param verbosity: The verbosity level to print out at.
|
|
102
|
+
`verbosity=1` Print out just the node name and time it took to execute
|
|
103
|
+
`verbosity=2`. Print out inputs of the node + results on execute
|
|
104
|
+
:param print_fn: A function that takes a string and prints it out -- defaults to print. Pass in a logger function, etc... if you so choose.
|
|
105
|
+
:param node_filter: A function that takes a node name and a node tags dict and returns a boolean. If the boolean is True, the node will be printed out.
|
|
106
|
+
If False, it will not be printed out.
|
|
107
|
+
"""
|
|
108
|
+
PrintLn._validate_verbosity(verbosity)
|
|
109
|
+
self.verbosity = verbosity
|
|
110
|
+
self.print_fn = print_fn
|
|
111
|
+
self.timer_dict = {} # quick dict to track the time it took to execute a node
|
|
112
|
+
if node_filter is None:
|
|
113
|
+
node_filter = lambda node_name, node_tags: True # noqa E731
|
|
114
|
+
self.node_filter = node_filter
|
|
115
|
+
|
|
116
|
+
def run_before_node_execution(
|
|
117
|
+
self,
|
|
118
|
+
*,
|
|
119
|
+
node_name: str,
|
|
120
|
+
node_tags: dict[str, Any],
|
|
121
|
+
node_kwargs: dict[str, Any],
|
|
122
|
+
task_id: str | None,
|
|
123
|
+
**future_kwargs: Any,
|
|
124
|
+
):
|
|
125
|
+
"""Runs before a node executes. Prints out the node name and inputs if verbosity is 2.
|
|
126
|
+
|
|
127
|
+
:param node_name: Name of the node
|
|
128
|
+
:param node_tags: Tags of the node
|
|
129
|
+
:param node_kwargs: Keyword arguments of the node
|
|
130
|
+
:param task_id: ID of the task that the node is in, if any
|
|
131
|
+
:param future_kwargs: Additional keyword arguments that may be passed to the hook yet are ignored for now
|
|
132
|
+
"""
|
|
133
|
+
if not should_run_node(node_name, node_tags, self.node_filter):
|
|
134
|
+
return
|
|
135
|
+
node_unique_id = self._format_node_name(node_name, task_id)
|
|
136
|
+
self.timer_dict[node_unique_id] = time.time()
|
|
137
|
+
message = f"Executing node: {node_unique_id}."
|
|
138
|
+
if self.verbosity == 2:
|
|
139
|
+
message += f" Inputs: \n{pprint.pformat(node_kwargs)}"
|
|
140
|
+
self.print_fn(message)
|
|
141
|
+
|
|
142
|
+
def run_after_node_execution(
|
|
143
|
+
self,
|
|
144
|
+
*,
|
|
145
|
+
node_name: str,
|
|
146
|
+
node_tags: dict[str, Any],
|
|
147
|
+
node_kwargs: dict[str, Any],
|
|
148
|
+
result: Any,
|
|
149
|
+
error: Exception | None,
|
|
150
|
+
success: bool,
|
|
151
|
+
task_id: str | None,
|
|
152
|
+
**future_kwargs: Any,
|
|
153
|
+
):
|
|
154
|
+
"""Runs after a node executes. Prints out the node name and time it took, the output if verbosity is 1.
|
|
155
|
+
|
|
156
|
+
:param node_name: Name of the node
|
|
157
|
+
:param node_tags: Tags of the node
|
|
158
|
+
:param node_kwargs: Keyword arguments passed to the node
|
|
159
|
+
:param result: Result of the node
|
|
160
|
+
:param error: Error of the node
|
|
161
|
+
:param success: Whether the node was successful or not
|
|
162
|
+
:param task_id: ID of the task that the node is in, if any
|
|
163
|
+
:param future_kwargs: Additional keyword arguments that may be passed to the hook yet are ignored for now
|
|
164
|
+
"""
|
|
165
|
+
if not should_run_node(node_name, node_tags, self.node_filter):
|
|
166
|
+
return
|
|
167
|
+
node_unique_id = self._format_node_name(node_name, task_id)
|
|
168
|
+
time_delta = time.time() - self.timer_dict[node_unique_id]
|
|
169
|
+
time_delta_formatted = self._format_time_delta(time_delta)
|
|
170
|
+
message = f"Finished debugging node: {node_unique_id} in {time_delta_formatted}. Status: {'Success' if success else 'Failure'}."
|
|
171
|
+
if self.verbosity == 2:
|
|
172
|
+
if success:
|
|
173
|
+
message += f" Result: \n{pprint.pformat(result)}\n"
|
|
174
|
+
else:
|
|
175
|
+
message += f" Error: \n{pprint.pformat(error)}"
|
|
176
|
+
self.print_fn(message)
|
|
177
|
+
del self.timer_dict[node_name]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class PDBDebugger(NodeExecutionHook, NodeExecutionMethod):
|
|
181
|
+
"""Class to inject a PDB debugger into a node execution. This is still somewhat experimental as it is a debugging utility.
|
|
182
|
+
We reserve the right to change the API and the implementation of this class in the future.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
CONTEXT = dict()
|
|
186
|
+
|
|
187
|
+
def __init__(
|
|
188
|
+
self,
|
|
189
|
+
node_filter: NodeFilter,
|
|
190
|
+
before: bool = False,
|
|
191
|
+
during: bool = False,
|
|
192
|
+
after: bool = False,
|
|
193
|
+
):
|
|
194
|
+
"""Creates a PDB debugger. This has three possible modes:
|
|
195
|
+
1. Before -- places you in a function with (a) node information, and (b) inputs
|
|
196
|
+
2. During -- runs the node with pdb.run. Note this may not always work or give what you expect as
|
|
197
|
+
node functions are often wrapped in multiple levels of input modifications/whatnot. That said, it should give you something.
|
|
198
|
+
Also note that this is not (currently) compatible with graph adapters.
|
|
199
|
+
3. After -- places you in a function with (a) node information, (b) inputs, and (c) results
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
:param node_filter: A function that takes a node name and a node tags dict and returns a boolean. If the boolean is True, the node will be printed out.
|
|
203
|
+
:param before: Whether to place you in a PDB debugger before a node executes
|
|
204
|
+
:param during: Whether to place you in a PDB debugger during a node's execution
|
|
205
|
+
:param after: Whether to place you in a PDB debugger after a node executes
|
|
206
|
+
"""
|
|
207
|
+
self.node_filter = node_filter
|
|
208
|
+
self.run_before = before
|
|
209
|
+
self.run_during = during
|
|
210
|
+
self.run_after = after
|
|
211
|
+
|
|
212
|
+
def run_to_execute_node(
|
|
213
|
+
self,
|
|
214
|
+
*,
|
|
215
|
+
node_name: str,
|
|
216
|
+
node_tags: dict[str, Any],
|
|
217
|
+
node_callable: Any,
|
|
218
|
+
node_kwargs: dict[str, Any],
|
|
219
|
+
task_id: str | None,
|
|
220
|
+
**future_kwargs: Any,
|
|
221
|
+
) -> Any:
|
|
222
|
+
"""Executes the node with a PDB debugger. This modifies the global PDBDebugger.CONTEXT variable to contain information about the node,
|
|
223
|
+
so you can access it while debugging.
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
:param node_name: Name of the node
|
|
227
|
+
:param node_tags: Tags of the node
|
|
228
|
+
:param node_callable: Callable function of the node
|
|
229
|
+
:param node_kwargs: Keyword arguments passed to the node
|
|
230
|
+
:param task_id: ID of the task that the node is in, if any
|
|
231
|
+
:param future_kwargs: Additional keyword arguments that may be passed to the hook yet are ignored for now
|
|
232
|
+
:return: Result of the node
|
|
233
|
+
"""
|
|
234
|
+
if not should_run_node(node_name, node_tags, self.node_filter) or not self.run_during:
|
|
235
|
+
return node_callable(**node_kwargs)
|
|
236
|
+
PDBDebugger.CONTEXT = {
|
|
237
|
+
"node_name": node_name,
|
|
238
|
+
"node_tags": node_tags,
|
|
239
|
+
"node_callable": node_callable,
|
|
240
|
+
"node_kwargs": node_kwargs,
|
|
241
|
+
"task_id": task_id,
|
|
242
|
+
"future_kwargs": future_kwargs,
|
|
243
|
+
}
|
|
244
|
+
logger.warning(
|
|
245
|
+
f"Placing you in a PDB debugger for node {node_name}."
|
|
246
|
+
"\nYou can access additional node information via PDBDebugger.CONTEXT. Data is:"
|
|
247
|
+
f"\n - node_name: {PDBDebugger._truncate_repr(node_name)}"
|
|
248
|
+
f"\n - node_tags: {PDBDebugger._truncate_repr(node_tags)}"
|
|
249
|
+
f"\n - node_callable: {PDBDebugger._truncate_repr(node_callable)}"
|
|
250
|
+
f"\n - node_kwargs: {PDBDebugger._truncate_repr(', '.join(list(node_kwargs.keys())))}"
|
|
251
|
+
f"\n - task_id: {PDBDebugger._truncate_repr(task_id)}"
|
|
252
|
+
f"\n - future_kwargs: {PDBDebugger._truncate_repr(future_kwargs)}"
|
|
253
|
+
)
|
|
254
|
+
out = pdb.runcall(node_callable, **node_kwargs)
|
|
255
|
+
logger.info(f"Finished executing node {node_name}.")
|
|
256
|
+
return out
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def _truncate_repr(obj: Any, num_chars: int = 80) -> str:
|
|
260
|
+
"""Truncates the repr of an object to 100 characters."""
|
|
261
|
+
if isinstance(obj, str):
|
|
262
|
+
return obj[:num_chars]
|
|
263
|
+
return repr(obj)[:num_chars]
|
|
264
|
+
|
|
265
|
+
def run_before_node_execution(
|
|
266
|
+
self,
|
|
267
|
+
*,
|
|
268
|
+
node_name: str,
|
|
269
|
+
node_tags: dict[str, Any],
|
|
270
|
+
node_kwargs: dict[str, Any],
|
|
271
|
+
node_return_type: type,
|
|
272
|
+
task_id: str | None,
|
|
273
|
+
**future_kwargs: Any,
|
|
274
|
+
):
|
|
275
|
+
"""Executes before a node executes. Does nothing, just runs pdb.set_trace()
|
|
276
|
+
|
|
277
|
+
:param node_name: Name of the node
|
|
278
|
+
:param node_tags: Tags of the node
|
|
279
|
+
:param node_kwargs: Keyword arguments passed to the node
|
|
280
|
+
:param node_return_type: Return type of the node
|
|
281
|
+
:param task_id: ID of the task that the node is in, if any
|
|
282
|
+
:param future_kwargs: Additional keyword arguments that may be passed to the hook yet are ignored for now
|
|
283
|
+
:return: Result of the node
|
|
284
|
+
"""
|
|
285
|
+
if should_run_node(node_name, node_tags, self.node_filter) and self.run_before:
|
|
286
|
+
logger.warning(
|
|
287
|
+
f"Placing you in a PDB debugger prior to execution of node: {node_name}."
|
|
288
|
+
"\nYou can access additional node information via the following variables:"
|
|
289
|
+
f"\n - node_name: {PDBDebugger._truncate_repr(node_name)}"
|
|
290
|
+
f"\n - node_tags: {PDBDebugger._truncate_repr(node_tags)}"
|
|
291
|
+
f"\n - node_kwargs: {PDBDebugger._truncate_repr(', '.join(list(node_kwargs.keys())))}"
|
|
292
|
+
f"\n - node_return_type: {PDBDebugger._truncate_repr(node_return_type)}"
|
|
293
|
+
f"\n - task_id: {PDBDebugger._truncate_repr(task_id)}"
|
|
294
|
+
)
|
|
295
|
+
pdb.set_trace()
|
|
296
|
+
|
|
297
|
+
def run_after_node_execution(
|
|
298
|
+
self,
|
|
299
|
+
*,
|
|
300
|
+
node_name: str,
|
|
301
|
+
node_tags: dict[str, Any],
|
|
302
|
+
node_kwargs: dict[str, Any],
|
|
303
|
+
node_return_type: type,
|
|
304
|
+
result: Any,
|
|
305
|
+
error: Exception | None,
|
|
306
|
+
success: bool,
|
|
307
|
+
task_id: str | None,
|
|
308
|
+
**future_kwargs: Any,
|
|
309
|
+
):
|
|
310
|
+
"""Executes after a node, whether or not it was successful. Does nothing, just runs pdb.set_trace().
|
|
311
|
+
|
|
312
|
+
:param node_name: Name of the node
|
|
313
|
+
:param node_tags: Tags of the node
|
|
314
|
+
:param node_kwargs: Keyword arguments passed to the node
|
|
315
|
+
:param node_return_type: Return type of the node
|
|
316
|
+
:param result: Result of the node, None if there was an error
|
|
317
|
+
:param error: Error of the node, None if there was no error
|
|
318
|
+
:param success: Whether the node ran successful or not
|
|
319
|
+
:param task_id: Task ID of the node, if any
|
|
320
|
+
:param future_kwargs: Additional keyword arguments that may be passed to the hook yet are ignored for now
|
|
321
|
+
"""
|
|
322
|
+
if should_run_node(node_name, node_tags, self.node_filter) and self.run_after:
|
|
323
|
+
logger.warning(
|
|
324
|
+
f"Placing you in a PDB debugger post execution of node: {node_name}."
|
|
325
|
+
"\nYou can access additional node information via the following variables:"
|
|
326
|
+
f"\n - node_name: {PDBDebugger._truncate_repr(node_name)}"
|
|
327
|
+
f"\n - node_tags: {PDBDebugger._truncate_repr(node_tags)}"
|
|
328
|
+
f"\n - node_kwargs: {PDBDebugger._truncate_repr(', '.join(list(node_kwargs.keys())))}"
|
|
329
|
+
f"\n - node_return_type: {PDBDebugger._truncate_repr(node_return_type)}"
|
|
330
|
+
f"\n - result: {PDBDebugger._truncate_repr(result)}"
|
|
331
|
+
f"\n - error: {PDBDebugger._truncate_repr(error)}"
|
|
332
|
+
f"\n - success: {PDBDebugger._truncate_repr(success)}"
|
|
333
|
+
f"\n - task_id: {PDBDebugger._truncate_repr(task_id)}"
|
|
334
|
+
)
|
|
335
|
+
pdb.set_trace()
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class CacheAdapter(NodeExecutionHook, NodeExecutionMethod, GraphExecutionHook):
|
|
339
|
+
"""Class to cache node results on disk with a key based on the node code implementation and inputs.
|
|
340
|
+
Following runs with the same key can load node results and skip computation.
|
|
341
|
+
|
|
342
|
+
The cache `_nodes_history` entry returns an append-only list of results added to the cache.
|
|
343
|
+
e.g., the last value in the list `cache["_node_history"][node_name]` is the most recent cached node.
|
|
344
|
+
|
|
345
|
+
Notes:
|
|
346
|
+
- It uses the stdlib `shelve` module and the pickle format, which makes results dependent
|
|
347
|
+
on the Python version. Use materialization for persistent results
|
|
348
|
+
- There are no utility to manage cache size so you'll have to delete it periodically. Look
|
|
349
|
+
at the diskcache plugin for Hamilton `hamilton.plugins.h_diskcache` for better cache management.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
nodes_history_key: str = "_nodes_history"
|
|
353
|
+
|
|
354
|
+
def __init__(self, cache_vars: list[str] | None = None, cache_path: str = "./hamilton-cache"):
|
|
355
|
+
"""Initialize the cache
|
|
356
|
+
|
|
357
|
+
:param cache_vars: List of nodes for which to store/load results. Passing None will use the cache
|
|
358
|
+
for all nodes. Default is None.
|
|
359
|
+
:param cache_path: File path to the cache. The file name doesn't need an extension.
|
|
360
|
+
"""
|
|
361
|
+
self.cache_vars = cache_vars or []
|
|
362
|
+
self.cache_path = cache_path
|
|
363
|
+
self.cache = shelve.open(self.cache_path)
|
|
364
|
+
self.nodes_history: dict[str, list[str]] = self.cache.get(
|
|
365
|
+
key=CacheAdapter.nodes_history_key, default=dict()
|
|
366
|
+
)
|
|
367
|
+
self.used_nodes_hash: dict[str, str] = dict()
|
|
368
|
+
self.cache.close()
|
|
369
|
+
|
|
370
|
+
logger.warning(
|
|
371
|
+
"The `CacheAdapter` is deprecated and will be removed in Hamilton 2.0. "
|
|
372
|
+
"Consider enabling the core caching feature via `Builder.with_cache()`. "
|
|
373
|
+
"This might not be 1-to-1 replacement, so please reach out if there are missing features. "
|
|
374
|
+
"See https://hamilton.apache.org/concepts/caching/ to learn more."
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def run_before_graph_execution(self, *, graph: HamiltonGraph, **kwargs):
|
|
378
|
+
"""Set `cache_vars` to all nodes if received None during `__init__`"""
|
|
379
|
+
self.cache = shelve.open(self.cache_path)
|
|
380
|
+
if len(self.cache_vars) == 0:
|
|
381
|
+
self.cache_vars = [n.name for n in graph.nodes]
|
|
382
|
+
|
|
383
|
+
def run_to_execute_node(
|
|
384
|
+
self, *, node_name: str, node_callable: Any, node_kwargs: dict[str, Any], **kwargs
|
|
385
|
+
):
|
|
386
|
+
"""Create cache key based on node callable hash (equiv. to HamiltonNode.version) and
|
|
387
|
+
the node inputs (`node_kwargs`).If key in cache (cache hit), load result; else (cache miss),
|
|
388
|
+
compute the node and append node name to `used_nodes_hash`.
|
|
389
|
+
|
|
390
|
+
Note:
|
|
391
|
+
- the callable hash is stored in `used_nodes_hash` because it's required to create the
|
|
392
|
+
key in `run_after_node_execution` and the callable won't be accessible to recompute it
|
|
393
|
+
"""
|
|
394
|
+
if node_name not in self.cache_vars:
|
|
395
|
+
return node_callable(**node_kwargs)
|
|
396
|
+
|
|
397
|
+
source_of_node_callable = node_callable
|
|
398
|
+
while isinstance(source_of_node_callable, partial): # handle partials
|
|
399
|
+
source_of_node_callable = source_of_node_callable.func
|
|
400
|
+
node_hash = graph_types.hash_source_code(source_of_node_callable, strip=True)
|
|
401
|
+
cache_key = CacheAdapter.create_key(node_hash, node_kwargs)
|
|
402
|
+
|
|
403
|
+
from_cache = self.cache.get(cache_key, None)
|
|
404
|
+
if from_cache is not None:
|
|
405
|
+
return from_cache
|
|
406
|
+
|
|
407
|
+
self.used_nodes_hash[node_name] = node_hash
|
|
408
|
+
self.nodes_history[node_name] = self.nodes_history.get(node_name, []) + [node_hash]
|
|
409
|
+
return node_callable(**node_kwargs)
|
|
410
|
+
|
|
411
|
+
def run_after_node_execution(
|
|
412
|
+
self, *, node_name: str, node_kwargs: dict[str, Any], result: Any, **kwargs
|
|
413
|
+
):
|
|
414
|
+
"""If `run_to_execute_node` was a cache miss (hash stored in `used_nodes_hash`),
|
|
415
|
+
store the computed result in cache
|
|
416
|
+
"""
|
|
417
|
+
if node_name not in self.cache_vars:
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
node_hash = self.used_nodes_hash.get(node_name)
|
|
421
|
+
if node_hash is None:
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
cache_key = CacheAdapter.create_key(node_hash, node_kwargs)
|
|
425
|
+
self.cache[cache_key] = result
|
|
426
|
+
|
|
427
|
+
def run_after_graph_execution(self, *args, **kwargs):
|
|
428
|
+
"""After completing execution, overwrite nodes_history_key in cache and close"""
|
|
429
|
+
# TODO updating `nodes_history` at graph completion instead of after node execution
|
|
430
|
+
# means a desync is possible if the graph fails. Could lead to missing keys in
|
|
431
|
+
# `nodes_history`
|
|
432
|
+
self.cache[CacheAdapter.nodes_history_key] = self.nodes_history
|
|
433
|
+
self.cache.close()
|
|
434
|
+
|
|
435
|
+
def run_before_node_execution(self, *args, **kwargs):
|
|
436
|
+
"""Placeholder required to subclass `NodeExecutionMethod`"""
|
|
437
|
+
pass
|
|
438
|
+
|
|
439
|
+
@staticmethod
|
|
440
|
+
def create_key(node_hash: str, node_inputs: dict[str, Any]) -> str:
|
|
441
|
+
"""Pickle objects into bytes then get their hash value"""
|
|
442
|
+
digest = hashlib.sha256()
|
|
443
|
+
digest.update(node_hash.encode())
|
|
444
|
+
|
|
445
|
+
for ins in node_inputs.values():
|
|
446
|
+
digest.update(pickle.dumps(ins))
|
|
447
|
+
|
|
448
|
+
return digest.hexdigest()
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def wait_random(mean: float, stddev: float):
|
|
452
|
+
sleep_time = random.gauss(mu=mean, sigma=stddev)
|
|
453
|
+
if sleep_time < 0:
|
|
454
|
+
sleep_time = 0
|
|
455
|
+
time.sleep(sleep_time)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class SlowDownYouMoveTooFast(NodeExecutionHook):
|
|
459
|
+
"""This hook makes your DAG run slower. Just pass in a negative value for the sleep time to make it go faster...
|
|
460
|
+
In all seriousness though, there is absolutely no (good) reason to use this hook. Its dumb, and just for testing.
|
|
461
|
+
"""
|
|
462
|
+
|
|
463
|
+
def __init__(self, sleep_time_mean: float, sleep_time_std: float):
|
|
464
|
+
"""In all seriousness, don't use this
|
|
465
|
+
|
|
466
|
+
:param sleep_time_mean: Mean of sleep time
|
|
467
|
+
:param sleep_time_std: Stddev of sleep time
|
|
468
|
+
"""
|
|
469
|
+
self.sleep_time_mean = sleep_time_mean
|
|
470
|
+
self.sleep_time_std = sleep_time_std
|
|
471
|
+
|
|
472
|
+
def run_before_node_execution(self, **future_kwargs: Any):
|
|
473
|
+
"""Waits for a fixed set of time before node execution"""
|
|
474
|
+
wait_random(self.sleep_time_mean, self.sleep_time_std)
|
|
475
|
+
|
|
476
|
+
def run_after_node_execution(self, **future_kwargs: Any):
|
|
477
|
+
"""Does nothing"""
|
|
478
|
+
pass
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
class FunctionInputOutputTypeChecker(NodeExecutionHook):
|
|
482
|
+
"""This lifecycle hook checks the input and output types of a function.
|
|
483
|
+
|
|
484
|
+
It is a simple, but very strict type check against the declared type with what was actually received.
|
|
485
|
+
E.g. if you don't want to check the types of a dictionary, don't annotate it with a type.
|
|
486
|
+
"""
|
|
487
|
+
|
|
488
|
+
def __init__(self, check_input: bool = True, check_output: bool = True):
|
|
489
|
+
"""Constructor.
|
|
490
|
+
|
|
491
|
+
:param check_input: check inputs to all functions
|
|
492
|
+
:param check_output: check outputs to all functions
|
|
493
|
+
"""
|
|
494
|
+
self.check_input = check_input
|
|
495
|
+
self.check_output = check_output
|
|
496
|
+
|
|
497
|
+
def run_before_node_execution(
|
|
498
|
+
self,
|
|
499
|
+
node_name: str,
|
|
500
|
+
node_tags: dict[str, Any],
|
|
501
|
+
node_kwargs: dict[str, Any],
|
|
502
|
+
node_return_type: type,
|
|
503
|
+
task_id: str | None,
|
|
504
|
+
run_id: str,
|
|
505
|
+
node_input_types: dict[str, Any],
|
|
506
|
+
**future_kwargs: Any,
|
|
507
|
+
):
|
|
508
|
+
"""Checks that the result type matches the expected node return type."""
|
|
509
|
+
if self.check_input:
|
|
510
|
+
for input_name, input_value in node_kwargs.items():
|
|
511
|
+
if not htypes.check_instance(input_value, node_input_types[input_name]):
|
|
512
|
+
raise TypeError(
|
|
513
|
+
f"Node {node_name} received an input of type {type(input_value)} for {input_name}, expected {node_input_types[input_name]}"
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
def run_after_node_execution(
|
|
517
|
+
self,
|
|
518
|
+
node_name: str,
|
|
519
|
+
node_tags: dict[str, Any],
|
|
520
|
+
node_kwargs: dict[str, Any],
|
|
521
|
+
node_return_type: type,
|
|
522
|
+
result: Any,
|
|
523
|
+
error: Exception | None,
|
|
524
|
+
success: bool,
|
|
525
|
+
task_id: str | None,
|
|
526
|
+
run_id: str,
|
|
527
|
+
**future_kwargs: Any,
|
|
528
|
+
):
|
|
529
|
+
"""Checks that the result type matches the expected node return type."""
|
|
530
|
+
if self.check_output:
|
|
531
|
+
# Replace the isinstance check in your code with check_instance
|
|
532
|
+
if not htypes.check_instance(result, node_return_type):
|
|
533
|
+
raise TypeError(
|
|
534
|
+
f"Node {node_name} returned a result of type {type(result)}, expected {node_return_type}"
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
SENTINEL_DEFAULT = None # sentinel value -- lazy for now
|
|
539
|
+
INJECTION_ALLOWED = "injection is requested"
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def accept_error_sentinels(func: Callable):
|
|
543
|
+
"""Tag a function to allow passing in error sentinels.
|
|
544
|
+
|
|
545
|
+
For use with ``GracefulErrorAdapter``. The standard adapter behavior is to skip a node
|
|
546
|
+
when an error sentinel is one of its inputs. This decorator will cause the node to
|
|
547
|
+
run, and place the error sentinel into the appropriate input.
|
|
548
|
+
|
|
549
|
+
Take care to ensure your sentinels are easily distinguishable if you do this - see the
|
|
550
|
+
note in the GracefulErrorAdapater docstring.
|
|
551
|
+
|
|
552
|
+
A use case is any data or computation aggregation step that still wants partial results,
|
|
553
|
+
or considers a failure interesting enough to log or notify.
|
|
554
|
+
|
|
555
|
+
.. code-block:: python
|
|
556
|
+
|
|
557
|
+
SENTINEL = object()
|
|
558
|
+
|
|
559
|
+
...
|
|
560
|
+
|
|
561
|
+
@accept_error_sentinels
|
|
562
|
+
def results_gathering(result_1: float, result_2: float) -> dict[str, Any]:
|
|
563
|
+
answer = {}
|
|
564
|
+
for name, res in zip(["result 1", "result 2"], [result_1, result_2])
|
|
565
|
+
answer[name] = res
|
|
566
|
+
if res is SENTINEL:
|
|
567
|
+
answer[name] = "Node failure: no result"
|
|
568
|
+
# You may want side-effects for a failure.
|
|
569
|
+
_send_text_that_your_runs_errored()
|
|
570
|
+
return answer
|
|
571
|
+
|
|
572
|
+
...
|
|
573
|
+
adapter = GracefulErrorAdapter(sentinel_value=SENTINEL)
|
|
574
|
+
...
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
"""
|
|
578
|
+
# This inline import is not ideal -- we have to do this due to a circular reference
|
|
579
|
+
# See PR + comments here https://github.com/apache/hamilton/pull/1017
|
|
580
|
+
# TODO -- fix the circular reference -- we should be able to depend on this and no pull in lifecycle methods
|
|
581
|
+
from hamilton.function_modifiers.metadata import tag
|
|
582
|
+
|
|
583
|
+
_the_tag = tag(
|
|
584
|
+
**{"hamilton.error_sentinel": INJECTION_ALLOWED}, bypass_reserved_namespaces_=True
|
|
585
|
+
)
|
|
586
|
+
return _the_tag(func)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class GracefulErrorAdapter(NodeExecutionMethod):
|
|
590
|
+
"""Gracefully handles errors in a graph's execution. This allows you to proceed despite failure,
|
|
591
|
+
dynamically pruning branches. While it still runs every node, it replaces them with no-ops if any upstream
|
|
592
|
+
required dependencies fail (including optional dependencies).
|
|
593
|
+
"""
|
|
594
|
+
|
|
595
|
+
def __init__(
|
|
596
|
+
self,
|
|
597
|
+
error_to_catch: type[Exception],
|
|
598
|
+
sentinel_value: Any = SENTINEL_DEFAULT,
|
|
599
|
+
try_all_parallel: bool = True,
|
|
600
|
+
allow_injection: bool = True,
|
|
601
|
+
):
|
|
602
|
+
"""Initializes the adapter. Allows you to customize the error to catch (which exception
|
|
603
|
+
your graph will throw to indicate failure), as well as the sentinel value to use in place of
|
|
604
|
+
a node's result if it fails (this defaults to ``None``).
|
|
605
|
+
|
|
606
|
+
Note that this is currently only compatible with the dict-based result builder (use at your
|
|
607
|
+
own risk with pandas series, etc...).
|
|
608
|
+
|
|
609
|
+
Be careful using ``None`` as the default -- feel free to replace it with a sentinel value
|
|
610
|
+
of your choice (this could negatively impact your graph's execution if you actually *do* intend
|
|
611
|
+
to use ``None`` return values).
|
|
612
|
+
|
|
613
|
+
You can use this as follows:
|
|
614
|
+
|
|
615
|
+
.. code-block:: python
|
|
616
|
+
|
|
617
|
+
# my_module.py
|
|
618
|
+
# custom exception
|
|
619
|
+
class DoNotProceed(Exception):
|
|
620
|
+
pass
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def wont_proceed() -> int:
|
|
624
|
+
raise DoNotProceed()
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def will_proceed() -> int:
|
|
628
|
+
return 1
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def never_reached(wont_proceed: int) -> int:
|
|
632
|
+
return 1 # this should not be reached
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
dr = (
|
|
636
|
+
driver.Builder()
|
|
637
|
+
.with_modules(my_module)
|
|
638
|
+
.with_adapters(
|
|
639
|
+
default.GracefulErrorAdapter(
|
|
640
|
+
error_to_catch=DoNotProceed,
|
|
641
|
+
sentinel_value=None
|
|
642
|
+
)
|
|
643
|
+
)
|
|
644
|
+
.build()
|
|
645
|
+
)
|
|
646
|
+
dr.execute(
|
|
647
|
+
["will_proceed", "never_reached"]
|
|
648
|
+
) # will return {'will_proceed': 1, 'never_reached': None}
|
|
649
|
+
|
|
650
|
+
Note you can customize the error you want it to fail on and the sentinel value to use in place of a node's result if it fails.
|
|
651
|
+
|
|
652
|
+
For Parallelizable nodes, this adapter will attempt to iterate over the node outputs.
|
|
653
|
+
If an error occurs, the sentinel value is returned and no more iterations over the node
|
|
654
|
+
will occur. Meaning if item (3) fails out of 1,2,3,4,5, 4/5 will not run. If you set
|
|
655
|
+
``try_all_parallel`` to be False, it only sends one sentinel value into the parallelize sub-dag.
|
|
656
|
+
|
|
657
|
+
Here's an example for parallelizable to demonstrate try_all_parallel:
|
|
658
|
+
|
|
659
|
+
.. code-block:: python
|
|
660
|
+
|
|
661
|
+
# parallel_module.py
|
|
662
|
+
# custom exception
|
|
663
|
+
class DoNotProceed(Exception):
|
|
664
|
+
pass
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def start_point() -> Parallelizable[int]:
|
|
668
|
+
for i in range(5):
|
|
669
|
+
if i == 3:
|
|
670
|
+
raise DoNotProceed()
|
|
671
|
+
yield i
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def inner(start_point: int) -> int:
|
|
675
|
+
return start_point
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def gather(inner: Collect[int]) -> list[int]:
|
|
679
|
+
return inner
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
dr = (
|
|
683
|
+
driver.Builder()
|
|
684
|
+
.with_modules(parallel_module)
|
|
685
|
+
.with_adapters(
|
|
686
|
+
default.GracefulErrorAdapter(
|
|
687
|
+
error_to_catch=DoNotProceed,
|
|
688
|
+
sentinel_value=None,
|
|
689
|
+
try_all_parallel=True,
|
|
690
|
+
)
|
|
691
|
+
)
|
|
692
|
+
.build()
|
|
693
|
+
)
|
|
694
|
+
dr.execute(["gather"]) # will return {'gather': [0,1,2,None]}
|
|
695
|
+
|
|
696
|
+
dr = (
|
|
697
|
+
driver.Builder()
|
|
698
|
+
.with_modules(parallel_module)
|
|
699
|
+
.with_adapters(
|
|
700
|
+
default.GracefulErrorAdapter(
|
|
701
|
+
error_to_catch=DoNotProceed,
|
|
702
|
+
sentinel_value=None,
|
|
703
|
+
try_all_parallel=False,
|
|
704
|
+
)
|
|
705
|
+
)
|
|
706
|
+
.build()
|
|
707
|
+
)
|
|
708
|
+
dr.execute(["gather"]) # will return {'gather': [None]}
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
:param error_to_catch: The error to catch
|
|
712
|
+
:param sentinel_value: The sentinel value to use in place of a node's result if it fails
|
|
713
|
+
:param try_all_parallel: Gather parallelizable outputs until a failure, then add a Sentinel.
|
|
714
|
+
:param allow_injection: Flag for considering the ``accept_error_sentinels`` tag. Defaults to True.
|
|
715
|
+
"""
|
|
716
|
+
self.error_to_catch = error_to_catch
|
|
717
|
+
self.sentinel_value = sentinel_value
|
|
718
|
+
self.try_all_parallel = try_all_parallel
|
|
719
|
+
self.allow_injection = allow_injection
|
|
720
|
+
|
|
721
|
+
def run_to_execute_node(
|
|
722
|
+
self,
|
|
723
|
+
*,
|
|
724
|
+
node_callable: Any,
|
|
725
|
+
node_kwargs: dict[str, Any],
|
|
726
|
+
is_expand: bool,
|
|
727
|
+
is_collect: bool,
|
|
728
|
+
**future_kwargs: Any,
|
|
729
|
+
) -> Any:
|
|
730
|
+
# You can use the `is_expand` to see if the node is parallelizable
|
|
731
|
+
# You can use the `is_collect` to see if the node is a collect node
|
|
732
|
+
# TODO -- if it is parallelizable, run the generator in a special way (E.G. loop through the node callable
|
|
733
|
+
# and truncate it/provide sentinels for every failure)
|
|
734
|
+
# TODO -- decide what to do with collect
|
|
735
|
+
"""Executes a node. If the node fails, returns the sentinel value."""
|
|
736
|
+
default_return = [self.sentinel_value] if is_expand else self.sentinel_value
|
|
737
|
+
_node_tags = future_kwargs["node_tags"]
|
|
738
|
+
can_inject = _node_tags.get("hamilton.error_sentinel", "") == INJECTION_ALLOWED
|
|
739
|
+
can_inject = can_inject and self.allow_injection
|
|
740
|
+
|
|
741
|
+
if not can_inject:
|
|
742
|
+
for _key, value in node_kwargs.items():
|
|
743
|
+
if type(self.sentinel_value) is type(value):
|
|
744
|
+
if self.sentinel_value == value: # == versus is
|
|
745
|
+
return default_return
|
|
746
|
+
if not is_expand:
|
|
747
|
+
try:
|
|
748
|
+
return node_callable(**node_kwargs)
|
|
749
|
+
except self.error_to_catch:
|
|
750
|
+
return self.sentinel_value
|
|
751
|
+
|
|
752
|
+
if not self.try_all_parallel:
|
|
753
|
+
gen_func = node_callable
|
|
754
|
+
else:
|
|
755
|
+
# Grab the partial-ized function that is a parallelizable.
|
|
756
|
+
gen_func = node_callable.keywords["_callable"]
|
|
757
|
+
try:
|
|
758
|
+
gen = gen_func(**node_kwargs)
|
|
759
|
+
except self.error_to_catch:
|
|
760
|
+
return [self.sentinel_value]
|
|
761
|
+
results: list[Any] = []
|
|
762
|
+
try:
|
|
763
|
+
for _res in gen:
|
|
764
|
+
results.append(_res)
|
|
765
|
+
except self.error_to_catch:
|
|
766
|
+
if self.try_all_parallel:
|
|
767
|
+
results.append(self.sentinel_value)
|
|
768
|
+
else:
|
|
769
|
+
results = [self.sentinel_value]
|
|
770
|
+
return results
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
class NoEdgeAndInputTypeChecking(EdgeConnectionHook):
|
|
774
|
+
"""Permissive adapter to help you skip edge and input type checking.
|
|
775
|
+
|
|
776
|
+
Useful for development.
|
|
777
|
+
|
|
778
|
+
.. code-block:: python
|
|
779
|
+
|
|
780
|
+
from hamilton import driver
|
|
781
|
+
from hamilton.lifecycle import NoEdgeAndInputTypeChecking
|
|
782
|
+
|
|
783
|
+
dr = driver.Builder().with_adapters(NoEdgeAndInputTypeChecking()).build()
|
|
784
|
+
|
|
785
|
+
# now driver is built without any type checking
|
|
786
|
+
dr.execute([...], ...)
|
|
787
|
+
|
|
788
|
+
"""
|
|
789
|
+
|
|
790
|
+
def check_edge_types_match(self, type_from: type, type_to: type, **kwargs: Any) -> bool:
|
|
791
|
+
"""This is run to check if edge types match. Note that this is an OR functionality
|
|
792
|
+
-- this is run after we do some default checks, so this can only be permissive.
|
|
793
|
+
Return True - always
|
|
794
|
+
"""
|
|
795
|
+
return True
|
|
796
|
+
|
|
797
|
+
def validate_input(self, node_type: type, input_value: Any, **kwargs: Any) -> bool:
|
|
798
|
+
"""This is run to check if the input is valid for the node type. Note that this is an OR functionality
|
|
799
|
+
-- this is run after we do some default checks, so this can only be permissive.
|
|
800
|
+
Returns True - always.
|
|
801
|
+
"""
|
|
802
|
+
return True
|