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,282 @@
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 types import ModuleType
20
+ from typing import Any, get_type_hints
21
+
22
+ import polars as pl
23
+
24
+ from hamilton import base, node, registry
25
+ from hamilton.function_modifiers.expanders import extract_columns
26
+ from hamilton.function_modifiers.recursive import (
27
+ _default_inject_parameter,
28
+ subdag,
29
+ with_columns_base,
30
+ )
31
+ from hamilton.plugins.polars_lazyframe_extensions import DATAFRAME_TYPE
32
+
33
+
34
+ class PolarsLazyFrameResult(base.ResultMixin):
35
+ """A ResultBuilder that produces a polars dataframe.
36
+
37
+ Use this when you want to create a polars dataframe from the outputs. Caveat: you need to ensure that the length
38
+ of the outputs is the same, otherwise you will get an error; mixed outputs aren't that well handled.
39
+
40
+ To use:
41
+
42
+ .. code-block:: python
43
+
44
+ from hamilton import base, driver
45
+ from hamilton.plugins import polars_extensions
46
+
47
+ polars_builder = polars_extensions.PolarsLazyFrameResult()
48
+ adapter = base.SimplePythonGraphAdapter(polars_builder)
49
+ dr = driver.Driver(config, *modules, adapter=adapter)
50
+ df = dr.execute([...], inputs=...) # returns polars dataframe
51
+
52
+ Note: this is just a first attempt at something for Polars. Think it should handle more? Come chat/open a PR!
53
+ """
54
+
55
+ def build_result(self, **outputs: dict[str, pl.Series | pl.LazyFrame | Any]) -> pl.LazyFrame:
56
+ """This is the method that Hamilton will call to build the final result. It will pass in the results
57
+ of the requested outputs that you passed in to the execute() method.
58
+
59
+ Note: this function could do smarter things; looking for contributions here!
60
+
61
+ :param outputs: The results of the requested outputs.
62
+ :return: a polars DataFrame.
63
+ """
64
+ if len(outputs) == 1:
65
+ (value,) = outputs.values() # this works because it's length 1.
66
+ if isinstance(value, pl.LazyFrame): # it's a lazyframe
67
+ return value
68
+ return pl.LazyFrame(outputs)
69
+
70
+ def output_type(self) -> type:
71
+ return pl.LazyFrame
72
+
73
+
74
+ class with_columns(with_columns_base):
75
+ """Initializes a with_columns decorator for polars.
76
+
77
+ This allows you to efficiently run groups of map operations on a dataframe. We support
78
+ both eager and lazy mode in polars. For lazy execution, use pl.LazyFrame and the subsequent
79
+ operations should be typed as pl.Expr. See examples/polars/with_columns for a practical
80
+ implementation in both variations.
81
+
82
+ The lazy execution would be:
83
+
84
+ .. code-block:: python
85
+
86
+ # my_module.py
87
+ def a_b_average(a: pl.Expr, b: pl.Expr) -> pl.Expr:
88
+ return (a + b) / 2
89
+
90
+
91
+ .. code-block:: python
92
+
93
+ # with_columns_module.py
94
+ def a_plus_b(a: pl.Expr, b: pl.Expr) -> pl.Expr:
95
+ return a + b
96
+
97
+
98
+ # the with_columns call
99
+ @with_columns(
100
+ *[my_module], # Load from any module
101
+ *[a_plus_b], # or list operations directly
102
+ columns_to_pass=["a_from_df", "b_from_df"], # The columns to pass from the dataframe to
103
+ # the subdag
104
+ select=["a_plus_b", "a_b_average"], # The columns to append to the dataframe
105
+ )
106
+ def final_df(initial_df: pl.LazyFrame) -> pl.LazyFrame:
107
+ # process, or just return unprocessed
108
+ ...
109
+
110
+ Note that the operation is "append", meaning that the columns that are selected are appended
111
+ onto the dataframe.
112
+
113
+ If the function takes multiple dataframes, the dataframe input to process will always be
114
+ the first argument. This will be passed to the subdag, transformed, and passed back to the function.
115
+ This follows the hamilton rule of reference by parameter name. To demonstarte this, in the code
116
+ above, the dataframe that is passed to the subdag is `initial_df`. That is transformed
117
+ by the subdag, and then returned as the final dataframe.
118
+
119
+ You can read it as:
120
+
121
+ "final_df is a function that transforms the upstream dataframe initial_df, running the transformations
122
+ from my_module. It starts with the columns a_from_df and b_from_df, and then adds the columns
123
+ a, b, and a_plus_b to the dataframe. It then returns the dataframe, and does some processing on it."
124
+
125
+ In case you need more flexibility you can alternatively use ``on_input``, for example,
126
+
127
+ .. code-block:: python
128
+
129
+ # with_columns_module.py
130
+ def a_from_df() -> pl.Expr:
131
+ return pl.col(a).alias("a") / 100
132
+
133
+ def b_from_df() -> pd.Expr:
134
+ return pl.col(a).alias("b") / 100
135
+
136
+
137
+ # the with_columns call
138
+ @with_columns(
139
+ *[my_module],
140
+ on_input="initial_df",
141
+ select=["a_from_df", "b_from_df", "a_plus_b", "a_b_average"],
142
+ )
143
+ def final_df(initial_df: pl.LazyFrame) -> pl.LazyFrame:
144
+ # process, or just return unprocessed
145
+ ...
146
+
147
+ the above would output a dataframe where the two columns ``a`` and ``b`` get
148
+ overwritten.
149
+ """
150
+
151
+ def __init__(
152
+ self,
153
+ *load_from: Callable | ModuleType,
154
+ columns_to_pass: list[str] = None,
155
+ pass_dataframe_as: str = None,
156
+ on_input: str = None,
157
+ select: list[str] = None,
158
+ namespace: str = None,
159
+ config_required: list[str] = None,
160
+ ):
161
+ """Instantiates a ``@with_columns`` decorator.
162
+
163
+ :param load_from: The functions or modules that will be used to generate the group of map operations.
164
+ :param columns_to_pass: The initial schema of the dataframe. This is used to determine which
165
+ upstream inputs should be taken from the dataframe, and which shouldn't. Note that, if this is
166
+ left empty (and external_inputs is as well), we will assume that all dependencies come
167
+ from the dataframe. This cannot be used in conjunction with on_input.
168
+ :param on_input: The name of the dataframe that we're modifying, as known to the subdag.
169
+ If you pass this in, you are responsible for extracting columns out. If not provided, you have
170
+ to pass columns_to_pass in, and we will extract the columns out on the first parameter for you.
171
+ :param select: The end nodes that represent columns to be appended to the original dataframe
172
+ via with_columns. Existing columns will be overridden. The selected nodes need to have the
173
+ corresponding column type, in this case pl.Expr, to be appended to the original dataframe.
174
+ :param namespace: The namespace of the nodes, so they don't clash with the global namespace
175
+ and so this can be reused. If its left out, there will be no namespace (in which case you'll want
176
+ to be careful about repeating it/reusing the nodes in other parts of the DAG.)
177
+ :param config_required: the list of config keys that are required to resolve any functions. Pass in None\
178
+ if you want the functions/modules to have access to all possible config.
179
+ """
180
+
181
+ if pass_dataframe_as is not None:
182
+ raise NotImplementedError(
183
+ "We currently do not support pass_dataframe_as for pandas. Please reach out if you need this "
184
+ "functionality."
185
+ )
186
+
187
+ super().__init__(
188
+ *load_from,
189
+ columns_to_pass=columns_to_pass,
190
+ on_input=on_input,
191
+ select=select,
192
+ namespace=namespace,
193
+ config_required=config_required,
194
+ dataframe_type=DATAFRAME_TYPE,
195
+ )
196
+
197
+ def _create_column_nodes(
198
+ self, fn: Callable, inject_parameter: str, params: dict[str, type[type]]
199
+ ) -> list[node.Node]:
200
+ output_type = params[inject_parameter]
201
+
202
+ def temp_fn(**kwargs) -> Any:
203
+ return kwargs[inject_parameter]
204
+
205
+ # We recreate the df node to use extract columns
206
+ temp_node = node.Node(
207
+ name=inject_parameter,
208
+ typ=output_type,
209
+ callabl=temp_fn,
210
+ input_types={inject_parameter: output_type},
211
+ )
212
+
213
+ extract_columns_decorator = extract_columns(*self.initial_schema)
214
+
215
+ out_nodes = extract_columns_decorator.transform_node(temp_node, config={}, fn=temp_fn)
216
+ return out_nodes[1:]
217
+
218
+ def get_initial_nodes(
219
+ self, fn: Callable, params: dict[str, type[type]]
220
+ ) -> tuple[str, Collection[node.Node]]:
221
+ """Selects the correct dataframe and optionally extracts out columns."""
222
+ inject_parameter = _default_inject_parameter(fn=fn, target_dataframe=self.target_dataframe)
223
+
224
+ with_columns_base.validate_dataframe(
225
+ fn=fn,
226
+ inject_parameter=inject_parameter,
227
+ params=params,
228
+ required_type=self.dataframe_type,
229
+ )
230
+
231
+ initial_nodes = (
232
+ []
233
+ if self.target_dataframe is not None
234
+ else self._create_column_nodes(fn=fn, inject_parameter=inject_parameter, params=params)
235
+ )
236
+
237
+ return inject_parameter, initial_nodes
238
+
239
+ def get_subdag_nodes(self, fn: Callable, config: dict[str, Any]) -> Collection[node.Node]:
240
+ return subdag.collect_nodes(config, self.subdag_functions)
241
+
242
+ def chain_subdag_nodes(
243
+ self, fn: Callable, inject_parameter: str, generated_nodes: Collection[node.Node]
244
+ ) -> node.Node:
245
+ "Node that adds to / overrides columns for the original dataframe based on selected output."
246
+
247
+ if self.select is None:
248
+ self.select = [
249
+ sink_node.name
250
+ for sink_node in generated_nodes
251
+ if sink_node.type == registry.get_column_type_from_df_type(self.dataframe_type)
252
+ ]
253
+
254
+ def new_callable(**kwargs) -> Any:
255
+ df = kwargs[inject_parameter]
256
+ columns_to_append = {}
257
+ for column in self.select:
258
+ columns_to_append[column] = kwargs[column]
259
+
260
+ return df.with_columns(**columns_to_append)
261
+
262
+ column_type = registry.get_column_type_from_df_type(self.dataframe_type)
263
+ input_map = {column: column_type for column in self.select}
264
+ input_map[inject_parameter] = self.dataframe_type
265
+ merge_node = node.Node(
266
+ name="_append",
267
+ typ=self.dataframe_type,
268
+ callabl=new_callable,
269
+ input_types=input_map,
270
+ )
271
+ output_nodes = generated_nodes + [merge_node]
272
+ return output_nodes, merge_node.name
273
+
274
+ def validate(self, fn: Callable):
275
+ inject_parameter = _default_inject_parameter(fn=fn, target_dataframe=self.target_dataframe)
276
+ params = get_type_hints(fn)
277
+ with_columns_base.validate_dataframe(
278
+ fn=fn,
279
+ inject_parameter=inject_parameter,
280
+ params=params,
281
+ required_type=self.dataframe_type,
282
+ )
@@ -0,0 +1,56 @@
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 typing import Any
19
+
20
+ import pyarrow
21
+ from pyarrow.interchange import from_dataframe
22
+
23
+ from hamilton.lifecycle.api import ResultBuilder
24
+
25
+
26
+ class PyarrowTableResult(ResultBuilder):
27
+ """Add this result builder to a materializer's `combine` statement to convert your dataframe
28
+ object to a pyarrow representation and make it compatible with pyarrow DataSavers.
29
+
30
+ It implicitly support input_type == Any, but it expects dataframe objects implementing
31
+ the dataframe interchange protocol: ref: https://arrow.apache.org/docs/python/interchange_protocol.html
32
+ for example:
33
+ - pandas
34
+ - polars
35
+ - dask
36
+ - vaex
37
+ - ibis
38
+ - duckdb results
39
+ """
40
+
41
+ def output_type(self) -> type:
42
+ return pyarrow.Table
43
+
44
+ def build_result(self, **outputs: Any) -> Any:
45
+ """This function converts objects implementing the `__dataframe__` protocol to
46
+ a pyarrow table. It doesn't support receiving multiple outputs because it can't
47
+ handle any joining logic.
48
+
49
+ ref: https://arrow.apache.org/docs/python/interchange_protocol.html
50
+ """
51
+ if len(outputs) != 1:
52
+ raise AssertionError(
53
+ "PyarrowTableResult can only receive 1 output, i.e., only one item in `to.SAVER(dependencies=[])`"
54
+ f"It received {len(outputs)} outputs."
55
+ )
56
+ return from_dataframe(next(iter(outputs.values())))
@@ -0,0 +1,127 @@
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
+
19
+ from pydantic import BaseModel
20
+
21
+ from hamilton import node
22
+ from hamilton.data_quality import base as dq_base
23
+ from hamilton.function_modifiers import InvalidDecoratorException
24
+ from hamilton.function_modifiers import base as fm_base
25
+ from hamilton.function_modifiers import check_output as base_check_output
26
+ from hamilton.function_modifiers.validation import BaseDataValidationDecorator
27
+ from hamilton.htypes import custom_subclass_check
28
+
29
+
30
+ class check_output(BaseDataValidationDecorator):
31
+ def __init__(
32
+ self,
33
+ importance: str = dq_base.DataValidationLevel.WARN.value,
34
+ target: fm_base.TargetType = None,
35
+ ):
36
+ """Specific output-checker for pydantic models (requires ``pydantic>=2.0``).
37
+ This decorator utilizes the output type of the function, which can be any subclass of pydantic.BaseModel.
38
+ The function output must be declared with a type hint.
39
+
40
+ :param model: The pydantic model to use for validation. If this is not provided, then the output type of the function is used.
41
+ :param importance: Importance level (either "warn" or "fail") -- see documentation for check_output for more details.
42
+ :param target: The target of the decorator -- see documentation for check_output for more details.
43
+
44
+ Here is an example of how to use this decorator with a function that returns a pydantic model:
45
+
46
+ .. code-block:: python
47
+
48
+ from hamilton.plugins import h_pydantic
49
+ from pydantic import BaseModel
50
+
51
+ class MyModel(BaseModel):
52
+ a: int
53
+ b: float
54
+ c: str
55
+
56
+ @h_pydantic.check_output()
57
+ def foo() -> MyModel:
58
+ return MyModel(a=1, b=2.0, c="hello")
59
+
60
+ Alternatively, you can return a dictionary from the function (type checkers will probably
61
+ complain about this):
62
+
63
+ .. code-block:: python
64
+
65
+ from hamilton.plugins import h_pydantic
66
+ from pydantic import BaseModel
67
+
68
+ class MyModel(BaseModel):
69
+ a: int
70
+ b: float
71
+ c: str
72
+
73
+ @h_pydantic.check_output()
74
+ def foo() -> MyModel:
75
+ return {"a": 1, "b": 2.0, "c": "hello"}
76
+
77
+ You can also use pydantic validation through ``function_modifiers.check_output`` by
78
+ providing the model as an argument:
79
+
80
+ .. code-block:: python
81
+
82
+ from typing import Any
83
+
84
+ from hamilton import function_modifiers
85
+ from pydantic import BaseModel
86
+
87
+ class MyModel(BaseModel):
88
+ a: int
89
+ b: float
90
+ c: str
91
+
92
+ @function_modifiers.check_output(model=MyModel)
93
+ def foo() -> dict[str, Any]:
94
+ return {"a": 1, "b": 2.0, "c": "hello"}
95
+
96
+ Note, that because we do not (yet) support modification of the output, the validation is
97
+ performed in strict mode, meaning that no data coercion is performed. For example, the
98
+ following function will *fail* validation:
99
+
100
+ .. code-block:: python
101
+
102
+ from hamilton.plugins import h_pydantic
103
+ from pydantic import BaseModel
104
+
105
+ class MyModel(BaseModel):
106
+ a: int # Defined as an int
107
+
108
+ @h_pydantic.check_output() # This will fail validation!
109
+ def foo() -> MyModel:
110
+ return MyModel(a="1") # Assigned as a string
111
+
112
+ For more information about strict mode see the pydantic docs: https://docs.pydantic.dev/latest/concepts/strict_mode/
113
+
114
+ """
115
+ super(check_output, self).__init__(target)
116
+ self.importance = importance
117
+ self.target = target
118
+
119
+ def get_validators(self, node_to_validate: node.Node) -> list[dq_base.DataValidator]:
120
+ output_type = node_to_validate.type
121
+ if not custom_subclass_check(output_type, BaseModel):
122
+ raise InvalidDecoratorException(
123
+ f"Output of function {node_to_validate.name} must be a Pydantic model"
124
+ )
125
+ return base_check_output(
126
+ importance=self.importance, model=output_type, target_=self.target
127
+ ).get_validators(node_to_validate)