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
hamilton/base.py ADDED
@@ -0,0 +1,466 @@
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
+ """This module contains base constructs for executing a hamilton graph.
19
+ It should only import hamilton.node, numpy, pandas.
20
+ It cannot import hamilton.graph, or hamilton.driver.
21
+ """
22
+
23
+ import abc
24
+ import collections
25
+ import logging
26
+ from typing import Any
27
+
28
+ import numpy as np
29
+ import pandas as pd
30
+ from pandas.core.indexes import extension as pd_extension
31
+
32
+ from hamilton.lifecycle import api as lifecycle_api
33
+
34
+ try:
35
+ from . import htypes, node
36
+ except ImportError:
37
+ import node
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ class ResultMixin(lifecycle_api.LegacyResultMixin):
43
+ """Legacy result builder -- see lifecycle methods for more information."""
44
+
45
+ pass
46
+
47
+
48
+ class DictResult(ResultMixin):
49
+ """Simple function that returns the dict of column -> value results.
50
+
51
+ It returns the results as a dictionary, where the keys map to outputs requested,
52
+ and values map to what was computed for those values.
53
+
54
+ Use this when you want to:
55
+
56
+ 1. debug dataflows.
57
+ 2. have heterogeneous return types.
58
+ 3. Want to manually transform the result into something of your choosing.
59
+
60
+
61
+ .. code-block:: python
62
+
63
+ from hamilton import base, driver
64
+
65
+ dict_builder = base.DictResult()
66
+ adapter = base.SimplePythonGraphAdapter(dict_builder)
67
+ dr = driver.Driver(config, *modules, adapter=adapter)
68
+ dict_result = dr.execute([...], inputs=...)
69
+
70
+ Note, if you just want the dict result + the SimplePythonGraphAdapter, you can use the
71
+ DefaultAdapter
72
+
73
+ .. code-block:: python
74
+
75
+ adapter = base.DefaultAdapter()
76
+ """
77
+
78
+ @staticmethod
79
+ def build_result(**outputs: dict[str, Any]) -> dict:
80
+ """This function builds a simple dict of output -> computed values."""
81
+ return outputs
82
+
83
+ def input_types(self) -> list[type[type]] | None:
84
+ return [Any]
85
+
86
+ def output_type(self) -> type:
87
+ return dict[str, Any]
88
+
89
+
90
+ class PandasDataFrameResult(ResultMixin):
91
+ """Mixin for building a pandas dataframe from the result.
92
+
93
+ It returns the results as a Pandas Dataframe, where the columns map to outputs requested, and values map to what\
94
+ was computed for those values. Note: this only works if the computed values are pandas series, or scalar values.
95
+
96
+ Use this when you want to create a pandas dataframe.
97
+
98
+ Example:
99
+
100
+ .. code-block:: python
101
+
102
+ from hamilton import base, driver
103
+ df_builder = base.PandasDataFrameResult()
104
+ adapter = base.SimplePythonGraphAdapter(df_builder)
105
+ dr = driver.Driver(config, *modules, adapter=adapter)
106
+ df = dr.execute([...], inputs=...)
107
+ """
108
+
109
+ @staticmethod
110
+ def pandas_index_types(
111
+ outputs: dict[str, Any],
112
+ ) -> tuple[dict[str, list[str]], dict[str, list[str]], dict[str, list[str]]]:
113
+ """This function creates three dictionaries according to whether there is an index type or not.
114
+
115
+ The three dicts we create are:
116
+ 1. Dict of index type to list of outputs that match it.
117
+ 2. Dict of time series / categorical index types to list of outputs that match it.
118
+ 3. Dict of `no-index` key to list of outputs with no index type.
119
+
120
+ :param outputs: the dict we're trying to create a result from.
121
+ :return: dict of all index types, dict of time series/categorical index types, dict if there is no index
122
+ """
123
+ all_index_types = collections.defaultdict(list)
124
+ time_indexes = collections.defaultdict(list)
125
+ no_indexes = collections.defaultdict(list)
126
+
127
+ def index_key_name(pd_object: pd.DataFrame | pd.Series) -> str:
128
+ """Creates a string helping identify the index and it's type.
129
+ Useful for disambiguating time related indexes."""
130
+ return f"{pd_object.index.__class__.__name__}:::{pd_object.index.dtype}"
131
+
132
+ def get_parent_time_index_type():
133
+ """Helper to pull the right time index parent class."""
134
+ if hasattr(pd_extension, "NDArrayBackedExtensionIndex"):
135
+ index_type = pd_extension.NDArrayBackedExtensionIndex
136
+ else:
137
+ index_type = None # weird case, but not worth breaking for.
138
+ return index_type
139
+
140
+ for output_name, output_value in outputs.items():
141
+ if isinstance(
142
+ output_value, (pd.DataFrame, pd.Series)
143
+ ): # if it has an index -- let's grab it's type
144
+ dict_key = index_key_name(output_value)
145
+ if isinstance(output_value.index, get_parent_time_index_type()):
146
+ # it's a time index -- these will produce garbage if not aligned properly.
147
+ time_indexes[dict_key].append(output_name)
148
+ elif isinstance(
149
+ output_value, pd.Index
150
+ ): # there is no index on this - so it's just an integer one.
151
+ int_index = pd.Series(
152
+ [1, 2, 3], index=[0, 1, 2]
153
+ ) # dummy to get right values for string.
154
+ dict_key = index_key_name(int_index)
155
+ else:
156
+ dict_key = "no-index"
157
+ no_indexes[dict_key].append(output_name)
158
+ all_index_types[dict_key].append(output_name)
159
+ return all_index_types, time_indexes, no_indexes
160
+
161
+ @staticmethod
162
+ def check_pandas_index_types_match(
163
+ all_index_types: dict[str, list[str]],
164
+ time_indexes: dict[str, list[str]],
165
+ no_indexes: dict[str, list[str]],
166
+ ) -> bool:
167
+ """Checks that pandas index types match.
168
+
169
+ This only logs warning errors, and if debug is enabled, a debug statement to list index types.
170
+ """
171
+ no_index_length = len(no_indexes)
172
+ time_indexes_length = len(time_indexes)
173
+ all_indexes_length = len(all_index_types)
174
+ number_with_indexes = all_indexes_length - no_index_length
175
+ types_match = True # default to True
176
+ # if there is more than one time index
177
+ if time_indexes_length > 1:
178
+ logger.warning(
179
+ "WARNING: Time/Categorical index type mismatches detected - check output to ensure Pandas "
180
+ "is doing what you intend to do. Else change the index types to match. Set logger to debug "
181
+ "to see index types."
182
+ )
183
+ types_match = False
184
+ # if there is more than one index type and it's not explained by the time indexes then
185
+ if number_with_indexes > 1 and all_indexes_length > time_indexes_length:
186
+ logger.warning(
187
+ "WARNING: Multiple index types detected - check output to ensure Pandas is "
188
+ "doing what you intend to do. Else change the index types to match. Set logger to debug to "
189
+ "see index types."
190
+ )
191
+ types_match = False
192
+ elif number_with_indexes == 1 and no_index_length > 0:
193
+ logger.warning(
194
+ f"WARNING: a single pandas index was found, but there are also {len(no_indexes['no-index'])} "
195
+ "outputs without an index. Please check whether the dataframe created matches what what you "
196
+ "expect to happen."
197
+ )
198
+ # Strictly speaking the index types match -- there is only one -- so setting to True.
199
+ types_match = True
200
+ # if all indexes matches no indexes
201
+ elif no_index_length == all_indexes_length:
202
+ logger.warning(
203
+ "It appears no Pandas index type was detected (ignore this warning if you're using DASK for now.) "
204
+ "Please check whether the dataframe created matches what what you expect to happen."
205
+ )
206
+ types_match = False
207
+ if logger.isEnabledFor(logging.DEBUG):
208
+ import pprint
209
+
210
+ pretty_string = pprint.pformat(dict(all_index_types))
211
+ logger.debug(f"Index types encountered:\n{pretty_string}.")
212
+ return types_match
213
+
214
+ @staticmethod
215
+ def build_result(**outputs: dict[str, Any]) -> pd.DataFrame:
216
+ """Builds a Pandas DataFrame from the outputs.
217
+
218
+ This function will check the index types of the outputs, and log warnings if they don't match.
219
+ The behavior of pd.Dataframe(outputs) is that it will do an outer join based on indexes of the Series passed in.
220
+
221
+ :param outputs: the outputs to build a dataframe from.
222
+ """
223
+ # TODO check inputs are pd.Series, arrays, or scalars -- else error
224
+ output_index_type_tuple = PandasDataFrameResult.pandas_index_types(outputs)
225
+ # this next line just log warnings
226
+ # we don't actually care about the result since this is the current default behavior.
227
+ PandasDataFrameResult.check_pandas_index_types_match(*output_index_type_tuple)
228
+
229
+ if len(outputs) == 1:
230
+ (value,) = outputs.values() # this works because it's length 1.
231
+ if isinstance(value, pd.DataFrame):
232
+ return value
233
+
234
+ if not any(pd.api.types.is_list_like(value) for value in outputs.values()):
235
+ # If we're dealing with all values that don't have any "index" that could be created
236
+ # (i.e. scalars, objects) coerce the output to a single-row, multi-column dataframe.
237
+ return pd.DataFrame([outputs])
238
+ #
239
+ contains_df = any(isinstance(value, pd.DataFrame) for value in outputs.values())
240
+ if contains_df:
241
+ # build the dataframe from the outputs
242
+ return PandasDataFrameResult.build_dataframe_with_dataframes(outputs)
243
+ # don't do anything special if dataframes aren't in the output.
244
+ return pd.DataFrame(outputs) # this does an implicit outer join based on index.
245
+
246
+ @staticmethod
247
+ def build_dataframe_with_dataframes(outputs: dict[str, Any]) -> pd.DataFrame:
248
+ """Builds a dataframe from the outputs in an "outer join" manner based on index.
249
+
250
+ The behavior of pd.Dataframe(outputs) is that it will do an outer join based on indexes of the Series passed in.
251
+ To handle dataframes, we unpack the dataframe into a dict of series, check to ensure that no columns are
252
+ redefined in a rolling fashion going in order of the outputs requested. This then results in an "enlarged"
253
+ outputs dict that is then passed to pd.Dataframe(outputs) to get the final dataframe.
254
+
255
+ :param outputs: The outputs to build the dataframe from.
256
+ :return: A dataframe with the outputs.
257
+ """
258
+
259
+ def get_output_name(output_name: str, column_name: str) -> str:
260
+ """Add function prefix to columns.
261
+ Note this means that they stop being valid python identifiers due to the `.` in the string.
262
+ """
263
+ return f"{output_name}.{column_name}"
264
+
265
+ flattened_outputs = {}
266
+ for name, output in outputs.items():
267
+ if isinstance(output, pd.DataFrame):
268
+ if logger.isEnabledFor(logging.DEBUG):
269
+ logger.debug(
270
+ f"Unpacking dataframe {name} into dict of series with columns {list(output.columns)}."
271
+ )
272
+
273
+ df_dict = {
274
+ get_output_name(name, col_name): col_value
275
+ for col_name, col_value in output.to_dict(orient="series").items()
276
+ }
277
+ flattened_outputs.update(df_dict)
278
+ elif isinstance(output, pd.Series):
279
+ if name in flattened_outputs:
280
+ raise ValueError(
281
+ f"Series {name} already exists in the output. "
282
+ f"Please rename the series to avoid this error, or determine from where the initial series is "
283
+ f"being added; it may be coming from a dataframe that is being unpacked."
284
+ )
285
+ flattened_outputs[name] = output
286
+ else:
287
+ if name in flattened_outputs:
288
+ raise ValueError(
289
+ f"Non series output {name} already exists in the output. "
290
+ f"Please rename this output to avoid this error, or determine from where the initial value is "
291
+ f"being added; it may be coming from a dataframe that is being unpacked."
292
+ )
293
+ flattened_outputs[name] = output
294
+
295
+ return pd.DataFrame(flattened_outputs)
296
+
297
+ def input_types(self) -> list[type[type]]:
298
+ """Currently this just shoves anything into a dataframe. We should probably
299
+ tighten this up."""
300
+ return [Any]
301
+
302
+ def output_type(self) -> type:
303
+ return pd.DataFrame
304
+
305
+
306
+ class StrictIndexTypePandasDataFrameResult(PandasDataFrameResult):
307
+ """A ResultBuilder that produces a dataframe only if the index types match exactly.
308
+
309
+ Note: If there is no index type on some outputs, e.g. the value is a scalar, as long as there exists a single \
310
+ pandas index type, no error will be thrown, because a dataframe can be easily created.
311
+
312
+ Use this when you want to create a pandas dataframe from the outputs, but you want to ensure that the index types \
313
+ match exactly.
314
+
315
+ To use:
316
+
317
+ .. code-block:: python
318
+
319
+ from hamilton import base, driver
320
+ strict_builder = base.StrictIndexTypePandasDataFrameResult()
321
+ adapter = base.SimplePythonGraphAdapter(strict_builder)
322
+ dr = driver.Driver(config, *modules, adapter=adapter)
323
+ df = dr.execute([...], inputs=...) # this will now error if index types mismatch.
324
+ """
325
+
326
+ @staticmethod
327
+ def build_result(**outputs: dict[str, Any]) -> pd.DataFrame:
328
+ # TODO check inputs are pd.Series, arrays, or scalars -- else error
329
+ output_index_type_tuple = PandasDataFrameResult.pandas_index_types(outputs)
330
+ indexes_match = PandasDataFrameResult.check_pandas_index_types_match(
331
+ *output_index_type_tuple
332
+ )
333
+ if not indexes_match:
334
+ import pprint
335
+
336
+ pretty_string = pprint.pformat(dict(output_index_type_tuple[0]))
337
+ raise ValueError(
338
+ "Error: pandas index types did not match exactly. "
339
+ f"Found the following indexes:\n{pretty_string}"
340
+ )
341
+
342
+ return PandasDataFrameResult.build_result(**outputs)
343
+
344
+
345
+ class NumpyMatrixResult(ResultMixin):
346
+ """Mixin for building a Numpy Matrix from the result of walking the graph.
347
+
348
+ All inputs to the build_result function are expected to be numpy arrays.
349
+
350
+ .. code-block:: python
351
+
352
+ from hamilton import base, driver
353
+
354
+ adapter = base.SimplePythonGraphAdapter(base.NumpyMatrixResult())
355
+ dr = driver.Driver(config, *modules, adapter=adapter)
356
+ numpy_matrix = dr.execute([...], inputs=...)
357
+ """
358
+
359
+ @staticmethod
360
+ def build_result(**outputs: dict[str, Any]) -> np.matrix:
361
+ """Builds a numpy matrix from the passed in, inputs.
362
+
363
+ Note: this does not check that the inputs are all numpy arrays/array like things.
364
+
365
+ :param outputs: function_name -> np.array.
366
+ :return: numpy matrix
367
+ """
368
+ # TODO check inputs are all numpy arrays/array like things -- else error
369
+ num_rows = -1
370
+ columns_with_lengths = collections.OrderedDict()
371
+ for col, val in outputs.items(): # assumption is fixed order
372
+ if isinstance(val, (int, float)): # TODO add more things here
373
+ columns_with_lengths[(col, 1)] = val
374
+ else:
375
+ length = len(val)
376
+ if num_rows == -1:
377
+ num_rows = length
378
+ elif length == num_rows:
379
+ # we're good
380
+ pass
381
+ else:
382
+ raise ValueError(
383
+ f"Error, got non scalar result that mismatches length of other vector. "
384
+ f"Got {length} for {col} instead of {num_rows}."
385
+ )
386
+ columns_with_lengths[(col, num_rows)] = val
387
+ list_of_columns = []
388
+ for (col, length), val in columns_with_lengths.items():
389
+ if length != num_rows and length == 1:
390
+ list_of_columns.append([val] * num_rows) # expand single values into a full row
391
+ elif length == num_rows:
392
+ list_of_columns.append(list(val))
393
+ else:
394
+ raise ValueError(
395
+ f"Do not know how to make this column {col} with length {length} have {num_rows} rows"
396
+ )
397
+ # Create the matrix with columns as rows and then transpose
398
+ return np.asmatrix(list_of_columns).T
399
+
400
+ def input_types(self) -> list[type[type]]:
401
+ """Currently returns anything as numpy types are relatively new and"""
402
+ return [Any] # Typing
403
+
404
+ def output_type(self) -> type:
405
+ return pd.DataFrame
406
+
407
+
408
+ class HamiltonGraphAdapter(lifecycle_api.GraphAdapter, abc.ABC):
409
+ """Legacy graph adapter -- see lifecycle methods for more information."""
410
+
411
+ pass
412
+
413
+
414
+ class SimplePythonDataFrameGraphAdapter(HamiltonGraphAdapter, PandasDataFrameResult):
415
+ """This is the original Hamilton graph adapter. It uses plain python and builds a dataframe result.
416
+
417
+ This executes the Hamilton dataflow locally on a machine in a single threaded,
418
+ single process fashion. It assumes a pandas dataframe as a result.
419
+
420
+ Use this when you want to execute on a single machine, without parallelization, and you want a
421
+ pandas dataframe as output.
422
+ """
423
+
424
+ @staticmethod
425
+ def check_input_type(node_type: type, input_value: Any) -> bool:
426
+ return htypes.check_input_type(node_type, input_value)
427
+
428
+ @staticmethod
429
+ def check_node_type_equivalence(node_type: type, input_type: type) -> bool:
430
+ return node_type == input_type
431
+
432
+ def execute_node(self, node: node.Node, kwargs: dict[str, Any]) -> Any:
433
+ return node.callable(**kwargs)
434
+
435
+
436
+ class SimplePythonGraphAdapter(SimplePythonDataFrameGraphAdapter):
437
+ """This class allows you to swap out the build_result very easily.
438
+
439
+ This executes the Hamilton dataflow locally on a machine in a single threaded, single process fashion. It allows\
440
+ you to specify a ResultBuilder to control the return type of what ``execute()`` returns.
441
+
442
+ Currently this extends SimplePythonDataFrameGraphAdapter, although that's largely for legacy reasons (and can probably be changed).
443
+
444
+ TODO -- change this to extend the right class.
445
+ """
446
+
447
+ def __init__(self, result_builder: ResultMixin = None):
448
+ """Allows you to swap out the build_result very easily.
449
+
450
+ :param result_builder: A ResultMixin object that will be used to build the result.
451
+ """
452
+ if result_builder is None:
453
+ result_builder = DictResult()
454
+ self.result_builder = result_builder
455
+
456
+ def build_result(self, **outputs: dict[str, Any]) -> Any:
457
+ """Delegates to the result builder function supplied."""
458
+ return self.result_builder.build_result(**outputs)
459
+
460
+ def output_type(self) -> type:
461
+ return self.result_builder.output_type()
462
+
463
+
464
+ class DefaultAdapter(SimplePythonGraphAdapter):
465
+ """This is a shortcut for the SimplePythonGraphAdapter. It does the exact same thing,
466
+ but allows for easier access/naming."""
@@ -0,0 +1,16 @@
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.