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,1130 @@
|
|
|
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
|
+
"""Base lifecycle hooks/methods. This is *not* a public facing API -- see api.py for classes to extend.
|
|
19
|
+
This contains two sets of components:
|
|
20
|
+
|
|
21
|
+
1. Hooks/methods -- these are classes that customizes Hamilton's execution. They are called at specific
|
|
22
|
+
points in Hamilton's execution, and can be used to customize performance. There are specific rules about hooks
|
|
23
|
+
and methods
|
|
24
|
+
- Methods can not (currently) be layered. This is because they replace a component of Hamilton's execution
|
|
25
|
+
- Hooks can be layered. Multiple of the same hooks can be called at any given point.
|
|
26
|
+
2. Auxiliary tooling to register/manage hooks
|
|
27
|
+
- LifecycleAdapterSet -- this is a class that manages a set of lifecycle adapters. It allows us to call
|
|
28
|
+
all the lifecycle hooks/methods in a given set, and to determine if a given hook/method is implemented.
|
|
29
|
+
- lifecycle -- this is a decorator container that allows us to register hooks/methods. It is used as follows:
|
|
30
|
+
|
|
31
|
+
To implement a new method/hook type:
|
|
32
|
+
1. Create a class that has a single method (see below for examples)
|
|
33
|
+
2. Decorate the class with the lifecycle decorator, passing in the name of the method/hook. This must correspond to a method on the class.
|
|
34
|
+
3. Add to the LifecycleAdapter type
|
|
35
|
+
4. Call out to the hook at different points in the lifecycle
|
|
36
|
+
|
|
37
|
+
Note that you can have one async hook/method and one sync hook/method in the same class. Some hooks/methods
|
|
38
|
+
are coupled to certain execution contexts. While they all live here for now, we could easily envision moving them
|
|
39
|
+
externally.
|
|
40
|
+
|
|
41
|
+
To build an implementation of a hook/method, all you have to do is extend any number of classes.
|
|
42
|
+
See api.py for implementations.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
import abc
|
|
46
|
+
import asyncio
|
|
47
|
+
import collections
|
|
48
|
+
import dataclasses
|
|
49
|
+
import inspect
|
|
50
|
+
from collections.abc import Callable
|
|
51
|
+
from types import ModuleType
|
|
52
|
+
from typing import TYPE_CHECKING, Any, Union
|
|
53
|
+
|
|
54
|
+
from hamilton import htypes
|
|
55
|
+
|
|
56
|
+
# We need this because of a (required) circular reference
|
|
57
|
+
# Graph depends on lifecycle_base, as it uses the lifecycle hooks
|
|
58
|
+
# lifecycle_base has elements that use type-hinting with the FunctionGraph object
|
|
59
|
+
# This is OK -- in a *real* compiled language this wouldn't be an issue
|
|
60
|
+
# (you'd have header types in C, and java should be smart enough). Given that we're using
|
|
61
|
+
# python, which (our usage of) leans type-hinting trigger-happy, this will suffice.
|
|
62
|
+
if TYPE_CHECKING:
|
|
63
|
+
from hamilton import graph, node
|
|
64
|
+
from hamilton.execution.grouping import NodeGroupPurpose
|
|
65
|
+
else:
|
|
66
|
+
NodeGroupPurpose = None
|
|
67
|
+
|
|
68
|
+
# All of these are internal APIs. Specifically, structure required to manage a set of
|
|
69
|
+
# hooks/methods/validators that we will likely expand. We store them in constants (rather than, say, a more complex single object)
|
|
70
|
+
# as it is a clear, simple way to manage the metadata. This allows us to track the registered hooks/methods/validators.
|
|
71
|
+
|
|
72
|
+
# A set of registered hooks -- each one refers to a string
|
|
73
|
+
REGISTERED_SYNC_HOOKS: set[str] = set()
|
|
74
|
+
REGISTERED_ASYNC_HOOKS: set[str] = set()
|
|
75
|
+
|
|
76
|
+
# A set of registered methods -- each one refers to a string, which is the name of the metho
|
|
77
|
+
REGISTERED_SYNC_METHODS: set[str] = set()
|
|
78
|
+
REGISTERED_ASYNC_METHODS: set[str] = set()
|
|
79
|
+
|
|
80
|
+
# A set of registered validators -- these have attached Exception data to them
|
|
81
|
+
# Note we do not curently have async validators -- see no need now
|
|
82
|
+
REGISTERED_SYNC_VALIDATORS: set[str] = set()
|
|
83
|
+
|
|
84
|
+
# constants to refer to internally for hooks
|
|
85
|
+
SYNC_HOOK = "hooks"
|
|
86
|
+
ASYNC_HOOK = "async_hooks"
|
|
87
|
+
|
|
88
|
+
# constants to refer to internally for methods
|
|
89
|
+
SYNC_METHOD = "methods"
|
|
90
|
+
ASYNC_METHOD = "async_methods"
|
|
91
|
+
|
|
92
|
+
# constants to refer to internally for validators
|
|
93
|
+
SYNC_VALIDATOR = "validators"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclasses.dataclass
|
|
97
|
+
class ValidationResult:
|
|
98
|
+
success: bool
|
|
99
|
+
error: str | None
|
|
100
|
+
validator: object # validator so we can make the error message more friendly
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ValidationException(Exception):
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class InvalidLifecycleAdapter(Exception):
|
|
108
|
+
"""Container exception to indicate that a lifecycle adapter is invalid."""
|
|
109
|
+
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def validate_lifecycle_adapter_function(
|
|
114
|
+
fn: Callable, returns_value: bool, return_type: type | None = None
|
|
115
|
+
):
|
|
116
|
+
"""Validates that a function has arguments that are keyword-only,
|
|
117
|
+
and either does or does not return a value, depending on the value of returns_value.
|
|
118
|
+
|
|
119
|
+
:param fn: The function to validate
|
|
120
|
+
:param returns_value: Whether the function should return a value or not
|
|
121
|
+
"""
|
|
122
|
+
sig = inspect.signature(fn)
|
|
123
|
+
if returns_value:
|
|
124
|
+
if sig.return_annotation is inspect.Signature.empty:
|
|
125
|
+
raise InvalidLifecycleAdapter(
|
|
126
|
+
f"Lifecycle methods must return a value, but {fn} does not have a return annotation."
|
|
127
|
+
)
|
|
128
|
+
if return_type is not None and not htypes.custom_subclass_check(
|
|
129
|
+
sig.return_annotation, return_type
|
|
130
|
+
):
|
|
131
|
+
raise InvalidLifecycleAdapter(
|
|
132
|
+
f"Lifecycle methods must return a value of type {return_type}, "
|
|
133
|
+
f"but {fn} has a return annotation of "
|
|
134
|
+
f"type {sig.return_annotation}."
|
|
135
|
+
)
|
|
136
|
+
if not returns_value and sig.return_annotation is not inspect.Signature.empty:
|
|
137
|
+
raise InvalidLifecycleAdapter(
|
|
138
|
+
f"Lifecycle hooks/validators must not return a value, but {fn} has a return annotation."
|
|
139
|
+
)
|
|
140
|
+
for param in sig.parameters.values():
|
|
141
|
+
if param.kind != inspect.Parameter.KEYWORD_ONLY and param.name != "self":
|
|
142
|
+
raise InvalidLifecycleAdapter(
|
|
143
|
+
f"Lifecycle methods/hooks can only have keyword-only arguments. "
|
|
144
|
+
f"Method/hook {fn} has argument {param} that is not keyword-only."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def validate_hook_fn(fn: Callable):
|
|
149
|
+
"""Validates that a function forms a valid hook. This means:
|
|
150
|
+
1. Function returns nothing
|
|
151
|
+
2. Function must consist of only kwarg-only arguments
|
|
152
|
+
|
|
153
|
+
:param fn: The function to validate
|
|
154
|
+
:raises InvalidLifecycleAdapter: If the function is not a valid hook
|
|
155
|
+
"""
|
|
156
|
+
validate_lifecycle_adapter_function(fn, returns_value=False)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def validate_method_fn(fn: Callable):
|
|
160
|
+
"""Validates that a function forms a valid method. This means:
|
|
161
|
+
1. Function returns a value
|
|
162
|
+
2. Functions must consist of only kwarg-only arguments
|
|
163
|
+
|
|
164
|
+
:param fn: The function to validate
|
|
165
|
+
:raises InvalidLifecycleAdapter: If the function is not a valid method
|
|
166
|
+
"""
|
|
167
|
+
validate_lifecycle_adapter_function(fn, returns_value=True)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def validate_validator_fn(fn: Callable):
|
|
171
|
+
"""Ensures that a function forms a registerable "validator". These are currently the same rules as "hooks".
|
|
172
|
+
While they should also raise an exception, that is not possible to express in the type annotation.
|
|
173
|
+
|
|
174
|
+
:param fn: Function to validate
|
|
175
|
+
:raises InvalidLifecycleAdapter: If the function is not a valid validator
|
|
176
|
+
"""
|
|
177
|
+
if inspect.iscoroutinefunction(fn):
|
|
178
|
+
raise InvalidLifecycleAdapter(
|
|
179
|
+
f"Lifecycle validators must (so far) be synchronous, but {fn} is an async function. "
|
|
180
|
+
)
|
|
181
|
+
validate_lifecycle_adapter_function(fn, returns_value=True)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class lifecycle:
|
|
185
|
+
"""Container class for decorators to register hooks/methods.
|
|
186
|
+
This is just a container so it looks clean (`@lifecycle.base_hook(...)`), but we could easily move it out.
|
|
187
|
+
What do these decorators do?
|
|
188
|
+
1. We decorate a class with a method/hook/validator call
|
|
189
|
+
2. This implies that there exists a function by that name
|
|
190
|
+
3. We validate that that function has an appropriate signature
|
|
191
|
+
4. We store this in the appropriate registry (see the constants above)
|
|
192
|
+
Then, when we want to perform a hook/method/validator, we can ask the AdapterLifecycleSet to do so.
|
|
193
|
+
It crawls up the MRO, looking to see which classes are in the registry, then elects which functions to run.
|
|
194
|
+
See LifecycleAdapterSet for more information.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def base_hook(cls, fn_name: str):
|
|
199
|
+
"""Hooks get called at distinct stages of Hamilton's execution.
|
|
200
|
+
These can be layered together, and potentially coupled to other hooks.
|
|
201
|
+
|
|
202
|
+
:param fn_name: Name of the function that will reside in the class we're decorating
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def decorator(clazz):
|
|
206
|
+
fn = getattr(clazz, fn_name, None)
|
|
207
|
+
if fn is None:
|
|
208
|
+
raise ValueError(
|
|
209
|
+
f"Class {clazz} does not have a method {fn_name}, but is "
|
|
210
|
+
f'decorated with @lifecycle.base_hook("{fn_name}"). The parameter '
|
|
211
|
+
f"to @lifecycle.base_hook must be the name "
|
|
212
|
+
f"of a method on the class."
|
|
213
|
+
)
|
|
214
|
+
validate_hook_fn(fn)
|
|
215
|
+
if inspect.iscoroutinefunction(fn):
|
|
216
|
+
setattr(clazz, ASYNC_HOOK, fn_name)
|
|
217
|
+
REGISTERED_ASYNC_HOOKS.add(fn_name)
|
|
218
|
+
else:
|
|
219
|
+
setattr(clazz, SYNC_HOOK, fn_name)
|
|
220
|
+
REGISTERED_SYNC_HOOKS.add(fn_name)
|
|
221
|
+
return clazz
|
|
222
|
+
|
|
223
|
+
return decorator
|
|
224
|
+
|
|
225
|
+
@classmethod
|
|
226
|
+
def base_method(cls, fn_name: str):
|
|
227
|
+
"""Methods replace the default behavior of Hamilton at a given stage.
|
|
228
|
+
Thus they can only be called once, and not layered. TODO -- determine
|
|
229
|
+
how to allow multiple/have precedence for custom behavior.
|
|
230
|
+
|
|
231
|
+
:param fn_name: Name of the function in the class we're registering.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
def decorator(clazz):
|
|
235
|
+
fn = getattr(clazz, fn_name, None)
|
|
236
|
+
if fn is None:
|
|
237
|
+
raise ValueError(
|
|
238
|
+
f"Class {clazz} does not have a method {fn_name}, but is "
|
|
239
|
+
f'decorated with @lifecycle.base_hook("{fn_name}"). The parameter '
|
|
240
|
+
f"to @lifecycle.base_hook must be the name "
|
|
241
|
+
f"of a method on the class."
|
|
242
|
+
)
|
|
243
|
+
validate_method_fn(fn)
|
|
244
|
+
if inspect.iscoroutinefunction(fn):
|
|
245
|
+
setattr(clazz, ASYNC_METHOD, fn_name)
|
|
246
|
+
REGISTERED_ASYNC_METHODS.add(fn_name)
|
|
247
|
+
else:
|
|
248
|
+
setattr(clazz, SYNC_METHOD, fn_name)
|
|
249
|
+
REGISTERED_SYNC_METHODS.add(fn_name)
|
|
250
|
+
return clazz
|
|
251
|
+
|
|
252
|
+
return decorator
|
|
253
|
+
|
|
254
|
+
@classmethod
|
|
255
|
+
def base_validator(cls, fn_name: str):
|
|
256
|
+
"""Validators are hooks that return a validation result (tuple[success: bool, message: Optional[str]]).
|
|
257
|
+
They provide custom validation logic that runs statically (before the DAG), rather than dynamically (during the DAG run),
|
|
258
|
+
and multiple can be layered together.
|
|
259
|
+
|
|
260
|
+
:param fn_name: Name of the function in the class we're registering.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
def decorator(clazz):
|
|
264
|
+
fn = getattr(clazz, fn_name, None)
|
|
265
|
+
if fn is None:
|
|
266
|
+
raise ValueError(
|
|
267
|
+
f"Class {clazz} does not have a method {fn_name}, but is "
|
|
268
|
+
f'decorated with @lifecycle.base_validator("{fn_name}"). The parameter '
|
|
269
|
+
f"to @lifecycle.base_hook must be the name "
|
|
270
|
+
f"of a method on the class."
|
|
271
|
+
)
|
|
272
|
+
validate_validator_fn(fn)
|
|
273
|
+
setattr(clazz, SYNC_VALIDATOR, fn_name)
|
|
274
|
+
REGISTERED_SYNC_VALIDATORS.add(fn_name)
|
|
275
|
+
return clazz
|
|
276
|
+
|
|
277
|
+
return decorator
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@lifecycle.base_hook("pre_do_anything")
|
|
281
|
+
class BasePreDoAnythingHook(abc.ABC):
|
|
282
|
+
@abc.abstractmethod
|
|
283
|
+
def pre_do_anything(self):
|
|
284
|
+
"""Synchronous hook that gets called before doing anything, in the constructor of the driver."""
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@lifecycle.base_method("do_check_edge_types_match")
|
|
289
|
+
class BaseDoCheckEdgeTypesMatch(abc.ABC):
|
|
290
|
+
@abc.abstractmethod
|
|
291
|
+
def do_check_edge_types_match(self, *, type_from: type, type_to: type) -> bool:
|
|
292
|
+
"""Method that checks whether two types are equivalent. This is used when the function graph is being created.
|
|
293
|
+
|
|
294
|
+
:param type_from: The type of the node that is the source of the edge.
|
|
295
|
+
:param type_to: The type of the node that is the destination of the edge.
|
|
296
|
+
:return bool: Whether or not they are equivalent
|
|
297
|
+
"""
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@lifecycle.base_method("do_validate_input")
|
|
302
|
+
class BaseDoValidateInput(abc.ABC):
|
|
303
|
+
@abc.abstractmethod
|
|
304
|
+
def do_validate_input(self, *, node_type: type, input_value: Any) -> bool:
|
|
305
|
+
"""Method that an input value maches an expected type.
|
|
306
|
+
|
|
307
|
+
:param node_type: The type of the node.
|
|
308
|
+
:param input_value: The value that we want to validate.
|
|
309
|
+
:return: Whether or not the input value matches the expected type.
|
|
310
|
+
"""
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@lifecycle.base_validator("validate_node")
|
|
315
|
+
class BaseValidateNode(abc.ABC):
|
|
316
|
+
@abc.abstractmethod
|
|
317
|
+
def validate_node(self, *, created_node: "node.Node") -> tuple[bool, Exception | None]:
|
|
318
|
+
"""Validates a node. This will raise an InvalidNodeException
|
|
319
|
+
if the node is invalid.
|
|
320
|
+
|
|
321
|
+
:param created_node: Node that was created.
|
|
322
|
+
:raises InvalidNodeException: If the node is invalid.
|
|
323
|
+
"""
|
|
324
|
+
pass
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@lifecycle.base_validator("validate_graph")
|
|
328
|
+
class BaseValidateGraph(abc.ABC):
|
|
329
|
+
@abc.abstractmethod
|
|
330
|
+
def validate_graph(
|
|
331
|
+
self,
|
|
332
|
+
*,
|
|
333
|
+
graph: "graph.FunctionGraph",
|
|
334
|
+
modules: list[ModuleType],
|
|
335
|
+
config: dict[str, Any],
|
|
336
|
+
) -> tuple[bool, str | None]:
|
|
337
|
+
"""Validates the graph. This will raise an InvalidNodeException
|
|
338
|
+
|
|
339
|
+
:param graph: Graph that has been constructed.
|
|
340
|
+
:param modules: Modules passed into the graph
|
|
341
|
+
:param config: Config passed into the graph
|
|
342
|
+
:return: A (is_valid, error_message) tuple
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@lifecycle.base_hook("post_graph_construct")
|
|
347
|
+
class BasePostGraphConstruct(abc.ABC):
|
|
348
|
+
@abc.abstractmethod
|
|
349
|
+
def post_graph_construct(
|
|
350
|
+
self,
|
|
351
|
+
*,
|
|
352
|
+
graph: "graph.FunctionGraph",
|
|
353
|
+
modules: list[ModuleType],
|
|
354
|
+
config: dict[str, Any],
|
|
355
|
+
):
|
|
356
|
+
"""Hooks that is called after the graph is constructed.
|
|
357
|
+
|
|
358
|
+
:param graph: Graph that has been constructed.
|
|
359
|
+
:param modules: Modules passed into the graph
|
|
360
|
+
:param config: Config passed into the graph
|
|
361
|
+
"""
|
|
362
|
+
pass
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@lifecycle.base_hook("post_graph_construct")
|
|
366
|
+
class BasePostGraphConstructAsync(abc.ABC):
|
|
367
|
+
@abc.abstractmethod
|
|
368
|
+
async def post_graph_construct(
|
|
369
|
+
self,
|
|
370
|
+
*,
|
|
371
|
+
graph: "graph.FunctionGraph",
|
|
372
|
+
modules: list[ModuleType],
|
|
373
|
+
config: dict[str, Any],
|
|
374
|
+
):
|
|
375
|
+
"""Asynchronous hook that is called after the graph is constructed.
|
|
376
|
+
|
|
377
|
+
:param graph: Graph that has been constructed.
|
|
378
|
+
:param modules: Modules passed into the graph
|
|
379
|
+
:param config: Config passed into the graph
|
|
380
|
+
"""
|
|
381
|
+
pass
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@lifecycle.base_hook("pre_graph_execute")
|
|
385
|
+
class BasePreGraphExecute(abc.ABC):
|
|
386
|
+
@abc.abstractmethod
|
|
387
|
+
def pre_graph_execute(
|
|
388
|
+
self,
|
|
389
|
+
*,
|
|
390
|
+
run_id: str,
|
|
391
|
+
graph: "graph.FunctionGraph",
|
|
392
|
+
final_vars: list[str],
|
|
393
|
+
inputs: dict[str, Any],
|
|
394
|
+
overrides: dict[str, Any],
|
|
395
|
+
):
|
|
396
|
+
"""Hook that is called immediately prior to graph execution.
|
|
397
|
+
|
|
398
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
399
|
+
:param graph: Graph that is being executed
|
|
400
|
+
:param final_vars: Variables we are extracting from the graph
|
|
401
|
+
:param inputs: Inputs to the graph
|
|
402
|
+
:param overrides: Overrides to graph execution
|
|
403
|
+
"""
|
|
404
|
+
pass
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@lifecycle.base_hook("pre_graph_execute")
|
|
408
|
+
class BasePreGraphExecuteAsync(abc.ABC):
|
|
409
|
+
@abc.abstractmethod
|
|
410
|
+
async def pre_graph_execute(
|
|
411
|
+
self,
|
|
412
|
+
*,
|
|
413
|
+
run_id: str,
|
|
414
|
+
graph: "graph.FunctionGraph",
|
|
415
|
+
final_vars: list[str],
|
|
416
|
+
inputs: dict[str, Any],
|
|
417
|
+
overrides: dict[str, Any],
|
|
418
|
+
):
|
|
419
|
+
"""Asynchronous Hook that is called immediately prior to graph execution.
|
|
420
|
+
|
|
421
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
422
|
+
:param graph: Graph that is being executed
|
|
423
|
+
:param final_vars: Variables we are extracting from the graph
|
|
424
|
+
:param inputs: Inputs to the graph
|
|
425
|
+
:param overrides: Overrides to graph execution
|
|
426
|
+
"""
|
|
427
|
+
pass
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@lifecycle.base_hook("post_task_group")
|
|
431
|
+
class BasePostTaskGroup(abc.ABC):
|
|
432
|
+
@abc.abstractmethod
|
|
433
|
+
def post_task_group(self, *, run_id: str, task_ids: list[str]):
|
|
434
|
+
"""Hook that is called immediately after a task group is created. Note that this is only useful in dynamic
|
|
435
|
+
execution, although we reserve the right to add this back into the standard hamilton execution pattern.
|
|
436
|
+
|
|
437
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
438
|
+
:param task_ids: IDs of tasks that are in the group."""
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@lifecycle.base_hook("post_task_expand")
|
|
443
|
+
class BasePostTaskExpand(abc.ABC):
|
|
444
|
+
@abc.abstractmethod
|
|
445
|
+
def post_task_expand(self, *, run_id: str, task_id: str, parameters: dict[str, Any]):
|
|
446
|
+
"""Hook that is called immediately after a task is expanded into parallelizable tasks. Note that this is only useful
|
|
447
|
+
in dynamic execution.
|
|
448
|
+
|
|
449
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
450
|
+
:param task_id: ID of the task.
|
|
451
|
+
:param parameters: Parameters that are being passed to each of the expanded tasks."""
|
|
452
|
+
pass
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@lifecycle.base_hook("pre_task_submission")
|
|
456
|
+
class BasePreTaskSubmission(abc.ABC):
|
|
457
|
+
@abc.abstractmethod
|
|
458
|
+
def pre_task_submission(
|
|
459
|
+
self,
|
|
460
|
+
*,
|
|
461
|
+
run_id: str,
|
|
462
|
+
task_id: str,
|
|
463
|
+
nodes: list["node.Node"],
|
|
464
|
+
inputs: dict[str, Any],
|
|
465
|
+
overrides: dict[str, Any],
|
|
466
|
+
spawning_task_id: str | None,
|
|
467
|
+
purpose: NodeGroupPurpose,
|
|
468
|
+
):
|
|
469
|
+
"""Hook that is called immediately prior to task submission to an executor as a task future.
|
|
470
|
+
Note that this is only useful in dynamic execution, although we reserve the right to add this back
|
|
471
|
+
into the standard hamilton execution pattern.
|
|
472
|
+
|
|
473
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
474
|
+
:param task_id: ID of the task.
|
|
475
|
+
:param nodes: Nodes that are being executed.
|
|
476
|
+
:param inputs: Inputs to the task.
|
|
477
|
+
:param overrides: Overrides to task execution.
|
|
478
|
+
:param spawning_task_id: ID of the task that spawned this task.
|
|
479
|
+
:param purpose: Purpose of the current task group.
|
|
480
|
+
"""
|
|
481
|
+
pass
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@lifecycle.base_hook("pre_task_execute")
|
|
485
|
+
class BasePreTaskExecute(abc.ABC):
|
|
486
|
+
@abc.abstractmethod
|
|
487
|
+
def pre_task_execute(
|
|
488
|
+
self,
|
|
489
|
+
*,
|
|
490
|
+
run_id: str,
|
|
491
|
+
task_id: str,
|
|
492
|
+
nodes: list["node.Node"],
|
|
493
|
+
inputs: dict[str, Any],
|
|
494
|
+
overrides: dict[str, Any],
|
|
495
|
+
spawning_task_id: str | None,
|
|
496
|
+
purpose: NodeGroupPurpose,
|
|
497
|
+
):
|
|
498
|
+
"""Hook that is called immediately prior to task execution. Note that this is only useful in dynamic
|
|
499
|
+
execution, although we reserve the right to add this back into the standard hamilton execution pattern.
|
|
500
|
+
|
|
501
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
502
|
+
:param task_id: ID of the task, unique in scope of the driver.
|
|
503
|
+
:param nodes: Nodes that are being executed
|
|
504
|
+
:param inputs: Inputs to the task
|
|
505
|
+
:param overrides: Overrides to task execution
|
|
506
|
+
:param spawning_task_id: ID of the task that spawned this task
|
|
507
|
+
:param purpose: Purpose of the current task group
|
|
508
|
+
"""
|
|
509
|
+
pass
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
@lifecycle.base_hook("pre_task_execute")
|
|
513
|
+
class BasePreTaskExecuteAsync(abc.ABC):
|
|
514
|
+
@abc.abstractmethod
|
|
515
|
+
async def pre_task_execute(
|
|
516
|
+
self,
|
|
517
|
+
*,
|
|
518
|
+
run_id: str,
|
|
519
|
+
task_id: str,
|
|
520
|
+
nodes: list["node.Node"],
|
|
521
|
+
inputs: dict[str, Any],
|
|
522
|
+
overrides: dict[str, Any],
|
|
523
|
+
spawning_task_id: str | None,
|
|
524
|
+
purpose: NodeGroupPurpose,
|
|
525
|
+
):
|
|
526
|
+
"""Hook that is called immediately prior to task execution. Note that this is only useful in dynamic
|
|
527
|
+
execution, although we reserve the right to add this back into the standard hamilton execution pattern.
|
|
528
|
+
|
|
529
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
530
|
+
:param task_id: ID of the task, unique in scope of the driver.
|
|
531
|
+
:param nodes: Nodes that are being executed
|
|
532
|
+
:param inputs: Inputs to the task
|
|
533
|
+
:param overrides: Overrides to task execution
|
|
534
|
+
:param spawning_task_id: ID of the task that spawned this task
|
|
535
|
+
:param purpose: Purpose of the current task group
|
|
536
|
+
"""
|
|
537
|
+
pass
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
@lifecycle.base_hook("pre_node_execute")
|
|
541
|
+
class BasePreNodeExecute(abc.ABC):
|
|
542
|
+
@abc.abstractmethod
|
|
543
|
+
def pre_node_execute(
|
|
544
|
+
self,
|
|
545
|
+
*,
|
|
546
|
+
run_id: str,
|
|
547
|
+
node_: "node.Node",
|
|
548
|
+
kwargs: dict[str, Any],
|
|
549
|
+
task_id: str | None = None,
|
|
550
|
+
):
|
|
551
|
+
"""Hook that is called immediately prior to node execution.
|
|
552
|
+
|
|
553
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
554
|
+
:param node_: Node that is being executed
|
|
555
|
+
:param kwargs: Keyword arguments that are being passed into the node
|
|
556
|
+
:param task_id: ID of the task, defaults to None if not in a task setting
|
|
557
|
+
"""
|
|
558
|
+
pass
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
@lifecycle.base_hook("pre_node_execute")
|
|
562
|
+
class BasePreNodeExecuteAsync(abc.ABC):
|
|
563
|
+
@abc.abstractmethod
|
|
564
|
+
async def pre_node_execute(
|
|
565
|
+
self,
|
|
566
|
+
*,
|
|
567
|
+
run_id: str,
|
|
568
|
+
node_: "node.Node",
|
|
569
|
+
kwargs: dict[str, Any],
|
|
570
|
+
task_id: str | None = None,
|
|
571
|
+
):
|
|
572
|
+
"""Asynchronous hook that is called immediately prior to node execution.
|
|
573
|
+
|
|
574
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
575
|
+
:param node_: Node that is being executed
|
|
576
|
+
:param kwargs: Keyword arguments that are being passed into the node
|
|
577
|
+
:param task_id: ID of the task, defaults to None if not in a task setting
|
|
578
|
+
"""
|
|
579
|
+
pass
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
@lifecycle.base_method("do_node_execute")
|
|
583
|
+
class BaseDoNodeExecute(abc.ABC):
|
|
584
|
+
@abc.abstractmethod
|
|
585
|
+
def do_node_execute(
|
|
586
|
+
self,
|
|
587
|
+
*,
|
|
588
|
+
run_id: str,
|
|
589
|
+
node_: "node.Node",
|
|
590
|
+
kwargs: dict[str, Any],
|
|
591
|
+
task_id: str | None = None,
|
|
592
|
+
) -> Any:
|
|
593
|
+
"""Method that is called to implement node execution. This can replace the execution of a node
|
|
594
|
+
with something all together, augment it, or delegate it.
|
|
595
|
+
|
|
596
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
597
|
+
:param node_: Node that is being executed
|
|
598
|
+
:param kwargs: Keyword arguments that are being passed into the node
|
|
599
|
+
:param task_id: ID of the task, defaults to None if not in a task setting
|
|
600
|
+
"""
|
|
601
|
+
pass
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
@lifecycle.base_method("do_remote_execute")
|
|
605
|
+
class BaseDoRemoteExecute(abc.ABC):
|
|
606
|
+
@abc.abstractmethod
|
|
607
|
+
def do_remote_execute(
|
|
608
|
+
self,
|
|
609
|
+
*,
|
|
610
|
+
node: "node.Node",
|
|
611
|
+
kwargs: dict[str, Any],
|
|
612
|
+
execute_lifecycle_for_node: Callable,
|
|
613
|
+
) -> Any:
|
|
614
|
+
"""Method that is called to implement correct remote execution of hooks. This makes sure that all the pre-node and post-node hooks get executed in the remote environment which is necessary for some adapters. Node execution is called the same as before through "do_node_execute".
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
:param node: Node that is being executed
|
|
618
|
+
:param kwargs: Keyword arguments that are being passed into the node
|
|
619
|
+
:param execute_lifecycle_for_node: Function executing lifecycle_hooks and lifecycle_methods
|
|
620
|
+
"""
|
|
621
|
+
pass
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
@lifecycle.base_method("do_node_execute")
|
|
625
|
+
class BaseDoNodeExecuteAsync(abc.ABC):
|
|
626
|
+
@abc.abstractmethod
|
|
627
|
+
async def do_node_execute(
|
|
628
|
+
self,
|
|
629
|
+
*,
|
|
630
|
+
run_id: str,
|
|
631
|
+
node_: "node.Node",
|
|
632
|
+
kwargs: dict[str, Any],
|
|
633
|
+
task_id: str | None = None,
|
|
634
|
+
) -> Any:
|
|
635
|
+
"""Asynchronous method that is called to implement node execution. This can replace the execution of a node
|
|
636
|
+
with something all together, augment it, or delegate it.
|
|
637
|
+
|
|
638
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
639
|
+
:param node_: Node that is being executed
|
|
640
|
+
:param kwargs: Keyword arguments that are being passed into the node
|
|
641
|
+
:param task_id: ID of the task, defaults to None if not in a task setting
|
|
642
|
+
"""
|
|
643
|
+
pass
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
@lifecycle.base_hook("post_node_execute")
|
|
647
|
+
class BasePostNodeExecute(abc.ABC):
|
|
648
|
+
@abc.abstractmethod
|
|
649
|
+
def post_node_execute(
|
|
650
|
+
self,
|
|
651
|
+
*,
|
|
652
|
+
run_id: str,
|
|
653
|
+
node_: "node.Node",
|
|
654
|
+
kwargs: dict[str, Any],
|
|
655
|
+
success: bool,
|
|
656
|
+
error: Exception | None,
|
|
657
|
+
result: Any | None,
|
|
658
|
+
task_id: str | None = None,
|
|
659
|
+
):
|
|
660
|
+
"""Hook that is called immediately after node execution.
|
|
661
|
+
|
|
662
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
663
|
+
:param node_: Node that is being executed
|
|
664
|
+
:param kwargs: Keyword arguments that are being passed into the node
|
|
665
|
+
:param success: Whether or not the node executed successfully
|
|
666
|
+
:param error: The error that was raised, if any
|
|
667
|
+
:param result: The result of the node execution, if no error was raised
|
|
668
|
+
:param task_id: ID of the task, defaults to None if not in a task-based execution
|
|
669
|
+
"""
|
|
670
|
+
pass
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
@lifecycle.base_hook("post_node_execute")
|
|
674
|
+
class BasePostNodeExecuteAsync(abc.ABC):
|
|
675
|
+
@abc.abstractmethod
|
|
676
|
+
async def post_node_execute(
|
|
677
|
+
self,
|
|
678
|
+
*,
|
|
679
|
+
run_id: str,
|
|
680
|
+
node_: "node.Node",
|
|
681
|
+
kwargs: dict[str, Any],
|
|
682
|
+
success: bool,
|
|
683
|
+
error: Exception | None,
|
|
684
|
+
result: Any,
|
|
685
|
+
task_id: str | None = None,
|
|
686
|
+
):
|
|
687
|
+
"""Hook that is called immediately after node execution.
|
|
688
|
+
|
|
689
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
690
|
+
:param node_: Node that is being executed
|
|
691
|
+
:param kwargs: Keyword arguments that are being passed into the node
|
|
692
|
+
:param success: Whether or not the node executed successfully
|
|
693
|
+
:param error: The error that was raised, if any
|
|
694
|
+
:param result: The result of the node execution, if no error was raised
|
|
695
|
+
:param task_id: ID of the task, defaults to None if not in a task-based execution
|
|
696
|
+
"""
|
|
697
|
+
pass
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
@lifecycle.base_hook("post_task_execute")
|
|
701
|
+
class BasePostTaskExecute(abc.ABC):
|
|
702
|
+
@abc.abstractmethod
|
|
703
|
+
def post_task_execute(
|
|
704
|
+
self,
|
|
705
|
+
*,
|
|
706
|
+
run_id: str,
|
|
707
|
+
task_id: str,
|
|
708
|
+
nodes: list["node.Node"],
|
|
709
|
+
results: dict[str, Any] | None,
|
|
710
|
+
success: bool,
|
|
711
|
+
error: Exception,
|
|
712
|
+
spawning_task_id: str | None,
|
|
713
|
+
purpose: NodeGroupPurpose,
|
|
714
|
+
):
|
|
715
|
+
"""Hook called immediately after task execution. Note that this is only useful in dynamic
|
|
716
|
+
execution, although we reserve the right to add this back into the standard hamilton execution pattern.
|
|
717
|
+
|
|
718
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
719
|
+
:param task_id: ID of the task
|
|
720
|
+
:param nodes: Nodes that were executed
|
|
721
|
+
:param results: Results of the task
|
|
722
|
+
:param success: Whether or not the task executed successfully
|
|
723
|
+
:param error: The error that was raised, if any
|
|
724
|
+
:param spawning_task_id: ID of the task that spawned this task
|
|
725
|
+
:param purpose: Purpose of the current task group
|
|
726
|
+
"""
|
|
727
|
+
pass
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
@lifecycle.base_hook("post_task_execute")
|
|
731
|
+
class BasePostTaskExecuteAsync(abc.ABC):
|
|
732
|
+
@abc.abstractmethod
|
|
733
|
+
async def post_task_execute(
|
|
734
|
+
self,
|
|
735
|
+
*,
|
|
736
|
+
run_id: str,
|
|
737
|
+
task_id: str,
|
|
738
|
+
nodes: list["node.Node"],
|
|
739
|
+
results: dict[str, Any] | None,
|
|
740
|
+
success: bool,
|
|
741
|
+
error: Exception,
|
|
742
|
+
spawning_task_id: str | None,
|
|
743
|
+
purpose: NodeGroupPurpose,
|
|
744
|
+
):
|
|
745
|
+
"""Asynchronous Hook called immediately after task execution. Note that this is only useful in dynamic
|
|
746
|
+
execution, although we reserve the right to add this back into the standard hamilton execution pattern.
|
|
747
|
+
|
|
748
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
749
|
+
:param task_id: ID of the task
|
|
750
|
+
:param nodes: Nodes that were executed
|
|
751
|
+
:param results: Results of the task
|
|
752
|
+
:param success: Whether or not the task executed successfully
|
|
753
|
+
:param error: The error that was raised, if any
|
|
754
|
+
:param spawning_task_id: ID of the task that spawned this task
|
|
755
|
+
:param purpose: Purpose of the current task group
|
|
756
|
+
"""
|
|
757
|
+
pass
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
@lifecycle.base_hook("post_task_return")
|
|
761
|
+
class BasePostTaskReturn(abc.ABC):
|
|
762
|
+
@abc.abstractmethod
|
|
763
|
+
def post_task_return(
|
|
764
|
+
self,
|
|
765
|
+
*,
|
|
766
|
+
run_id: str,
|
|
767
|
+
task_id: str,
|
|
768
|
+
nodes: list["node.Node"],
|
|
769
|
+
result: Any,
|
|
770
|
+
success: bool,
|
|
771
|
+
error: Exception,
|
|
772
|
+
spawning_task_id: str | None,
|
|
773
|
+
purpose: NodeGroupPurpose,
|
|
774
|
+
):
|
|
775
|
+
"""Hook called immediately after a task returns from an executor. Note that this is only
|
|
776
|
+
useful in dynamic execution, although we reserve the right to add this back into the
|
|
777
|
+
standard hamilton execution pattern.
|
|
778
|
+
|
|
779
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
780
|
+
:param task_id: ID of the task
|
|
781
|
+
:param result: Return value of the task.
|
|
782
|
+
:param success: Whether or not the task executed successfully
|
|
783
|
+
:param error: The error that was raised, if any
|
|
784
|
+
:param spawning_task_id: ID of the task that spawned this task
|
|
785
|
+
:param purpose: Purpose of the current task group
|
|
786
|
+
"""
|
|
787
|
+
pass
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
@lifecycle.base_hook("post_graph_execute")
|
|
791
|
+
class BasePostGraphExecute(abc.ABC):
|
|
792
|
+
@abc.abstractmethod
|
|
793
|
+
def post_graph_execute(
|
|
794
|
+
self,
|
|
795
|
+
*,
|
|
796
|
+
run_id: str,
|
|
797
|
+
graph: "graph.FunctionGraph",
|
|
798
|
+
success: bool,
|
|
799
|
+
error: Exception | None,
|
|
800
|
+
results: dict[str, Any] | None,
|
|
801
|
+
):
|
|
802
|
+
"""Hook called immediately after graph execution.
|
|
803
|
+
|
|
804
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
805
|
+
:param graph: Graph that was executed
|
|
806
|
+
:param success: Whether or not the graph executed successfully
|
|
807
|
+
:param error: Error that was raised, if any
|
|
808
|
+
:param results: Results of the graph execution
|
|
809
|
+
"""
|
|
810
|
+
pass
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
@lifecycle.base_hook("post_graph_execute")
|
|
814
|
+
class BasePostGraphExecuteAsync(abc.ABC):
|
|
815
|
+
@abc.abstractmethod
|
|
816
|
+
async def post_graph_execute(
|
|
817
|
+
self,
|
|
818
|
+
*,
|
|
819
|
+
run_id: str,
|
|
820
|
+
graph: "graph.FunctionGraph",
|
|
821
|
+
success: bool,
|
|
822
|
+
error: Exception | None,
|
|
823
|
+
results: dict[str, Any] | None,
|
|
824
|
+
):
|
|
825
|
+
"""Asynchronous Hook called immediately after graph execution.
|
|
826
|
+
|
|
827
|
+
:param run_id: ID of the run, unique in scope of the driver.
|
|
828
|
+
:param graph: Graph that was executed
|
|
829
|
+
:param success: Whether or not the graph executed successfully
|
|
830
|
+
:param error: Error that was raised, if any
|
|
831
|
+
:param results: Results of the graph execution
|
|
832
|
+
"""
|
|
833
|
+
pass
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
@lifecycle.base_method("do_build_result")
|
|
837
|
+
class BaseDoBuildResult(abc.ABC):
|
|
838
|
+
@abc.abstractmethod
|
|
839
|
+
def do_build_result(self, *, outputs: Any) -> Any:
|
|
840
|
+
"""Method that is called to build the result of the graph execution.
|
|
841
|
+
|
|
842
|
+
:param outputs: Output of the node execution
|
|
843
|
+
:return: The final result
|
|
844
|
+
"""
|
|
845
|
+
pass
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
# This is the type of a lifecycle adapter -- these types utilize
|
|
849
|
+
|
|
850
|
+
LifecycleAdapter = Union[
|
|
851
|
+
BasePreDoAnythingHook,
|
|
852
|
+
BaseDoCheckEdgeTypesMatch,
|
|
853
|
+
BaseDoValidateInput,
|
|
854
|
+
BaseValidateNode,
|
|
855
|
+
BaseValidateGraph,
|
|
856
|
+
BasePostGraphConstruct,
|
|
857
|
+
BasePostGraphConstructAsync,
|
|
858
|
+
BasePreGraphExecute,
|
|
859
|
+
BasePreGraphExecuteAsync,
|
|
860
|
+
BasePostTaskGroup,
|
|
861
|
+
BasePostTaskExpand,
|
|
862
|
+
BasePreTaskSubmission,
|
|
863
|
+
BasePostTaskReturn,
|
|
864
|
+
BasePreTaskExecute,
|
|
865
|
+
BasePreTaskExecuteAsync,
|
|
866
|
+
BasePreNodeExecute,
|
|
867
|
+
BasePreNodeExecuteAsync,
|
|
868
|
+
BaseDoNodeExecute,
|
|
869
|
+
BaseDoNodeExecuteAsync,
|
|
870
|
+
BasePostNodeExecute,
|
|
871
|
+
BasePostNodeExecuteAsync,
|
|
872
|
+
BasePostTaskExecute,
|
|
873
|
+
BasePostTaskExecuteAsync,
|
|
874
|
+
BasePostGraphExecute,
|
|
875
|
+
BasePostGraphExecuteAsync,
|
|
876
|
+
BaseDoBuildResult,
|
|
877
|
+
]
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
class LifecycleAdapterSet:
|
|
881
|
+
"""An internal class that groups together all the lifecycle adapters.
|
|
882
|
+
This allows us to call methods through a delegation pattern, enabling us to add
|
|
883
|
+
whatever callbacks, logging, error-handling, etc... we need globally. While this
|
|
884
|
+
does increase the stack trace in an error, it should be pretty easy to figure out what'g going on.
|
|
885
|
+
"""
|
|
886
|
+
|
|
887
|
+
def __init__(self, *adapters: LifecycleAdapter):
|
|
888
|
+
"""Initializes the adapter set.
|
|
889
|
+
|
|
890
|
+
:param adapters: Adapters to group together
|
|
891
|
+
"""
|
|
892
|
+
self._adapters = self._uniqify_adapters(adapters)
|
|
893
|
+
self.sync_hooks, self.async_hooks = self._get_lifecycle_hooks()
|
|
894
|
+
self.sync_methods, self.async_methods = self._get_lifecycle_methods()
|
|
895
|
+
self.sync_validators = self._get_lifecycle_validators()
|
|
896
|
+
|
|
897
|
+
def _uniqify_adapters(self, adapters: list[LifecycleAdapter]) -> list[LifecycleAdapter]:
|
|
898
|
+
"""Removes duplicate adapters from the list of adapters -- this often happens on how they're passed in
|
|
899
|
+
and we don't want to have the same adapter twice. Specifically, this came up due to parsing/splitting out adapters
|
|
900
|
+
with async lifecycle hooks -- there were cases in which we were passed duplicates. This was compounded as we would pass
|
|
901
|
+
adapters to other adapter sets and end up further duplicating.
|
|
902
|
+
|
|
903
|
+
TODO -- remove this and ensure that no case passes in duplicates.
|
|
904
|
+
"""
|
|
905
|
+
|
|
906
|
+
seen = set()
|
|
907
|
+
return [
|
|
908
|
+
adapter for adapter in adapters if not (id(adapter) in seen or seen.add(id(adapter)))
|
|
909
|
+
]
|
|
910
|
+
|
|
911
|
+
def _get_lifecycle_validators(
|
|
912
|
+
self,
|
|
913
|
+
) -> dict[str, list[LifecycleAdapter]]:
|
|
914
|
+
sync_validators = collections.defaultdict(set)
|
|
915
|
+
for adapter in self.adapters:
|
|
916
|
+
for cls in inspect.getmro(adapter.__class__):
|
|
917
|
+
sync_validator = getattr(cls, SYNC_VALIDATOR, None)
|
|
918
|
+
if sync_validator is not None:
|
|
919
|
+
sync_validators[sync_validator].add(adapter)
|
|
920
|
+
return {validator: list(adapters) for validator, adapters in sync_validators.items()}
|
|
921
|
+
|
|
922
|
+
def _get_lifecycle_hooks(
|
|
923
|
+
self,
|
|
924
|
+
) -> tuple[dict[str, list[LifecycleAdapter]], dict[str, list[LifecycleAdapter]]]:
|
|
925
|
+
sync_hooks = collections.defaultdict(list)
|
|
926
|
+
async_hooks = collections.defaultdict(list)
|
|
927
|
+
for adapter in self.adapters:
|
|
928
|
+
for cls in inspect.getmro(adapter.__class__):
|
|
929
|
+
sync_hook = getattr(cls, SYNC_HOOK, None)
|
|
930
|
+
if sync_hook is not None:
|
|
931
|
+
if adapter not in sync_hooks[sync_hook]:
|
|
932
|
+
sync_hooks[sync_hook].append(adapter)
|
|
933
|
+
async_hook = getattr(cls, ASYNC_HOOK, None)
|
|
934
|
+
if async_hook is not None:
|
|
935
|
+
if adapter not in async_hooks[async_hook]:
|
|
936
|
+
async_hooks[async_hook].append(adapter)
|
|
937
|
+
return (
|
|
938
|
+
{hook: adapters for hook, adapters in sync_hooks.items()},
|
|
939
|
+
{hook: adapters for hook, adapters in async_hooks.items()},
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
def _get_lifecycle_methods(
|
|
943
|
+
self,
|
|
944
|
+
) -> tuple[dict[str, list[LifecycleAdapter]], dict[str, list[LifecycleAdapter]]]:
|
|
945
|
+
sync_methods = collections.defaultdict(set)
|
|
946
|
+
async_methods = collections.defaultdict(set)
|
|
947
|
+
for adapter in self.adapters:
|
|
948
|
+
for cls in inspect.getmro(adapter.__class__):
|
|
949
|
+
sync_method = getattr(cls, SYNC_METHOD, None)
|
|
950
|
+
if sync_method is not None:
|
|
951
|
+
sync_methods[sync_method].add(adapter)
|
|
952
|
+
async_method = getattr(cls, ASYNC_METHOD, None)
|
|
953
|
+
if async_method is not None:
|
|
954
|
+
async_methods[async_method].add(adapter)
|
|
955
|
+
multiple_implementations_sync = [
|
|
956
|
+
method for method, adapters in sync_methods.items() if len(adapters) > 1
|
|
957
|
+
]
|
|
958
|
+
multiple_implementations_async = [
|
|
959
|
+
method for method, adapters in async_methods.items() if len(adapters) > 1
|
|
960
|
+
]
|
|
961
|
+
if len(multiple_implementations_sync) > 0 or len(multiple_implementations_async) > 0:
|
|
962
|
+
raise ValueError(
|
|
963
|
+
f"Multiple adapters cannot (currently) implement the same lifecycle method. "
|
|
964
|
+
f"Sync methods: {multiple_implementations_sync}. "
|
|
965
|
+
f"Async methods: {multiple_implementations_async}"
|
|
966
|
+
)
|
|
967
|
+
return (
|
|
968
|
+
{method: list(adapters) for method, adapters in sync_methods.items()},
|
|
969
|
+
{method: list(adapters) for method, adapters in async_methods.items()},
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
def does_hook(self, hook_name: str, is_async: bool | None = None) -> bool:
|
|
973
|
+
"""Whether or not a hook is implemented by any of the adapters in this group.
|
|
974
|
+
If this hook is not registered, this will raise a ValueError.
|
|
975
|
+
|
|
976
|
+
:param hook_name: Name of the hook
|
|
977
|
+
:param is_async: Whether you want the async version or not
|
|
978
|
+
:return: True if this adapter set does this hook, False otherwise
|
|
979
|
+
"""
|
|
980
|
+
either = is_async is None
|
|
981
|
+
if (is_async or either) and hook_name not in REGISTERED_ASYNC_HOOKS:
|
|
982
|
+
raise ValueError(
|
|
983
|
+
f"Hook {hook_name} is not registered as an asynchronous lifecycle hook. "
|
|
984
|
+
f"Registered hooks are {REGISTERED_ASYNC_HOOKS}"
|
|
985
|
+
)
|
|
986
|
+
if ((not is_async) or either) and hook_name not in REGISTERED_SYNC_HOOKS:
|
|
987
|
+
raise ValueError(
|
|
988
|
+
f"Hook {hook_name} is not registered as a synchronous lifecycle hook. "
|
|
989
|
+
f"Registered hooks are {REGISTERED_SYNC_HOOKS}"
|
|
990
|
+
)
|
|
991
|
+
has_async = hook_name in self.async_hooks
|
|
992
|
+
has_sync = hook_name in self.sync_hooks
|
|
993
|
+
return (has_async or has_sync) if either else has_async if is_async else has_sync
|
|
994
|
+
|
|
995
|
+
def does_method(self, method_name: str, is_async: bool | None = None) -> bool:
|
|
996
|
+
"""Whether a method is implemented by any of the adapters in this group.
|
|
997
|
+
If this method is not registered, this will raise a ValueError.
|
|
998
|
+
|
|
999
|
+
:param method_name: Name of the method
|
|
1000
|
+
:param is_async: Whether you want the async version or not
|
|
1001
|
+
:return: True if this adapter set does this method, False otherwise
|
|
1002
|
+
"""
|
|
1003
|
+
either = is_async is None
|
|
1004
|
+
if (is_async or either) and method_name not in REGISTERED_ASYNC_METHODS:
|
|
1005
|
+
raise ValueError(
|
|
1006
|
+
f"Method {method_name} is not registered as an asynchronous lifecycle method. "
|
|
1007
|
+
f"Registered methods are {REGISTERED_ASYNC_METHODS}"
|
|
1008
|
+
)
|
|
1009
|
+
if ((not is_async) or either) and method_name not in REGISTERED_SYNC_METHODS:
|
|
1010
|
+
raise ValueError(
|
|
1011
|
+
f"Method {method_name} is not registered as a synchronous lifecycle method. "
|
|
1012
|
+
f"Registered methods are {REGISTERED_SYNC_METHODS}"
|
|
1013
|
+
)
|
|
1014
|
+
has_async = method_name in self.async_methods
|
|
1015
|
+
has_sync = method_name in self.sync_methods
|
|
1016
|
+
return (has_async or has_sync) if either else has_async if is_async else has_sync
|
|
1017
|
+
|
|
1018
|
+
def does_validation(self, validator_name: str) -> bool:
|
|
1019
|
+
"""Whether a validator is implemented by any of the adapters in this group.
|
|
1020
|
+
|
|
1021
|
+
:param validator_name: Name of the validator
|
|
1022
|
+
:param is_async: Whether you want the async version or not
|
|
1023
|
+
:return: True if this adapter set does this validator, False otherwise
|
|
1024
|
+
"""
|
|
1025
|
+
if validator_name not in REGISTERED_SYNC_VALIDATORS:
|
|
1026
|
+
raise ValueError(
|
|
1027
|
+
f"Validator {validator_name} is not registered as a lifecycle validator. "
|
|
1028
|
+
f"Registered validators are {REGISTERED_SYNC_VALIDATORS}"
|
|
1029
|
+
)
|
|
1030
|
+
return validator_name in self.sync_validators
|
|
1031
|
+
|
|
1032
|
+
def call_all_lifecycle_hooks_sync(self, hook_name: str, **kwargs):
|
|
1033
|
+
"""Calls all the lifecycle hooks in this group, by hook name (stage)
|
|
1034
|
+
|
|
1035
|
+
:param hook_name: Name of the hooks to call
|
|
1036
|
+
:param kwargs: Keyword arguments to pass into the hook
|
|
1037
|
+
"""
|
|
1038
|
+
for adapter in self.sync_hooks.get(hook_name, []):
|
|
1039
|
+
getattr(adapter, hook_name)(**kwargs)
|
|
1040
|
+
|
|
1041
|
+
async def call_all_lifecycle_hooks_async(self, hook_name: str, **kwargs):
|
|
1042
|
+
"""Calls all the lifecycle hooks in this group, by hook name (stage).
|
|
1043
|
+
|
|
1044
|
+
:param hook_name: Name of the hook
|
|
1045
|
+
:param kwargs: Keyword arguments to pass into the hook
|
|
1046
|
+
"""
|
|
1047
|
+
futures = []
|
|
1048
|
+
for adapter in self.async_hooks.get(hook_name, []):
|
|
1049
|
+
futures.append(getattr(adapter, hook_name)(**kwargs))
|
|
1050
|
+
await asyncio.gather(*futures)
|
|
1051
|
+
|
|
1052
|
+
async def call_all_lifecycle_hooks_sync_and_async(self, hook_name: str, **kwargs):
|
|
1053
|
+
"""Calls all the lifecycle hooks whether they are sync or async
|
|
1054
|
+
|
|
1055
|
+
:param hook_name: name of the hook
|
|
1056
|
+
:param kwargs: keyword arguments for the hook
|
|
1057
|
+
"""
|
|
1058
|
+
self.call_all_lifecycle_hooks_sync(hook_name, **kwargs)
|
|
1059
|
+
await self.call_all_lifecycle_hooks_async(hook_name, **kwargs)
|
|
1060
|
+
|
|
1061
|
+
def call_lifecycle_method_sync(self, method_name: str, **kwargs) -> Any:
|
|
1062
|
+
"""Calls a lifecycle method in this group, by method name.
|
|
1063
|
+
|
|
1064
|
+
:param method_name: Name of the method
|
|
1065
|
+
:param kwargs: Keyword arguments to pass into the method
|
|
1066
|
+
:return: The result of the method
|
|
1067
|
+
"""
|
|
1068
|
+
if method_name not in REGISTERED_SYNC_METHODS:
|
|
1069
|
+
raise ValueError(
|
|
1070
|
+
f"Method {method_name} is not registered as a synchronous lifecycle method. "
|
|
1071
|
+
f"Registered methods are {REGISTERED_SYNC_METHODS}"
|
|
1072
|
+
)
|
|
1073
|
+
if method_name not in self.sync_methods:
|
|
1074
|
+
raise ValueError(
|
|
1075
|
+
f"Method {method_name} is not implemented by any of the adapters in this group. " # TODO _- improve the error message
|
|
1076
|
+
f"Registered methods are {self.sync_methods}"
|
|
1077
|
+
)
|
|
1078
|
+
(adapter,) = self.sync_methods[method_name]
|
|
1079
|
+
return getattr(adapter, method_name)(**kwargs)
|
|
1080
|
+
|
|
1081
|
+
async def call_lifecycle_method_async(self, method_name: str, **kwargs):
|
|
1082
|
+
"""Call a lifecycle method in this group, by method name, async
|
|
1083
|
+
|
|
1084
|
+
:param method_name: Name of the method
|
|
1085
|
+
:param kwargs: Keyword arguments to pass into the method
|
|
1086
|
+
:return: The result of the method
|
|
1087
|
+
"""
|
|
1088
|
+
if method_name not in REGISTERED_ASYNC_METHODS:
|
|
1089
|
+
raise ValueError(
|
|
1090
|
+
f"Method {method_name} is not registered as an asynchronous lifecycle method. "
|
|
1091
|
+
f"Registered methods are {REGISTERED_ASYNC_METHODS}"
|
|
1092
|
+
)
|
|
1093
|
+
(adapter,) = self.async_methods[method_name]
|
|
1094
|
+
return await getattr(adapter, method_name)(**kwargs)
|
|
1095
|
+
|
|
1096
|
+
def call_all_validators_sync(
|
|
1097
|
+
self, validator_name: str, output_only_failures: bool = True, **kwargs
|
|
1098
|
+
) -> list[ValidationResult]:
|
|
1099
|
+
"""Calls all the lifecycle validators in this group, by validator name (stage)
|
|
1100
|
+
|
|
1101
|
+
:param validator_name: Name of the validators to call
|
|
1102
|
+
:param kwargs: Keyword arguments to pass into the validator
|
|
1103
|
+
:param output_only_failures: Whether to output only failures
|
|
1104
|
+
"""
|
|
1105
|
+
results = []
|
|
1106
|
+
for adapter in self.sync_validators[validator_name]:
|
|
1107
|
+
is_valid, message = getattr(adapter, validator_name)(**kwargs)
|
|
1108
|
+
if not is_valid or not output_only_failures:
|
|
1109
|
+
results.append(ValidationResult(success=is_valid, error=message, validator=adapter))
|
|
1110
|
+
return results
|
|
1111
|
+
|
|
1112
|
+
@property
|
|
1113
|
+
def adapters(self) -> list[LifecycleAdapter]:
|
|
1114
|
+
"""Gives the adapters in this group
|
|
1115
|
+
|
|
1116
|
+
:return: A list of adapters
|
|
1117
|
+
"""
|
|
1118
|
+
return self._adapters
|
|
1119
|
+
|
|
1120
|
+
async def ainit(self):
|
|
1121
|
+
"""Asynchronously initializes the adapters in this group. This is so we can avoid having an async constructor
|
|
1122
|
+
-- it is an implicit internal-facing contract -- the async adapters are allowed one ainit()
|
|
1123
|
+
method that will be called by the driver.
|
|
1124
|
+
|
|
1125
|
+
Note this is not public-facing -- E.G. you cannot expect to define this on your own adapters. We may consider adding
|
|
1126
|
+
a ``pre_do_anything`` async hook and removing this, but for now this should suffice.
|
|
1127
|
+
"""
|
|
1128
|
+
for adapter in self.adapters:
|
|
1129
|
+
if hasattr(adapter, "ainit"):
|
|
1130
|
+
await adapter.ainit()
|