apache-hamilton 1.90.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apache_hamilton-1.90.0.dev0.dist-info/METADATA +407 -0
- apache_hamilton-1.90.0.dev0.dist-info/RECORD +151 -0
- apache_hamilton-1.90.0.dev0.dist-info/WHEEL +4 -0
- apache_hamilton-1.90.0.dev0.dist-info/entry_points.txt +9 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/DISCLAIMER +10 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/LICENSE +228 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/NOTICE +5 -0
- hamilton/__init__.py +24 -0
- hamilton/ad_hoc_utils.py +132 -0
- hamilton/async_driver.py +465 -0
- hamilton/base.py +466 -0
- hamilton/caching/__init__.py +16 -0
- hamilton/caching/adapter.py +1475 -0
- hamilton/caching/cache_key.py +70 -0
- hamilton/caching/fingerprinting.py +287 -0
- hamilton/caching/stores/__init__.py +16 -0
- hamilton/caching/stores/base.py +242 -0
- hamilton/caching/stores/file.py +140 -0
- hamilton/caching/stores/memory.py +297 -0
- hamilton/caching/stores/sqlite.py +282 -0
- hamilton/caching/stores/utils.py +40 -0
- hamilton/cli/__init__.py +16 -0
- hamilton/cli/__main__.py +328 -0
- hamilton/cli/commands.py +126 -0
- hamilton/cli/logic.py +338 -0
- hamilton/common/__init__.py +76 -0
- hamilton/contrib/__init__.py +41 -0
- hamilton/data_quality/__init__.py +16 -0
- hamilton/data_quality/base.py +198 -0
- hamilton/data_quality/default_validators.py +560 -0
- hamilton/data_quality/pandera_validators.py +121 -0
- hamilton/dataflows/__init__.py +726 -0
- hamilton/dataflows/template/README.md +25 -0
- hamilton/dataflows/template/__init__.py +54 -0
- hamilton/dataflows/template/author.md +28 -0
- hamilton/dataflows/template/requirements.txt +0 -0
- hamilton/dataflows/template/tags.json +7 -0
- hamilton/dataflows/template/valid_configs.jsonl +1 -0
- hamilton/dev_utils/__init__.py +16 -0
- hamilton/dev_utils/deprecation.py +204 -0
- hamilton/driver.py +2112 -0
- hamilton/execution/__init__.py +16 -0
- hamilton/execution/debugging_utils.py +56 -0
- hamilton/execution/executors.py +502 -0
- hamilton/execution/graph_functions.py +421 -0
- hamilton/execution/grouping.py +430 -0
- hamilton/execution/state.py +539 -0
- hamilton/experimental/__init__.py +27 -0
- hamilton/experimental/databackend.py +61 -0
- hamilton/experimental/decorators/__init__.py +16 -0
- hamilton/experimental/decorators/parameterize_frame.py +233 -0
- hamilton/experimental/h_async.py +29 -0
- hamilton/experimental/h_cache.py +413 -0
- hamilton/experimental/h_dask.py +28 -0
- hamilton/experimental/h_databackends.py +174 -0
- hamilton/experimental/h_ray.py +28 -0
- hamilton/experimental/h_spark.py +32 -0
- hamilton/function_modifiers/README +40 -0
- hamilton/function_modifiers/__init__.py +121 -0
- hamilton/function_modifiers/adapters.py +900 -0
- hamilton/function_modifiers/base.py +859 -0
- hamilton/function_modifiers/configuration.py +310 -0
- hamilton/function_modifiers/delayed.py +202 -0
- hamilton/function_modifiers/dependencies.py +246 -0
- hamilton/function_modifiers/expanders.py +1230 -0
- hamilton/function_modifiers/macros.py +1634 -0
- hamilton/function_modifiers/metadata.py +434 -0
- hamilton/function_modifiers/recursive.py +908 -0
- hamilton/function_modifiers/validation.py +289 -0
- hamilton/function_modifiers_base.py +31 -0
- hamilton/graph.py +1153 -0
- hamilton/graph_types.py +264 -0
- hamilton/graph_utils.py +41 -0
- hamilton/htypes.py +450 -0
- hamilton/io/__init__.py +32 -0
- hamilton/io/data_adapters.py +216 -0
- hamilton/io/default_data_loaders.py +224 -0
- hamilton/io/materialization.py +500 -0
- hamilton/io/utils.py +158 -0
- hamilton/lifecycle/__init__.py +67 -0
- hamilton/lifecycle/api.py +833 -0
- hamilton/lifecycle/base.py +1130 -0
- hamilton/lifecycle/default.py +802 -0
- hamilton/log_setup.py +47 -0
- hamilton/models.py +92 -0
- hamilton/node.py +449 -0
- hamilton/plugins/README.md +48 -0
- hamilton/plugins/__init__.py +16 -0
- hamilton/plugins/dask_extensions.py +47 -0
- hamilton/plugins/dlt_extensions.py +161 -0
- hamilton/plugins/geopandas_extensions.py +49 -0
- hamilton/plugins/h_dask.py +331 -0
- hamilton/plugins/h_ddog.py +522 -0
- hamilton/plugins/h_diskcache.py +163 -0
- hamilton/plugins/h_experiments/__init__.py +22 -0
- hamilton/plugins/h_experiments/__main__.py +62 -0
- hamilton/plugins/h_experiments/cache.py +39 -0
- hamilton/plugins/h_experiments/data_model.py +68 -0
- hamilton/plugins/h_experiments/hook.py +219 -0
- hamilton/plugins/h_experiments/server.py +375 -0
- hamilton/plugins/h_kedro.py +152 -0
- hamilton/plugins/h_logging.py +454 -0
- hamilton/plugins/h_mcp/__init__.py +28 -0
- hamilton/plugins/h_mcp/__main__.py +33 -0
- hamilton/plugins/h_mcp/_helpers.py +129 -0
- hamilton/plugins/h_mcp/_templates.py +417 -0
- hamilton/plugins/h_mcp/server.py +328 -0
- hamilton/plugins/h_mlflow.py +335 -0
- hamilton/plugins/h_narwhals.py +134 -0
- hamilton/plugins/h_openlineage.py +400 -0
- hamilton/plugins/h_opentelemetry.py +167 -0
- hamilton/plugins/h_pandas.py +257 -0
- hamilton/plugins/h_pandera.py +117 -0
- hamilton/plugins/h_polars.py +304 -0
- hamilton/plugins/h_polars_lazyframe.py +282 -0
- hamilton/plugins/h_pyarrow.py +56 -0
- hamilton/plugins/h_pydantic.py +127 -0
- hamilton/plugins/h_ray.py +242 -0
- hamilton/plugins/h_rich.py +142 -0
- hamilton/plugins/h_schema.py +493 -0
- hamilton/plugins/h_slack.py +100 -0
- hamilton/plugins/h_spark.py +1380 -0
- hamilton/plugins/h_threadpool.py +125 -0
- hamilton/plugins/h_tqdm.py +122 -0
- hamilton/plugins/h_vaex.py +129 -0
- hamilton/plugins/huggingface_extensions.py +236 -0
- hamilton/plugins/ibis_extensions.py +93 -0
- hamilton/plugins/jupyter_magic.py +622 -0
- hamilton/plugins/kedro_extensions.py +117 -0
- hamilton/plugins/lightgbm_extensions.py +99 -0
- hamilton/plugins/matplotlib_extensions.py +108 -0
- hamilton/plugins/mlflow_extensions.py +216 -0
- hamilton/plugins/numpy_extensions.py +105 -0
- hamilton/plugins/pandas_extensions.py +1763 -0
- hamilton/plugins/plotly_extensions.py +150 -0
- hamilton/plugins/polars_extensions.py +71 -0
- hamilton/plugins/polars_implementations.py +25 -0
- hamilton/plugins/polars_lazyframe_extensions.py +302 -0
- hamilton/plugins/polars_post_1_0_0_extensions.py +877 -0
- hamilton/plugins/polars_pre_1_0_0_extension.py +836 -0
- hamilton/plugins/pydantic_extensions.py +98 -0
- hamilton/plugins/pyspark_pandas_extensions.py +47 -0
- hamilton/plugins/sklearn_plot_extensions.py +129 -0
- hamilton/plugins/spark_extensions.py +105 -0
- hamilton/plugins/vaex_extensions.py +51 -0
- hamilton/plugins/xgboost_extensions.py +91 -0
- hamilton/plugins/yaml_extensions.py +89 -0
- hamilton/registry.py +254 -0
- hamilton/settings.py +18 -0
- hamilton/telemetry.py +50 -0
- hamilton/version.py +18 -0
|
@@ -0,0 +1,310 @@
|
|
|
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
|
+
from collections.abc import Callable, Collection
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from . import base
|
|
22
|
+
|
|
23
|
+
"""Decorators that handle the configuration of a function. These can be viewed as
|
|
24
|
+
replacing if/else/switch statements in standard dataflow definition libraries"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConfigResolver:
|
|
28
|
+
"""Base class for resolving configuration so we can share the tooling between different functions."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, resolves: Callable[[dict[str, Any]], bool], config_used: list[str]):
|
|
31
|
+
self.resolves = resolves
|
|
32
|
+
self._config_used = config_used
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def optional_config(self) -> dict[str, Any]:
|
|
36
|
+
"""Gives the optional configuration for this resolver -- to be used by the @config decorator."""
|
|
37
|
+
return {key: None for key in self._config_used}
|
|
38
|
+
|
|
39
|
+
def __call__(self, config: dict[str, Any]) -> bool:
|
|
40
|
+
return self.resolves(config)
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def when(**key_value_pairs) -> "ConfigResolver":
|
|
44
|
+
"""Gives a resolver that resolves iff all keys in the config are equal to the corresponding value.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
:param key_value_pairs: Keys and corresponding values to look up in the config
|
|
48
|
+
:return: a configuration decorator
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def resolves(configuration: dict[str, Any]) -> bool:
|
|
52
|
+
return all(value == configuration.get(key) for key, value in key_value_pairs.items())
|
|
53
|
+
|
|
54
|
+
return ConfigResolver(resolves, config_used=list(key_value_pairs.keys()))
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def when_not(**key_value_pairs: Any) -> "ConfigResolver":
|
|
58
|
+
"""Gives a resolver that resolves iff the keys in the config are all not equal to the corresponding value
|
|
59
|
+
|
|
60
|
+
:param key_value_pairs: Keys and corresponding values to look up in the config
|
|
61
|
+
:return: a configuration decorator
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def resolves(configuration: dict[str, Any]) -> bool:
|
|
65
|
+
return all(value != configuration.get(key) for key, value in key_value_pairs.items())
|
|
66
|
+
|
|
67
|
+
return ConfigResolver(resolves, config_used=list(key_value_pairs.keys()))
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def when_in(**key_value_group_pairs: Collection[Any]) -> "ConfigResolver":
|
|
71
|
+
"""Gives a resolver that the function if all the
|
|
72
|
+
values corresponding to the config keys are equal to one of items in the list of values.
|
|
73
|
+
|
|
74
|
+
:param key_value_group_pairs: pairs of key-value mappings where the value is a list of possible values
|
|
75
|
+
:return: a configuration decorator
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def resolves(configuration: dict[str, Any]) -> bool:
|
|
79
|
+
return all(
|
|
80
|
+
configuration.get(key) in value for key, value in key_value_group_pairs.items()
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return ConfigResolver(resolves, config_used=list(key_value_group_pairs.keys()))
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def when_not_in(**key_value_group_pairs: Collection[Any]) -> "ConfigResolver":
|
|
87
|
+
"""Gives a decorator that resolves the function only if none of the keys are in the list of values.
|
|
88
|
+
|
|
89
|
+
:param key_value_group_pairs: pairs of key-value mappings where the value is a list of possible values
|
|
90
|
+
:return: a configuration decorator
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def resolves(configuration: dict[str, Any]) -> bool:
|
|
94
|
+
return all(
|
|
95
|
+
configuration.get(key) not in value for key, value in key_value_group_pairs.items()
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return ConfigResolver(resolves, config_used=list(key_value_group_pairs.keys()))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class config(base.NodeResolver):
|
|
102
|
+
"""Decorator class that determines whether a function should be in the DAG based on some configuration variable.
|
|
103
|
+
|
|
104
|
+
Notes:
|
|
105
|
+
|
|
106
|
+
1. Currently, functions that exist in all configurations have to be disjoint.
|
|
107
|
+
|
|
108
|
+
2. There is currently no ``@config.otherwise(...)`` decorator, so make sure to have ``config.when`` specify set of \
|
|
109
|
+
configuration possibilities. Any missing cases will not have that output (and subsequent downstream functions \
|
|
110
|
+
may error out if they ask for it).
|
|
111
|
+
|
|
112
|
+
3. To make this easier, we have a few more ``@config`` decorators:
|
|
113
|
+
* ``@config.when_not(param=value)`` Will be included if the parameter is _not_ equal to the value specified.
|
|
114
|
+
* ``@config.when_in(param=[value1, value2, ...])`` Will be included if the parameter is equal to one of the \
|
|
115
|
+
specified values.
|
|
116
|
+
* ``@config.when_not_in(param=[value1, value2, ...])`` Will be included if the parameter is not equal to any \
|
|
117
|
+
of the specified values.
|
|
118
|
+
* ``@config`` If you're feeling adventurous, you can pass in a lambda function that takes in the entire \
|
|
119
|
+
configuration and resolves to ``True`` or ``False``. You probably don't want to do this.
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
|
|
123
|
+
.. code-block:: python
|
|
124
|
+
|
|
125
|
+
@config.when_in(business_line=["mens","kids"], region=["uk"])
|
|
126
|
+
def LEAD_LOG_BASS_MODEL_TIMES_TREND(
|
|
127
|
+
TREND_BSTS_WOMENS_ACQUISITIONS: pd.Series,
|
|
128
|
+
LEAD_LOG_BASS_MODEL_SIGNUPS_NON_REFERRAL: pd.Series) -> pd.Series:
|
|
129
|
+
# logic
|
|
130
|
+
...
|
|
131
|
+
|
|
132
|
+
Example - use of `__suffix` to differentiate between functions with the same name. This is required if you want to
|
|
133
|
+
use the same function name in multiple configurations. Hamilton will automatically drop the suffix for you. The
|
|
134
|
+
following will ensure only one function is registered with the name `my_transform`:
|
|
135
|
+
|
|
136
|
+
.. code-block:: python
|
|
137
|
+
|
|
138
|
+
@config.when(region="us")
|
|
139
|
+
def my_transform__us(some_input: pd.Series, some_input_b: pd.Series) -> pd.Series:
|
|
140
|
+
# logic
|
|
141
|
+
...
|
|
142
|
+
|
|
143
|
+
@config.when(region="uk")
|
|
144
|
+
def my_transform__uk(some_input: pd.Series, some_input_c: pd.Series) -> pd.Series:
|
|
145
|
+
# logic
|
|
146
|
+
...
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
``@config`` If you're feeling adventurous, you can pass in a lambda function that takes in the entire configuration\
|
|
150
|
+
and resolves to ``True`` or ``False``. You probably don't want to do this.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
resolves: Callable[[dict[str, Any]], bool],
|
|
156
|
+
target_name: str = None,
|
|
157
|
+
config_used: list[str] = None,
|
|
158
|
+
):
|
|
159
|
+
"""Decorator that resolves a function based on the configuration...
|
|
160
|
+
|
|
161
|
+
:param resolves: the python function to use to resolve whether the wrapped function should exist in the graph \
|
|
162
|
+
or not.
|
|
163
|
+
:param target_name: Optional. The name of the "function"/"node" that we want to attach @config to.
|
|
164
|
+
:param config_used: Optional. The list of config names that this function uses.
|
|
165
|
+
"""
|
|
166
|
+
self.does_resolve = resolves
|
|
167
|
+
self.target_name = target_name
|
|
168
|
+
self._config_used = config_used
|
|
169
|
+
|
|
170
|
+
def required_config(self) -> list[str] | None:
|
|
171
|
+
"""This returns the required configuration elements. Note that "none"
|
|
172
|
+
is a sentinel value that means that we actaully don't know what
|
|
173
|
+
it uses. If either required or optional configs are None, we
|
|
174
|
+
pass the entire configuration.
|
|
175
|
+
|
|
176
|
+
Note that this can still return None due to the @config(resolver) decorator.
|
|
177
|
+
We will likely be deprecating this in 2.0, in favor of a (to be added) config.custom.
|
|
178
|
+
Still thinking this over...
|
|
179
|
+
|
|
180
|
+
:return: The list of required config elements, or None if we don't have any idea.
|
|
181
|
+
"""
|
|
182
|
+
return None if self._config_used is None else []
|
|
183
|
+
|
|
184
|
+
def optional_config(self) -> dict[str, Any] | None:
|
|
185
|
+
"""Everything is optional with None as the required value"""
|
|
186
|
+
return {key: None for key in self._config_used} if self._config_used is not None else None
|
|
187
|
+
|
|
188
|
+
def _get_function_name(self, fn: Callable) -> str:
|
|
189
|
+
if self.target_name is not None:
|
|
190
|
+
return self.target_name
|
|
191
|
+
return base.sanitize_function_name(fn.__name__)
|
|
192
|
+
|
|
193
|
+
def resolve(self, fn, config: dict[str, Any]) -> Callable:
|
|
194
|
+
if not self.does_resolve(config):
|
|
195
|
+
return None
|
|
196
|
+
# attaches config keys used to resolve function
|
|
197
|
+
fn.__config_decorated__ = (
|
|
198
|
+
self._config_used if self._config_used is not None else ["__UNKNOWN__"]
|
|
199
|
+
)
|
|
200
|
+
fn.__original_name__ = fn.__name__
|
|
201
|
+
fn.__name__ = self._get_function_name(fn) # TODO -- copy function to not mutate it
|
|
202
|
+
return fn
|
|
203
|
+
|
|
204
|
+
def validate(self, fn):
|
|
205
|
+
if fn.__name__.endswith("__"):
|
|
206
|
+
raise base.InvalidDecoratorException(
|
|
207
|
+
"Config will always use the portion of the function name before the last __. For example, signups__v2 will map to signups, whereas"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def when(name=None, **key_value_pairs) -> "config":
|
|
212
|
+
"""Yields a decorator that resolves the function if all keys in the config are equal to the corresponding value.
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
:param key_value_pairs: Keys and corresponding values to look up in the config
|
|
216
|
+
:return: a configuration decorator
|
|
217
|
+
"""
|
|
218
|
+
resolver = ConfigResolver.when(**key_value_pairs)
|
|
219
|
+
return config(resolver, target_name=name, config_used=list(resolver.optional_config))
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def when_not(name=None, **key_value_pairs: Any) -> "config":
|
|
223
|
+
"""Yields a decorator that resolves the function if none keys in the config are equal to the corresponding value
|
|
224
|
+
|
|
225
|
+
``@config.when_not(param=value)`` will be included if the parameter is _not_ equal to the value specified.
|
|
226
|
+
|
|
227
|
+
:param key_value_pairs: Keys and corresponding values to look up in the config
|
|
228
|
+
:return: a configuration decorator
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
resolver = ConfigResolver.when_not(**key_value_pairs)
|
|
232
|
+
return config(resolver, target_name=name, config_used=list(resolver.optional_config))
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def when_in(name=None, **key_value_group_pairs: Collection[Any]) -> "config":
|
|
236
|
+
"""Yields a decorator that resolves the function if all of the
|
|
237
|
+
values corresponding to the config keys are equal to one of items in the list of values.
|
|
238
|
+
|
|
239
|
+
``@config.when_in(param=[value1, value2, ...])`` Will be included if the parameter is equal to one of the \
|
|
240
|
+
specified values.
|
|
241
|
+
|
|
242
|
+
:param key_value_group_pairs: pairs of key-value mappings where the value is a list of possible values
|
|
243
|
+
:return: a configuration decorator
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
resolver = ConfigResolver.when_in(**key_value_group_pairs)
|
|
247
|
+
return config(resolver, target_name=name, config_used=list(resolver.optional_config))
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
def when_not_in(**key_value_group_pairs: Collection[Any]) -> "config":
|
|
251
|
+
"""Yields a decorator that resolves the function only if none of the keys are in the list of values.
|
|
252
|
+
|
|
253
|
+
``@config.when_not_in(param=[value1, value2, ...])`` Will be included if the parameter is not equal to any of \
|
|
254
|
+
the specified values.
|
|
255
|
+
|
|
256
|
+
:param key_value_group_pairs: pairs of key-value mappings where the value is a list of possible values
|
|
257
|
+
:return: a configuration decorator
|
|
258
|
+
|
|
259
|
+
.. code-block:: python
|
|
260
|
+
|
|
261
|
+
@config.when_not_in(business_line=["mens","kids"], region=["uk"])
|
|
262
|
+
def LEAD_LOG_BASS_MODEL_TIMES_TREND(
|
|
263
|
+
TREND_BSTS_WOMENS_ACQUISITIONS: pd.Series,
|
|
264
|
+
LEAD_LOG_BASS_MODEL_SIGNUPS_NON_REFERRAL: pd.Series) -> pd.Series:
|
|
265
|
+
|
|
266
|
+
above will resolve for config has `{"business_line": "womens", "region": "us"}`,
|
|
267
|
+
but not for configs that have `{"business_line": "mens", "region": "us"}`,
|
|
268
|
+
`{"business_line": "kids", "region": "us"}`, or `{"region": "uk"}`.
|
|
269
|
+
|
|
270
|
+
.. seealso::
|
|
271
|
+
:ref:config.when_not
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
resolver = ConfigResolver.when_not_in(**key_value_group_pairs)
|
|
275
|
+
return config(resolver, config_used=list(resolver.optional_config))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class hamilton_exclude(base.NodeResolver):
|
|
279
|
+
"""Decorator class that excludes a function from the DAG.
|
|
280
|
+
|
|
281
|
+
The preferred way to hide functions from the Hamilton DAG is to prefix them with "_". However,
|
|
282
|
+
for the exceptional case, it can be useful for decorating helper functions without the need to prefix
|
|
283
|
+
them with "_" and use them either inside other nodes or in conjunction with ``step`` or ``apply_to``.
|
|
284
|
+
|
|
285
|
+
.. code-block:: python
|
|
286
|
+
|
|
287
|
+
@hamilton_exclude
|
|
288
|
+
def helper(...) -> ...:
|
|
289
|
+
'''This will not be part of the DAG'''
|
|
290
|
+
...
|
|
291
|
+
|
|
292
|
+
You may also want to use this decorator for excluding functions in legacy code that would raise
|
|
293
|
+
and error in Hamilton (for example missing type hints).
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
def __init__(self):
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
def resolve(self, *args, **kwargs) -> Callable | None:
|
|
300
|
+
"""Returning None defaults to not be included in the DAG.
|
|
301
|
+
|
|
302
|
+
:param fn: Function to resolve
|
|
303
|
+
:param config: DAG config
|
|
304
|
+
:return: None to not be included in the DAG
|
|
305
|
+
"""
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
def validate(self, fn):
|
|
309
|
+
"""Any function should work."""
|
|
310
|
+
pass
|
|
@@ -0,0 +1,202 @@
|
|
|
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 enum
|
|
19
|
+
import inspect
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from hamilton import settings
|
|
24
|
+
from hamilton.function_modifiers.base import (
|
|
25
|
+
DynamicResolver,
|
|
26
|
+
InvalidDecoratorException,
|
|
27
|
+
NodeTransformLifecycle,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ResolveAt(enum.Enum):
|
|
32
|
+
CONFIG_AVAILABLE = "config_available"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
VALID_PARAM_KINDS = [inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def extract_and_validate_params(fn: Callable) -> tuple[list[str], dict[str, Any]]:
|
|
39
|
+
"""Gets the parameters from a function, while validating that
|
|
40
|
+
the function has *only* named arguments.
|
|
41
|
+
|
|
42
|
+
:param fn: Function to extract parameters from
|
|
43
|
+
:return: List of parameter names
|
|
44
|
+
:raises InvalidDecoratorException: If the function has any non kwargs-friendly arguments
|
|
45
|
+
"""
|
|
46
|
+
invalid_params = []
|
|
47
|
+
required_params = []
|
|
48
|
+
optional_params = {}
|
|
49
|
+
sig = inspect.signature(fn)
|
|
50
|
+
for key, value in inspect.signature(fn).parameters.items():
|
|
51
|
+
if value.kind not in VALID_PARAM_KINDS:
|
|
52
|
+
invalid_params.append(key)
|
|
53
|
+
else:
|
|
54
|
+
if value.default is not value.empty:
|
|
55
|
+
optional_params[key] = value.default
|
|
56
|
+
else:
|
|
57
|
+
required_params.append(key)
|
|
58
|
+
if invalid_params:
|
|
59
|
+
raise InvalidDecoratorException(
|
|
60
|
+
f"Configuration-parsing functions can only except keyword-friendly arguments. "
|
|
61
|
+
f"Instead got signature: {sig}"
|
|
62
|
+
)
|
|
63
|
+
return required_params, optional_params
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class resolve(DynamicResolver):
|
|
67
|
+
"""Decorator class to delay evaluation of decorators until after the configuration is available.
|
|
68
|
+
Note: this is a power-user feature, and you have to enable power-user mode! To do so, you have
|
|
69
|
+
to add the configuration hamilton.enable_power_user_mode=True to the config you pass into the
|
|
70
|
+
driver.
|
|
71
|
+
|
|
72
|
+
If not, this will break when it tries to instantiate a DAG.
|
|
73
|
+
|
|
74
|
+
This is particularly useful when you don't know how you want your functions to resolve until
|
|
75
|
+
configuration time. Say, for example, we want to add two series, and we need to pass the set of
|
|
76
|
+
series to add as a configuration parameter, as we'll be changing it regularly. Without this,
|
|
77
|
+
you would have to have them as part of the same dataframe. E.G.
|
|
78
|
+
|
|
79
|
+
.. code-block:: python
|
|
80
|
+
|
|
81
|
+
@parameterize_values(
|
|
82
|
+
series_sum_1={"s1": "series_1", "s2": "series_2"},
|
|
83
|
+
series_sum_2={"s1": "series_3", "s2": "series_4"},
|
|
84
|
+
)
|
|
85
|
+
def summation(df: pd.DataFrame, s1: str, s2: str) -> pd.Series:
|
|
86
|
+
return df[s1] + df[s2]
|
|
87
|
+
|
|
88
|
+
Note that there are a lot of benefits to this code, but it is a workaround for the fact that
|
|
89
|
+
we cannot configure the dependencies. With the `@resolve` decorator, we can actually dynamically
|
|
90
|
+
set the shape of the DAG based on config:
|
|
91
|
+
|
|
92
|
+
.. code-block:: python
|
|
93
|
+
|
|
94
|
+
from hamilton.function_modifiers import resolve, ResolveAt
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@resolve(
|
|
98
|
+
when=ResolveAt.CONFIG_AVAILABLE,
|
|
99
|
+
decorate_with=lambda first_series_sum, second_series_sum: parameterize_sources(
|
|
100
|
+
series_sum_1={"s1": first_series_sum[0], "s2": second_series_sum[1]},
|
|
101
|
+
series_sum_2={"s1": second_series_sum[1], "s2": second_series_sum[2]},
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
def summation(s1: pd.Series, s2: pd.Series) -> pd.Series:
|
|
105
|
+
return s1 + s2
|
|
106
|
+
|
|
107
|
+
Note how this works:
|
|
108
|
+
|
|
109
|
+
1. The `decorate_with` argument is a function that gives you the decorator you want to apply.
|
|
110
|
+
Currently its "hamilton-esque" -- while we do not require it to be typed, you can use a separate
|
|
111
|
+
configuration-reoslver function (and include type information). This lambda function must return
|
|
112
|
+
a decorator.
|
|
113
|
+
|
|
114
|
+
2. The `when` argument is the point at which you want to resolve the decorator. Currently, we
|
|
115
|
+
only support `ResolveAt.CONFIG_AVAILABLE`, which means that the decorator will be resolved at compile
|
|
116
|
+
time, E.G. when the driver is instantiated.
|
|
117
|
+
|
|
118
|
+
3. This is then run and dynamically resolved.
|
|
119
|
+
|
|
120
|
+
This is powerful, but the code is uglier. It's meant to be used in some very specific cases,
|
|
121
|
+
E.G. When you want time-series data on a per-column basis (E.G. once per month), and don't want
|
|
122
|
+
that hardcoded. While it is possible to store this up in a JSON file and run parameterization on
|
|
123
|
+
the loaded result as a global variable, it is much cleaner to pass it through the DAG, which
|
|
124
|
+
is why we support it. However, since the code goes against one of Hamilton's primary tenets (
|
|
125
|
+
that all code is highly readable), we require that you enable power_user_mode.
|
|
126
|
+
|
|
127
|
+
We *highly* recommend that you put all functions decorated with this in their own module,
|
|
128
|
+
keeping it separate from the rest of your functions. This way, you can import/build DAGs from
|
|
129
|
+
the rest of your functions without turning on power-user mode.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(self, *, when: ResolveAt, decorate_with: Callable[..., NodeTransformLifecycle]):
|
|
133
|
+
"""Initializes a delayed decorator that gets called at some specific resolution time.
|
|
134
|
+
|
|
135
|
+
:param decorate_with: Function that takes required and optional parameters/returns a decorator.
|
|
136
|
+
:param when: When to resolve the decorator. Currently only supports `ResolveAt.CONFIG_AVAILABLE`.
|
|
137
|
+
"""
|
|
138
|
+
if when != ResolveAt.CONFIG_AVAILABLE:
|
|
139
|
+
raise ValueError("Dynamic functions must be configured at config time!")
|
|
140
|
+
self.until = when
|
|
141
|
+
self.decorate_with = decorate_with
|
|
142
|
+
self._required_config, self._optional_config = extract_and_validate_params(decorate_with)
|
|
143
|
+
|
|
144
|
+
def required_config(self) -> list[str] | None:
|
|
145
|
+
return self._required_config
|
|
146
|
+
|
|
147
|
+
def optional_config(self) -> dict[str, Any] | None:
|
|
148
|
+
return self._optional_config
|
|
149
|
+
|
|
150
|
+
def resolve(self, config: dict[str, Any], fn: Callable) -> NodeTransformLifecycle:
|
|
151
|
+
if not config[settings.ENABLE_POWER_USER_MODE]:
|
|
152
|
+
raise InvalidDecoratorException(
|
|
153
|
+
"Dynamic functions are only allowed in power user mode!"
|
|
154
|
+
"Why? This is occasionally needed to enable highly flexible "
|
|
155
|
+
"dataflows, but it can compromise readability if you're not "
|
|
156
|
+
"careful! To enable power user mode, pass in the configuration "
|
|
157
|
+
f"parameter {settings.ENABLE_POWER_USER_MODE}=True to your driver."
|
|
158
|
+
)
|
|
159
|
+
missing_configs = []
|
|
160
|
+
for item in self.required_config():
|
|
161
|
+
if item not in config:
|
|
162
|
+
missing_configs.append(item)
|
|
163
|
+
if missing_configs:
|
|
164
|
+
raise InvalidDecoratorException(
|
|
165
|
+
f"Config items: {missing_configs} declared "
|
|
166
|
+
f"but not provided for decorator: {self} on fn: {fn}"
|
|
167
|
+
)
|
|
168
|
+
kwargs = {key: config[key] for key in self._required_config}
|
|
169
|
+
for key in self._optional_config:
|
|
170
|
+
if key in config:
|
|
171
|
+
kwargs[key] = config[key]
|
|
172
|
+
return self.decorate_with(**kwargs)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class resolve_from_config(resolve):
|
|
176
|
+
"""Decorator class to delay evaluation of decorators until after the configuration is available.
|
|
177
|
+
Note: this is a power-user feature, and you have to enable power-user mode! To do so, you have
|
|
178
|
+
to add the configuration hamilton.enable_power_user_mode=True to the config you pass into the
|
|
179
|
+
driver.
|
|
180
|
+
|
|
181
|
+
This is a convenience decorator that is a subclass of `resolve` and passes
|
|
182
|
+
`ResolveAt.CONFIG_AVAILABLE` to the `when` argument such that the decorator is resoled at
|
|
183
|
+
compile time, E.G. when the driver is instantiated.
|
|
184
|
+
|
|
185
|
+
.. code-block:: python
|
|
186
|
+
|
|
187
|
+
from hamilton.function_modifiers import resolve, ResolveAt
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@resolve_from_config(
|
|
191
|
+
decorate_with=lambda first_series_sum, second_series_sum: parameterize_sources(
|
|
192
|
+
series_sum_1={"s1": first_series_sum[0], "s2": second_series_sum[1]},
|
|
193
|
+
series_sum_2={"s1": second_series_sum[1], "s2": second_series_sum[2]},
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
def summation(s1: pd.Series, s2: pd.Series) -> pd.Series:
|
|
197
|
+
return s1 + s2
|
|
198
|
+
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def __init__(self, *, decorate_with: Callable[..., NodeTransformLifecycle]):
|
|
202
|
+
super().__init__(when=ResolveAt.CONFIG_AVAILABLE, decorate_with=decorate_with)
|