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,125 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ from collections.abc import Callable
19
+ from concurrent.futures import Future, ThreadPoolExecutor
20
+ from typing import Any
21
+
22
+ from hamilton import lifecycle, node
23
+ from hamilton.lifecycle import base
24
+
25
+
26
+ def _new_fn(fn: Callable, **fn_kwargs) -> Any:
27
+ """Function that runs in the thread.
28
+
29
+ It can recursively check for Futures because we don't have to worry about
30
+ process serialization.
31
+ :param fn: Function to run
32
+ :param fn_kwargs: Keyword arguments to pass to the function
33
+ """
34
+ for k, v in fn_kwargs.items():
35
+ if isinstance(v, Future):
36
+ while isinstance(v, Future):
37
+ v = v.result() # this blocks until the future is resolved
38
+ fn_kwargs[k] = v
39
+ # execute the function once all the futures are resolved
40
+ return fn(**fn_kwargs)
41
+
42
+
43
+ class FutureAdapter(base.BaseDoRemoteExecute, lifecycle.ResultBuilder):
44
+ """Adapter that lazily submits each function for execution to a ThreadpoolExecutor.
45
+
46
+ This adapter has similar behavior to the async Hamilton driver which allows for parallel execution of functions.
47
+
48
+ This adapter works because we don't have to worry about object serialization.
49
+
50
+ Caveats:
51
+ - DAGs with lots of CPU intense functions will limit usefulness of this adapter, unless they release the GIL.
52
+ - DAGs with lots of I/O bound work will benefit from this adapter, e.g. making API calls.
53
+ - The max parallelism is limited by the number of threads in the ThreadPoolExecutor.
54
+
55
+ Unsupported behavior:
56
+ - The FutureAdapter does not support DAGs with Parallelizable & Collect functions. This is due to laziness
57
+ rather than anything inherently technical. If you'd like this feature, please open an issue on the Hamilton
58
+ repository.
59
+
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ max_workers: int = None,
65
+ thread_name_prefix: str = "",
66
+ result_builder: lifecycle.ResultBuilder = None,
67
+ ):
68
+ """Constructor.
69
+ :param max_workers: The maximum number of threads that can be used to execute the given calls.
70
+ :param thread_name_prefix: An optional name prefix to give our threads.
71
+ :param result_builder: Optional. Result builder to use for building the result.
72
+ """
73
+ self.executor = ThreadPoolExecutor(
74
+ max_workers=max_workers, thread_name_prefix=thread_name_prefix
75
+ )
76
+ self.result_builder = result_builder
77
+
78
+ def input_types(self) -> list[type[type]]:
79
+ """Gives the applicable types to this result builder.
80
+ This is optional for backwards compatibility, but is recommended.
81
+
82
+ :return: A list of types that this can apply to.
83
+ """
84
+ # since this wraps a potential result builder, expose the input types of the wrapped
85
+ # result builder doesn't make sense.
86
+ return [Any]
87
+
88
+ def output_type(self) -> type:
89
+ """Returns the output type of this result builder
90
+ :return: the type that this creates
91
+ """
92
+ if self.result_builder:
93
+ return self.result_builder.output_type()
94
+ return Any
95
+
96
+ def do_remote_execute(
97
+ self,
98
+ *,
99
+ execute_lifecycle_for_node: Callable,
100
+ node: node.Node,
101
+ **kwargs: dict[str, Any],
102
+ ) -> Any:
103
+ """Function that submits the passed in function to the ThreadPoolExecutor to be executed
104
+ after wrapping it with the _new_fn function.
105
+
106
+ :param node: Node that is being executed
107
+ :param execute_lifecycle_for_node: Function executing lifecycle_hooks and lifecycle_methods
108
+ :param kwargs: Keyword arguments that are being passed into the function
109
+ """
110
+ return self.executor.submit(_new_fn, execute_lifecycle_for_node, **kwargs)
111
+
112
+ def build_result(self, **outputs: Any) -> Any:
113
+ """Given a set of outputs, build the result.
114
+
115
+ This function will block until all futures are resolved.
116
+
117
+ :param outputs: the outputs from the execution of the graph.
118
+ :return: the result of the execution of the graph.
119
+ """
120
+ for k, v in outputs.items():
121
+ if isinstance(v, Future):
122
+ outputs[k] = v.result()
123
+ if self.result_builder:
124
+ return self.result_builder.build_result(**outputs)
125
+ return outputs
@@ -0,0 +1,122 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ from collections.abc import Collection
19
+ from typing import Any
20
+
21
+ import tqdm
22
+
23
+ from hamilton import graph_types
24
+ from hamilton.lifecycle import GraphExecutionHook, NodeExecutionHook
25
+
26
+
27
+ class ProgressBar(
28
+ GraphExecutionHook,
29
+ NodeExecutionHook,
30
+ ):
31
+ """An adapter that uses tqdm to show progress bars for the graph execution.
32
+
33
+ Note: you need to have tqdm installed for this to work.
34
+ If you don't have it installed, you can install it with `pip install tqdm`
35
+ (or `pip install sf-hamilton[tqdm]` -- use quotes if you're using zsh).
36
+
37
+ .. code-block:: python
38
+
39
+ from hamilton.plugins import h_tqdm
40
+
41
+ dr = (
42
+ driver.Builder()
43
+ .with_config({})
44
+ .with_modules(some_modules)
45
+ .with_adapters(h_tqdm.ProgressBar(desc="DAG-NAME"))
46
+ .build()
47
+ )
48
+ # and then when you call .execute() or .materialize() you'll get a progress bar!
49
+ """
50
+
51
+ def __init__(self, desc: str = "Graph execution", max_node_name_width: int = 50, **kwargs):
52
+ """Create a new Progress Bar adapter.
53
+
54
+ :param desc: The description to show in the progress bar. E.g. DAG Name is a good choice.
55
+ :param kwargs: Additional kwargs to pass to TQDM. See TQDM docs for more info.
56
+ :param node_name_target_width: the target width for the node name so that the progress bar is consistent. If this is None, it will take the longest, until it hits max_node_name_width.
57
+
58
+ """
59
+ self.desc = desc
60
+ self.kwargs = kwargs
61
+ self.node_name_target_width = (
62
+ None # what we target padding for -- starts at None as we adjust.
63
+ )
64
+ self.max_node_name_width = max_node_name_width # what we cap the padding at.
65
+ self.progress_bar = None
66
+
67
+ def _get_node_name_display(self, node_name: str) -> str:
68
+ """Gives the node name display given a max width and a node name. Max width could be DAG-dependent."""
69
+ out = (
70
+ node_name
71
+ if len(node_name) <= self.node_name_target_width
72
+ else node_name[: self.node_name_target_width - 3] + "..."
73
+ )
74
+ if len(out) < self.node_name_target_width:
75
+ out += " " * (self.node_name_target_width - len(out))
76
+ return out
77
+
78
+ def run_before_graph_execution(
79
+ self,
80
+ *,
81
+ graph: graph_types.HamiltonGraph,
82
+ final_vars: list[str],
83
+ inputs: dict[str, Any],
84
+ overrides: dict[str, Any],
85
+ execution_path: Collection[str],
86
+ **future_kwargs: Any,
87
+ ):
88
+ total_node_to_execute = len(execution_path)
89
+ max_node_name_length = min(
90
+ max([len(node) for node in execution_path]), self.max_node_name_width
91
+ )
92
+ if self.node_name_target_width is None:
93
+ self.node_name_target_width = max_node_name_length
94
+ self.progress_bar = tqdm.tqdm(
95
+ desc=self.desc, unit="funcs", total=total_node_to_execute, **self.kwargs
96
+ )
97
+
98
+ def run_before_node_execution(
99
+ self,
100
+ *,
101
+ node_name: str,
102
+ node_tags: dict[str, Any],
103
+ node_kwargs: dict[str, Any],
104
+ node_return_type: type,
105
+ task_id: str | None,
106
+ **future_kwargs: Any,
107
+ ):
108
+ name_display = self._get_node_name_display(node_name)
109
+ self.progress_bar.set_description_str(f"{self.desc} -> {name_display}")
110
+
111
+ def run_after_node_execution(self, **future_kwargs):
112
+ self.progress_bar.update(1)
113
+
114
+ def run_after_graph_execution(self, **future_kwargs):
115
+ name_part = "Execution Complete!"
116
+ if len(name_part) > self.node_name_target_width:
117
+ padding = ""
118
+ else:
119
+ padding = " " * (self.node_name_target_width - len(name_part))
120
+ self.progress_bar.set_description_str(f"{self.desc} -> {name_part + padding}")
121
+ self.progress_bar.set_postfix({})
122
+ self.progress_bar.close()
@@ -0,0 +1,129 @@
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 numpy as np
21
+ import pandas as pd
22
+
23
+ from hamilton import base
24
+
25
+ try:
26
+ import vaex
27
+ except ImportError as e:
28
+ raise NotImplementedError("Vaex is not installed.") from e
29
+
30
+
31
+ class VaexDataFrameResult(base.ResultMixin):
32
+ """A ResultBuilder that produces a Vaex dataframe.
33
+
34
+ Use this when you want to create a Vaex dataframe from the outputs.
35
+ Caveat: you need to ensure that the length
36
+ of the outputs is the same (except scalars), otherwise you will get an error;
37
+ mixed outputs aren't that well handled.
38
+
39
+ To use:
40
+
41
+ .. code-block:: python
42
+
43
+ from hamilton import base, driver
44
+ from hamilton.plugins import h_vaex
45
+
46
+ vaex_builder = h_vaex.VaexDataFrameResult()
47
+ adapter = base.SimplePythonGraphAdapter(vaex_builder)
48
+ dr = driver.Driver(config, *modules, adapter=adapter)
49
+ df = dr.execute([...], inputs=...) # returns vaex dataframe
50
+
51
+ Note: this is just a first attempt at something for Vaex.
52
+ Think it should handle more? Come chat/open a PR!
53
+ """
54
+
55
+ def build_result(
56
+ self,
57
+ **outputs: dict[str, vaex.expression.Expression | vaex.dataframe.DataFrame | Any],
58
+ ):
59
+ """This is the method that Hamilton will call to build the final result.
60
+ It will pass in the results of the requested outputs that
61
+ you passed in to the execute() method.
62
+
63
+ :param outputs: The results of the requested outputs.
64
+ :return: a Vaex DataFrame.
65
+ """
66
+
67
+ # We split all outputs into DataFrames, arrays and scalars
68
+ dfs: list[vaex.dataframe.DataFrame] = [] # Vaex DataFrames from outputs
69
+ arrays: dict[str, np.ndarray] = dict() # arrays from outputs
70
+ scalars: dict[str, Any] = dict() # scalars from outputs
71
+
72
+ for name, value in outputs.items():
73
+ if isinstance(value, vaex.dataframe.DataFrame):
74
+ dfs.append(value)
75
+ elif isinstance(value, vaex.expression.Expression):
76
+ nparray = value.to_numpy()
77
+ if nparray.ndim == 0: # value is scalar
78
+ scalars[name] = nparray.item()
79
+ elif nparray.shape == (1,): # value is scalar
80
+ scalars[name] = nparray[0]
81
+ else: # value is array
82
+ arrays[name] = nparray
83
+ elif isinstance(value, np.ndarray):
84
+ if value.ndim == 0: # value is scalar
85
+ scalars[name] = value.item()
86
+ elif value.shape == (1,): # value is scalar
87
+ scalars[name] = value[0]
88
+ else: # value is array
89
+ arrays[name] = value
90
+ elif pd.api.types.is_scalar(value): # value is scalar
91
+ scalars[name] = value
92
+ else:
93
+ value_type = str(type(value))
94
+ message = f"VaexDataFrameResult doesn't support {value_type}"
95
+ raise NotImplementedError(message)
96
+
97
+ df = None
98
+
99
+ if arrays:
100
+ # Check if all arrays have correct and identical shapes.
101
+ first_expression_shape = next(arrays.values().__iter__()).shape
102
+ if len(first_expression_shape) > 1:
103
+ raise NotImplementedError(
104
+ "VaexDataFrameResult supports only one-dimensional Expression results"
105
+ )
106
+ for _name, a in arrays.items():
107
+ if a.shape != first_expression_shape:
108
+ raise NotImplementedError(
109
+ "VaexDataFrameResult supports Expression results with same dimension only"
110
+ )
111
+
112
+ # All scalars become arrays with the same shape as other arrays.
113
+ for name, scalar in scalars.items():
114
+ arrays[name] = np.full(first_expression_shape, scalar)
115
+
116
+ df = vaex.from_arrays(**arrays)
117
+
118
+ elif scalars:
119
+ # There are not arrays in outputs,
120
+ # so we construct Vaex DataFrame with one row consisting of scalars.
121
+ df = vaex.from_arrays(**{name: np.array([value]) for name, value in scalars.items()})
122
+
123
+ if df:
124
+ dfs.append(df)
125
+
126
+ return vaex.concat(dfs)
127
+
128
+ def output_type(self) -> type:
129
+ return vaex.dataframe.DataFrame
@@ -0,0 +1,236 @@
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, Mapping, Sequence
20
+ from os import PathLike
21
+ from typing import (
22
+ Any,
23
+ BinaryIO,
24
+ )
25
+
26
+ try:
27
+ from datasets import (
28
+ Dataset,
29
+ DatasetDict,
30
+ DownloadConfig,
31
+ DownloadMode,
32
+ Features,
33
+ IterableDataset,
34
+ IterableDatasetDict,
35
+ VerificationMode,
36
+ Version,
37
+ load_dataset,
38
+ )
39
+ from datasets.formatting.formatting import LazyBatch
40
+ except ImportError as e:
41
+ raise NotImplementedError("huggingface datasets library is not installed.") from e
42
+
43
+ try:
44
+ import lancedb
45
+ from lancedb import table # noqa: F401
46
+ except ImportError:
47
+ lancedb = None
48
+
49
+ from hamilton import registry
50
+ from hamilton.io import utils
51
+ from hamilton.io.data_adapters import DataLoader, DataSaver
52
+
53
+ COLUMN_FRIENDLY_DF_TYPE = False
54
+
55
+ HF_types = (DatasetDict, Dataset, IterableDatasetDict, IterableDataset)
56
+
57
+
58
+ @dataclasses.dataclass
59
+ class HuggingFaceDSLoader(DataLoader):
60
+ """Data loader for hugging face datasets. Uses load_data method."""
61
+
62
+ path: str
63
+ dataset_name: str | None = None # this can't be `name` because it clashes with `.name()`
64
+ data_dir: str | None = None
65
+ data_files: str | Sequence[str] | Mapping[str, str | Sequence[str]] | None = None
66
+ split: str | None = None
67
+ cache_dir: str | None = None
68
+ features: Features | None = None
69
+ download_config: DownloadConfig | None = None
70
+ download_mode: DownloadMode | str | None = None
71
+ verification_mode: VerificationMode | str | None = None
72
+ ignore_verifications = "deprecated"
73
+ keep_in_memory: bool | None = None
74
+ save_infos: bool = False
75
+ revision: str | Version | None = None
76
+ token: bool | str | None = None
77
+ use_auth_token = "deprecated"
78
+ task = "deprecated"
79
+ streaming: bool = False
80
+ num_proc: int | None = None
81
+ storage_options: dict | None = None
82
+ config_kwargs: dict | None = None
83
+
84
+ @classmethod
85
+ def applicable_types(cls) -> Collection[type]:
86
+ return list(HF_types)
87
+
88
+ def _get_loading_kwargs(self) -> dict:
89
+ # Puts kwargs in a dict
90
+ kwargs = dataclasses.asdict(self)
91
+ # we send path separately
92
+ del kwargs["path"]
93
+ config_kwargs: dict | None = kwargs.pop("config_kwargs", None)
94
+ if config_kwargs:
95
+ # add config kwargs as needed.
96
+ kwargs.update(config_kwargs)
97
+
98
+ # need to pass in name
99
+ kwargs["name"] = kwargs.pop("dataset_name", None)
100
+
101
+ return kwargs
102
+
103
+ def load_data(self, type_: type) -> tuple[HF_types, dict[str, Any]]:
104
+ """Loads the data set given the path and class values."""
105
+ ds = load_dataset(self.path, **self._get_loading_kwargs())
106
+ is_dataset = isinstance(ds, Dataset)
107
+ f_meta = {"path": self.path}
108
+ ds_meta = {"rows": ds.num_rows, "columns": ds.column_names}
109
+ if is_dataset:
110
+ ds_meta["size_in_bytes"] = ds.size_in_bytes
111
+ ds_meta["features"] = ds.features.to_dict()
112
+ return ds, {"file_metadata": f_meta, "dataset_metadata": ds_meta}
113
+
114
+ @classmethod
115
+ def name(cls) -> str:
116
+ return "hf_dataset"
117
+
118
+
119
+ @dataclasses.dataclass
120
+ class HuggingFaceDSParquetSaver(DataSaver):
121
+ """Saves a Huggingface dataset to parquet."""
122
+
123
+ path_or_buf: PathLike | BinaryIO
124
+ batch_size: int | None = None
125
+ parquet_writer_kwargs: dict | None = None
126
+
127
+ @classmethod
128
+ def applicable_types(cls) -> Collection[type]:
129
+ return list(HF_types)
130
+
131
+ @classmethod
132
+ def applies_to(cls, type_: type[type]) -> bool:
133
+ return type_ in HF_types
134
+
135
+ def _get_saving_kwargs(self) -> dict:
136
+ # Puts kwargs in a dict
137
+ kwargs = dataclasses.asdict(self)
138
+ # we put path_or_buff as a positional argument
139
+ del kwargs["path_or_buf"]
140
+ parquet_writer_kwargs: dict | None = kwargs.pop("parquet_writer_kwargs", None)
141
+ if parquet_writer_kwargs:
142
+ # add config kwargs as needed.
143
+ kwargs.update(parquet_writer_kwargs)
144
+
145
+ return kwargs
146
+
147
+ def save_data(self, ds: HF_types) -> dict[str, Any]:
148
+ """Saves the data to parquet."""
149
+ is_dataset = isinstance(ds, Dataset)
150
+ ds.to_parquet(self.path_or_buf, **self._get_saving_kwargs())
151
+ ds_meta = {
152
+ "rows": ds.num_rows,
153
+ "columns": ds.column_names,
154
+ }
155
+ if is_dataset:
156
+ ds_meta.update({"size_in_bytes": ds.size_in_bytes, "features": ds.features.to_dict()})
157
+ if isinstance(self.path_or_buf, BinaryIO):
158
+ f_meta = {}
159
+ else:
160
+ f_meta = (utils.get_file_metadata(self.path_or_buf),)
161
+ return {"file_metadata": f_meta, "dataset_metadata": ds_meta}
162
+
163
+ @classmethod
164
+ def name(cls) -> str:
165
+ return "parquet"
166
+
167
+
168
+ # we do this here just in case lancedb is not installed.
169
+ if lancedb is not None:
170
+
171
+ def _batch_write(
172
+ dataset_batch: LazyBatch, db: lancedb.DBConnection, table_name: str, columns: str
173
+ ) -> None:
174
+ """Helper function to batch write to lancedb."""
175
+ if columns is None:
176
+ data = dataset_batch.pa_table
177
+ else:
178
+ data = dataset_batch.pa_table.select(columns)
179
+ try:
180
+ db.create_table(table_name, data)
181
+ except (OSError, ValueError):
182
+ tbl = db.open_table(table_name)
183
+ tbl.add(data)
184
+ return None
185
+
186
+ @dataclasses.dataclass
187
+ class HuggingFaceDSLanceDBSaver(DataSaver):
188
+ """Data saver that saves Huggingface datasets to lancedb."""
189
+
190
+ db_client: lancedb.DBConnection
191
+ table_name: str
192
+ columns_to_write: list[str] = None # None means all.
193
+ write_batch_size: int = 100
194
+
195
+ @classmethod
196
+ def applicable_types(cls) -> Collection[type]:
197
+ return list(HF_types)
198
+
199
+ def save_data(self, ds: HF_types) -> dict[str, Any]:
200
+ """This batches writes to lancedb."""
201
+ ds.map(
202
+ _batch_write,
203
+ batched=True,
204
+ batch_size=self.write_batch_size,
205
+ fn_kwargs={
206
+ "db": self.db_client,
207
+ "table_name": self.table_name,
208
+ "columns": self.columns_to_write,
209
+ },
210
+ desc=f"writing to lancedb table {self.table_name}",
211
+ )
212
+ is_dataset = isinstance(ds, Dataset)
213
+ ds_meta = {
214
+ "rows": ds.num_rows,
215
+ "columns": ds.column_names,
216
+ }
217
+ if is_dataset:
218
+ ds_meta.update(
219
+ {"size_in_bytes": ds.size_in_bytes, "features": ds.features.to_dict()}
220
+ )
221
+ return {"db_meta": {"table_name": self.table_name}, "dataset_metadata": ds_meta}
222
+
223
+ @classmethod
224
+ def name(cls) -> str:
225
+ return "lancedb"
226
+
227
+
228
+ def register_data_loaders_savers():
229
+ loaders = [HuggingFaceDSLoader, HuggingFaceDSParquetSaver]
230
+ if lancedb:
231
+ loaders.append(HuggingFaceDSLanceDBSaver)
232
+ for loader in loaders:
233
+ registry.register_adapter(loader)
234
+
235
+
236
+ register_data_loaders_savers()