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
hamilton/async_driver.py
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import inspect
|
|
20
|
+
import logging
|
|
21
|
+
import typing
|
|
22
|
+
import uuid
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import hamilton.lifecycle.base as lifecycle_base
|
|
26
|
+
from hamilton import base, driver, graph, lifecycle, node
|
|
27
|
+
from hamilton.execution.graph_functions import create_error_message
|
|
28
|
+
from hamilton.io.materialization import ExtractorFactory, MaterializerFactory
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def await_dict_of_tasks(task_dict: dict[str, typing.Awaitable]) -> dict[str, Any]:
|
|
34
|
+
"""Util to await a dictionary of tasks as asyncio.gather is kind of garbage"""
|
|
35
|
+
keys = sorted(task_dict.keys())
|
|
36
|
+
coroutines = [task_dict[key] for key in keys]
|
|
37
|
+
coroutines_gathered = await asyncio.gather(*coroutines)
|
|
38
|
+
return dict(zip(keys, coroutines_gathered, strict=False))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def process_value(val: Any) -> Any:
|
|
42
|
+
"""Helper function to process the value of a potential awaitable.
|
|
43
|
+
This is very simple -- all it does is await the value if its not already resolved.
|
|
44
|
+
|
|
45
|
+
:param val: Value to process.
|
|
46
|
+
:return: The value (awaited if it is a coroutine, raw otherwise).
|
|
47
|
+
"""
|
|
48
|
+
if not inspect.isawaitable(val):
|
|
49
|
+
return val
|
|
50
|
+
return await val
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AsyncGraphAdapter(lifecycle_base.BaseDoNodeExecute, lifecycle.ResultBuilder):
|
|
54
|
+
"""Graph adapter for use with the :class:`AsyncDriver` class."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
result_builder: base.ResultMixin = None,
|
|
59
|
+
async_lifecycle_adapters: lifecycle_base.LifecycleAdapterSet | None = None,
|
|
60
|
+
):
|
|
61
|
+
"""Creates an AsyncGraphAdapter class. Note this will *only* work with the AsyncDriver class.
|
|
62
|
+
|
|
63
|
+
Some things to note:
|
|
64
|
+
|
|
65
|
+
1. This executes everything at the end (recursively). E.G. the final DAG nodes are awaited
|
|
66
|
+
2. This does *not* work with decorators when the async function is being decorated. That is\
|
|
67
|
+
because that function is called directly within the decorator, so we cannot await it.
|
|
68
|
+
"""
|
|
69
|
+
super(AsyncGraphAdapter, self).__init__()
|
|
70
|
+
self.adapter = (
|
|
71
|
+
async_lifecycle_adapters
|
|
72
|
+
if async_lifecycle_adapters is not None
|
|
73
|
+
else lifecycle_base.LifecycleAdapterSet()
|
|
74
|
+
)
|
|
75
|
+
self.result_builder = result_builder or base.PandasDataFrameResult()
|
|
76
|
+
self.is_initialized = False
|
|
77
|
+
|
|
78
|
+
def do_node_execute(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
run_id: str,
|
|
82
|
+
node_: node.Node,
|
|
83
|
+
kwargs: dict[str, typing.Any],
|
|
84
|
+
task_id: str | None = None,
|
|
85
|
+
) -> typing.Any:
|
|
86
|
+
"""Executes a node. Note this doesn't actually execute it -- rather, it returns a task.
|
|
87
|
+
This does *not* use async def, as we want it to be awaited on later -- this await is done
|
|
88
|
+
in processing parameters of downstream functions/final results. We can ensure that as
|
|
89
|
+
we also run the driver that this corresponds to.
|
|
90
|
+
|
|
91
|
+
Note that this assumes that everything is awaitable, even if it isn't.
|
|
92
|
+
In that case, it just wraps it in one.
|
|
93
|
+
|
|
94
|
+
:param task_id:
|
|
95
|
+
:param node_:
|
|
96
|
+
:param run_id:
|
|
97
|
+
:param node: Node to wrap
|
|
98
|
+
:param kwargs: Keyword arguments (either coroutines or raw values) to call it with
|
|
99
|
+
:return: A task
|
|
100
|
+
"""
|
|
101
|
+
callabl = node_.callable
|
|
102
|
+
|
|
103
|
+
async def new_fn(fn=callabl, **fn_kwargs):
|
|
104
|
+
task_dict = {key: process_value(value) for key, value in fn_kwargs.items()}
|
|
105
|
+
fn_kwargs = await await_dict_of_tasks(task_dict)
|
|
106
|
+
error = None
|
|
107
|
+
result = None
|
|
108
|
+
success = True
|
|
109
|
+
pre_node_execute_errored = False
|
|
110
|
+
try:
|
|
111
|
+
if self.adapter.does_hook("pre_node_execute", is_async=True):
|
|
112
|
+
try:
|
|
113
|
+
await self.adapter.call_all_lifecycle_hooks_async(
|
|
114
|
+
"pre_node_execute",
|
|
115
|
+
run_id=run_id,
|
|
116
|
+
node_=node_,
|
|
117
|
+
kwargs=fn_kwargs,
|
|
118
|
+
task_id=task_id,
|
|
119
|
+
)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
pre_node_execute_errored = True
|
|
122
|
+
raise e
|
|
123
|
+
# TODO -- consider how to use node execution methods in the future
|
|
124
|
+
# This is messy as it is a method called within a method...
|
|
125
|
+
# if self.adapter.does_method("do_node_execute", is_async=False):
|
|
126
|
+
# result = self.adapter.call_lifecycle_method_sync(
|
|
127
|
+
# "do_node_execute",
|
|
128
|
+
# run_id=run_id,
|
|
129
|
+
# node_=node_,
|
|
130
|
+
# kwargs=kwargs,
|
|
131
|
+
# task_id=task_id,
|
|
132
|
+
# )
|
|
133
|
+
# else:
|
|
134
|
+
|
|
135
|
+
result = (
|
|
136
|
+
await fn(**fn_kwargs) if asyncio.iscoroutinefunction(fn) else fn(**fn_kwargs)
|
|
137
|
+
)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
success = False
|
|
140
|
+
error = e
|
|
141
|
+
step = "[pre-node-execute:async]" if pre_node_execute_errored else ""
|
|
142
|
+
message = create_error_message(kwargs, node_, step)
|
|
143
|
+
logger.exception(message)
|
|
144
|
+
raise
|
|
145
|
+
finally:
|
|
146
|
+
if not pre_node_execute_errored and self.adapter.does_hook(
|
|
147
|
+
"post_node_execute", is_async=True
|
|
148
|
+
):
|
|
149
|
+
try:
|
|
150
|
+
await self.adapter.call_all_lifecycle_hooks_async(
|
|
151
|
+
"post_node_execute",
|
|
152
|
+
run_id=run_id,
|
|
153
|
+
node_=node_,
|
|
154
|
+
kwargs=fn_kwargs,
|
|
155
|
+
success=success,
|
|
156
|
+
error=error,
|
|
157
|
+
result=result,
|
|
158
|
+
task_id=task_id,
|
|
159
|
+
)
|
|
160
|
+
except Exception:
|
|
161
|
+
message = create_error_message(kwargs, node_, "[post-node-execute]")
|
|
162
|
+
logger.exception(message)
|
|
163
|
+
raise
|
|
164
|
+
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
coroutine = new_fn(**kwargs)
|
|
168
|
+
task = asyncio.create_task(coroutine)
|
|
169
|
+
return task
|
|
170
|
+
|
|
171
|
+
def build_result(self, **outputs: Any) -> Any:
|
|
172
|
+
return self.result_builder.build_result(**outputs)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def separate_sync_from_async(
|
|
176
|
+
adapters: list[lifecycle.LifecycleAdapter],
|
|
177
|
+
) -> tuple[list[lifecycle.LifecycleAdapter], list[lifecycle.LifecycleAdapter]]:
|
|
178
|
+
"""Separates the sync and async adapters from a list of adapters.
|
|
179
|
+
Note this only works with hooks -- we'll be dealing with methods later.
|
|
180
|
+
|
|
181
|
+
:param adapters: List of adapters
|
|
182
|
+
:return: Tuple of sync adapters, async adapters
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
adapter_set = lifecycle_base.LifecycleAdapterSet(*adapters)
|
|
186
|
+
# this is using internal(ish) fields (.sync_hooks/.async_hooks) -- we should probably expose it
|
|
187
|
+
# For now this is OK
|
|
188
|
+
# Note those are dict[hook_name, list[hook]], so we have to flatten
|
|
189
|
+
return (
|
|
190
|
+
[sync_adapter for adapters in adapter_set.sync_hooks.values() for sync_adapter in adapters],
|
|
191
|
+
[
|
|
192
|
+
async_adapter
|
|
193
|
+
for adapters in adapter_set.async_hooks.values()
|
|
194
|
+
for async_adapter in adapters
|
|
195
|
+
],
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class AsyncDriver(driver.Driver):
|
|
200
|
+
"""Async driver. This is a driver that uses the AsyncGraphAdapter to execute the graph.
|
|
201
|
+
|
|
202
|
+
.. code-block:: python
|
|
203
|
+
|
|
204
|
+
dr = async_driver.AsyncDriver({}, async_module, result_builder=base.DictResult())
|
|
205
|
+
df = await dr.execute([...], inputs=...)
|
|
206
|
+
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
def __init__(
|
|
210
|
+
self,
|
|
211
|
+
config,
|
|
212
|
+
*modules,
|
|
213
|
+
result_builder: base.ResultMixin | None = None,
|
|
214
|
+
adapters: list[lifecycle.LifecycleAdapter] = None,
|
|
215
|
+
allow_module_overrides: bool = False,
|
|
216
|
+
):
|
|
217
|
+
"""Instantiates an asynchronous driver.
|
|
218
|
+
|
|
219
|
+
You will also need to call `ainit` to initialize the driver if you have any hooks/adapters.
|
|
220
|
+
|
|
221
|
+
Note that this is not the desired API -- you should be using the :py:class:`hamilton.async_driver.Builder` class to create the driver.
|
|
222
|
+
|
|
223
|
+
This will only (currently) work properly with asynchronous lifecycle hooks, and does not support methods or validators.
|
|
224
|
+
You can still pass in synchronous lifecycle hooks, but they may behave strangely.
|
|
225
|
+
|
|
226
|
+
:param config: Config to build the graph
|
|
227
|
+
:param modules: Modules to crawl for fns/graph nodes
|
|
228
|
+
:param result_builder: Results mixin to compile the graph's final results. TBD whether this should be included in the long run.
|
|
229
|
+
:param adapters: Adapters to use for lifecycle methods.
|
|
230
|
+
:param allow_module_overrides: Optional. Same named functions get overridden by later modules.
|
|
231
|
+
The order of listing the modules is important, since later ones will overwrite the previous ones.
|
|
232
|
+
This is a global call affecting all imported modules.
|
|
233
|
+
See https://github.com/apache/hamilton/tree/main/examples/module_overrides for more info.
|
|
234
|
+
"""
|
|
235
|
+
if adapters is None:
|
|
236
|
+
adapters = []
|
|
237
|
+
sync_adapters, async_adapters = separate_sync_from_async(adapters)
|
|
238
|
+
|
|
239
|
+
# we'll need to use this in multiple contexts so we'll keep it around for later
|
|
240
|
+
|
|
241
|
+
result_builders = [adapter for adapter in adapters if isinstance(adapter, base.ResultMixin)]
|
|
242
|
+
if result_builder is not None:
|
|
243
|
+
result_builders.append(result_builder)
|
|
244
|
+
if len(result_builders) > 1:
|
|
245
|
+
raise ValueError(
|
|
246
|
+
"You cannot pass more than one result builder to the async driver. "
|
|
247
|
+
"Please pass in a single result builder"
|
|
248
|
+
)
|
|
249
|
+
# it will be defaulted by the graph adapter
|
|
250
|
+
result_builder = result_builders[0] if len(result_builders) == 1 else None
|
|
251
|
+
super(AsyncDriver, self).__init__(
|
|
252
|
+
config,
|
|
253
|
+
*modules,
|
|
254
|
+
adapter=[
|
|
255
|
+
# We pass in the async adapters here as this can call node-level hooks
|
|
256
|
+
# Otherwise we trust the driver/fn graph to call sync adapters
|
|
257
|
+
AsyncGraphAdapter(
|
|
258
|
+
result_builder=result_builder,
|
|
259
|
+
async_lifecycle_adapters=lifecycle_base.LifecycleAdapterSet(*async_adapters),
|
|
260
|
+
),
|
|
261
|
+
# We pass in the sync adapters here as this can call
|
|
262
|
+
*sync_adapters,
|
|
263
|
+
*async_adapters, # note async adapters will not be called during synchronous execution -- this is for access later
|
|
264
|
+
],
|
|
265
|
+
allow_module_overrides=allow_module_overrides,
|
|
266
|
+
)
|
|
267
|
+
self.initialized = False
|
|
268
|
+
|
|
269
|
+
async def ainit(self) -> "AsyncDriver":
|
|
270
|
+
"""Initializes the driver when using async. This only exists for backwards compatibility.
|
|
271
|
+
In Hamilton 2.0, we will be using an asynchronous constructor.
|
|
272
|
+
See https://dev.to/akarshan/asynchronous-python-magic-how-to-create-awaitable-constructors-with-asyncmixin-18j5.
|
|
273
|
+
"""
|
|
274
|
+
if self.initialized:
|
|
275
|
+
# this way it can be called twice
|
|
276
|
+
return self
|
|
277
|
+
if self.adapter.does_hook("post_graph_construct", is_async=True):
|
|
278
|
+
await self.adapter.call_all_lifecycle_hooks_async(
|
|
279
|
+
"post_graph_construct",
|
|
280
|
+
graph=self.graph,
|
|
281
|
+
modules=self.graph_modules,
|
|
282
|
+
config=self.config,
|
|
283
|
+
)
|
|
284
|
+
await self.adapter.ainit()
|
|
285
|
+
self.initialized = True
|
|
286
|
+
return self
|
|
287
|
+
|
|
288
|
+
async def raw_execute(
|
|
289
|
+
self,
|
|
290
|
+
final_vars: list[str],
|
|
291
|
+
overrides: dict[str, Any] = None,
|
|
292
|
+
display_graph: bool = False, # don't care
|
|
293
|
+
inputs: dict[str, Any] = None,
|
|
294
|
+
_fn_graph: graph.FunctionGraph = None,
|
|
295
|
+
) -> dict[str, Any]:
|
|
296
|
+
"""Executes the graph, returning a dictionary of strings (node keys) to final results.
|
|
297
|
+
|
|
298
|
+
:param final_vars: Variables to execute (+ upstream)
|
|
299
|
+
:param overrides: Overrides for nodes
|
|
300
|
+
:param display_graph: whether or not to display graph -- this is not supported.
|
|
301
|
+
:param inputs: Inputs for DAG runtime calculation
|
|
302
|
+
:param _fn_graph: Function graph for compatibility with superclass -- unused
|
|
303
|
+
:return: A dict of key -> result
|
|
304
|
+
"""
|
|
305
|
+
assert _fn_graph is None, (
|
|
306
|
+
"_fn_graph must not be provided "
|
|
307
|
+
"-- the only reason you'd do this is to use materialize(), which is not supported yet.."
|
|
308
|
+
)
|
|
309
|
+
run_id = str(uuid.uuid4())
|
|
310
|
+
nodes, user_nodes = self.graph.get_upstream_nodes(final_vars, inputs, overrides)
|
|
311
|
+
memoized_computation = dict() # memoized storage
|
|
312
|
+
if self.adapter.does_hook("pre_graph_execute"):
|
|
313
|
+
await self.adapter.call_all_lifecycle_hooks_sync_and_async(
|
|
314
|
+
"pre_graph_execute",
|
|
315
|
+
run_id=run_id,
|
|
316
|
+
graph=self.graph,
|
|
317
|
+
final_vars=final_vars,
|
|
318
|
+
inputs=inputs,
|
|
319
|
+
overrides=overrides,
|
|
320
|
+
)
|
|
321
|
+
results = None
|
|
322
|
+
error = None
|
|
323
|
+
success = False
|
|
324
|
+
try:
|
|
325
|
+
self.graph.execute(nodes, memoized_computation, overrides, inputs, run_id=run_id)
|
|
326
|
+
if display_graph:
|
|
327
|
+
raise ValueError(
|
|
328
|
+
"display_graph=True is not supported for the async graph adapter. "
|
|
329
|
+
"Instead you should be using visualize_execution."
|
|
330
|
+
)
|
|
331
|
+
task_dict = {
|
|
332
|
+
key: asyncio.create_task(process_value(memoized_computation[key]))
|
|
333
|
+
for key in final_vars
|
|
334
|
+
}
|
|
335
|
+
results = await await_dict_of_tasks(task_dict)
|
|
336
|
+
success = True
|
|
337
|
+
except Exception as e:
|
|
338
|
+
error = e
|
|
339
|
+
success = False
|
|
340
|
+
raise e
|
|
341
|
+
finally:
|
|
342
|
+
if self.adapter.does_hook("post_graph_execute", is_async=None):
|
|
343
|
+
await self.adapter.call_all_lifecycle_hooks_sync_and_async(
|
|
344
|
+
"post_graph_execute",
|
|
345
|
+
run_id=run_id,
|
|
346
|
+
graph=self.graph,
|
|
347
|
+
success=success,
|
|
348
|
+
error=error,
|
|
349
|
+
results=results,
|
|
350
|
+
)
|
|
351
|
+
return results
|
|
352
|
+
|
|
353
|
+
async def execute(
|
|
354
|
+
self,
|
|
355
|
+
final_vars: list[str],
|
|
356
|
+
overrides: dict[str, Any] = None,
|
|
357
|
+
display_graph: bool = False,
|
|
358
|
+
inputs: dict[str, Any] = None,
|
|
359
|
+
) -> Any:
|
|
360
|
+
"""Executes computation.
|
|
361
|
+
|
|
362
|
+
:param final_vars: the final list of variables we want to compute.
|
|
363
|
+
:param overrides: values that will override "nodes" in the DAG.
|
|
364
|
+
:param display_graph: DEPRECATED. Whether we want to display the graph being computed.
|
|
365
|
+
:param inputs: Runtime inputs to the DAG.
|
|
366
|
+
:return: an object consisting of the variables requested, matching the type returned by the GraphAdapter.
|
|
367
|
+
See constructor for how the GraphAdapter is initialized. The default one right now returns a pandas
|
|
368
|
+
dataframe.
|
|
369
|
+
"""
|
|
370
|
+
if display_graph:
|
|
371
|
+
raise ValueError(
|
|
372
|
+
"display_graph=True is not supported for the async graph adapter. "
|
|
373
|
+
"Instead you should be using visualize_execution."
|
|
374
|
+
)
|
|
375
|
+
_final_vars = self._create_final_vars(final_vars)
|
|
376
|
+
try:
|
|
377
|
+
outputs = await self.raw_execute(_final_vars, overrides, display_graph, inputs=inputs)
|
|
378
|
+
# Currently we don't allow async build results, but we could.
|
|
379
|
+
if self.adapter.does_method("do_build_result", is_async=False):
|
|
380
|
+
return self.adapter.call_lifecycle_method_sync("do_build_result", outputs=outputs)
|
|
381
|
+
return outputs
|
|
382
|
+
except Exception as e:
|
|
383
|
+
logger.error(driver.SLACK_ERROR_MESSAGE)
|
|
384
|
+
raise e
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class Builder(driver.Builder):
|
|
388
|
+
"""Builder for the async driver. This is equivalent to the standard builder, but has a more limited API.
|
|
389
|
+
Note this does not support dynamic execution or materializers (for now).
|
|
390
|
+
|
|
391
|
+
Here is an example of how you might use it to get the tracker working:
|
|
392
|
+
|
|
393
|
+
.. code-block:: python
|
|
394
|
+
|
|
395
|
+
from hamilton_sdk import tracker
|
|
396
|
+
|
|
397
|
+
tracker_async = adapters.AsyncHamiltonTracker(
|
|
398
|
+
project_id=1,
|
|
399
|
+
username="elijah",
|
|
400
|
+
dag_name="async_tracker",
|
|
401
|
+
)
|
|
402
|
+
dr = (
|
|
403
|
+
await async_driver.Builder()
|
|
404
|
+
.with_modules(async_module)
|
|
405
|
+
.with_adapters(tracking_async)
|
|
406
|
+
.build()
|
|
407
|
+
)
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
def __init__(self):
|
|
411
|
+
super(Builder, self).__init__()
|
|
412
|
+
|
|
413
|
+
def _not_supported(self, method_name: str, additional_message: str = ""):
|
|
414
|
+
raise ValueError(
|
|
415
|
+
f"Builder().{method_name}() is not supported for the async driver. {additional_message}"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
def enable_dynamic_execution(self, *, allow_experimental_mode: bool = False) -> "Builder":
|
|
419
|
+
self._not_supported("enable_dynamic_execution")
|
|
420
|
+
|
|
421
|
+
def with_materializers(
|
|
422
|
+
self, *materializers: ExtractorFactory | MaterializerFactory
|
|
423
|
+
) -> "Builder":
|
|
424
|
+
self._not_supported("with_materializers")
|
|
425
|
+
|
|
426
|
+
def with_adapter(self, adapter: base.HamiltonGraphAdapter) -> "Builder":
|
|
427
|
+
self._not_supported(
|
|
428
|
+
"with_adapter",
|
|
429
|
+
"Use with_adapters instead to pass in the tracker (or other async hooks/methods)",
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
def build_without_init(self) -> AsyncDriver:
|
|
433
|
+
"""Allows you to build the async driver without initialization. Use this at
|
|
434
|
+
your own risk -- we highly recommend calling `.ainit` on the final result.
|
|
435
|
+
|
|
436
|
+
:return:
|
|
437
|
+
"""
|
|
438
|
+
adapters = self.adapters if self.adapters is not None else []
|
|
439
|
+
if self.legacy_graph_adapter is not None:
|
|
440
|
+
adapters.append(self.legacy_graph_adapter)
|
|
441
|
+
|
|
442
|
+
# We should really be doing this in the constructor
|
|
443
|
+
# but the AsyncGraphAdapter originally used the pandas builder
|
|
444
|
+
# so we pass in the right one to ensure backwards compatibility
|
|
445
|
+
# This will become the default API soon, so it's OK to put the complexity here
|
|
446
|
+
result_builders = [adapter for adapter in adapters if isinstance(adapter, base.ResultMixin)]
|
|
447
|
+
specified_result_builder = base.DictResult() if len(result_builders) == 0 else None
|
|
448
|
+
return AsyncDriver(
|
|
449
|
+
self.config,
|
|
450
|
+
*self.modules,
|
|
451
|
+
adapters=self.adapters,
|
|
452
|
+
result_builder=specified_result_builder,
|
|
453
|
+
allow_module_overrides=self._allow_module_overrides,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
async def build(self):
|
|
457
|
+
"""Builds the async driver. This also initializes it, hence the async definition.
|
|
458
|
+
If you don't want to use async, you can use `build_without_init` and call `ainit` later,
|
|
459
|
+
but we recommend using this in an asynchronous lifespan management function (E.G. in fastAPI),
|
|
460
|
+
or something similar.
|
|
461
|
+
|
|
462
|
+
:return: The fully
|
|
463
|
+
"""
|
|
464
|
+
dr = self.build_without_init()
|
|
465
|
+
return await dr.ainit()
|