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,500 @@
|
|
|
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 dataclasses
|
|
19
|
+
import functools
|
|
20
|
+
import inspect
|
|
21
|
+
import typing
|
|
22
|
+
from typing import Any, Protocol
|
|
23
|
+
|
|
24
|
+
from hamilton import base, common, graph, lifecycle, node
|
|
25
|
+
from hamilton.function_modifiers.adapters import LoadFromDecorator, SaveToDecorator
|
|
26
|
+
from hamilton.function_modifiers.dependencies import SingleDependency, value
|
|
27
|
+
from hamilton.graph import FunctionGraph, update_dependencies
|
|
28
|
+
from hamilton.io.data_adapters import DataLoader, DataSaver
|
|
29
|
+
from hamilton.registry import LOADER_REGISTRY, SAVER_REGISTRY
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class materialization_meta__(type):
|
|
33
|
+
"""Metaclass for the to. materializer. This is specifically to allow class access method.
|
|
34
|
+
This only exists to add more helpful error messages. We dynamically assign the attributes
|
|
35
|
+
below (see `_set_materializer_attrs`), which helps with auto-complete.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __new__(cls, name, bases, clsdict):
|
|
39
|
+
"""Boiler plate for a metaclass -- this just instantiates it as a type. It also sets the
|
|
40
|
+
annotations to be available later.
|
|
41
|
+
"""
|
|
42
|
+
clsobj = super().__new__(cls, name, bases, clsdict)
|
|
43
|
+
clsobj.__annotations__ = {}
|
|
44
|
+
return clsobj
|
|
45
|
+
|
|
46
|
+
def __getattr__(cls, item: str) -> "_MaterializerFactoryProtocol":
|
|
47
|
+
"""This *just* exists to provide a more helpful error message. If you try to access
|
|
48
|
+
a property that doesn't exist, we'll raise an error that tells you what properties
|
|
49
|
+
are available/where to learn more."""
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
return super().__getattribute__(item)
|
|
53
|
+
except AttributeError as e:
|
|
54
|
+
if item in SAVER_REGISTRY:
|
|
55
|
+
# In this case we want to dynamically access it after registering
|
|
56
|
+
# This is so that we can register post-importing
|
|
57
|
+
# Note that this will register all new ones in bulk --
|
|
58
|
+
# which will (in most cases) be a one-time-cost
|
|
59
|
+
_set_materializer_attrs()
|
|
60
|
+
return super().__getattribute__(item)
|
|
61
|
+
raise AttributeError(
|
|
62
|
+
f"No data materializer named: {item}. "
|
|
63
|
+
f"Available materializers are: {list(SAVER_REGISTRY.keys())}. "
|
|
64
|
+
"If you've gotten to this point, you either (1) Have created the custom materializer and not "
|
|
65
|
+
"registered it (in which case, call registry.register_adapter...) "
|
|
66
|
+
"(2) spelled the materializer name wrong, or (e) are trying to use a materialiazer that does"
|
|
67
|
+
"not exist (yet). For a list of available materialiazers, see "
|
|
68
|
+
"https://hamilton.readthedocs.io/reference/io/available-data-adapters/#data"
|
|
69
|
+
"-loaders "
|
|
70
|
+
) from e
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class extractor_meta__(type):
|
|
74
|
+
"""Metaclass for the from_. extractor pattern. This is specifically to allow class access method.
|
|
75
|
+
This only exists to add more helpful error messages. We dynamically assign the attributes
|
|
76
|
+
below (see `_set_extractor_attrs`), which helps with auto-complete.
|
|
77
|
+
|
|
78
|
+
TODO -- reduce shared code between this and the loader above.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __new__(cls, name, bases, clsdict):
|
|
82
|
+
"""Boiler plate for a metaclass -- this just instantiates it as a type. It also sets the
|
|
83
|
+
annotations to be available later.
|
|
84
|
+
"""
|
|
85
|
+
clsobj = super().__new__(cls, name, bases, clsdict)
|
|
86
|
+
clsobj.__annotations__ = {}
|
|
87
|
+
return clsobj
|
|
88
|
+
|
|
89
|
+
def __getattr__(cls, item: str) -> "_ExtractorFactoryProtocol":
|
|
90
|
+
"""This *just* exists to provide a more helpful error message. If you try to access
|
|
91
|
+
a property that doesn't exist, we'll raise an error that tells you what properties
|
|
92
|
+
are available/where to learn more."""
|
|
93
|
+
try:
|
|
94
|
+
return super().__getattribute__(item)
|
|
95
|
+
except AttributeError as e:
|
|
96
|
+
if item in LOADER_REGISTRY:
|
|
97
|
+
# See note on data savers/__getattr__ above
|
|
98
|
+
_set_materializer_attrs()
|
|
99
|
+
return super().__getattribute__(item)
|
|
100
|
+
raise AttributeError(
|
|
101
|
+
f"No data loader named: {item}. "
|
|
102
|
+
f"Available loaders are: {LOADER_REGISTRY.keys()}. "
|
|
103
|
+
"If you've gotten to this point, you either (1) spelled the "
|
|
104
|
+
"loader name wrong, (2) are trying to use a loader that does"
|
|
105
|
+
"not exist (yet). For a list of available loaders, see "
|
|
106
|
+
"https://hamilton.readthedocs.io/reference/io/available-data-adapters/#data"
|
|
107
|
+
"-loaders "
|
|
108
|
+
) from e
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def process_kwargs(
|
|
112
|
+
data_saver_kwargs: dict[str, Any | SingleDependency],
|
|
113
|
+
) -> dict[str, SingleDependency]:
|
|
114
|
+
"""Processes raw strings from the user, converting them into dependency specs.
|
|
115
|
+
This goes according to the following rules.
|
|
116
|
+
|
|
117
|
+
1. If it is a `SingleDependency`, we leave it alone
|
|
118
|
+
2. If it is anything else, we treat it as a literal, and convert it to a value(...)
|
|
119
|
+
|
|
120
|
+
This is so that everything is in the shape of source/value, and we can uniformally handle it later.
|
|
121
|
+
|
|
122
|
+
:param data_saver_kwargs: Kwargs passed in from the user
|
|
123
|
+
:return: Processed kwargs
|
|
124
|
+
"""
|
|
125
|
+
processed_kwargs = {}
|
|
126
|
+
for kwarg, kwarg_val in data_saver_kwargs.items():
|
|
127
|
+
if not isinstance(kwarg_val, SingleDependency):
|
|
128
|
+
processed_kwargs[kwarg] = value(kwarg_val)
|
|
129
|
+
else:
|
|
130
|
+
processed_kwargs[kwarg] = kwarg_val
|
|
131
|
+
return processed_kwargs
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ExtractorFactory:
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
target: str,
|
|
138
|
+
loaders: list[type[DataLoader]],
|
|
139
|
+
**data_loader_kwargs: Any | SingleDependency,
|
|
140
|
+
):
|
|
141
|
+
"""Instantiates an ExtractorFactory. Note this is not a public API -- this is
|
|
142
|
+
internally what gets called (through a factory method) to create it. Called using `from_`,
|
|
143
|
+
E.G. `from_.csv`.
|
|
144
|
+
|
|
145
|
+
:param target: Parameter, into which we're loading the data
|
|
146
|
+
:param loaders: A list of data loaders that are viable candidates, given the key after `from_`
|
|
147
|
+
:param data_loader_kwargs: Keyword arguments for the data loaders.
|
|
148
|
+
"""
|
|
149
|
+
self.target = target
|
|
150
|
+
self.loaders = loaders
|
|
151
|
+
self.data_loader_kwargs = process_kwargs(data_loader_kwargs)
|
|
152
|
+
|
|
153
|
+
def generate_nodes(self, fn_graph: graph.FunctionGraph) -> list[node.Node]:
|
|
154
|
+
"""Resolves the extractor, returning the set of nodes that should get
|
|
155
|
+
added to the function graph. Note that this is an upsert operation --
|
|
156
|
+
these nodes can replace existing nodes.
|
|
157
|
+
|
|
158
|
+
:param fn_graph: Function graph
|
|
159
|
+
:return: List of nodes to add/upsert to the graph
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
decorator = LoadFromDecorator(self.loaders, self.target, **self.data_loader_kwargs)
|
|
163
|
+
# TODO -- add some nodes to the graph
|
|
164
|
+
node_with_target = fn_graph.nodes.get(self.target)
|
|
165
|
+
if node_with_target is None:
|
|
166
|
+
raise ValueError(
|
|
167
|
+
f"Could not find node with name: {self.target} in function "
|
|
168
|
+
f"graph. Available nodes: {list(fn_graph.nodes.keys()) + [...] if len(fn_graph.nodes) > 10 else []}"
|
|
169
|
+
)
|
|
170
|
+
return decorator.get_loader_nodes(self.target, node_with_target.type, namespace=None)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class MaterializerFactory:
|
|
174
|
+
"""Basic factory for creating materializers. Note that this should only ever be instantiated
|
|
175
|
+
through `to.<name>`, which conducts polymorphic lookup to find the appropriate materializer.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
id: str,
|
|
181
|
+
savers: list[type[DataSaver]],
|
|
182
|
+
result_builder: base.ResultMixin | None,
|
|
183
|
+
dependencies: list[str | Any],
|
|
184
|
+
**data_saver_kwargs: Any,
|
|
185
|
+
):
|
|
186
|
+
"""Creates a materializer factory.
|
|
187
|
+
|
|
188
|
+
:param name: Name of the node that this will represent in the DAG.
|
|
189
|
+
:param savers: Potential data savers to use (these will be filtered down from the ones
|
|
190
|
+
registered to <output_format> when `to.<output_format>` is called.)
|
|
191
|
+
:param result_builder: ResultBuilder that joins the result of the dependencies together.
|
|
192
|
+
:param dependencies: Nodes, the results of which on which this depends
|
|
193
|
+
:param data_saver_kwargs: kwargs to be passed to the data_savers. Either literal values,
|
|
194
|
+
or `source`/`value`.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
self.id = id
|
|
198
|
+
self.savers = savers
|
|
199
|
+
self.result_builder = result_builder
|
|
200
|
+
self.dependencies = dependencies
|
|
201
|
+
self.data_saver_kwargs = process_kwargs(data_saver_kwargs)
|
|
202
|
+
|
|
203
|
+
def sanitize_dependencies(self, module_set: set[str]) -> "MaterializerFactory":
|
|
204
|
+
"""Sanitizes the dependencies to ensure they're strings.
|
|
205
|
+
|
|
206
|
+
This replaces the internal value for self.dependencies and returns a new object.
|
|
207
|
+
We return a new object to not modify the one passed in.
|
|
208
|
+
|
|
209
|
+
:param module_set: modules that "functions" could come from if that's passed in.
|
|
210
|
+
:return: new object with sanitized_dependencies.
|
|
211
|
+
"""
|
|
212
|
+
final_vars = common.convert_output_values(self.dependencies, module_set)
|
|
213
|
+
return MaterializerFactory(
|
|
214
|
+
self.id,
|
|
215
|
+
self.savers,
|
|
216
|
+
self.result_builder,
|
|
217
|
+
final_vars,
|
|
218
|
+
**self.data_saver_kwargs,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def _resolve_dependencies(self, fn_graph: graph.FunctionGraph) -> list[node.Node]:
|
|
222
|
+
out = []
|
|
223
|
+
missing_nodes = []
|
|
224
|
+
for name in self.dependencies:
|
|
225
|
+
if name in fn_graph.nodes:
|
|
226
|
+
out.append(fn_graph.nodes[name])
|
|
227
|
+
else:
|
|
228
|
+
missing_nodes.append(name)
|
|
229
|
+
if missing_nodes:
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"Materializer {self.id} has dependencies that are not in the graph: {missing_nodes}."
|
|
232
|
+
)
|
|
233
|
+
return [fn_graph.nodes[name] for name in self.dependencies]
|
|
234
|
+
|
|
235
|
+
def generate_nodes(self, fn_graph: graph.FunctionGraph) -> list[node.Node]:
|
|
236
|
+
"""Generates additional nodes from a materializer, returning the set of nodes that should get
|
|
237
|
+
appended to the function graph. This does two things:
|
|
238
|
+
|
|
239
|
+
1. Adds a node that handles result-building
|
|
240
|
+
2. Adds a node that handles data-saving, reusing the data saver functionality.
|
|
241
|
+
|
|
242
|
+
:param graph: Function Graph to which we are adding the materializer
|
|
243
|
+
:return: List of nodes to add to the graph
|
|
244
|
+
"""
|
|
245
|
+
node_dependencies = self._resolve_dependencies(fn_graph)
|
|
246
|
+
|
|
247
|
+
def join_function(**kwargs):
|
|
248
|
+
return self.result_builder.build_result(**kwargs)
|
|
249
|
+
|
|
250
|
+
out = []
|
|
251
|
+
if self.result_builder is None:
|
|
252
|
+
if len(node_dependencies) != 1:
|
|
253
|
+
raise ValueError(
|
|
254
|
+
"Must specify result builder via combine= key word argument if the materializer has more than "
|
|
255
|
+
"one dependency it is materializing. Otherwise we have no way to join them and know what to pass! "
|
|
256
|
+
f"See materializer {self.id}."
|
|
257
|
+
)
|
|
258
|
+
save_dep = node_dependencies[0]
|
|
259
|
+
else:
|
|
260
|
+
join_node = node.Node(
|
|
261
|
+
name=f"{self.id}_build_result",
|
|
262
|
+
typ=self.result_builder.output_type(),
|
|
263
|
+
doc_string=f"Builds the result for {self.id} materializer",
|
|
264
|
+
callabl=join_function,
|
|
265
|
+
input_types={dep.name: dep.type for dep in node_dependencies},
|
|
266
|
+
originating_functions=(
|
|
267
|
+
None if self.result_builder is None else [self.result_builder.build_result]
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
out.append(join_node)
|
|
271
|
+
save_dep = join_node
|
|
272
|
+
|
|
273
|
+
out.append(
|
|
274
|
+
# We can reuse the functionality in the save_to decorator
|
|
275
|
+
SaveToDecorator(self.savers, self.id, **self.data_saver_kwargs).create_saver_node(
|
|
276
|
+
save_dep, {}, save_dep.callable
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
return out
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@typing.runtime_checkable
|
|
283
|
+
class _MaterializerFactoryProtocol(Protocol):
|
|
284
|
+
"""Typing for the create_materializer_factory function"""
|
|
285
|
+
|
|
286
|
+
def __call__(
|
|
287
|
+
self,
|
|
288
|
+
id: str,
|
|
289
|
+
dependencies: list[str],
|
|
290
|
+
combine: lifecycle.ResultBuilder = None,
|
|
291
|
+
**kwargs: str | SingleDependency,
|
|
292
|
+
) -> MaterializerFactory:
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@typing.runtime_checkable
|
|
297
|
+
class _ExtractorFactoryProtocol(Protocol):
|
|
298
|
+
def __call__(self, target: str, **kwargs: str | SingleDependency) -> ExtractorFactory:
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def partial_materializer(data_savers: list[type[DataSaver]]) -> _MaterializerFactoryProtocol:
|
|
303
|
+
"""Creates a partial materializer, with the specified data savers."""
|
|
304
|
+
|
|
305
|
+
def create_materializer_factory(
|
|
306
|
+
id: str,
|
|
307
|
+
dependencies: list[str],
|
|
308
|
+
combine: base.ResultMixin = None,
|
|
309
|
+
**kwargs: typing.Any,
|
|
310
|
+
) -> MaterializerFactory:
|
|
311
|
+
return MaterializerFactory(
|
|
312
|
+
id=id,
|
|
313
|
+
savers=data_savers,
|
|
314
|
+
result_builder=combine,
|
|
315
|
+
dependencies=dependencies,
|
|
316
|
+
**kwargs,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
return create_materializer_factory
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def partial_extractor(
|
|
323
|
+
data_loaders: list[type[DataLoader]],
|
|
324
|
+
) -> _ExtractorFactoryProtocol:
|
|
325
|
+
"""Creates a partial materializer, with the specified data savers."""
|
|
326
|
+
|
|
327
|
+
def create_extractor_factory(
|
|
328
|
+
target: str,
|
|
329
|
+
**kwargs: typing.Any,
|
|
330
|
+
) -> ExtractorFactory:
|
|
331
|
+
return ExtractorFactory(
|
|
332
|
+
target=target,
|
|
333
|
+
loaders=data_loaders,
|
|
334
|
+
**kwargs,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return create_extractor_factory
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class Materialize(metaclass=materialization_meta__):
|
|
341
|
+
"""Materialize class to facilitate easy reference. Note that you should never need to refer
|
|
342
|
+
to this directly. Rather, this should be referred as `to` in hamilton.io."""
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class to(Materialize):
|
|
346
|
+
"""This is the entry point for Materialization. Note that this is coupled
|
|
347
|
+
with the driver's materialize function -- properties are dynamically assigned
|
|
348
|
+
based on data savers that have been registered, allowing you to call `to.csv`/`to.json`.
|
|
349
|
+
For full documentation, see the documentation for the `materialize` function in the hamilton
|
|
350
|
+
driver."""
|
|
351
|
+
|
|
352
|
+
pass
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class Extract(metaclass=extractor_meta__):
|
|
356
|
+
"""Extract class to facilitate easy reference. Note that you should never need to refer
|
|
357
|
+
to this directly. Rather, this should be referred as `from_` in hamilton.io."""
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class from_(Extract):
|
|
361
|
+
"""This is the entry point for Extraction. Note that this is coupled
|
|
362
|
+
with the driver's materialize function -- properties are dynamically assigned
|
|
363
|
+
based on data loaders that have been registered, allowing you to call `from_.csv`/`from_.json`.
|
|
364
|
+
For full documentation, see the documentation for the `materialize` function in the hamilton
|
|
365
|
+
driver.
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _set_materializer_attrs():
|
|
370
|
+
"""Sets materialization attributes for easy reference. This sets it to the available keys.
|
|
371
|
+
This is so one can get auto-complete"""
|
|
372
|
+
|
|
373
|
+
def with_modified_signature(
|
|
374
|
+
fn: type[_MaterializerFactoryProtocol],
|
|
375
|
+
dataclasses_union: list[type[dataclasses.dataclass]],
|
|
376
|
+
key: str,
|
|
377
|
+
):
|
|
378
|
+
"""Modifies the signature to include the parameters from *all* dataclasses.
|
|
379
|
+
Note this just replaces **kwargs with the union of the parameters. Its not
|
|
380
|
+
strictly correct, as (a) its a superset of the available ones and (b) it doesn't
|
|
381
|
+
include the source/value parameters. However, this can help the development experience
|
|
382
|
+
on jupyter notebooks, and is a good enough approximation for now.
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
:param fn: Function to modify -- will change the signature.
|
|
386
|
+
:param dataclasses_union: All dataclasses to union.
|
|
387
|
+
:param key: Key to use for the materializer.
|
|
388
|
+
:return: The function without **kwargs and with the union of the parameters.
|
|
389
|
+
"""
|
|
390
|
+
original_signature = inspect.signature(fn)
|
|
391
|
+
original_parameters = list(original_signature.parameters.values())
|
|
392
|
+
|
|
393
|
+
new_parameters = []
|
|
394
|
+
seen = set()
|
|
395
|
+
for dataclass in dataclasses_union:
|
|
396
|
+
for field in dataclasses.fields(dataclass):
|
|
397
|
+
if field.name not in seen:
|
|
398
|
+
new_parameters.append(
|
|
399
|
+
inspect.Parameter(
|
|
400
|
+
field.name,
|
|
401
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
402
|
+
annotation=field.type,
|
|
403
|
+
default=None,
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
seen.add(field.name)
|
|
407
|
+
|
|
408
|
+
# Combining old and new parameters
|
|
409
|
+
# Checking for position of **kwargs and insert new params before
|
|
410
|
+
for idx, param in enumerate(original_parameters): # noqa
|
|
411
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
412
|
+
break
|
|
413
|
+
else:
|
|
414
|
+
idx = len(original_parameters)
|
|
415
|
+
|
|
416
|
+
# Insert new parameters while respecting the order
|
|
417
|
+
combined_parameters = original_parameters[:idx] + new_parameters + original_parameters[idx:]
|
|
418
|
+
combined_parameters = [param for param in combined_parameters if param.name != "kwargs"]
|
|
419
|
+
|
|
420
|
+
# Creating a new signature with combined parameters
|
|
421
|
+
new_signature = original_signature.replace(parameters=combined_parameters)
|
|
422
|
+
|
|
423
|
+
# Creating a new function with the new signature
|
|
424
|
+
@functools.wraps(fn)
|
|
425
|
+
def wrapper(*args, **kwargs):
|
|
426
|
+
bound_arguments = new_signature.bind(*args, **kwargs)
|
|
427
|
+
return fn(*bound_arguments.args, **bound_arguments.kwargs)
|
|
428
|
+
|
|
429
|
+
# Assign the new signature to the wrapper function
|
|
430
|
+
wrapper.__signature__ = new_signature
|
|
431
|
+
wrapper.__doc__ = f"""
|
|
432
|
+
Materializes data to {key} format. Note that the parameters are a superset of possible parameters -- this might depend on
|
|
433
|
+
the actual type of the data passed in. For more information, see: https://hamilton.apache.org/reference/io/available-data-adapters/#data-loaders.
|
|
434
|
+
You can also pass `source` and `value` in as kwargs.
|
|
435
|
+
"""
|
|
436
|
+
return wrapper
|
|
437
|
+
|
|
438
|
+
# Go through savers and loaders and add them to the class
|
|
439
|
+
# This way we can access with from_.xyz/to.xyz
|
|
440
|
+
for registry, cls_target, adapter_type, partial_factory in [
|
|
441
|
+
(SAVER_REGISTRY, Materialize, DataSaver, partial_materializer),
|
|
442
|
+
(LOADER_REGISTRY, Extract, DataLoader, partial_extractor),
|
|
443
|
+
]:
|
|
444
|
+
for key, potential_loaders in registry.items():
|
|
445
|
+
loaders = [loader for loader in potential_loaders if issubclass(loader, adapter_type)]
|
|
446
|
+
if len(loaders) > 0:
|
|
447
|
+
partial = partial_factory(potential_loaders)
|
|
448
|
+
partial_with_signature = with_modified_signature(partial, potential_loaders, key)
|
|
449
|
+
setattr(cls_target, key, partial_with_signature)
|
|
450
|
+
cls_target.__annotations__[key] = type(partial_with_signature)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
_set_materializer_attrs()
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def modify_graph(
|
|
457
|
+
fn_graph: FunctionGraph,
|
|
458
|
+
materializer_factories: list[MaterializerFactory],
|
|
459
|
+
extractor_factories: list[ExtractorFactory],
|
|
460
|
+
) -> FunctionGraph:
|
|
461
|
+
"""Modifies the function graph, adding in the specified materialization/loader nodes.
|
|
462
|
+
|
|
463
|
+
Note that this is not simple -- adding materializers is different from adding loaders:
|
|
464
|
+
1. Materializers can simply be appended to the beginning of the graph
|
|
465
|
+
2. loaders (currently) function as "injected nodes", meaning they behave as overrides.
|
|
466
|
+
We don't actually *feed* them in as overrides (as that would have to be a 2-pass process).
|
|
467
|
+
Rather, we add them to the graph, then prune the paths that are upstream of *just them*,
|
|
468
|
+
that can't possibly be executed
|
|
469
|
+
|
|
470
|
+
:param graph: Graph to modify.
|
|
471
|
+
:param materializer_factories: Materializer factories (created by to.xyz) to add to the graph
|
|
472
|
+
:param extractor_factories: Loader factories (created by from_.xyz) to add to the graph
|
|
473
|
+
:return: A new graph with the materializers.
|
|
474
|
+
"""
|
|
475
|
+
materializer_nodes = []
|
|
476
|
+
for materializer in materializer_factories:
|
|
477
|
+
materializer_nodes.extend(materializer.generate_nodes(fn_graph))
|
|
478
|
+
fn_graph = fn_graph.with_nodes({node_.name: node_ for node_ in materializer_nodes})
|
|
479
|
+
loader_nodes = []
|
|
480
|
+
# We want to treat this as an override
|
|
481
|
+
# For now what we'll do is:
|
|
482
|
+
# 1. Replace the nodes we're replacing
|
|
483
|
+
# 2. Update dependencies
|
|
484
|
+
# This will leave some dangling nodes, but we can deal with those later...
|
|
485
|
+
for loader in extractor_factories:
|
|
486
|
+
loader_nodes.extend(loader.generate_nodes(fn_graph))
|
|
487
|
+
graph_nodes = fn_graph.nodes.copy()
|
|
488
|
+
for loader_node in loader_nodes:
|
|
489
|
+
graph_nodes[loader_node.name] = loader_node
|
|
490
|
+
# Simpler to just create a new one
|
|
491
|
+
# This leaks some details slightly -- E.G. how the dependencies work
|
|
492
|
+
# We should probably add this as part of the functiong raph constructor?
|
|
493
|
+
# For now this will be OK
|
|
494
|
+
fn_graph = graph.FunctionGraph(
|
|
495
|
+
nodes=update_dependencies(graph_nodes, fn_graph.adapter),
|
|
496
|
+
config=fn_graph.config,
|
|
497
|
+
adapter=fn_graph.adapter,
|
|
498
|
+
)
|
|
499
|
+
# TODO -- prune the nodes that are upstream *only* of the old loader nodes, and not
|
|
500
|
+
return fn_graph
|
hamilton/io/utils.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
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 os
|
|
19
|
+
import time
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from os import PathLike
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
from urllib import parse
|
|
25
|
+
|
|
26
|
+
import pandas as pd
|
|
27
|
+
|
|
28
|
+
DATAFRAME_METADATA = "dataframe_metadata"
|
|
29
|
+
SQL_METADATA = "sql_metadata"
|
|
30
|
+
FILE_METADATA = "file_metadata"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_file_metadata(path: str | Path | PathLike) -> dict[str, Any]:
|
|
34
|
+
"""Gives metadata from loading a file.
|
|
35
|
+
|
|
36
|
+
Note: we reserve the right to change this schema. So if you're using this come
|
|
37
|
+
chat so that we can make sure we don't break your code.
|
|
38
|
+
|
|
39
|
+
This includes:
|
|
40
|
+
- the file size
|
|
41
|
+
- the file path
|
|
42
|
+
- the last modified time
|
|
43
|
+
- the current time
|
|
44
|
+
"""
|
|
45
|
+
if isinstance(path, Path):
|
|
46
|
+
path = str(path)
|
|
47
|
+
parsed = parse.urlparse(path)
|
|
48
|
+
size = None
|
|
49
|
+
scheme = parsed.scheme
|
|
50
|
+
last_modified = time.time()
|
|
51
|
+
timestamp = datetime.now().utcnow().timestamp()
|
|
52
|
+
notes = f"File metadata is unsupported for scheme: {scheme} or path: {path} does not exist."
|
|
53
|
+
|
|
54
|
+
# Check if the path is a local file path (Windows drive can be listed as a scheme)
|
|
55
|
+
is_win_path = parsed.scheme and len(parsed.scheme) == 1 and parsed.scheme.isalpha()
|
|
56
|
+
if (parsed.scheme == "" or is_win_path) and os.path.exists(path):
|
|
57
|
+
size = os.path.getsize(path)
|
|
58
|
+
last_modified = os.path.getmtime(path)
|
|
59
|
+
notes = ""
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
FILE_METADATA: {
|
|
63
|
+
"size": size,
|
|
64
|
+
"path": path,
|
|
65
|
+
"last_modified": last_modified,
|
|
66
|
+
"timestamp": timestamp,
|
|
67
|
+
"scheme": scheme,
|
|
68
|
+
"notes": notes,
|
|
69
|
+
"__version__": "1.0.0",
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_dataframe_metadata(df: pd.DataFrame) -> dict[str, Any]:
|
|
75
|
+
"""Gives metadata from loading a dataframe.
|
|
76
|
+
|
|
77
|
+
Note: we reserve the right to change this schema. So if you're using this come
|
|
78
|
+
chat so that we can make sure we don't break your code.
|
|
79
|
+
|
|
80
|
+
This includes:
|
|
81
|
+
- the number of rows
|
|
82
|
+
- the number of columns
|
|
83
|
+
- the column names
|
|
84
|
+
- the data types
|
|
85
|
+
"""
|
|
86
|
+
metadata = {"__version__": "1.0.0"}
|
|
87
|
+
try:
|
|
88
|
+
metadata["rows"] = len(df)
|
|
89
|
+
except TypeError:
|
|
90
|
+
metadata["rows"] = None
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
metadata["columns"] = len(df.columns)
|
|
94
|
+
except (AttributeError, TypeError):
|
|
95
|
+
metadata["columns"] = None
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
metadata["column_names"] = list(df.columns)
|
|
99
|
+
except (AttributeError, TypeError):
|
|
100
|
+
metadata["column_names"] = None
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
metadata["datatypes"] = [str(t) for t in list(df.dtypes)]
|
|
104
|
+
except (AttributeError, TypeError):
|
|
105
|
+
metadata["datatypes"] = None
|
|
106
|
+
return {DATAFRAME_METADATA: metadata}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_file_and_dataframe_metadata(path: str, df: pd.DataFrame) -> dict[str, Any]:
|
|
110
|
+
"""Gives metadata from loading a file and a dataframe.
|
|
111
|
+
|
|
112
|
+
Note: we reserve the right to change this schema. So if you're using this come
|
|
113
|
+
chat so that we can make sure we don't break your code.
|
|
114
|
+
|
|
115
|
+
This includes:
|
|
116
|
+
file_meta:
|
|
117
|
+
- the file size
|
|
118
|
+
- the file path
|
|
119
|
+
- the last modified time
|
|
120
|
+
- the current time
|
|
121
|
+
dataframe_meta:
|
|
122
|
+
- the number of rows
|
|
123
|
+
- the number of columns
|
|
124
|
+
- the column names
|
|
125
|
+
- the data types
|
|
126
|
+
"""
|
|
127
|
+
return {**get_file_metadata(path), **get_dataframe_metadata(df)}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_sql_metadata(query_or_table: str, results: int | pd.DataFrame) -> dict[str, Any]:
|
|
131
|
+
"""Gives metadata from reading a SQL table or writing to SQL db.
|
|
132
|
+
|
|
133
|
+
Note: we reserve the right to change this schema. So if you're using this come
|
|
134
|
+
chat so that we can make sure we don't break your code.
|
|
135
|
+
|
|
136
|
+
This includes:
|
|
137
|
+
- the number of rows read, added, or to add.
|
|
138
|
+
- the sql query (e.g., "SELECT foo FROM bar")
|
|
139
|
+
- the table name (e.g., "bar")
|
|
140
|
+
- the current time
|
|
141
|
+
"""
|
|
142
|
+
query = query_or_table if "SELECT" in query_or_table else None
|
|
143
|
+
table_name = query_or_table if "SELECT" not in query_or_table else None
|
|
144
|
+
if isinstance(results, int):
|
|
145
|
+
rows = results
|
|
146
|
+
elif isinstance(results, pd.DataFrame):
|
|
147
|
+
rows = len(results)
|
|
148
|
+
else:
|
|
149
|
+
rows = None
|
|
150
|
+
return {
|
|
151
|
+
SQL_METADATA: {
|
|
152
|
+
"rows": rows,
|
|
153
|
+
"query": query,
|
|
154
|
+
"table_name": table_name,
|
|
155
|
+
"timestamp": datetime.now().utcnow().timestamp(),
|
|
156
|
+
"__version__": "1.0.0",
|
|
157
|
+
}
|
|
158
|
+
}
|