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,560 @@
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
+ import numbers
20
+ from collections.abc import Iterable
21
+ from typing import Any
22
+
23
+ import numpy as np
24
+ import pandas as pd
25
+
26
+ from hamilton.data_quality import base
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class DataInRangeValidatorPandasSeries(base.BaseDefaultValidator):
32
+ def __init__(self, range: tuple[float, float], importance: str):
33
+ """Data validator that tells if data is in a range. This applies to primitives (ints, floats).
34
+
35
+ :param range: Inclusive range of parameters
36
+ """
37
+ super(DataInRangeValidatorPandasSeries, self).__init__(importance=importance)
38
+ self.range = range
39
+
40
+ @classmethod
41
+ def arg(cls) -> str:
42
+ return "range"
43
+
44
+ @classmethod
45
+ def applies_to(cls, datatype: type[type]) -> bool:
46
+ return issubclass(datatype, pd.Series) # TODO -- handle dataframes?
47
+
48
+ def description(self) -> str:
49
+ return f"Validates that the datapoint falls within the range ({self.range[0]}, {self.range[1]})"
50
+
51
+ def validate(self, data: pd.Series) -> base.ValidationResult:
52
+ min_, max_ = self.range
53
+ between = data.between(min_, max_, inclusive="both")
54
+ counts = between.value_counts().to_dict()
55
+ in_range = counts.get(True, 0)
56
+ out_range = counts.get(False, 0)
57
+ passes = out_range == 0
58
+ message = (
59
+ f"Series contains {in_range} values in range ({min_},{max_}), and {out_range} outside."
60
+ )
61
+ return base.ValidationResult(
62
+ passes=passes,
63
+ message=message,
64
+ diagnostics={
65
+ "range": self.range,
66
+ "in_range": in_range,
67
+ "out_range": out_range,
68
+ "data_size": len(data),
69
+ },
70
+ )
71
+
72
+
73
+ class DataInValuesValidatorPandasSeries(base.BaseDefaultValidator):
74
+ def __init__(self, values_in: Iterable[Any], importance: str):
75
+ """Data validator that tells if data is in a set of specified values within a pandas series.
76
+
77
+ Note: we will ignore empty/NA values here. If you do not want empty values, use the
78
+ MaxFractionNansValidatorPandasSeries or the AllowNaNsValidatorPandasSeries validator.
79
+
80
+ :param values_in: list of valid values
81
+ """
82
+ super(DataInValuesValidatorPandasSeries, self).__init__(importance=importance)
83
+ self.values = frozenset(values_in)
84
+
85
+ @classmethod
86
+ def arg(cls) -> str:
87
+ return "values_in"
88
+
89
+ @classmethod
90
+ def applies_to(cls, datatype: type[type]) -> bool:
91
+ return issubclass(datatype, pd.Series) # TODO -- handle dataframes?
92
+
93
+ def description(self) -> str:
94
+ return f"Validates that all data points are from a fixed set of values: ({self.values}), ignoring NA values."
95
+
96
+ def validate(self, data: pd.Series) -> base.ValidationResult:
97
+ na_values = data.isna()
98
+ valid_values = data.isin(self.values)
99
+ final_valid_values = valid_values | na_values # data is in valid values, or is NA
100
+ counts = final_valid_values.value_counts().to_dict()
101
+ correct_values_count = counts.get(True, 0)
102
+ incorrect_values_count = counts.get(False, 0)
103
+ incorrect_values = data[~valid_values].value_counts().to_dict()
104
+ passes = incorrect_values_count == 0
105
+ message = (
106
+ f"Series contains {correct_values_count} correct values,"
107
+ f" and {incorrect_values_count} incorrect values."
108
+ )
109
+ if not passes:
110
+ message += f" Valid values are: {self.values}."
111
+ return base.ValidationResult(
112
+ passes=passes,
113
+ message=message,
114
+ diagnostics={
115
+ "values": self.values,
116
+ "correct_values_count": correct_values_count,
117
+ "incorrect_values_count": incorrect_values_count,
118
+ "incorrect_values": incorrect_values,
119
+ "data_size": len(data),
120
+ },
121
+ )
122
+
123
+
124
+ class DataInRangeValidatorPrimitives(base.BaseDefaultValidator):
125
+ def __init__(self, range: tuple[numbers.Real, numbers.Real], importance: str):
126
+ """Data validator that tells if data is in a range. This applies to primitives (ints, floats).
127
+
128
+ :param range: Inclusive range of parameters
129
+ """
130
+ super(DataInRangeValidatorPrimitives, self).__init__(importance=importance)
131
+ self.range = range
132
+
133
+ @classmethod
134
+ def applies_to(cls, datatype: type[type]) -> bool:
135
+ return issubclass(datatype, numbers.Real)
136
+
137
+ def description(self) -> str:
138
+ return f"Validates that the datapoint falls within the range ({self.range[0]}, {self.range[1]})"
139
+
140
+ def validate(self, data: numbers.Real) -> base.ValidationResult:
141
+ min_, max_ = self.range
142
+ if hasattr(data, "dask"):
143
+ data = data.compute()
144
+ passes = min_ <= data <= max_
145
+ if passes:
146
+ message = f"Data point {data} falls within acceptable range: ({min_}, {max_})"
147
+ else:
148
+ message = f"Data point {data} does not fall within acceptable range: ({min_}, {max_})"
149
+ return base.ValidationResult(
150
+ passes=passes,
151
+ message=message,
152
+ diagnostics={"range": self.range, "value": data},
153
+ )
154
+
155
+ @classmethod
156
+ def arg(cls) -> str:
157
+ return "range"
158
+
159
+
160
+ class DataInValuesValidatorPrimitives(base.BaseDefaultValidator):
161
+ def __init__(self, values_in: Iterable[Any], importance: str):
162
+ """Data validator that tells if python primitive type data is in a set of specified values.
163
+
164
+ :param values_in: list of valid values
165
+ """
166
+ super(DataInValuesValidatorPrimitives, self).__init__(importance=importance)
167
+ self.values = frozenset(values_in)
168
+
169
+ @classmethod
170
+ def arg(cls) -> str:
171
+ return "values_in"
172
+
173
+ @classmethod
174
+ def applies_to(cls, datatype: type[type]) -> bool:
175
+ return issubclass(datatype, numbers.Real) or issubclass(
176
+ datatype, str
177
+ ) # TODO support list, dict and typing.* variants
178
+
179
+ def description(self) -> str:
180
+ return f"Validates that python values are from a fixed set of values: ({self.values})."
181
+
182
+ def validate(self, data: numbers.Real | str) -> base.ValidationResult:
183
+ if hasattr(data, "dask"):
184
+ data = data.compute()
185
+ is_valid_value = data in self.values
186
+ message = f"Primitive python value was valid is {is_valid_value}."
187
+ if not is_valid_value:
188
+ message += f" Correct possible values are {self.values}."
189
+ return base.ValidationResult(
190
+ passes=is_valid_value,
191
+ message=message,
192
+ diagnostics={
193
+ "values": self.values,
194
+ "was_correct": is_valid_value,
195
+ "incorrect_value": None if is_valid_value else data,
196
+ "data_size": 1,
197
+ },
198
+ )
199
+
200
+
201
+ class MaxFractionNansValidatorPandasSeries(base.BaseDefaultValidator):
202
+ def __init__(self, max_fraction_nans: float, importance: str):
203
+ super(MaxFractionNansValidatorPandasSeries, self).__init__(importance=importance)
204
+ MaxFractionNansValidatorPandasSeries._validate_max_fraction_nans(max_fraction_nans)
205
+ self.max_fraction_nans = max_fraction_nans
206
+
207
+ @staticmethod
208
+ def _to_percent(fraction: float):
209
+ return "{0:.2%}".format(fraction)
210
+
211
+ @classmethod
212
+ def applies_to(cls, datatype: type[type]) -> bool:
213
+ return issubclass(datatype, pd.Series)
214
+
215
+ def description(self) -> str:
216
+ return f"Validates that no more than {MaxFractionNansValidatorPandasSeries._to_percent(self.max_fraction_nans)} of the data is Nan."
217
+
218
+ def validate(self, data: pd.Series) -> base.ValidationResult:
219
+ total_length = len(data)
220
+ total_na = data.isna().sum()
221
+ fraction_na = total_na / total_length if total_length > 0 else 0
222
+ passes = fraction_na <= self.max_fraction_nans
223
+ return base.ValidationResult(
224
+ passes=passes,
225
+ message=f"Out of {total_length} items in the series, {total_na} of them are Nan, "
226
+ f"representing: {MaxFractionNansValidatorPandasSeries._to_percent(fraction_na)}. "
227
+ f"Max allowable Nans is: {MaxFractionNansValidatorPandasSeries._to_percent(self.max_fraction_nans)},"
228
+ f" so this {'passes' if passes else 'does not pass'}.",
229
+ diagnostics={
230
+ "total_nan": total_na,
231
+ "total_length": total_length,
232
+ "fraction_na": fraction_na,
233
+ "max_fraction_na": self.max_fraction_nans,
234
+ },
235
+ )
236
+
237
+ @classmethod
238
+ def arg(cls) -> str:
239
+ return "max_fraction_nans"
240
+
241
+ @staticmethod
242
+ def _validate_max_fraction_nans(max_fraction_nans: float):
243
+ if not (0 <= max_fraction_nans <= 1):
244
+ raise ValueError("Maximum fraction allowed to be nan must be in range [0,1]")
245
+
246
+
247
+ class AllowNaNsValidatorPandasSeries(MaxFractionNansValidatorPandasSeries):
248
+ def __init__(self, allow_nans: bool, importance: str):
249
+ if allow_nans:
250
+ raise ValueError(
251
+ f"Only allowed to block Nans with this validator."
252
+ f"Otherwise leave blank or specify the percentage of Nans using {MaxFractionNansValidatorPandasSeries.name()}"
253
+ )
254
+ super(AllowNaNsValidatorPandasSeries, self).__init__(
255
+ max_fraction_nans=0 if not allow_nans else 1.0, importance=importance
256
+ )
257
+
258
+ @classmethod
259
+ def arg(cls) -> str:
260
+ return "allow_nans"
261
+
262
+
263
+ class DataTypeValidatorPandasSeries(base.BaseDefaultValidator):
264
+ def __init__(self, data_type: type[type], importance: str):
265
+ """Constructor
266
+
267
+ :param data_type: the numpy data type to expect.
268
+ """
269
+ super(DataTypeValidatorPandasSeries, self).__init__(importance=importance)
270
+ DataTypeValidatorPandasSeries.datatype = data_type
271
+ self.datatype = data_type
272
+
273
+ @classmethod
274
+ def applies_to(cls, datatype: type[type]) -> bool:
275
+ return issubclass(datatype, pd.Series)
276
+
277
+ def description(self) -> str:
278
+ return f"Validates that the datatype of the pandas series is a subclass of: {self.datatype}"
279
+
280
+ def validate(self, data: pd.Series) -> base.ValidationResult:
281
+ dtype = data.dtype
282
+ if hasattr(dtype, "type"):
283
+ dtype = dtype.type
284
+ passes = np.issubdtype(dtype, self.datatype)
285
+ return base.ValidationResult(
286
+ passes=passes,
287
+ message=f"Requires subclass of datatype: {self.datatype}. Got datatype: {dtype}. This {'is' if passes else 'is not'} a match.",
288
+ diagnostics={"required_dtype": self.datatype, "actual_dtype": dtype},
289
+ )
290
+
291
+ @classmethod
292
+ def arg(cls) -> str:
293
+ return "data_type"
294
+
295
+
296
+ class DataTypeValidatorPrimitives(base.BaseDefaultValidator):
297
+ def __init__(self, data_type: type[type], importance: str):
298
+ """Constructor
299
+
300
+ :param data_type: the python data type to expect.
301
+ """
302
+ super(DataTypeValidatorPrimitives, self).__init__(importance=importance)
303
+ DataTypeValidatorPrimitives.datatype = data_type
304
+ self.datatype = data_type
305
+
306
+ @classmethod
307
+ def applies_to(cls, datatype: type[type]) -> bool:
308
+ return issubclass(datatype, numbers.Real) or datatype in (str, bool)
309
+
310
+ def description(self) -> str:
311
+ return f"Validates that the datatype of the pandas series is a subclass of: {self.datatype}"
312
+
313
+ def validate(
314
+ self, data: numbers.Real | str | bool | int | float | list | dict
315
+ ) -> base.ValidationResult:
316
+ if hasattr(data, "dask"):
317
+ data = data.compute()
318
+ passes = isinstance(data, self.datatype)
319
+ return base.ValidationResult(
320
+ passes=passes,
321
+ message=f"Requires data type: {self.datatype}. "
322
+ f"Got data type: {type(data)}. This {'is' if passes else 'is not'} a match.",
323
+ diagnostics={
324
+ "required_data_type": self.datatype,
325
+ "actual_data_type": type(data),
326
+ },
327
+ )
328
+
329
+ @classmethod
330
+ def arg(cls) -> str:
331
+ return "data_type"
332
+
333
+
334
+ class MaxStandardDevValidatorPandasSeries(base.BaseDefaultValidator):
335
+ def __init__(self, max_standard_dev: float, importance: str):
336
+ super(MaxStandardDevValidatorPandasSeries, self).__init__(importance)
337
+ self.max_standard_dev = max_standard_dev
338
+
339
+ @classmethod
340
+ def applies_to(cls, datatype: type[type]) -> bool:
341
+ return issubclass(datatype, pd.Series)
342
+
343
+ def description(self) -> str:
344
+ return f"Validates that the standard deviation of a pandas series is no greater than : {self.max_standard_dev}"
345
+
346
+ def validate(self, data: pd.Series) -> base.ValidationResult:
347
+ standard_dev = data.std()
348
+ passes = standard_dev <= self.max_standard_dev
349
+ return base.ValidationResult(
350
+ passes=passes,
351
+ message=f"Max allowable standard dev is: {self.max_standard_dev}. "
352
+ f"Dataset stddev is : {standard_dev}. "
353
+ f"This {'passes' if passes else 'does not pass'}.",
354
+ diagnostics={
355
+ "standard_dev": standard_dev,
356
+ "max_standard_dev": self.max_standard_dev,
357
+ },
358
+ )
359
+
360
+ @classmethod
361
+ def arg(cls) -> str:
362
+ return "max_standard_dev"
363
+
364
+
365
+ class MeanInRangeValidatorPandasSeries(base.BaseDefaultValidator):
366
+ def __init__(self, mean_in_range: tuple[float, float], importance: str):
367
+ super(MeanInRangeValidatorPandasSeries, self).__init__(importance)
368
+ self.mean_in_range = mean_in_range
369
+
370
+ @classmethod
371
+ def applies_to(cls, datatype: type[type]) -> bool:
372
+ return issubclass(datatype, pd.Series)
373
+
374
+ def description(self) -> str:
375
+ return f"Validates that a pandas series has mean in range [{self.mean_in_range[0]}, {self.mean_in_range[1]}]"
376
+
377
+ def validate(self, data: pd.Series) -> base.ValidationResult:
378
+ dataset_mean = data.mean()
379
+ min_, max_ = self.mean_in_range
380
+ passes = min_ <= dataset_mean <= max_
381
+ return base.ValidationResult(
382
+ passes=passes,
383
+ message=f"Dataset has mean: {dataset_mean}. This {'is ' if passes else 'is not '} "
384
+ f"in the required range: [{self.mean_in_range[0]}, {self.mean_in_range[1]}].",
385
+ diagnostics={
386
+ "dataset_mean": dataset_mean,
387
+ "mean_in_range": self.mean_in_range,
388
+ },
389
+ )
390
+
391
+ @classmethod
392
+ def arg(cls) -> str:
393
+ return "mean_in_range"
394
+
395
+
396
+ class AllowNoneValidator(base.BaseDefaultValidator):
397
+ def __init__(self, allow_none: bool, importance: str):
398
+ super(AllowNoneValidator, self).__init__(importance)
399
+ self.allow_none = allow_none
400
+
401
+ @classmethod
402
+ def applies_to(cls, datatype: type[type]) -> bool:
403
+ return True
404
+
405
+ def description(self) -> str:
406
+ if self.allow_none:
407
+ return "No-op validator."
408
+ return "Validates that an output ;is not None"
409
+
410
+ def validate(self, data: Any) -> base.ValidationResult:
411
+ passes = True
412
+ if not self.allow_none:
413
+ if data is None:
414
+ passes = False
415
+ return base.ValidationResult(
416
+ passes=passes,
417
+ message=(
418
+ f"Data is not allowed to be None, got {data}" if not passes else "Data is not None"
419
+ ),
420
+ diagnostics={}, # Nothing necessary here...
421
+ )
422
+
423
+ @classmethod
424
+ def arg(cls) -> str:
425
+ return "allow_none"
426
+
427
+
428
+ class StrContainsValidator(base.BaseDefaultValidator):
429
+ def __init__(self, contains: str | list[str], importance: str):
430
+ super(StrContainsValidator, self).__init__(importance)
431
+ if isinstance(contains, str):
432
+ self.contains = [contains]
433
+ else:
434
+ self.contains = contains
435
+
436
+ @classmethod
437
+ def applies_to(cls, datatype: type[type]) -> bool:
438
+ return datatype == str
439
+
440
+ def description(self) -> str:
441
+ return f"Validates that a string contains [{self.contains}] within it."
442
+
443
+ def validate(self, data: str) -> base.ValidationResult:
444
+ passes = all([c in data for c in self.contains])
445
+ return base.ValidationResult(
446
+ passes=passes,
447
+ message=(f"String did not contain {self.contains}" if not passes else "All good."),
448
+ diagnostics=(
449
+ {"contains": self.contains, "data": data if len(data) < 100 else data[:100]}
450
+ if not passes
451
+ else {}
452
+ ),
453
+ )
454
+
455
+ @classmethod
456
+ def arg(cls) -> str:
457
+ return "contains"
458
+
459
+
460
+ class StrDoesNotContainValidator(base.BaseDefaultValidator):
461
+ def __init__(self, does_not_contain: str | list[str], importance: str):
462
+ super(StrDoesNotContainValidator, self).__init__(importance)
463
+ if isinstance(does_not_contain, str):
464
+ self.does_not_contain = [does_not_contain]
465
+ else:
466
+ self.does_not_contain = does_not_contain
467
+
468
+ @classmethod
469
+ def applies_to(cls, datatype: type[type]) -> bool:
470
+ return datatype == str
471
+
472
+ def description(self) -> str:
473
+ return f"Validates that a string does not contain [{self.does_not_contain}] within it."
474
+
475
+ def validate(self, data: str) -> base.ValidationResult:
476
+ passes = all([c not in data for c in self.does_not_contain])
477
+ return base.ValidationResult(
478
+ passes=passes,
479
+ message=(f"String did contain {self.does_not_contain}" if not passes else "All good."),
480
+ diagnostics=(
481
+ {
482
+ "does_not_contain": self.does_not_contain,
483
+ "data": data if len(data) < 100 else data[:100],
484
+ }
485
+ if not passes
486
+ else {}
487
+ ),
488
+ )
489
+
490
+ @classmethod
491
+ def arg(cls) -> str:
492
+ return "does_not_contain"
493
+
494
+
495
+ AVAILABLE_DEFAULT_VALIDATORS = [
496
+ AllowNaNsValidatorPandasSeries,
497
+ DataInRangeValidatorPandasSeries,
498
+ DataInRangeValidatorPrimitives,
499
+ DataInValuesValidatorPandasSeries,
500
+ DataInValuesValidatorPrimitives,
501
+ DataTypeValidatorPandasSeries,
502
+ DataTypeValidatorPrimitives,
503
+ MaxFractionNansValidatorPandasSeries,
504
+ MaxStandardDevValidatorPandasSeries,
505
+ MeanInRangeValidatorPandasSeries,
506
+ AllowNoneValidator,
507
+ StrContainsValidator,
508
+ StrDoesNotContainValidator,
509
+ ]
510
+
511
+
512
+ def _append_pandera_to_default_validators():
513
+ """Utility method to append pandera validators as needed"""
514
+ try:
515
+ import pandera # noqa: F401
516
+ except ModuleNotFoundError:
517
+ logger.info(
518
+ "Cannot import pandera from pandera_validators. Run pip install sf-hamilton[pandera] if needed."
519
+ )
520
+ return
521
+ from hamilton.data_quality import pandera_validators
522
+
523
+ AVAILABLE_DEFAULT_VALIDATORS.extend(pandera_validators.PANDERA_VALIDATORS)
524
+
525
+
526
+ _append_pandera_to_default_validators()
527
+
528
+
529
+ def resolve_default_validators(
530
+ output_type: type[type],
531
+ importance: str,
532
+ available_validators: list[type[base.BaseDefaultValidator]] = None,
533
+ **default_validator_kwargs,
534
+ ) -> list[base.BaseDefaultValidator]:
535
+ """Resolves default validators given a set pof parameters and the type to which they apply.
536
+ Note that each (kwarg, type) combination should map to a validator
537
+ :param importance: importance level of the validator to instantiate
538
+ :param output_type: The type to which the validator should apply
539
+ :param available_validators: The available validators to choose from
540
+ :param default_validator_kwargs: Kwargs to use
541
+ :return: A list of validators to use
542
+ """
543
+ if available_validators is None:
544
+ available_validators = AVAILABLE_DEFAULT_VALIDATORS
545
+ validators = []
546
+ for key in default_validator_kwargs.keys():
547
+ for validator_cls in available_validators:
548
+ if key == validator_cls.arg() and validator_cls.applies_to(output_type):
549
+ validators.append(
550
+ validator_cls(**{key: default_validator_kwargs[key], "importance": importance})
551
+ )
552
+ break
553
+ else:
554
+ raise ValueError(
555
+ f"No registered subclass of BaseDefaultValidator is available "
556
+ f"for arg: {key} and type {output_type}. This either means (a) this arg-type "
557
+ f"contribution isn't supported or (b) this has not been added yet (but should be). "
558
+ f"In the case of (b), we welcome contributions. Get started at github.com/dagworks-inc/hamilton."
559
+ )
560
+ return validators
@@ -0,0 +1,121 @@
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 pandera as pa
21
+
22
+ from hamilton import registry
23
+ from hamilton.data_quality import base
24
+ from hamilton.htypes import custom_subclass_check
25
+
26
+ pandera_supported_extensions = frozenset(["pandas", "dask", "pyspark_pandas", "polars"])
27
+
28
+
29
+ class PanderaDataFrameValidator(base.BaseDefaultValidator):
30
+ """Pandera schema validator for dataframes"""
31
+
32
+ def __init__(self, schema: pa.DataFrameSchema, importance: str):
33
+ super(PanderaDataFrameValidator, self).__init__(importance)
34
+ self.schema = schema
35
+
36
+ @classmethod
37
+ def applies_to(cls, datatype: type[type]) -> bool:
38
+ for extension_name in pandera_supported_extensions:
39
+ if extension_name in registry.DF_TYPE_AND_COLUMN_TYPES:
40
+ df_type = registry.DF_TYPE_AND_COLUMN_TYPES[extension_name][registry.DATAFRAME_TYPE]
41
+ result = custom_subclass_check(datatype, df_type)
42
+ if result:
43
+ return True
44
+ return False
45
+
46
+ def description(self) -> str:
47
+ return "Validates that the returned dataframe matches the pander"
48
+
49
+ def validate(self, data: Any) -> base.ValidationResult:
50
+ try:
51
+ result = self.schema.validate(data, lazy=True, inplace=True)
52
+ if hasattr(result, "dask"):
53
+ result.compute()
54
+ except pa.errors.SchemaErrors as e:
55
+ return base.ValidationResult(
56
+ passes=False,
57
+ message=str(e),
58
+ diagnostics={"schema_errors": e.schema_errors},
59
+ )
60
+ return base.ValidationResult(
61
+ passes=True,
62
+ message=f"Data passes pandera check for schema {str(self.schema)}",
63
+ # TDOO -- add diagnostics data with serialized the schema
64
+ )
65
+
66
+ @classmethod
67
+ def arg(cls) -> str:
68
+ return "schema" # TODO -- determine whether we want to allow other schemas
69
+
70
+ @classmethod
71
+ def name(cls) -> str:
72
+ return "pandera_schema_validator"
73
+
74
+
75
+ class PanderaSeriesSchemaValidator(base.BaseDefaultValidator):
76
+ """Pandera schema validator for series"""
77
+
78
+ def __init__(self, schema: pa.SeriesSchema, importance: str):
79
+ super(PanderaSeriesSchemaValidator, self).__init__(importance)
80
+ self.schema = schema
81
+
82
+ @classmethod
83
+ def applies_to(cls, datatype: type[type]) -> bool:
84
+ for extension_name in pandera_supported_extensions:
85
+ if extension_name in registry.DF_TYPE_AND_COLUMN_TYPES:
86
+ df_type = registry.DF_TYPE_AND_COLUMN_TYPES[extension_name][registry.COLUMN_TYPE]
87
+ result = custom_subclass_check(datatype, df_type)
88
+ if result:
89
+ return True
90
+ return False
91
+
92
+ def description(self) -> str:
93
+ pass
94
+
95
+ def validate(self, data: Any) -> base.ValidationResult:
96
+ try:
97
+ result = self.schema.validate(data, lazy=True, inplace=True)
98
+ if hasattr(result, "dask"):
99
+ result.compute()
100
+ except pa.errors.SchemaErrors as e:
101
+ return base.ValidationResult(
102
+ passes=False,
103
+ message=str(e),
104
+ diagnostics={"schema_errors": e.schema_errors},
105
+ )
106
+ return base.ValidationResult(
107
+ passes=True,
108
+ message=f"Data passes pandera check for schema {str(self.schema)}",
109
+ # TDOO -- add diagnostics data with serialized the schema
110
+ )
111
+
112
+ @classmethod
113
+ def arg(cls) -> str:
114
+ return "schema" # TODO -- determine whether we want to allow other schemas
115
+
116
+ @classmethod
117
+ def name(cls) -> str:
118
+ return "pandera_schema_validator"
119
+
120
+
121
+ PANDERA_VALIDATORS = [PanderaDataFrameValidator, PanderaSeriesSchemaValidator]