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
hamilton/htypes.py ADDED
@@ -0,0 +1,450 @@
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 inspect
19
+ import sys
20
+ import typing
21
+ from collections.abc import Iterable
22
+ from typing import Any, Literal, Protocol, TypeVar, Union
23
+
24
+ import typing_inspect
25
+
26
+ from hamilton.registry import COLUMN_TYPE, DF_TYPE_AND_COLUMN_TYPES
27
+
28
+ BASE_ARGS_FOR_GENERICS = (typing.T,)
29
+
30
+
31
+ def _safe_subclass(candidate_type: type, base_type: type) -> bool:
32
+ """Safely checks subclass, returning False if python's subclass does not work.
33
+ This is *not* a true subclass check, and will not tell you whether hamilton
34
+ considers the types to be equivalent. Rather, it is used to short-circuit further
35
+ computation safely and avoid errors.
36
+
37
+ Note that we may end up with types that *should* be considered equivalent, but
38
+ are not. In that case we will deal with them -- its a better user experience and easier
39
+ to report than an error.
40
+
41
+ :param base_type: Base type to check against
42
+ :param candidate_type: Candidate type to check as a potential subclass
43
+ :return: Whether python considers them subclasses and will not break if subclass is called.
44
+ """
45
+ if len(_get_args(candidate_type)) > 0 or len(_get_args(base_type)) > 0:
46
+ return False
47
+ if inspect.isclass(candidate_type) and inspect.isclass(base_type):
48
+ return issubclass(candidate_type, base_type)
49
+ return False
50
+
51
+
52
+ def custom_subclass_check(requested_type: type, param_type: type):
53
+ """This is a custom check around generics & classes. It probably misses a few edge cases.
54
+
55
+ We will likely need to revisit this in the future (perhaps integrate with graphadapter?)
56
+
57
+ :param requested_type: Candidate subclass.
58
+ :param param_type: Type of parameter to check against.
59
+ :return: Whether or not requested_type is a valid subclass of param_type.
60
+ """
61
+ # handles case when someone is using primitives and generics
62
+ requested_origin_type = requested_type
63
+ param_type, _ = get_type_information(param_type)
64
+ param_origin_type = param_type
65
+ has_generic = False
66
+ if param_type == Any:
67
+ # any type is a valid subclass of Any.
68
+ return True
69
+ if _safe_subclass(requested_type, param_type):
70
+ return True
71
+ if typing_inspect.is_union_type(param_type):
72
+ for arg in _get_args(param_type):
73
+ if custom_subclass_check(requested_type, arg):
74
+ return True
75
+ if typing_inspect.is_generic_type(requested_type) or typing_inspect.is_tuple_type(
76
+ requested_type
77
+ ):
78
+ requested_origin_type = typing_inspect.get_origin(requested_type)
79
+ has_generic = True
80
+ if typing_inspect.is_generic_type(param_type) or typing_inspect.is_tuple_type(param_type):
81
+ param_origin_type = typing_inspect.get_origin(param_type)
82
+ has_generic = True
83
+ # TODO -- consider moving into a graph adapter or elsewhere -- this is perhaps a little too
84
+ # low-level
85
+ if has_generic and requested_origin_type == Parallelizable:
86
+ (requested_type_arg,) = _get_args(requested_type)
87
+ return custom_subclass_check(requested_type_arg, param_type)
88
+ if has_generic and param_origin_type == Collect:
89
+ (param_type_arg,) = _get_args(param_type)
90
+ return custom_subclass_check(requested_type, param_type_arg)
91
+ if requested_origin_type == param_origin_type or _safe_subclass(
92
+ requested_origin_type, param_origin_type
93
+ ):
94
+ if has_generic: # check the args match or they do not have them defined.
95
+ requested_args = _get_args(requested_type)
96
+ param_args = _get_args(param_type)
97
+ if (
98
+ requested_args
99
+ and param_args
100
+ and requested_args != BASE_ARGS_FOR_GENERICS
101
+ and param_args != BASE_ARGS_FOR_GENERICS
102
+ ):
103
+ return requested_args == param_args
104
+ return True
105
+ return False
106
+
107
+
108
+ def get_type_as_string(type_: type) -> str | None:
109
+ """Get a string representation of a type.
110
+
111
+ The logic supports the evolution of the type system between 3.8 and 3.10.
112
+ :param type_: Any Type object. Typically the node type found at Node.type.
113
+ :return: string representation of the type. An empty string if everything fails.
114
+ """
115
+
116
+ if _is_annotated_type(type_):
117
+ type_string = get_type_as_string(typing.get_args(type_)[0])
118
+ elif getattr(type_, "__name__", None):
119
+ type_string = type_.__name__
120
+ elif typing_inspect.get_origin(type_):
121
+ base_type = typing_inspect.get_origin(type_)
122
+ type_string = get_type_as_string(base_type)
123
+ elif getattr(type_, "__repr__", None):
124
+ type_string = type_.__repr__()
125
+ else:
126
+ type_string = None
127
+
128
+ return type_string
129
+
130
+
131
+ def types_match(param_type: type[type], required_node_type: Any) -> bool:
132
+ """Checks that we have "types" that "match".
133
+
134
+ Matching can be loose here -- and depends on the adapter being used as to what is
135
+ allowed. Otherwise it does a basic equality check.
136
+
137
+ :param adapter: the graph adapter to delegate to for one check.
138
+ :param param_type: the parameter type we're checking.
139
+ :param required_node_type: the expected parameter type to validate against.
140
+ :return: True if types are "matching", False otherwise.
141
+ """
142
+ if required_node_type == typing.Any:
143
+ return True
144
+ # type var -- straight == should suffice. Assume people understand what they're doing with TypeVar.
145
+ elif typing_inspect.is_typevar(required_node_type) or typing_inspect.is_typevar(param_type):
146
+ return required_node_type == param_type
147
+ elif required_node_type == param_type:
148
+ return True
149
+ elif custom_subclass_check(required_node_type, param_type):
150
+ return True
151
+ return False
152
+
153
+
154
+ _sys_version_info = sys.version_info
155
+ _version_tuple = (
156
+ _sys_version_info.major,
157
+ _sys_version_info.minor,
158
+ _sys_version_info.micro,
159
+ )
160
+
161
+ """
162
+ The following is purely for backwards compatibility
163
+ The behavior of annotated/get_args/get_origin has changed in recent versions
164
+ So we have to handle it accordingly
165
+ In 3.8/below we have to use the typing_extensions version
166
+
167
+ Also, note that it is currently called `column`, but
168
+ we will eventually want more options. E.G.
169
+
170
+ `dataset`
171
+ `scalar`
172
+ `tensor`
173
+
174
+ etc...
175
+
176
+ To do this, we'll likely extend from annotated, and add new types.
177
+ See `annotated` source code: https://github.com/python/cpython/blob/3.11/Lib/typing.py#L2122.
178
+
179
+ We can also potentially add validation in the types, and remove it from the validate.
180
+ """
181
+
182
+ ANNOTATE_ALLOWED = False
183
+ if _version_tuple < (3, 9, 0):
184
+ # Before 3.9 we use typing_extensions
185
+ import typing_extensions
186
+
187
+ column = typing_extensions.Annotated
188
+
189
+
190
+ else:
191
+ ANNOTATE_ALLOWED = True
192
+ from typing import Annotated
193
+
194
+ column = Annotated
195
+
196
+ if _version_tuple < (3, 9, 0):
197
+ import typing_extensions
198
+
199
+ _get_origin = typing_extensions.get_origin
200
+ _get_args = typing_extensions.get_args
201
+ else:
202
+ from typing import get_args as _get_args
203
+ from typing import get_origin as _get_origin
204
+
205
+
206
+ def _is_annotated_type(type_: type[type]) -> bool:
207
+ """Utility function to tell if a type is Annotated"""
208
+ return _get_origin(type_) == column
209
+
210
+
211
+ # Placeholder exception for invalid hamilton types
212
+ class InvalidTypeException(Exception):
213
+ pass
214
+
215
+
216
+ # Some valid series annotations
217
+ # We will likely have to expand
218
+ _valid_series_annotations = (
219
+ int,
220
+ float,
221
+ str,
222
+ bool,
223
+ )
224
+
225
+
226
+ def _is_valid_series_type(candidate_type: type[type]) -> bool:
227
+ """Tells if something is a valid series type, using the registry we have.
228
+
229
+ :param candidate_type: Type to check
230
+ :return: Whether it is a series (column) type that we have registered
231
+ """
232
+ for _key, types in DF_TYPE_AND_COLUMN_TYPES.items():
233
+ if COLUMN_TYPE not in types:
234
+ continue
235
+ if issubclass(candidate_type, types[COLUMN_TYPE]):
236
+ return True
237
+ return False
238
+
239
+
240
+ def validate_type_annotation(annotation: type[type]):
241
+ """Validates a type annotation for a hamilton function.
242
+ If it is not an Annotated type, it will be fine.
243
+ If it is the Annotated type, it will check that
244
+ it only has one type annotation and that that is valid (currently int, float, str, bool).
245
+
246
+ :param annotation: Annotation (e.g. Annotated[pd.Series, int])
247
+ :raises InvalidTypeException: If the annotation is invalid
248
+ """
249
+
250
+ if not _is_annotated_type(annotation):
251
+ # In this case we don't care too much -- hamilton accepts anything
252
+ return True
253
+ original, *annotations = _get_args(annotation)
254
+ # TODO -- use extensions/series types to do this more effectively
255
+ if not (_is_valid_series_type(original)):
256
+ raise InvalidTypeException(
257
+ f"Hamilton only accepts annotated types of series or equivalent. Got {original}"
258
+ )
259
+ if len(annotations) > 1 or len(annotations) == 0:
260
+ raise InvalidTypeException(
261
+ f"Hamilton only accepts one annotation per pd.Series. Got {annotations}"
262
+ )
263
+ subclasses_valid_annotation = False
264
+ (annotation,) = annotations
265
+ for valid_annotation in _valid_series_annotations:
266
+ if custom_subclass_check(annotation, valid_annotation):
267
+ subclasses_valid_annotation = True
268
+ if not subclasses_valid_annotation:
269
+ raise InvalidTypeException(
270
+ f"Hamilton only accepts annotations on series that are subclasses of one of {_valid_series_annotations}. "
271
+ f"Got {annotation}"
272
+ )
273
+
274
+
275
+ def get_type_information(some_type: Any) -> tuple[type[type], list]:
276
+ """Gets the type information for a given type.
277
+
278
+ If it is an annotated type, it will return the original type and the annotation.
279
+ If it is not an annotated type, it will return the type and empty list.
280
+
281
+ :param some_type: Type to get information for
282
+ :return: Tuple of type and list of annotations (or empty list)
283
+ """
284
+ if _is_annotated_type(some_type):
285
+ original, *annotations = _get_args(some_type)
286
+ return original, annotations
287
+ return some_type, []
288
+
289
+
290
+ # Type variables for annotations below
291
+ SequentialElement = TypeVar("SequentialElement", covariant=True)
292
+ ParallelizableElement = TypeVar("ParallelizableElement", covariant=True)
293
+ CollectElement = TypeVar("CollectElement", covariant=True)
294
+
295
+
296
+ # TODO -- support sequential operation
297
+ # class Sequential(Iterable[SequentialElement], Protocol[SequentialElement]):
298
+ # pass
299
+
300
+
301
+ class Parallelizable(Iterable[ParallelizableElement], Protocol[ParallelizableElement]):
302
+ """Marks the output of a function node as parallelizable.
303
+
304
+ Parallelizable outputs are expected to be iterable, where each element dynamically
305
+ generates a node. When using dynamic execution, each of these dynamic nodes can be
306
+ executed in parallel.
307
+
308
+ Because this uses dynamic execution, the builder method `enable_dynamic_execution`
309
+ must be called with `allow_experimental_mode=True`.
310
+ """
311
+
312
+ pass
313
+
314
+
315
+ def is_parallelizable_type(type_: type) -> bool:
316
+ return _get_origin(type_) == Parallelizable
317
+
318
+
319
+ class Collect(Iterable[CollectElement], Protocol[CollectElement]):
320
+ """Marks a function node parameter as collectable.
321
+
322
+ Collectable inputs are expected to be iterable, where each element is populated with
323
+ the results of dynamic nodes derived from parallelizable outputs.
324
+
325
+ Because this uses dynamic execution, the builder method `enable_dynamic_execution`
326
+ must be called with `allow_experimental_mode=True`.
327
+ """
328
+
329
+
330
+ def check_input_type(node_type: type, input_value: Any) -> bool:
331
+ """Checks an input value against the declare input type. This is a utility function to be
332
+ used for checking types against values. Note we are looser here than in custom_subclass_check,
333
+ as runtime-typing is less specific.
334
+
335
+ :param node_type: Type of the node to check against.
336
+ :param input_value: Value to check.
337
+ :return: True if the input value is of the correct type, False otherwise.
338
+ """
339
+ if node_type == Any:
340
+ return True
341
+ # In the case of dict[str, Any] (or equivalent) in python 3.9 +
342
+ # we need to double-check that its not generic, as the isinstance clause will break this
343
+ elif (
344
+ inspect.isclass(node_type)
345
+ and not typing_inspect.is_generic_type(node_type)
346
+ and isinstance(input_value, node_type)
347
+ ):
348
+ return True
349
+ elif typing_inspect.is_typevar(node_type): # skip runtime comparison for now.
350
+ return True
351
+ elif typing_inspect.is_generic_type(node_type) and typing_inspect.get_origin(node_type) == type(
352
+ input_value
353
+ ):
354
+ return True
355
+ elif typing_inspect.is_union_type(node_type):
356
+ union_types = typing_inspect.get_args(node_type)
357
+ return any([check_input_type(ut, input_value) for ut in union_types])
358
+ elif node_type == type(input_value):
359
+ return True
360
+ # check for literal and that the value is in the literals listed.
361
+ elif typing_inspect.is_literal_type(node_type) and input_value in typing_inspect.get_args(
362
+ node_type
363
+ ):
364
+ return True
365
+ # iterable (set, dict) is super class over sequence (list, tuple)
366
+ elif (
367
+ typing_inspect.is_generic_type(node_type)
368
+ and typing_inspect.get_origin(node_type)
369
+ in (list, tuple, typing_inspect.get_origin(typing.Sequence))
370
+ and isinstance(input_value, (list, tuple, typing_inspect.get_origin(typing.Sequence)))
371
+ ):
372
+ if typing_inspect.get_args(node_type):
373
+ # check first value in sequence -- if the type is specified.
374
+ for i in input_value: # this handles empty input case, e.g. [] or (), set()
375
+ return check_input_type(typing_inspect.get_args(node_type)[0], i)
376
+ return True
377
+ elif (
378
+ typing_inspect.is_generic_type(node_type)
379
+ and typing_inspect.get_origin(node_type)
380
+ in (set, typing_inspect.get_origin(typing.Iterable))
381
+ and isinstance(input_value, (set, typing_inspect.get_origin(typing.Iterable)))
382
+ ):
383
+ if typing_inspect.get_args(node_type):
384
+ # check first value in sequence -- if the type is specified.
385
+ for i in input_value: # this handles empty input case, e.g. [] or (), set()
386
+ return check_input_type(typing_inspect.get_args(node_type)[0], i)
387
+ return True
388
+
389
+ return False
390
+
391
+
392
+ # TODO: merge the above with this in some way. Right now they're separate because they have different
393
+ # behaviors. We should determine how to reconcile these and how further type checking capabilities,
394
+ # e.g. handling Annotated, Pandera, etc, should be handled...
395
+ def check_instance(obj: Any, type_: Any) -> bool:
396
+ """This function checks if an object is an instance of a given type. It supports generic types as well.
397
+
398
+ :param obj: The object to check.
399
+ :param type_: The type to check against. This can be a generic type like List[int] or Dict[str, Any].
400
+ :return: True if the object is an instance of the type, False otherwise.
401
+ """
402
+ if type_ == Any:
403
+ return True
404
+ # Get the origin of the type (i.e., the base class for generic types)
405
+ origin = getattr(type_, "__origin__", None)
406
+
407
+ # If the type has an origin, it's a generic type
408
+ if origin is not None:
409
+ # If the type is a Union type
410
+ if origin is Union:
411
+ return any(check_instance(obj, t) for t in type_.__args__)
412
+ elif origin is Literal:
413
+ return obj in type_.__args__
414
+ # Check if the object is an instance of the origin of the type
415
+ elif not isinstance(obj, origin):
416
+ return False
417
+
418
+ # If the type has arguments (i.e., it's a parameterized generic type like List[int])
419
+ if hasattr(type_, "__args__"):
420
+ # Get the element type(s) of the generic type
421
+ element_type = type_.__args__
422
+
423
+ # If the object is a dictionary
424
+ if isinstance(obj, dict):
425
+ all_items_meet_condition = True
426
+
427
+ # Iterate over each key-value pair in the dictionary
428
+ for key, value in obj.items():
429
+ # Check if the key is an instance of the first element type and the value is an instance of the second element type
430
+ key_is_correct_type = check_instance(key, element_type[0])
431
+ value_is_correct_type = check_instance(value, element_type[1])
432
+
433
+ # If either the key or the value is not the correct type, set the flag to False and break the loop
434
+ if not key_is_correct_type or not value_is_correct_type:
435
+ all_items_meet_condition = False
436
+ break
437
+
438
+ # Return the result
439
+ return all_items_meet_condition
440
+
441
+ # If the object is a list, set, or tuple
442
+ elif isinstance(obj, (list, set, tuple)):
443
+ element_type = element_type[0]
444
+ for i in obj:
445
+ if not check_instance(i, element_type):
446
+ return False
447
+ return True
448
+
449
+ # If the type is not a generic type, just use isinstance
450
+ return isinstance(obj, type_)
@@ -0,0 +1,32 @@
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 logging
19
+
20
+ from hamilton.io.default_data_loaders import DATA_ADAPTERS
21
+ from hamilton.registry import register_adapter
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ registered = False
26
+ # Register all the default ones
27
+ if not registered:
28
+ logger.debug(f"Registering default data loaders: {DATA_ADAPTERS}")
29
+ for data_loader in DATA_ADAPTERS:
30
+ register_adapter(data_loader)
31
+
32
+ registered = True
@@ -0,0 +1,216 @@
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 abc
19
+ import dataclasses
20
+ import typing
21
+ from collections.abc import Collection
22
+ from typing import Any
23
+
24
+ from hamilton.htypes import custom_subclass_check
25
+
26
+
27
+ class AdapterCommon(abc.ABC):
28
+ @classmethod
29
+ @abc.abstractmethod
30
+ def applicable_types(cls) -> Collection[type]:
31
+ """Returns the types that this data loader can load to.
32
+ These will be checked against the desired type to determine
33
+ whether this is a suitable loader for that type.
34
+
35
+ Note that a loader can load to multiple types. This is the function to
36
+ override if you want to add a new type to a data loader.
37
+
38
+ Note if you have any specific requirements for loading types (generic/whatnot),
39
+ you can override applies_to as well, but it will make it much harder to document/determine
40
+ what is happening.
41
+
42
+ :return:
43
+ """
44
+ pass
45
+
46
+ @classmethod
47
+ @abc.abstractmethod
48
+ def applies_to(cls, type_: type[type]) -> bool:
49
+ """Tells whether or not this adapter applies to the given type.
50
+
51
+ Note: you need to understand the edge direction to properly determine applicability.
52
+ For loading data, the loader type needs to be a subclass of the type being loaded into.
53
+ For saving data, the saver type needs to be a superclass of the type being passed in.
54
+
55
+ This is a classmethod as it will be easier to validate, and we have to
56
+ construct this, delayed, with a factory.
57
+
58
+ :param type_: Candidate type
59
+ :return: True if this adapter can be used with that type, False otherwise.
60
+ """
61
+ pass
62
+
63
+ @classmethod
64
+ @abc.abstractmethod
65
+ def name(cls) -> str:
66
+ """Returns the name of the data loader. This is used to register the data loader
67
+ with the load_from decorator.
68
+
69
+ :return: The name of the data loader.
70
+ """
71
+ pass
72
+
73
+ @classmethod
74
+ def _ensure_dataclass(cls):
75
+ if not dataclasses.is_dataclass(cls):
76
+ raise TypeError(
77
+ f"DataLoader subclasses must be dataclasses. {cls.__qualname__} is not."
78
+ f" Did you forget to add @dataclass?"
79
+ )
80
+
81
+ @classmethod
82
+ def get_required_arguments(cls) -> dict[str, type[type]]:
83
+ """Gives the required arguments for the class.
84
+ Note that this just uses the type hints from the dataclass.
85
+
86
+ :return: The required arguments for the class.
87
+ """
88
+ cls._ensure_dataclass()
89
+ type_hints = typing.get_type_hints(cls)
90
+ return {
91
+ field.name: type_hints.get(field.name)
92
+ for field in dataclasses.fields(cls)
93
+ if field.default == dataclasses.MISSING and field.default_factory == dataclasses.MISSING
94
+ }
95
+
96
+ @classmethod
97
+ def get_optional_arguments(cls) -> dict[str, type[type]]:
98
+ """Gives the optional arguments for the class.
99
+ Note that this just uses the type hints from the dataclass.
100
+
101
+ :return: The optional arguments for the class.
102
+ """
103
+ cls._ensure_dataclass()
104
+ type_hints = typing.get_type_hints(cls)
105
+ return {
106
+ field.name: type_hints.get(field.name)
107
+ for field in dataclasses.fields(cls)
108
+ if field.default != dataclasses.MISSING or field.default_factory != dataclasses.MISSING
109
+ }
110
+
111
+ @classmethod
112
+ def can_load(cls) -> bool:
113
+ """Returns whether this adapter can "load" data.
114
+ Subclasses are meant to implement this function to
115
+ tell the framework what to do with them.
116
+
117
+ :return:
118
+ """
119
+ return False
120
+
121
+ @classmethod
122
+ def can_save(cls) -> bool:
123
+ """Returns whether this adapter can "save" data.
124
+ Subclasses are meant to implement this function to
125
+ tell the framework what to do with them.
126
+
127
+ :return:
128
+ """
129
+ return False
130
+
131
+
132
+ class DataLoader(AdapterCommon, abc.ABC):
133
+ """Base class for data loaders. Data loaders are used to load data from a data source.
134
+ Note that they are inherently polymorphic -- they declare what type(s) they can load to,
135
+ and may choose to load differently depending on the type they are loading to.
136
+ """
137
+
138
+ @abc.abstractmethod
139
+ def load_data(self, type_: type[type]) -> tuple[type, dict[str, Any]]:
140
+ """Loads the data from the data source.
141
+ Note this uses the constructor parameters to determine
142
+ how to load the data.
143
+
144
+ :return: The type specified
145
+ """
146
+ pass
147
+
148
+ @classmethod
149
+ def can_load(cls) -> bool:
150
+ return True
151
+
152
+ @classmethod
153
+ def applies_to(cls, type_: type[type]) -> bool:
154
+ """Tells whether or not this data loader can load to a specific type.
155
+ For instance, a CSV data loader might be able to load to a dataframe,
156
+ a json, but not an integer.
157
+
158
+ I.e. is the adapter type a subclass of the passed in type?
159
+
160
+ This is a classmethod as it will be easier to validate, and we have to
161
+ construct this, delayed, with a factory.
162
+
163
+ :param type_: Candidate type
164
+ :return: True if this data loader can load to the type, False otherwise.
165
+ """
166
+ for load_to in cls.applicable_types():
167
+ # is the adapter type `load_to` a subclass of `type_` ?
168
+ if custom_subclass_check(load_to, type_):
169
+ return True
170
+ return False
171
+
172
+
173
+ class DataSaver(AdapterCommon, abc.ABC):
174
+ """Base class for data savers. Data savers are used to save data to a data source.
175
+ Note that they are inherently polymorphic -- they declare what type(s) they can save from,
176
+ and may choose to save differently depending on the type they are saving from.
177
+ """
178
+
179
+ @abc.abstractmethod
180
+ def save_data(self, data: Any) -> dict[str, Any]:
181
+ """Saves the data to the data source.
182
+ Note this uses the constructor parameters to determine
183
+ how to save the data.
184
+
185
+ :return: Any relevant metadata. This is up the the data saver, but will likely
186
+ include the URI, etc... This is going to be similar to the metadata returned
187
+ by the data loader in the loading tuple.
188
+ """
189
+ pass
190
+
191
+ @classmethod
192
+ def can_save(cls) -> bool:
193
+ return True
194
+
195
+ @classmethod
196
+ def applies_to(cls, type_: type[type]) -> bool:
197
+ """Tells whether or not this data saver can ingest a specific type to save it.
198
+
199
+ I.e. is the adapter type a superclass of the passed in type?
200
+
201
+ This is a classmethod as it will be easier to validate, and we have to
202
+ construct this, delayed, with a factory.
203
+
204
+ :param type_: Candidate type
205
+ :return: True if this data saver can handle to the type, False otherwise.
206
+ """
207
+ applicable_types = cls.applicable_types()
208
+ if len(applicable_types) > 1 and typing.Union[tuple(applicable_types)] == type_:
209
+ # if someone outputs the union of what we support we should match it.
210
+ return True
211
+ for save_to in applicable_types:
212
+ # is the adapter type `save_to` a superclass of `type_` ?
213
+ # i.e. is `type_` a subclass of `save_to` ?
214
+ if custom_subclass_check(type_, save_to):
215
+ return True
216
+ return False