apache-hamilton 1.90.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apache_hamilton-1.90.0.dev0.dist-info/METADATA +407 -0
- apache_hamilton-1.90.0.dev0.dist-info/RECORD +151 -0
- apache_hamilton-1.90.0.dev0.dist-info/WHEEL +4 -0
- apache_hamilton-1.90.0.dev0.dist-info/entry_points.txt +9 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/DISCLAIMER +10 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/LICENSE +228 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/NOTICE +5 -0
- hamilton/__init__.py +24 -0
- hamilton/ad_hoc_utils.py +132 -0
- hamilton/async_driver.py +465 -0
- hamilton/base.py +466 -0
- hamilton/caching/__init__.py +16 -0
- hamilton/caching/adapter.py +1475 -0
- hamilton/caching/cache_key.py +70 -0
- hamilton/caching/fingerprinting.py +287 -0
- hamilton/caching/stores/__init__.py +16 -0
- hamilton/caching/stores/base.py +242 -0
- hamilton/caching/stores/file.py +140 -0
- hamilton/caching/stores/memory.py +297 -0
- hamilton/caching/stores/sqlite.py +282 -0
- hamilton/caching/stores/utils.py +40 -0
- hamilton/cli/__init__.py +16 -0
- hamilton/cli/__main__.py +328 -0
- hamilton/cli/commands.py +126 -0
- hamilton/cli/logic.py +338 -0
- hamilton/common/__init__.py +76 -0
- hamilton/contrib/__init__.py +41 -0
- hamilton/data_quality/__init__.py +16 -0
- hamilton/data_quality/base.py +198 -0
- hamilton/data_quality/default_validators.py +560 -0
- hamilton/data_quality/pandera_validators.py +121 -0
- hamilton/dataflows/__init__.py +726 -0
- hamilton/dataflows/template/README.md +25 -0
- hamilton/dataflows/template/__init__.py +54 -0
- hamilton/dataflows/template/author.md +28 -0
- hamilton/dataflows/template/requirements.txt +0 -0
- hamilton/dataflows/template/tags.json +7 -0
- hamilton/dataflows/template/valid_configs.jsonl +1 -0
- hamilton/dev_utils/__init__.py +16 -0
- hamilton/dev_utils/deprecation.py +204 -0
- hamilton/driver.py +2112 -0
- hamilton/execution/__init__.py +16 -0
- hamilton/execution/debugging_utils.py +56 -0
- hamilton/execution/executors.py +502 -0
- hamilton/execution/graph_functions.py +421 -0
- hamilton/execution/grouping.py +430 -0
- hamilton/execution/state.py +539 -0
- hamilton/experimental/__init__.py +27 -0
- hamilton/experimental/databackend.py +61 -0
- hamilton/experimental/decorators/__init__.py +16 -0
- hamilton/experimental/decorators/parameterize_frame.py +233 -0
- hamilton/experimental/h_async.py +29 -0
- hamilton/experimental/h_cache.py +413 -0
- hamilton/experimental/h_dask.py +28 -0
- hamilton/experimental/h_databackends.py +174 -0
- hamilton/experimental/h_ray.py +28 -0
- hamilton/experimental/h_spark.py +32 -0
- hamilton/function_modifiers/README +40 -0
- hamilton/function_modifiers/__init__.py +121 -0
- hamilton/function_modifiers/adapters.py +900 -0
- hamilton/function_modifiers/base.py +859 -0
- hamilton/function_modifiers/configuration.py +310 -0
- hamilton/function_modifiers/delayed.py +202 -0
- hamilton/function_modifiers/dependencies.py +246 -0
- hamilton/function_modifiers/expanders.py +1230 -0
- hamilton/function_modifiers/macros.py +1634 -0
- hamilton/function_modifiers/metadata.py +434 -0
- hamilton/function_modifiers/recursive.py +908 -0
- hamilton/function_modifiers/validation.py +289 -0
- hamilton/function_modifiers_base.py +31 -0
- hamilton/graph.py +1153 -0
- hamilton/graph_types.py +264 -0
- hamilton/graph_utils.py +41 -0
- hamilton/htypes.py +450 -0
- hamilton/io/__init__.py +32 -0
- hamilton/io/data_adapters.py +216 -0
- hamilton/io/default_data_loaders.py +224 -0
- hamilton/io/materialization.py +500 -0
- hamilton/io/utils.py +158 -0
- hamilton/lifecycle/__init__.py +67 -0
- hamilton/lifecycle/api.py +833 -0
- hamilton/lifecycle/base.py +1130 -0
- hamilton/lifecycle/default.py +802 -0
- hamilton/log_setup.py +47 -0
- hamilton/models.py +92 -0
- hamilton/node.py +449 -0
- hamilton/plugins/README.md +48 -0
- hamilton/plugins/__init__.py +16 -0
- hamilton/plugins/dask_extensions.py +47 -0
- hamilton/plugins/dlt_extensions.py +161 -0
- hamilton/plugins/geopandas_extensions.py +49 -0
- hamilton/plugins/h_dask.py +331 -0
- hamilton/plugins/h_ddog.py +522 -0
- hamilton/plugins/h_diskcache.py +163 -0
- hamilton/plugins/h_experiments/__init__.py +22 -0
- hamilton/plugins/h_experiments/__main__.py +62 -0
- hamilton/plugins/h_experiments/cache.py +39 -0
- hamilton/plugins/h_experiments/data_model.py +68 -0
- hamilton/plugins/h_experiments/hook.py +219 -0
- hamilton/plugins/h_experiments/server.py +375 -0
- hamilton/plugins/h_kedro.py +152 -0
- hamilton/plugins/h_logging.py +454 -0
- hamilton/plugins/h_mcp/__init__.py +28 -0
- hamilton/plugins/h_mcp/__main__.py +33 -0
- hamilton/plugins/h_mcp/_helpers.py +129 -0
- hamilton/plugins/h_mcp/_templates.py +417 -0
- hamilton/plugins/h_mcp/server.py +328 -0
- hamilton/plugins/h_mlflow.py +335 -0
- hamilton/plugins/h_narwhals.py +134 -0
- hamilton/plugins/h_openlineage.py +400 -0
- hamilton/plugins/h_opentelemetry.py +167 -0
- hamilton/plugins/h_pandas.py +257 -0
- hamilton/plugins/h_pandera.py +117 -0
- hamilton/plugins/h_polars.py +304 -0
- hamilton/plugins/h_polars_lazyframe.py +282 -0
- hamilton/plugins/h_pyarrow.py +56 -0
- hamilton/plugins/h_pydantic.py +127 -0
- hamilton/plugins/h_ray.py +242 -0
- hamilton/plugins/h_rich.py +142 -0
- hamilton/plugins/h_schema.py +493 -0
- hamilton/plugins/h_slack.py +100 -0
- hamilton/plugins/h_spark.py +1380 -0
- hamilton/plugins/h_threadpool.py +125 -0
- hamilton/plugins/h_tqdm.py +122 -0
- hamilton/plugins/h_vaex.py +129 -0
- hamilton/plugins/huggingface_extensions.py +236 -0
- hamilton/plugins/ibis_extensions.py +93 -0
- hamilton/plugins/jupyter_magic.py +622 -0
- hamilton/plugins/kedro_extensions.py +117 -0
- hamilton/plugins/lightgbm_extensions.py +99 -0
- hamilton/plugins/matplotlib_extensions.py +108 -0
- hamilton/plugins/mlflow_extensions.py +216 -0
- hamilton/plugins/numpy_extensions.py +105 -0
- hamilton/plugins/pandas_extensions.py +1763 -0
- hamilton/plugins/plotly_extensions.py +150 -0
- hamilton/plugins/polars_extensions.py +71 -0
- hamilton/plugins/polars_implementations.py +25 -0
- hamilton/plugins/polars_lazyframe_extensions.py +302 -0
- hamilton/plugins/polars_post_1_0_0_extensions.py +877 -0
- hamilton/plugins/polars_pre_1_0_0_extension.py +836 -0
- hamilton/plugins/pydantic_extensions.py +98 -0
- hamilton/plugins/pyspark_pandas_extensions.py +47 -0
- hamilton/plugins/sklearn_plot_extensions.py +129 -0
- hamilton/plugins/spark_extensions.py +105 -0
- hamilton/plugins/vaex_extensions.py +51 -0
- hamilton/plugins/xgboost_extensions.py +91 -0
- hamilton/plugins/yaml_extensions.py +89 -0
- hamilton/registry.py +254 -0
- hamilton/settings.py +18 -0
- hamilton/telemetry.py +50 -0
- hamilton/version.py +18 -0
hamilton/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_)
|
hamilton/io/__init__.py
ADDED
|
@@ -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
|