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.
Files changed (151) hide show
  1. apache_hamilton-1.90.0.dev0.dist-info/METADATA +407 -0
  2. apache_hamilton-1.90.0.dev0.dist-info/RECORD +151 -0
  3. apache_hamilton-1.90.0.dev0.dist-info/WHEEL +4 -0
  4. apache_hamilton-1.90.0.dev0.dist-info/entry_points.txt +9 -0
  5. apache_hamilton-1.90.0.dev0.dist-info/licenses/DISCLAIMER +10 -0
  6. apache_hamilton-1.90.0.dev0.dist-info/licenses/LICENSE +228 -0
  7. apache_hamilton-1.90.0.dev0.dist-info/licenses/NOTICE +5 -0
  8. hamilton/__init__.py +24 -0
  9. hamilton/ad_hoc_utils.py +132 -0
  10. hamilton/async_driver.py +465 -0
  11. hamilton/base.py +466 -0
  12. hamilton/caching/__init__.py +16 -0
  13. hamilton/caching/adapter.py +1475 -0
  14. hamilton/caching/cache_key.py +70 -0
  15. hamilton/caching/fingerprinting.py +287 -0
  16. hamilton/caching/stores/__init__.py +16 -0
  17. hamilton/caching/stores/base.py +242 -0
  18. hamilton/caching/stores/file.py +140 -0
  19. hamilton/caching/stores/memory.py +297 -0
  20. hamilton/caching/stores/sqlite.py +282 -0
  21. hamilton/caching/stores/utils.py +40 -0
  22. hamilton/cli/__init__.py +16 -0
  23. hamilton/cli/__main__.py +328 -0
  24. hamilton/cli/commands.py +126 -0
  25. hamilton/cli/logic.py +338 -0
  26. hamilton/common/__init__.py +76 -0
  27. hamilton/contrib/__init__.py +41 -0
  28. hamilton/data_quality/__init__.py +16 -0
  29. hamilton/data_quality/base.py +198 -0
  30. hamilton/data_quality/default_validators.py +560 -0
  31. hamilton/data_quality/pandera_validators.py +121 -0
  32. hamilton/dataflows/__init__.py +726 -0
  33. hamilton/dataflows/template/README.md +25 -0
  34. hamilton/dataflows/template/__init__.py +54 -0
  35. hamilton/dataflows/template/author.md +28 -0
  36. hamilton/dataflows/template/requirements.txt +0 -0
  37. hamilton/dataflows/template/tags.json +7 -0
  38. hamilton/dataflows/template/valid_configs.jsonl +1 -0
  39. hamilton/dev_utils/__init__.py +16 -0
  40. hamilton/dev_utils/deprecation.py +204 -0
  41. hamilton/driver.py +2112 -0
  42. hamilton/execution/__init__.py +16 -0
  43. hamilton/execution/debugging_utils.py +56 -0
  44. hamilton/execution/executors.py +502 -0
  45. hamilton/execution/graph_functions.py +421 -0
  46. hamilton/execution/grouping.py +430 -0
  47. hamilton/execution/state.py +539 -0
  48. hamilton/experimental/__init__.py +27 -0
  49. hamilton/experimental/databackend.py +61 -0
  50. hamilton/experimental/decorators/__init__.py +16 -0
  51. hamilton/experimental/decorators/parameterize_frame.py +233 -0
  52. hamilton/experimental/h_async.py +29 -0
  53. hamilton/experimental/h_cache.py +413 -0
  54. hamilton/experimental/h_dask.py +28 -0
  55. hamilton/experimental/h_databackends.py +174 -0
  56. hamilton/experimental/h_ray.py +28 -0
  57. hamilton/experimental/h_spark.py +32 -0
  58. hamilton/function_modifiers/README +40 -0
  59. hamilton/function_modifiers/__init__.py +121 -0
  60. hamilton/function_modifiers/adapters.py +900 -0
  61. hamilton/function_modifiers/base.py +859 -0
  62. hamilton/function_modifiers/configuration.py +310 -0
  63. hamilton/function_modifiers/delayed.py +202 -0
  64. hamilton/function_modifiers/dependencies.py +246 -0
  65. hamilton/function_modifiers/expanders.py +1230 -0
  66. hamilton/function_modifiers/macros.py +1634 -0
  67. hamilton/function_modifiers/metadata.py +434 -0
  68. hamilton/function_modifiers/recursive.py +908 -0
  69. hamilton/function_modifiers/validation.py +289 -0
  70. hamilton/function_modifiers_base.py +31 -0
  71. hamilton/graph.py +1153 -0
  72. hamilton/graph_types.py +264 -0
  73. hamilton/graph_utils.py +41 -0
  74. hamilton/htypes.py +450 -0
  75. hamilton/io/__init__.py +32 -0
  76. hamilton/io/data_adapters.py +216 -0
  77. hamilton/io/default_data_loaders.py +224 -0
  78. hamilton/io/materialization.py +500 -0
  79. hamilton/io/utils.py +158 -0
  80. hamilton/lifecycle/__init__.py +67 -0
  81. hamilton/lifecycle/api.py +833 -0
  82. hamilton/lifecycle/base.py +1130 -0
  83. hamilton/lifecycle/default.py +802 -0
  84. hamilton/log_setup.py +47 -0
  85. hamilton/models.py +92 -0
  86. hamilton/node.py +449 -0
  87. hamilton/plugins/README.md +48 -0
  88. hamilton/plugins/__init__.py +16 -0
  89. hamilton/plugins/dask_extensions.py +47 -0
  90. hamilton/plugins/dlt_extensions.py +161 -0
  91. hamilton/plugins/geopandas_extensions.py +49 -0
  92. hamilton/plugins/h_dask.py +331 -0
  93. hamilton/plugins/h_ddog.py +522 -0
  94. hamilton/plugins/h_diskcache.py +163 -0
  95. hamilton/plugins/h_experiments/__init__.py +22 -0
  96. hamilton/plugins/h_experiments/__main__.py +62 -0
  97. hamilton/plugins/h_experiments/cache.py +39 -0
  98. hamilton/plugins/h_experiments/data_model.py +68 -0
  99. hamilton/plugins/h_experiments/hook.py +219 -0
  100. hamilton/plugins/h_experiments/server.py +375 -0
  101. hamilton/plugins/h_kedro.py +152 -0
  102. hamilton/plugins/h_logging.py +454 -0
  103. hamilton/plugins/h_mcp/__init__.py +28 -0
  104. hamilton/plugins/h_mcp/__main__.py +33 -0
  105. hamilton/plugins/h_mcp/_helpers.py +129 -0
  106. hamilton/plugins/h_mcp/_templates.py +417 -0
  107. hamilton/plugins/h_mcp/server.py +328 -0
  108. hamilton/plugins/h_mlflow.py +335 -0
  109. hamilton/plugins/h_narwhals.py +134 -0
  110. hamilton/plugins/h_openlineage.py +400 -0
  111. hamilton/plugins/h_opentelemetry.py +167 -0
  112. hamilton/plugins/h_pandas.py +257 -0
  113. hamilton/plugins/h_pandera.py +117 -0
  114. hamilton/plugins/h_polars.py +304 -0
  115. hamilton/plugins/h_polars_lazyframe.py +282 -0
  116. hamilton/plugins/h_pyarrow.py +56 -0
  117. hamilton/plugins/h_pydantic.py +127 -0
  118. hamilton/plugins/h_ray.py +242 -0
  119. hamilton/plugins/h_rich.py +142 -0
  120. hamilton/plugins/h_schema.py +493 -0
  121. hamilton/plugins/h_slack.py +100 -0
  122. hamilton/plugins/h_spark.py +1380 -0
  123. hamilton/plugins/h_threadpool.py +125 -0
  124. hamilton/plugins/h_tqdm.py +122 -0
  125. hamilton/plugins/h_vaex.py +129 -0
  126. hamilton/plugins/huggingface_extensions.py +236 -0
  127. hamilton/plugins/ibis_extensions.py +93 -0
  128. hamilton/plugins/jupyter_magic.py +622 -0
  129. hamilton/plugins/kedro_extensions.py +117 -0
  130. hamilton/plugins/lightgbm_extensions.py +99 -0
  131. hamilton/plugins/matplotlib_extensions.py +108 -0
  132. hamilton/plugins/mlflow_extensions.py +216 -0
  133. hamilton/plugins/numpy_extensions.py +105 -0
  134. hamilton/plugins/pandas_extensions.py +1763 -0
  135. hamilton/plugins/plotly_extensions.py +150 -0
  136. hamilton/plugins/polars_extensions.py +71 -0
  137. hamilton/plugins/polars_implementations.py +25 -0
  138. hamilton/plugins/polars_lazyframe_extensions.py +302 -0
  139. hamilton/plugins/polars_post_1_0_0_extensions.py +877 -0
  140. hamilton/plugins/polars_pre_1_0_0_extension.py +836 -0
  141. hamilton/plugins/pydantic_extensions.py +98 -0
  142. hamilton/plugins/pyspark_pandas_extensions.py +47 -0
  143. hamilton/plugins/sklearn_plot_extensions.py +129 -0
  144. hamilton/plugins/spark_extensions.py +105 -0
  145. hamilton/plugins/vaex_extensions.py +51 -0
  146. hamilton/plugins/xgboost_extensions.py +91 -0
  147. hamilton/plugins/yaml_extensions.py +89 -0
  148. hamilton/registry.py +254 -0
  149. hamilton/settings.py +18 -0
  150. hamilton/telemetry.py +50 -0
  151. 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()