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,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
+ }