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.
- apache_hamilton-1.90.0.dev0.dist-info/METADATA +407 -0
- apache_hamilton-1.90.0.dev0.dist-info/RECORD +151 -0
- apache_hamilton-1.90.0.dev0.dist-info/WHEEL +4 -0
- apache_hamilton-1.90.0.dev0.dist-info/entry_points.txt +9 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/DISCLAIMER +10 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/LICENSE +228 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/NOTICE +5 -0
- hamilton/__init__.py +24 -0
- hamilton/ad_hoc_utils.py +132 -0
- hamilton/async_driver.py +465 -0
- hamilton/base.py +466 -0
- hamilton/caching/__init__.py +16 -0
- hamilton/caching/adapter.py +1475 -0
- hamilton/caching/cache_key.py +70 -0
- hamilton/caching/fingerprinting.py +287 -0
- hamilton/caching/stores/__init__.py +16 -0
- hamilton/caching/stores/base.py +242 -0
- hamilton/caching/stores/file.py +140 -0
- hamilton/caching/stores/memory.py +297 -0
- hamilton/caching/stores/sqlite.py +282 -0
- hamilton/caching/stores/utils.py +40 -0
- hamilton/cli/__init__.py +16 -0
- hamilton/cli/__main__.py +328 -0
- hamilton/cli/commands.py +126 -0
- hamilton/cli/logic.py +338 -0
- hamilton/common/__init__.py +76 -0
- hamilton/contrib/__init__.py +41 -0
- hamilton/data_quality/__init__.py +16 -0
- hamilton/data_quality/base.py +198 -0
- hamilton/data_quality/default_validators.py +560 -0
- hamilton/data_quality/pandera_validators.py +121 -0
- hamilton/dataflows/__init__.py +726 -0
- hamilton/dataflows/template/README.md +25 -0
- hamilton/dataflows/template/__init__.py +54 -0
- hamilton/dataflows/template/author.md +28 -0
- hamilton/dataflows/template/requirements.txt +0 -0
- hamilton/dataflows/template/tags.json +7 -0
- hamilton/dataflows/template/valid_configs.jsonl +1 -0
- hamilton/dev_utils/__init__.py +16 -0
- hamilton/dev_utils/deprecation.py +204 -0
- hamilton/driver.py +2112 -0
- hamilton/execution/__init__.py +16 -0
- hamilton/execution/debugging_utils.py +56 -0
- hamilton/execution/executors.py +502 -0
- hamilton/execution/graph_functions.py +421 -0
- hamilton/execution/grouping.py +430 -0
- hamilton/execution/state.py +539 -0
- hamilton/experimental/__init__.py +27 -0
- hamilton/experimental/databackend.py +61 -0
- hamilton/experimental/decorators/__init__.py +16 -0
- hamilton/experimental/decorators/parameterize_frame.py +233 -0
- hamilton/experimental/h_async.py +29 -0
- hamilton/experimental/h_cache.py +413 -0
- hamilton/experimental/h_dask.py +28 -0
- hamilton/experimental/h_databackends.py +174 -0
- hamilton/experimental/h_ray.py +28 -0
- hamilton/experimental/h_spark.py +32 -0
- hamilton/function_modifiers/README +40 -0
- hamilton/function_modifiers/__init__.py +121 -0
- hamilton/function_modifiers/adapters.py +900 -0
- hamilton/function_modifiers/base.py +859 -0
- hamilton/function_modifiers/configuration.py +310 -0
- hamilton/function_modifiers/delayed.py +202 -0
- hamilton/function_modifiers/dependencies.py +246 -0
- hamilton/function_modifiers/expanders.py +1230 -0
- hamilton/function_modifiers/macros.py +1634 -0
- hamilton/function_modifiers/metadata.py +434 -0
- hamilton/function_modifiers/recursive.py +908 -0
- hamilton/function_modifiers/validation.py +289 -0
- hamilton/function_modifiers_base.py +31 -0
- hamilton/graph.py +1153 -0
- hamilton/graph_types.py +264 -0
- hamilton/graph_utils.py +41 -0
- hamilton/htypes.py +450 -0
- hamilton/io/__init__.py +32 -0
- hamilton/io/data_adapters.py +216 -0
- hamilton/io/default_data_loaders.py +224 -0
- hamilton/io/materialization.py +500 -0
- hamilton/io/utils.py +158 -0
- hamilton/lifecycle/__init__.py +67 -0
- hamilton/lifecycle/api.py +833 -0
- hamilton/lifecycle/base.py +1130 -0
- hamilton/lifecycle/default.py +802 -0
- hamilton/log_setup.py +47 -0
- hamilton/models.py +92 -0
- hamilton/node.py +449 -0
- hamilton/plugins/README.md +48 -0
- hamilton/plugins/__init__.py +16 -0
- hamilton/plugins/dask_extensions.py +47 -0
- hamilton/plugins/dlt_extensions.py +161 -0
- hamilton/plugins/geopandas_extensions.py +49 -0
- hamilton/plugins/h_dask.py +331 -0
- hamilton/plugins/h_ddog.py +522 -0
- hamilton/plugins/h_diskcache.py +163 -0
- hamilton/plugins/h_experiments/__init__.py +22 -0
- hamilton/plugins/h_experiments/__main__.py +62 -0
- hamilton/plugins/h_experiments/cache.py +39 -0
- hamilton/plugins/h_experiments/data_model.py +68 -0
- hamilton/plugins/h_experiments/hook.py +219 -0
- hamilton/plugins/h_experiments/server.py +375 -0
- hamilton/plugins/h_kedro.py +152 -0
- hamilton/plugins/h_logging.py +454 -0
- hamilton/plugins/h_mcp/__init__.py +28 -0
- hamilton/plugins/h_mcp/__main__.py +33 -0
- hamilton/plugins/h_mcp/_helpers.py +129 -0
- hamilton/plugins/h_mcp/_templates.py +417 -0
- hamilton/plugins/h_mcp/server.py +328 -0
- hamilton/plugins/h_mlflow.py +335 -0
- hamilton/plugins/h_narwhals.py +134 -0
- hamilton/plugins/h_openlineage.py +400 -0
- hamilton/plugins/h_opentelemetry.py +167 -0
- hamilton/plugins/h_pandas.py +257 -0
- hamilton/plugins/h_pandera.py +117 -0
- hamilton/plugins/h_polars.py +304 -0
- hamilton/plugins/h_polars_lazyframe.py +282 -0
- hamilton/plugins/h_pyarrow.py +56 -0
- hamilton/plugins/h_pydantic.py +127 -0
- hamilton/plugins/h_ray.py +242 -0
- hamilton/plugins/h_rich.py +142 -0
- hamilton/plugins/h_schema.py +493 -0
- hamilton/plugins/h_slack.py +100 -0
- hamilton/plugins/h_spark.py +1380 -0
- hamilton/plugins/h_threadpool.py +125 -0
- hamilton/plugins/h_tqdm.py +122 -0
- hamilton/plugins/h_vaex.py +129 -0
- hamilton/plugins/huggingface_extensions.py +236 -0
- hamilton/plugins/ibis_extensions.py +93 -0
- hamilton/plugins/jupyter_magic.py +622 -0
- hamilton/plugins/kedro_extensions.py +117 -0
- hamilton/plugins/lightgbm_extensions.py +99 -0
- hamilton/plugins/matplotlib_extensions.py +108 -0
- hamilton/plugins/mlflow_extensions.py +216 -0
- hamilton/plugins/numpy_extensions.py +105 -0
- hamilton/plugins/pandas_extensions.py +1763 -0
- hamilton/plugins/plotly_extensions.py +150 -0
- hamilton/plugins/polars_extensions.py +71 -0
- hamilton/plugins/polars_implementations.py +25 -0
- hamilton/plugins/polars_lazyframe_extensions.py +302 -0
- hamilton/plugins/polars_post_1_0_0_extensions.py +877 -0
- hamilton/plugins/polars_pre_1_0_0_extension.py +836 -0
- hamilton/plugins/pydantic_extensions.py +98 -0
- hamilton/plugins/pyspark_pandas_extensions.py +47 -0
- hamilton/plugins/sklearn_plot_extensions.py +129 -0
- hamilton/plugins/spark_extensions.py +105 -0
- hamilton/plugins/vaex_extensions.py +51 -0
- hamilton/plugins/xgboost_extensions.py +91 -0
- hamilton/plugins/yaml_extensions.py +89 -0
- hamilton/registry.py +254 -0
- hamilton/settings.py +18 -0
- hamilton/telemetry.py +50 -0
- 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()
|