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,47 @@
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
+ try:
21
+ import dask.dataframe as dd
22
+ except ImportError as e:
23
+ raise NotImplementedError("Dask is not installed.") from e
24
+
25
+ from hamilton import registry
26
+
27
+ DATAFRAME_TYPE = dd.DataFrame
28
+ COLUMN_TYPE = dd.Series
29
+
30
+
31
+ @registry.get_column.register(dd.DataFrame)
32
+ def get_column_dask(df: dd.DataFrame, column_name: str) -> dd.Series:
33
+ return df[column_name]
34
+
35
+
36
+ @registry.fill_with_scalar.register(dd.DataFrame)
37
+ def fill_with_scalar_dask(df: dd.DataFrame, column_name: str, value: Any) -> dd.DataFrame:
38
+ df[column_name] = value
39
+ return df
40
+
41
+
42
+ def register_types():
43
+ """Function to register the types for this extension."""
44
+ registry.register_types("dask", DATAFRAME_TYPE, COLUMN_TYPE)
45
+
46
+
47
+ register_types()
@@ -0,0 +1,161 @@
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 dataclasses
19
+ from collections.abc import Collection, Iterable, Sequence
20
+ from typing import Any, Literal
21
+
22
+ try:
23
+ import dlt
24
+ from dlt.common.destination.capabilities import TLoaderFileFormat
25
+ from dlt.common.schema import Schema, TColumnSchema
26
+
27
+ # importing TDestinationReferenceArg fails if Destination isn't imported
28
+ from dlt.extract.resource import DltResource
29
+
30
+ except ImportError as e:
31
+ # raise import error first
32
+ raise ImportError(f"Failed to import the DLT library. {e}") from e
33
+ except Exception as e:
34
+ # raise import error with custom message
35
+ raise ImportError("Failed to import the DLT library.") from e
36
+
37
+ import pandas as pd
38
+
39
+ from hamilton import registry
40
+ from hamilton.io import utils
41
+ from hamilton.io.data_adapters import DataLoader, DataSaver
42
+
43
+ DATAFRAME_TYPES = [Iterable, pd.DataFrame]
44
+
45
+ # TODO add types for other Dataframe libraries
46
+ try:
47
+ import pyarrow as pa
48
+
49
+ DATAFRAME_TYPES.extend([pa.Table, pa.RecordBatch])
50
+ except ModuleNotFoundError:
51
+ pass
52
+
53
+ # convert to tuple to dynamically define type `Union[DATAFRAME_TYPES]`
54
+ DATAFRAME_TYPES = tuple(DATAFRAME_TYPES)
55
+ COLUMN_FRIENDLY_DF_TYPE = False
56
+
57
+
58
+ @dataclasses.dataclass
59
+ class DltResourceLoader(DataLoader):
60
+ resource: DltResource
61
+
62
+ @classmethod
63
+ def name(cls) -> str:
64
+ return "dlt"
65
+
66
+ @classmethod
67
+ def applicable_types(cls) -> Collection[type]:
68
+ return [pd.DataFrame]
69
+
70
+ def load_data(self, type_: type) -> tuple[pd.DataFrame, dict[str, Any]]:
71
+ """Creates a pipeline and conduct `extract` and `normalize` steps.
72
+ Then, "load packages" are read with pandas
73
+ """
74
+ pipeline = dlt.pipeline(
75
+ pipeline_name="Hamilton-DltResourceLoader", destination="filesystem"
76
+ )
77
+ pipeline.extract(self.resource, loader_file_format="parquet")
78
+ normalize_info = pipeline.normalize()
79
+
80
+ partition_file_paths = []
81
+ package = normalize_info.load_packages[0]
82
+ for job in package.jobs["new_jobs"]:
83
+ if job.job_file_info.table_name == self.resource.name:
84
+ partition_file_paths.append(job.file_path)
85
+
86
+ # TODO use pyarrow directly to support different dataframe libraries
87
+ # ref: https://github.com/dlt-hub/verified-sources/blob/master/sources/filesystem/readers.py
88
+ # ref: https://arrow.apache.org/docs/python/generated/pyarrow.parquet.ParquetDataset.html#pyarrow.parquet.ParquetDataset
89
+ df = pd.concat([pd.read_parquet(f) for f in partition_file_paths], ignore_index=True)
90
+
91
+ # delete the pipeline
92
+ pipeline.drop()
93
+
94
+ metadata = utils.get_dataframe_metadata(df)
95
+ return df, metadata
96
+
97
+
98
+ # TODO handle behavior with `combine=`, currently only supports materializing a single node
99
+ @dataclasses.dataclass
100
+ class DltDestinationSaver(DataSaver):
101
+ """Materialize results using a dlt pipeline with the specified destination.
102
+
103
+ In reference to an Extract, Transform, Load (ETL) pipeline, here, the Hamilton
104
+ dataflow is responsible for Transform, and `DltDestination` for Load.
105
+ """
106
+
107
+ pipeline: dlt.Pipeline
108
+ table_name: str
109
+ primary_key: str | None = None
110
+ write_disposition: Literal["skip", "append", "replace", "merge"] | None = None
111
+ columns: Sequence[TColumnSchema] | None = None
112
+ schema: Schema | None = None
113
+ loader_file_format: TLoaderFileFormat | None = None
114
+
115
+ @classmethod
116
+ def name(cls) -> str:
117
+ return "dlt"
118
+
119
+ @classmethod
120
+ def applicable_types(cls) -> Collection[type]:
121
+ return DATAFRAME_TYPES
122
+
123
+ def _get_kwargs(self) -> dict:
124
+ kwargs = {}
125
+ fields_to_skip = ["pipeline"]
126
+ for field in dataclasses.fields(self):
127
+ field_value = getattr(self, field.name)
128
+ if field.name in fields_to_skip:
129
+ continue
130
+
131
+ if field_value != field.default:
132
+ kwargs[field.name] = field_value
133
+
134
+ return kwargs
135
+
136
+ # TODO get pyarrow table from polars, dask, etc.
137
+ def save_data(self, data) -> dict[str, Any]:
138
+ """
139
+ ref: https://dlthub.com/docs/dlt-ecosystem/verified-sources/arrow-pandas
140
+ """
141
+ if isinstance(data, dict):
142
+ raise NotImplementedError(
143
+ "DltDestinationSaver received data of type `dict`."
144
+ "Currently, it doesn't support specifying `combine=base.DictResult()`"
145
+ )
146
+
147
+ load_info = self.pipeline.run(data, **self._get_kwargs())
148
+ # follows the pattern of metadata output found in hamilton.io.utils
149
+ return {"dlt_metadata": load_info.asdict()}
150
+
151
+
152
+ def register_data_loaders():
153
+ """Function to register the data loaders for this extension."""
154
+ for loader in [
155
+ DltDestinationSaver,
156
+ DltResourceLoader,
157
+ ]:
158
+ registry.register_adapter(loader)
159
+
160
+
161
+ register_data_loaders()
@@ -0,0 +1,49 @@
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
+ try:
21
+ import geopandas as gpd
22
+ except ImportError as e:
23
+ raise NotImplementedError("geopandas is not installed.") from e
24
+
25
+ from hamilton import registry
26
+
27
+ DATAFRAME_TYPE = gpd.GeoDataFrame
28
+ COLUMN_TYPE = gpd.GeoSeries
29
+
30
+
31
+ @registry.get_column.register(gpd.GeoDataFrame)
32
+ def get_column_geopandas(df: gpd.GeoDataFrame, column_name: str) -> gpd.GeoSeries:
33
+ return df[column_name]
34
+
35
+
36
+ @registry.fill_with_scalar.register(gpd.GeoDataFrame)
37
+ def fill_with_scalar_geopandas(
38
+ df: gpd.GeoDataFrame, column_name: str, value: Any
39
+ ) -> gpd.GeoDataFrame:
40
+ df[column_name] = value
41
+ return df
42
+
43
+
44
+ def register_types():
45
+ """Function to register the types for this extension."""
46
+ registry.register_types("geopandas", DATAFRAME_TYPE, COLUMN_TYPE)
47
+
48
+
49
+ register_types()
@@ -0,0 +1,331 @@
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 typing
20
+
21
+ import dask.array
22
+ import dask.dataframe
23
+ import numpy as np
24
+ import pandas as pd
25
+ from dask import compute
26
+ from dask.base import tokenize
27
+ from dask.delayed import Delayed, delayed
28
+ from dask.distributed import Client as DaskClient
29
+
30
+ from hamilton import base, htypes, node
31
+ from hamilton.execution import executors
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ try:
36
+ from dask.dataframe.dask_expr import Scalar as dask_scalar
37
+ except ImportError:
38
+ # this is for older versions of dask
39
+ from dask.dataframe.core import Scalar as dask_scalar
40
+
41
+
42
+ class DaskGraphAdapter(base.HamiltonGraphAdapter):
43
+ """Class representing what's required to make Hamilton run on Dask.
44
+
45
+ This walks the graph and translates it to run onto `Dask <https://dask.org/>`__.
46
+
47
+ Use `pip install sf-hamilton[dask]` to get the dependencies required to run this.
48
+
49
+ Try this adapter when:
50
+
51
+ 1. Dask is a good choice to scale computation when you really can't do things in memory anymore with pandas. \
52
+ For most simple pandas operations, you should not have to do anything to scale! You just need to load in \
53
+ data via dask rather than pandas.
54
+ 2. Dask can help scale to larger data sets if running on a cluster -- you'll just have to switch to\
55
+ natively using their object types if that's the case (set use_delayed=False, and compute_at_end=False).
56
+ 3. Use this adapter if you want to utilize multiple cores on a single machine, or you want to scale to large \
57
+ data set sizes with a Dask cluster that you can connect to.
58
+ 4. The ONLY CAVEAT really is whether you use `delayed` or `dask datatypes` (or both).
59
+
60
+ Please read the following notes about its limitations.
61
+
62
+ Notes on scaling:
63
+ -----------------
64
+ - Multi-core on single machine ✅
65
+ - Distributed computation on a Dask cluster ✅
66
+ - Scales to any size of data supported by Dask ✅; assuming you load it appropriately via Dask loaders.
67
+ - Works best with Pandas 2.0+ and pyarrow backend.
68
+
69
+ Function return object types supported:
70
+ ---------------------------------------
71
+ - Works for any python object that can be serialized by the Dask framework. ✅
72
+
73
+ Pandas?
74
+ -------
75
+ Dask implements a good subset of the Pandas API:
76
+ - You might be able to get away with scaling without having to change your code at all!
77
+ - See https://docs.dask.org/en/latest/dataframe-api.html for Pandas supported APIs.
78
+ - If it is not supported by their API, you have to then read up and think about how to structure you hamilton\
79
+ function computation -- https://docs.dask.org/en/latest/dataframe.html
80
+ - if paired with DaskDataFrameResult & use_delayed=False & compute_at_end=False, it will help you produce a \
81
+ dask dataframe as a result that you can then convert back to pandas if you want.
82
+
83
+ Loading Data:
84
+ -------------
85
+ - see https://docs.dask.org/en/latest/best-practices.html#load-data-with-dask.
86
+ - we recommend creating a python module specifically encapsulating functions that help you load data.
87
+
88
+ CAVEATS with use_delayed=True:
89
+ ------------------------------
90
+ - If using `use_delayed=True` serialization costs can outweigh the benefits of parallelism, so you should \
91
+ benchmark your code to see if it's worth it.
92
+ - With this adapter & use_delayed=True, it can naively wrap all your functions with `delayed`, which will mean \
93
+ they will be executed and scheduled across the dask workers. This is a good choice if your computation is slow, \
94
+ or Hamilton graph is highly parallelizable.
95
+
96
+ DISCLAIMER -- this class is experimental, so signature changes are a possibility! But we'll aim to be backwards
97
+ compatible where possible.
98
+ """
99
+
100
+ def __init__(
101
+ self,
102
+ dask_client: DaskClient,
103
+ result_builder: base.ResultMixin = None,
104
+ visualize_kwargs: dict = None,
105
+ use_delayed: bool = True,
106
+ compute_at_end: bool = True,
107
+ ):
108
+ """Constructor
109
+
110
+ You have the ability to pass in a ResultMixin object to the constructor to control the return type that gets\
111
+ produced by running on Dask.
112
+
113
+ :param dask_client: the dask client -- we don't do anything with it, but thought that it would be useful\
114
+ to wire through here.
115
+ :param result_builder: The function that will build the result. Optional, defaults to pandas dataframe.
116
+ :param visualize_kwargs: Arguments to visualize the graph using dask's internals.\
117
+ **None**, means no visualization.\
118
+ **Dict**, means visualize -- see https://docs.dask.org/en/latest/api.html?highlight=visualize#dask.visualize\
119
+ for what to pass in.
120
+ :param use_delayed: Default is True for backwards compatibility. Whether to use dask.delayed to wrap every
121
+ function. Note: it is probably not necessary to mix this with using dask objects, e.g. dataframes/series.
122
+ They are by nature lazily computed and operate over the dask data types, so you don't need to wrap them
123
+ with delayed. Use delayed if you want to farm out computation.
124
+ :param compute_at_end: Default is True for backwards compatibility. Whether to compute() at the end.
125
+ That is, should `.compute()` be called in the result builder to quick off computation.
126
+ """
127
+ self.client = dask_client
128
+ self.result_builder = result_builder or base.PandasDataFrameResult()
129
+ self.visualize_kwargs = visualize_kwargs
130
+ self.use_delayed = use_delayed
131
+ self.compute_at_end = compute_at_end
132
+
133
+ @staticmethod
134
+ def check_input_type(node_type: type, input_value: typing.Any) -> bool:
135
+ # NOTE: the type of dask Delayed is unknown until they are computed
136
+ if isinstance(input_value, Delayed):
137
+ return True
138
+ elif node_type == pd.Series and isinstance(input_value, dask.dataframe.Series):
139
+ return True
140
+ elif node_type == np.array and isinstance(input_value, dask.array.Array):
141
+ return True
142
+ return htypes.check_input_type(node_type, input_value)
143
+
144
+ @staticmethod
145
+ def check_node_type_equivalence(node_type: type, input_type: type) -> bool:
146
+ if node_type == dask.array.Array and input_type == pd.Series:
147
+ return True
148
+ elif node_type == dask.dataframe.Series and input_type == pd.Series:
149
+ return True
150
+ return node_type == input_type
151
+
152
+ def execute_node(self, node: node.Node, kwargs: dict[str, typing.Any]) -> typing.Any:
153
+ """Function that is called as we walk the graph to determine how to execute a hamilton function.
154
+
155
+ :param node: the node from the graph.
156
+ :param kwargs: the arguments that should be passed to it.
157
+ :return: returns a dask delayed object.
158
+ """
159
+ if not self.use_delayed:
160
+ return node.callable(**kwargs)
161
+ # we want to ensure the name in dask corresponds to the node name, and not the wrapped
162
+ # function name that hamilton might have wrapped it with.
163
+ hash = tokenize(kwargs) # this is what the dask docs recommend.
164
+ name = node.name + hash
165
+ dask_key_name = str(node.name) + "_" + hash
166
+ return delayed(node.callable, name=name)(
167
+ **kwargs,
168
+ dask_key_name=dask_key_name, # this is what shows up in the dask console
169
+ )
170
+
171
+ def build_result(self, **outputs: dict[str, typing.Any]) -> typing.Any:
172
+ """Builds the result and brings it back to this running process.
173
+
174
+ :param outputs: the dictionary of key -> Union[delayed object reference | value]
175
+ :return: The type of object returned by self.result_builder. Note the following behaviors:
176
+ - if you use_delayed=True, then the result will be a delayed object.
177
+ - if you use_delayed=True & computed_at_end=True, then the result will be the return type
178
+ of self.result_builder.
179
+ - if you use_delayed=False & computed_at_end=True, this will only work if the self.result_builder
180
+ returns a dask type, as we will try to compute it.
181
+ - if you use_delayed=False & computed_at_end=False, this will return the result of self.result_builder.
182
+ """
183
+ if logger.isEnabledFor(logging.DEBUG):
184
+ for k, v in outputs.items():
185
+ logger.debug(f"Got column {k}, with type [{type(v)}].")
186
+ if self.use_delayed:
187
+ delayed_result = delayed(self.result_builder.build_result)(**outputs)
188
+ else:
189
+ delayed_result = self.result_builder.build_result(**outputs)
190
+ if self.visualize_kwargs is not None:
191
+ delayed_result.visualize(**self.visualize_kwargs)
192
+ if self.compute_at_end:
193
+ (result,) = compute(delayed_result)
194
+ return result
195
+ else:
196
+ return delayed_result
197
+
198
+
199
+ class DaskDataFrameResult(base.ResultMixin):
200
+ @staticmethod
201
+ def build_result(**outputs: dict[str, typing.Any]) -> typing.Any:
202
+ """Builds a dask dataframe from the outputs.
203
+
204
+ This has some assumptions:
205
+ 1. the order specified in the output will mirror the order of "joins" here.
206
+ 2. it tries to massage types into dask types where it can
207
+ 3. otherwise it duplicates any "scalars/objects" using the first valid input with an index as the
208
+ template. It assumes a single partition.
209
+ """
210
+
211
+ def get_output_name(output_name: str, column_name: str) -> str:
212
+ """Add function prefix to columns.
213
+ Note this means that they stop being valid python identifiers due to the `.` in the string.
214
+ """
215
+ return f"{output_name}.{column_name}"
216
+
217
+ if len(outputs) == 0:
218
+ raise ValueError("No outputs were specified. Cannot build a dataframe.")
219
+ if logger.isEnabledFor(logging.DEBUG):
220
+ for k, v in outputs.items():
221
+ logger.debug(f"Got column {k}, with type [{type(v)}].")
222
+
223
+ length = 0
224
+ index = None
225
+ massaged_outputs = {}
226
+ columns_expected = []
227
+ for k, v in outputs.items():
228
+ if isinstance(v, (dask.dataframe.Series, dask.dataframe.DataFrame)):
229
+ if length == 0:
230
+ length = len(v)
231
+ index = v.index
232
+ massaged_outputs[k] = v
233
+ if isinstance(v, dask.dataframe.Series):
234
+ columns_expected.append(k)
235
+ else:
236
+ columns_expected.extend([get_output_name(k, v_col) for v_col in v.columns])
237
+ elif isinstance(v, (pd.Series, pd.DataFrame)):
238
+ converted = dask.dataframe.from_pandas(v, npartitions=1)
239
+ massaged_outputs[k] = converted
240
+ if length == 0:
241
+ length = len(converted)
242
+ index = converted.index
243
+ if isinstance(v, pd.Series):
244
+ columns_expected.append(k)
245
+ else:
246
+ columns_expected.extend([get_output_name(k, v_col) for v_col in v.columns])
247
+ elif isinstance(v, (np.ndarray, np.generic)):
248
+ massaged_outputs[k] = dask.dataframe.from_array(v)
249
+ columns_expected.append(k)
250
+ elif isinstance(v, (list, tuple)):
251
+ massaged_outputs[k] = dask.dataframe.from_array(dask.array.from_array(v))
252
+ columns_expected.append(k)
253
+ elif isinstance(v, (dask_scalar,)):
254
+ scalar = v.compute()
255
+ if length == 0:
256
+ massaged_outputs[k] = dask.dataframe.from_pandas(
257
+ pd.DataFrame([scalar], index=[0]), npartitions=1
258
+ )
259
+ else:
260
+ massaged_outputs[k] = dask.dataframe.from_pandas(
261
+ pd.DataFrame([scalar] * length, index=index), npartitions=1
262
+ )
263
+ columns_expected.append(k)
264
+ elif isinstance(v, (int, float, str, bool, object)):
265
+ scalar = v
266
+ if length == 0:
267
+ massaged_outputs[k] = dask.dataframe.from_pandas(
268
+ pd.DataFrame([scalar], index=[0]), npartitions=1
269
+ )
270
+ else:
271
+ massaged_outputs[k] = dask.dataframe.from_pandas(
272
+ pd.DataFrame([scalar] * length, index=index), npartitions=1
273
+ )
274
+ columns_expected.append(k)
275
+ else:
276
+ raise ValueError(
277
+ f"Unknown type {type(v)} for output {k}. "
278
+ f"Do not know how to handle making a dataframe from this."
279
+ )
280
+
281
+ # assumption is that everything here is a dask series or dataframe
282
+ # we assume that we do column concatenation and that it's an outer join (TBD: make this configurable)
283
+ _df = dask.dataframe.concat([o for o in massaged_outputs.values()], axis=1, join="outer")
284
+ _df.columns = columns_expected
285
+ return _df
286
+
287
+
288
+ # TODO: add ResultMixins for dask types
289
+
290
+
291
+ class DaskExecutor(executors.TaskExecutor):
292
+ """A DaskExecutor for task-based execution on dask in the new Hamilton execution API."""
293
+
294
+ def __init__(self, *, client: DaskClient):
295
+ """Initializes the DaskExecutor. Note this currently takes in the client -- we will likely
296
+ add the ability to make it take in parameters to instantiate/tear down a client on its own.
297
+ This just allows full flexibility for now.
298
+
299
+ """
300
+ self.client = client
301
+
302
+ def init(self):
303
+ """No-op -- client already passed in by the user."""
304
+ pass
305
+
306
+ def finalize(self):
307
+ """No-op -- client already passed in by the user, who is responsible for shutting it
308
+ down."""
309
+ pass
310
+
311
+ def submit_task(self, task: executors.TaskImplementation) -> executors.TaskFuture:
312
+ """Submits a task using the dask futures API. Note that we are not using dask delayed --
313
+ as the idea is that tasks are potentially dynamic, meaning that we have to resolve some
314
+ before we create others. That makes the delayed API a little messier -- we would have to
315
+ call .compute() at certain steps. We *may* consider doing this, but for now, we are just
316
+ utilizing the futures API, and grouping it into tasks.
317
+
318
+ :param task: Task to execute (contains all arguments necessary)
319
+ :return: The future for the task
320
+ """
321
+
322
+ return executors.TaskFutureWrappingPythonFuture(
323
+ self.client.submit(executors.base_execute_task, task)
324
+ )
325
+
326
+ def can_submit_task(self) -> bool:
327
+ """For now we always can -- it will block on the dask side.
328
+
329
+ :return: True
330
+ """
331
+ return True