apache-hamilton 1.90.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. apache_hamilton-1.90.0.dev0.dist-info/METADATA +407 -0
  2. apache_hamilton-1.90.0.dev0.dist-info/RECORD +151 -0
  3. apache_hamilton-1.90.0.dev0.dist-info/WHEEL +4 -0
  4. apache_hamilton-1.90.0.dev0.dist-info/entry_points.txt +9 -0
  5. apache_hamilton-1.90.0.dev0.dist-info/licenses/DISCLAIMER +10 -0
  6. apache_hamilton-1.90.0.dev0.dist-info/licenses/LICENSE +228 -0
  7. apache_hamilton-1.90.0.dev0.dist-info/licenses/NOTICE +5 -0
  8. hamilton/__init__.py +24 -0
  9. hamilton/ad_hoc_utils.py +132 -0
  10. hamilton/async_driver.py +465 -0
  11. hamilton/base.py +466 -0
  12. hamilton/caching/__init__.py +16 -0
  13. hamilton/caching/adapter.py +1475 -0
  14. hamilton/caching/cache_key.py +70 -0
  15. hamilton/caching/fingerprinting.py +287 -0
  16. hamilton/caching/stores/__init__.py +16 -0
  17. hamilton/caching/stores/base.py +242 -0
  18. hamilton/caching/stores/file.py +140 -0
  19. hamilton/caching/stores/memory.py +297 -0
  20. hamilton/caching/stores/sqlite.py +282 -0
  21. hamilton/caching/stores/utils.py +40 -0
  22. hamilton/cli/__init__.py +16 -0
  23. hamilton/cli/__main__.py +328 -0
  24. hamilton/cli/commands.py +126 -0
  25. hamilton/cli/logic.py +338 -0
  26. hamilton/common/__init__.py +76 -0
  27. hamilton/contrib/__init__.py +41 -0
  28. hamilton/data_quality/__init__.py +16 -0
  29. hamilton/data_quality/base.py +198 -0
  30. hamilton/data_quality/default_validators.py +560 -0
  31. hamilton/data_quality/pandera_validators.py +121 -0
  32. hamilton/dataflows/__init__.py +726 -0
  33. hamilton/dataflows/template/README.md +25 -0
  34. hamilton/dataflows/template/__init__.py +54 -0
  35. hamilton/dataflows/template/author.md +28 -0
  36. hamilton/dataflows/template/requirements.txt +0 -0
  37. hamilton/dataflows/template/tags.json +7 -0
  38. hamilton/dataflows/template/valid_configs.jsonl +1 -0
  39. hamilton/dev_utils/__init__.py +16 -0
  40. hamilton/dev_utils/deprecation.py +204 -0
  41. hamilton/driver.py +2112 -0
  42. hamilton/execution/__init__.py +16 -0
  43. hamilton/execution/debugging_utils.py +56 -0
  44. hamilton/execution/executors.py +502 -0
  45. hamilton/execution/graph_functions.py +421 -0
  46. hamilton/execution/grouping.py +430 -0
  47. hamilton/execution/state.py +539 -0
  48. hamilton/experimental/__init__.py +27 -0
  49. hamilton/experimental/databackend.py +61 -0
  50. hamilton/experimental/decorators/__init__.py +16 -0
  51. hamilton/experimental/decorators/parameterize_frame.py +233 -0
  52. hamilton/experimental/h_async.py +29 -0
  53. hamilton/experimental/h_cache.py +413 -0
  54. hamilton/experimental/h_dask.py +28 -0
  55. hamilton/experimental/h_databackends.py +174 -0
  56. hamilton/experimental/h_ray.py +28 -0
  57. hamilton/experimental/h_spark.py +32 -0
  58. hamilton/function_modifiers/README +40 -0
  59. hamilton/function_modifiers/__init__.py +121 -0
  60. hamilton/function_modifiers/adapters.py +900 -0
  61. hamilton/function_modifiers/base.py +859 -0
  62. hamilton/function_modifiers/configuration.py +310 -0
  63. hamilton/function_modifiers/delayed.py +202 -0
  64. hamilton/function_modifiers/dependencies.py +246 -0
  65. hamilton/function_modifiers/expanders.py +1230 -0
  66. hamilton/function_modifiers/macros.py +1634 -0
  67. hamilton/function_modifiers/metadata.py +434 -0
  68. hamilton/function_modifiers/recursive.py +908 -0
  69. hamilton/function_modifiers/validation.py +289 -0
  70. hamilton/function_modifiers_base.py +31 -0
  71. hamilton/graph.py +1153 -0
  72. hamilton/graph_types.py +264 -0
  73. hamilton/graph_utils.py +41 -0
  74. hamilton/htypes.py +450 -0
  75. hamilton/io/__init__.py +32 -0
  76. hamilton/io/data_adapters.py +216 -0
  77. hamilton/io/default_data_loaders.py +224 -0
  78. hamilton/io/materialization.py +500 -0
  79. hamilton/io/utils.py +158 -0
  80. hamilton/lifecycle/__init__.py +67 -0
  81. hamilton/lifecycle/api.py +833 -0
  82. hamilton/lifecycle/base.py +1130 -0
  83. hamilton/lifecycle/default.py +802 -0
  84. hamilton/log_setup.py +47 -0
  85. hamilton/models.py +92 -0
  86. hamilton/node.py +449 -0
  87. hamilton/plugins/README.md +48 -0
  88. hamilton/plugins/__init__.py +16 -0
  89. hamilton/plugins/dask_extensions.py +47 -0
  90. hamilton/plugins/dlt_extensions.py +161 -0
  91. hamilton/plugins/geopandas_extensions.py +49 -0
  92. hamilton/plugins/h_dask.py +331 -0
  93. hamilton/plugins/h_ddog.py +522 -0
  94. hamilton/plugins/h_diskcache.py +163 -0
  95. hamilton/plugins/h_experiments/__init__.py +22 -0
  96. hamilton/plugins/h_experiments/__main__.py +62 -0
  97. hamilton/plugins/h_experiments/cache.py +39 -0
  98. hamilton/plugins/h_experiments/data_model.py +68 -0
  99. hamilton/plugins/h_experiments/hook.py +219 -0
  100. hamilton/plugins/h_experiments/server.py +375 -0
  101. hamilton/plugins/h_kedro.py +152 -0
  102. hamilton/plugins/h_logging.py +454 -0
  103. hamilton/plugins/h_mcp/__init__.py +28 -0
  104. hamilton/plugins/h_mcp/__main__.py +33 -0
  105. hamilton/plugins/h_mcp/_helpers.py +129 -0
  106. hamilton/plugins/h_mcp/_templates.py +417 -0
  107. hamilton/plugins/h_mcp/server.py +328 -0
  108. hamilton/plugins/h_mlflow.py +335 -0
  109. hamilton/plugins/h_narwhals.py +134 -0
  110. hamilton/plugins/h_openlineage.py +400 -0
  111. hamilton/plugins/h_opentelemetry.py +167 -0
  112. hamilton/plugins/h_pandas.py +257 -0
  113. hamilton/plugins/h_pandera.py +117 -0
  114. hamilton/plugins/h_polars.py +304 -0
  115. hamilton/plugins/h_polars_lazyframe.py +282 -0
  116. hamilton/plugins/h_pyarrow.py +56 -0
  117. hamilton/plugins/h_pydantic.py +127 -0
  118. hamilton/plugins/h_ray.py +242 -0
  119. hamilton/plugins/h_rich.py +142 -0
  120. hamilton/plugins/h_schema.py +493 -0
  121. hamilton/plugins/h_slack.py +100 -0
  122. hamilton/plugins/h_spark.py +1380 -0
  123. hamilton/plugins/h_threadpool.py +125 -0
  124. hamilton/plugins/h_tqdm.py +122 -0
  125. hamilton/plugins/h_vaex.py +129 -0
  126. hamilton/plugins/huggingface_extensions.py +236 -0
  127. hamilton/plugins/ibis_extensions.py +93 -0
  128. hamilton/plugins/jupyter_magic.py +622 -0
  129. hamilton/plugins/kedro_extensions.py +117 -0
  130. hamilton/plugins/lightgbm_extensions.py +99 -0
  131. hamilton/plugins/matplotlib_extensions.py +108 -0
  132. hamilton/plugins/mlflow_extensions.py +216 -0
  133. hamilton/plugins/numpy_extensions.py +105 -0
  134. hamilton/plugins/pandas_extensions.py +1763 -0
  135. hamilton/plugins/plotly_extensions.py +150 -0
  136. hamilton/plugins/polars_extensions.py +71 -0
  137. hamilton/plugins/polars_implementations.py +25 -0
  138. hamilton/plugins/polars_lazyframe_extensions.py +302 -0
  139. hamilton/plugins/polars_post_1_0_0_extensions.py +877 -0
  140. hamilton/plugins/polars_pre_1_0_0_extension.py +836 -0
  141. hamilton/plugins/pydantic_extensions.py +98 -0
  142. hamilton/plugins/pyspark_pandas_extensions.py +47 -0
  143. hamilton/plugins/sklearn_plot_extensions.py +129 -0
  144. hamilton/plugins/spark_extensions.py +105 -0
  145. hamilton/plugins/vaex_extensions.py +51 -0
  146. hamilton/plugins/xgboost_extensions.py +91 -0
  147. hamilton/plugins/yaml_extensions.py +89 -0
  148. hamilton/registry.py +254 -0
  149. hamilton/settings.py +18 -0
  150. hamilton/telemetry.py +50 -0
  151. hamilton/version.py +18 -0
@@ -0,0 +1,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)