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,134 @@
|
|
|
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 narwhals as nw
|
|
21
|
+
|
|
22
|
+
from hamilton.lifecycle import api
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NarwhalsAdapter(api.NodeExecutionMethod):
|
|
26
|
+
"""Adapter to make it simpler to use narwhals with Hamilton.
|
|
27
|
+
|
|
28
|
+
.. code-block:: python
|
|
29
|
+
|
|
30
|
+
from hamilton import base, driver
|
|
31
|
+
from hamilton.plugins import h_narwhals
|
|
32
|
+
import example
|
|
33
|
+
|
|
34
|
+
# pandas
|
|
35
|
+
dr = (
|
|
36
|
+
driver.Builder()
|
|
37
|
+
.with_config({"load": "pandas"})
|
|
38
|
+
.with_modules(example)
|
|
39
|
+
.with_adapters(
|
|
40
|
+
h_narwhals.NarwhalsAdapter(),
|
|
41
|
+
h_narwhals.NarwhalsDataFrameResultBuilder(
|
|
42
|
+
base.PandasDataFrameResult()
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
.build()
|
|
46
|
+
)
|
|
47
|
+
result = dr.execute(
|
|
48
|
+
[example.group_by_mean, example.example1],
|
|
49
|
+
inputs={"col_name": "a"}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def run_to_execute_node(
|
|
55
|
+
self,
|
|
56
|
+
*,
|
|
57
|
+
node_name: str,
|
|
58
|
+
node_tags: dict[str, Any],
|
|
59
|
+
node_callable: Any,
|
|
60
|
+
node_kwargs: dict[str, Any],
|
|
61
|
+
task_id: str | None,
|
|
62
|
+
**future_kwargs: Any,
|
|
63
|
+
) -> Any:
|
|
64
|
+
"""This method is responsible for executing the node and returning the result.
|
|
65
|
+
|
|
66
|
+
It uses `nw_kwargs` from the node tags to know if any special flags should be passed to the narwhals
|
|
67
|
+
decorator function.
|
|
68
|
+
|
|
69
|
+
:param node_name: Name of the node.
|
|
70
|
+
:param node_tags: Tags of the node.
|
|
71
|
+
:param node_callable: Callable of the node.
|
|
72
|
+
:param node_kwargs: Keyword arguments to pass to the node.
|
|
73
|
+
:param task_id: The ID of the task, none if not in a task-based environment
|
|
74
|
+
:param future_kwargs: Additional keyword arguments -- this is kept for backwards compatibility
|
|
75
|
+
:return: The result of the node execution -- up to you to return this.
|
|
76
|
+
"""
|
|
77
|
+
nw_kwargs = {}
|
|
78
|
+
if "nw_kwargs" in node_tags:
|
|
79
|
+
nw_kwargs = {k: True for k in node_tags["nw_kwargs"]}
|
|
80
|
+
nw_func = nw.narwhalify(node_callable, **nw_kwargs)
|
|
81
|
+
return nw_func(**node_kwargs)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class NarwhalsDataFrameResultBuilder(api.ResultBuilder):
|
|
85
|
+
"""Builds the result. It unwraps the narwhals parts of it and delegates to the passed in result builder.
|
|
86
|
+
|
|
87
|
+
.. code-block:: python
|
|
88
|
+
|
|
89
|
+
from hamilton import base, driver
|
|
90
|
+
from hamilton.plugins import h_narwhals, h_polars
|
|
91
|
+
import example
|
|
92
|
+
|
|
93
|
+
# polars
|
|
94
|
+
dr = (
|
|
95
|
+
driver.Builder()
|
|
96
|
+
.with_config({"load": "polars"})
|
|
97
|
+
.with_modules(example)
|
|
98
|
+
.with_adapters(
|
|
99
|
+
h_narwhals.NarwhalsAdapter(),
|
|
100
|
+
h_narwhals.NarwhalsDataFrameResultBuilder(
|
|
101
|
+
h_polars.PolarsDataFrameResult()
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
.build()
|
|
105
|
+
)
|
|
106
|
+
result = dr.execute(
|
|
107
|
+
["group_by_mean", "example1"],
|
|
108
|
+
inputs={"col_name": "a"}
|
|
109
|
+
)
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, result_builder: api.ResultBuilder | api.LegacyResultMixin):
|
|
113
|
+
self.result_builder = result_builder
|
|
114
|
+
|
|
115
|
+
def build_result(self, **outputs: Any) -> Any:
|
|
116
|
+
"""Given a set of outputs, build the result.
|
|
117
|
+
|
|
118
|
+
:param outputs: the outputs from the execution of the graph.
|
|
119
|
+
:return: the result of the execution of the graph.
|
|
120
|
+
"""
|
|
121
|
+
de_narwhaled_outputs = {}
|
|
122
|
+
for key, value in outputs.items():
|
|
123
|
+
if isinstance(value, (nw.DataFrame, nw.Series)):
|
|
124
|
+
de_narwhaled_outputs[key] = nw.to_native(value)
|
|
125
|
+
else:
|
|
126
|
+
de_narwhaled_outputs[key] = value
|
|
127
|
+
|
|
128
|
+
return self.result_builder.build_result(**de_narwhaled_outputs)
|
|
129
|
+
|
|
130
|
+
def output_type(self) -> type:
|
|
131
|
+
"""Returns the output type of this result builder
|
|
132
|
+
:return: the type that this creates
|
|
133
|
+
"""
|
|
134
|
+
return self.result_builder.output_type()
|
|
@@ -0,0 +1,400 @@
|
|
|
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 json
|
|
19
|
+
import sys
|
|
20
|
+
import traceback
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
import attr
|
|
25
|
+
from openlineage.client import OpenLineageClient, event_v2, facet_v2
|
|
26
|
+
|
|
27
|
+
from hamilton import graph as h_graph
|
|
28
|
+
from hamilton import graph_types, node
|
|
29
|
+
from hamilton.lifecycle import base
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@attr.s
|
|
33
|
+
class HamiltonFacet(facet_v2.RunFacet):
|
|
34
|
+
"""Class for Hamilton Facet."""
|
|
35
|
+
|
|
36
|
+
hamilton_run_id: str = attr.ib()
|
|
37
|
+
graph_version: str = attr.ib()
|
|
38
|
+
final_vars: list[str] = attr.ib()
|
|
39
|
+
inputs: list[str] = attr.ib()
|
|
40
|
+
overrides: list[str] = attr.ib()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_stack_trace(exception):
|
|
44
|
+
# Python changed this API in 3.10
|
|
45
|
+
if sys.version_info < (3, 10, 0):
|
|
46
|
+
return traceback.format_exception(
|
|
47
|
+
etype=type(exception), value=exception, tb=exception.__traceback__
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return "".join(traceback.format_exception(exception))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extract_schema_facet(metadata):
|
|
54
|
+
"""Extracts the schema facet from the metadata."""
|
|
55
|
+
if "dataframe_metadata" in metadata:
|
|
56
|
+
schema_datatypes = [
|
|
57
|
+
facet_v2.schema_dataset.SchemaDatasetFacetFields(
|
|
58
|
+
name=k,
|
|
59
|
+
type=v,
|
|
60
|
+
)
|
|
61
|
+
for k, v in zip(
|
|
62
|
+
metadata["dataframe_metadata"]["column_names"],
|
|
63
|
+
metadata["dataframe_metadata"]["datatypes"],
|
|
64
|
+
strict=False,
|
|
65
|
+
)
|
|
66
|
+
]
|
|
67
|
+
schema_facet = facet_v2.schema_dataset.SchemaDatasetFacet(
|
|
68
|
+
fields=schema_datatypes,
|
|
69
|
+
)
|
|
70
|
+
return schema_facet
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def create_input_dataset(namespace: str, metadata: dict, node_) -> list[event_v2.InputDataset]:
|
|
75
|
+
"""Creates the open lineage input dataset."""
|
|
76
|
+
datasource_facet = None
|
|
77
|
+
storage_facet = None
|
|
78
|
+
sql_facet = None
|
|
79
|
+
if "file_metadata" in metadata:
|
|
80
|
+
name = node_.name
|
|
81
|
+
if ".loader" in name:
|
|
82
|
+
name = name.split(".loader")[0]
|
|
83
|
+
path = metadata["file_metadata"]["path"]
|
|
84
|
+
format = path.split(".")[-1] if "." in name else "unknown"
|
|
85
|
+
storage_facet = facet_v2.storage_dataset.StorageDatasetFacet(
|
|
86
|
+
storageLayer="FileSystem",
|
|
87
|
+
fileFormat=format,
|
|
88
|
+
)
|
|
89
|
+
datasource_facet = facet_v2.datasource_dataset.DatasourceDatasetFacet(
|
|
90
|
+
name=name,
|
|
91
|
+
uri=path,
|
|
92
|
+
)
|
|
93
|
+
elif "sql_metadata" in metadata:
|
|
94
|
+
name = metadata["sql_metadata"]["table_name"]
|
|
95
|
+
sql_facet = facet_v2.sql_job.SQLJobFacet(
|
|
96
|
+
query=metadata["sql_metadata"]["query"],
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
name = "--UNKNOWN--"
|
|
100
|
+
schema_facet = extract_schema_facet(metadata)
|
|
101
|
+
inputFacets = {}
|
|
102
|
+
if storage_facet:
|
|
103
|
+
inputFacets["storage"] = storage_facet
|
|
104
|
+
if datasource_facet:
|
|
105
|
+
inputFacets["dataSource"] = datasource_facet
|
|
106
|
+
if schema_facet:
|
|
107
|
+
inputFacets["schema"] = schema_facet
|
|
108
|
+
if len(inputFacets) == 0:
|
|
109
|
+
inputFacets = None
|
|
110
|
+
inputs = [event_v2.InputDataset(namespace, name, facets=inputFacets)]
|
|
111
|
+
return inputs, sql_facet
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def create_output_dataset(namespace: str, metadata: dict, node_) -> list[event_v2.OutputDataset]:
|
|
115
|
+
"""Creates the open lineage output dataset."""
|
|
116
|
+
datasource_facet = None
|
|
117
|
+
storage_facet = None
|
|
118
|
+
if "file_metadata" in metadata:
|
|
119
|
+
name = metadata["file_metadata"]["path"]
|
|
120
|
+
format = name.split(".")[-1] if "." in name else "unknown"
|
|
121
|
+
storage_facet = facet_v2.storage_dataset.StorageDatasetFacet(
|
|
122
|
+
storageLayer="FileSystem",
|
|
123
|
+
fileFormat=format,
|
|
124
|
+
)
|
|
125
|
+
datasource_facet = facet_v2.datasource_dataset.DatasourceDatasetFacet(
|
|
126
|
+
name=node_.name,
|
|
127
|
+
uri=name,
|
|
128
|
+
)
|
|
129
|
+
elif "sql_metadata" in metadata:
|
|
130
|
+
name = metadata["sql_metadata"]["table_name"]
|
|
131
|
+
else:
|
|
132
|
+
name = "--UNKNOWN--"
|
|
133
|
+
schema_facet = extract_schema_facet(metadata)
|
|
134
|
+
outputFacets = {}
|
|
135
|
+
if storage_facet:
|
|
136
|
+
outputFacets["storage"] = storage_facet
|
|
137
|
+
if datasource_facet:
|
|
138
|
+
outputFacets["dataSource"] = datasource_facet
|
|
139
|
+
if schema_facet:
|
|
140
|
+
outputFacets["schema"] = schema_facet
|
|
141
|
+
if len(outputFacets) == 0:
|
|
142
|
+
outputFacets = None
|
|
143
|
+
outputs = [event_v2.OutputDataset(namespace, name, facets=outputFacets)]
|
|
144
|
+
return outputs
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class OpenLineageAdapter(
|
|
148
|
+
base.BasePreGraphExecute,
|
|
149
|
+
base.BasePreNodeExecute,
|
|
150
|
+
base.BasePostNodeExecute,
|
|
151
|
+
base.BasePostGraphExecute,
|
|
152
|
+
):
|
|
153
|
+
"""
|
|
154
|
+
This adapter emits OpenLineage events.
|
|
155
|
+
|
|
156
|
+
.. code-block:: python
|
|
157
|
+
|
|
158
|
+
# create the openlineage client
|
|
159
|
+
from openlineage.client import OpenLineageClient
|
|
160
|
+
|
|
161
|
+
# write to file
|
|
162
|
+
from openlineage.client.transport.file import FileConfig, FileTransport
|
|
163
|
+
file_config = FileConfig(
|
|
164
|
+
log_file_path="/path/to/your/file",
|
|
165
|
+
append=False,
|
|
166
|
+
)
|
|
167
|
+
client = OpenLineageClient(transport=FileTransport(file_config))
|
|
168
|
+
|
|
169
|
+
# write to HTTP, e.g. marquez
|
|
170
|
+
client = OpenLineageClient(url="http://localhost:5000")
|
|
171
|
+
|
|
172
|
+
# create the adapter
|
|
173
|
+
adapter = OpenLineageAdapter(client, "my_namespace", "my_job_name")
|
|
174
|
+
|
|
175
|
+
# add to Hamilton
|
|
176
|
+
# import your pipeline code
|
|
177
|
+
dr = driver.Builder().with_modules(YOUR_MODULES).with_adapters(adapter).build()
|
|
178
|
+
# execute as normal -- and openlineage events will be emitted
|
|
179
|
+
dr.execute(...)
|
|
180
|
+
|
|
181
|
+
Note for data lineage to be emitted, you must use the "materializer" abstraction to provide
|
|
182
|
+
metadata. See https://hamilton.apache.org/concepts/materialization/.
|
|
183
|
+
This can be done via the `@datasaver()` and `@dataloader()` decorators, or
|
|
184
|
+
using the `@load_from` or `@save_to` decorators, as well as passing in data savers
|
|
185
|
+
and data loaders via `.with_materializers()` on the Driver Builder, or via `.materialize()`
|
|
186
|
+
on the driver object.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
def __init__(self, client: OpenLineageClient, namespace: str, job_name: str):
|
|
190
|
+
"""Constructor. You pass in the OLClient.
|
|
191
|
+
|
|
192
|
+
:param self:
|
|
193
|
+
:param client:
|
|
194
|
+
:param namespace:
|
|
195
|
+
:param job_name:
|
|
196
|
+
:return:
|
|
197
|
+
"""
|
|
198
|
+
# self.transport = transport
|
|
199
|
+
self.client = client
|
|
200
|
+
self.namespace = namespace
|
|
201
|
+
self.job_name = job_name
|
|
202
|
+
|
|
203
|
+
def pre_graph_execute(
|
|
204
|
+
self,
|
|
205
|
+
run_id: str,
|
|
206
|
+
graph: h_graph.FunctionGraph,
|
|
207
|
+
final_vars: list[str],
|
|
208
|
+
inputs: dict[str, Any],
|
|
209
|
+
overrides: dict[str, Any],
|
|
210
|
+
):
|
|
211
|
+
"""
|
|
212
|
+
Emits a Run START event.
|
|
213
|
+
Emits a Job Event with the sourceCode Facet for the entire DAG as the job.
|
|
214
|
+
|
|
215
|
+
:param run_id:
|
|
216
|
+
:param graph:
|
|
217
|
+
:param final_vars:
|
|
218
|
+
:param inputs:
|
|
219
|
+
:param overrides:
|
|
220
|
+
:return:
|
|
221
|
+
"""
|
|
222
|
+
exportable_graph = graph_types.HamiltonGraph.from_graph(graph)
|
|
223
|
+
graph_version = exportable_graph.version
|
|
224
|
+
node_dict = [n.as_dict() for n in exportable_graph.nodes]
|
|
225
|
+
job = event_v2.Job(
|
|
226
|
+
namespace=self.namespace,
|
|
227
|
+
name=self.job_name,
|
|
228
|
+
facets={
|
|
229
|
+
"sourceCode": facet_v2.source_code_job.SourceCodeJobFacet(
|
|
230
|
+
language="python",
|
|
231
|
+
sourceCode=json.dumps(node_dict),
|
|
232
|
+
),
|
|
233
|
+
"jobType": facet_v2.job_type_job.JobTypeJobFacet(
|
|
234
|
+
processingType="BATCH",
|
|
235
|
+
integration="Hamilton",
|
|
236
|
+
jobType="DAG",
|
|
237
|
+
),
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
run = event_v2.Run(
|
|
241
|
+
runId=run_id,
|
|
242
|
+
facets={
|
|
243
|
+
"hamilton": HamiltonFacet(
|
|
244
|
+
hamilton_run_id=run_id,
|
|
245
|
+
graph_version=graph_version,
|
|
246
|
+
final_vars=final_vars,
|
|
247
|
+
inputs=list(inputs.keys()) if inputs else [],
|
|
248
|
+
overrides=list(overrides.keys()) if overrides else [],
|
|
249
|
+
)
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
run_event = event_v2.RunEvent(
|
|
253
|
+
eventType=event_v2.RunState.START,
|
|
254
|
+
eventTime=datetime.now(timezone.utc).isoformat(),
|
|
255
|
+
run=run,
|
|
256
|
+
job=job,
|
|
257
|
+
)
|
|
258
|
+
self.client.emit(run_event)
|
|
259
|
+
|
|
260
|
+
def pre_node_execute(
|
|
261
|
+
self, run_id: str, node_: node.Node, kwargs: dict[str, Any], task_id: str | None = None
|
|
262
|
+
):
|
|
263
|
+
"""No event emitted."""
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
def post_node_execute(
|
|
267
|
+
self,
|
|
268
|
+
run_id: str,
|
|
269
|
+
node_: node.Node,
|
|
270
|
+
kwargs: dict[str, Any],
|
|
271
|
+
success: bool,
|
|
272
|
+
error: Exception | None,
|
|
273
|
+
result: Any | None,
|
|
274
|
+
task_id: str | None = None,
|
|
275
|
+
):
|
|
276
|
+
"""
|
|
277
|
+
Run Event: will emit a RUNNING event with updates on input/outputs.
|
|
278
|
+
|
|
279
|
+
A Job Event will be emitted for graph execution, and additional SQLJob facet if data was loaded
|
|
280
|
+
from a SQL source.
|
|
281
|
+
|
|
282
|
+
A Dataset Event will be emitted if a dataloader or datasaver was used:
|
|
283
|
+
|
|
284
|
+
- input data set if loader
|
|
285
|
+
- output data set if saver
|
|
286
|
+
- appropriate facets will be added to the dataset where it makes sense.
|
|
287
|
+
|
|
288
|
+
TODO: attach statistics facets
|
|
289
|
+
|
|
290
|
+
:param run_id:
|
|
291
|
+
:param node_:
|
|
292
|
+
:param kwargs:
|
|
293
|
+
:param success:
|
|
294
|
+
:param error:
|
|
295
|
+
:param result:
|
|
296
|
+
:param task_id:
|
|
297
|
+
:return:
|
|
298
|
+
"""
|
|
299
|
+
if not success:
|
|
300
|
+
# do not emit anything
|
|
301
|
+
return
|
|
302
|
+
metadata = {}
|
|
303
|
+
saved_or_loaded = ""
|
|
304
|
+
if node_.tags.get("hamilton.data_saver") is True and isinstance(result, dict):
|
|
305
|
+
metadata = result
|
|
306
|
+
saved_or_loaded = "saved"
|
|
307
|
+
elif (
|
|
308
|
+
node_.tags.get("hamilton.data_loader") is True
|
|
309
|
+
and node_.tags.get("hamilton.data_loader.has_metadata") is True
|
|
310
|
+
and isinstance(result, tuple)
|
|
311
|
+
and len(result) == 2
|
|
312
|
+
and isinstance(result[1], dict)
|
|
313
|
+
):
|
|
314
|
+
metadata = result[1]
|
|
315
|
+
saved_or_loaded = "loaded"
|
|
316
|
+
if not metadata:
|
|
317
|
+
# no metadata to emit
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
inputs = []
|
|
321
|
+
outputs = []
|
|
322
|
+
sql_facet = None
|
|
323
|
+
if saved_or_loaded == "loaded":
|
|
324
|
+
inputs, sql_facet = create_input_dataset(self.namespace, metadata, node_)
|
|
325
|
+
else:
|
|
326
|
+
outputs = create_output_dataset(self.namespace, metadata, node_)
|
|
327
|
+
|
|
328
|
+
run = event_v2.Run(
|
|
329
|
+
runId=run_id,
|
|
330
|
+
)
|
|
331
|
+
job_facets = {}
|
|
332
|
+
if sql_facet:
|
|
333
|
+
job_facets["sql"] = sql_facet
|
|
334
|
+
job = event_v2.Job(namespace=self.namespace, name=self.job_name, facets=job_facets)
|
|
335
|
+
run_event = event_v2.RunEvent(
|
|
336
|
+
eventType=event_v2.RunState.RUNNING,
|
|
337
|
+
eventTime=datetime.now(timezone.utc).isoformat(),
|
|
338
|
+
run=run,
|
|
339
|
+
job=job,
|
|
340
|
+
inputs=inputs,
|
|
341
|
+
outputs=outputs,
|
|
342
|
+
)
|
|
343
|
+
self.client.emit(run_event)
|
|
344
|
+
|
|
345
|
+
def post_graph_execute(
|
|
346
|
+
self,
|
|
347
|
+
run_id: str,
|
|
348
|
+
graph: h_graph.FunctionGraph,
|
|
349
|
+
success: bool,
|
|
350
|
+
error: Exception | None,
|
|
351
|
+
results: dict[str, Any] | None,
|
|
352
|
+
):
|
|
353
|
+
"""Emits a Run COMPLETE or FAIL event.
|
|
354
|
+
|
|
355
|
+
:param run_id:
|
|
356
|
+
:param graph:
|
|
357
|
+
:param success:
|
|
358
|
+
:param error:
|
|
359
|
+
:param results:
|
|
360
|
+
:return:
|
|
361
|
+
"""
|
|
362
|
+
job = event_v2.Job(
|
|
363
|
+
namespace=self.namespace,
|
|
364
|
+
name=self.job_name,
|
|
365
|
+
)
|
|
366
|
+
facets = {}
|
|
367
|
+
run_event_type = event_v2.RunState.COMPLETE
|
|
368
|
+
if error:
|
|
369
|
+
run_event_type = event_v2.RunState.FAIL
|
|
370
|
+
error_message = str(error)
|
|
371
|
+
facets = {
|
|
372
|
+
"errorMessage": facet_v2.error_message_run.ErrorMessageRunFacet(
|
|
373
|
+
message=error_message,
|
|
374
|
+
stackTrace=get_stack_trace(error),
|
|
375
|
+
programmingLanguage="python",
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
run = event_v2.Run(runId=run_id, facets=facets)
|
|
379
|
+
|
|
380
|
+
run_event = event_v2.RunEvent(
|
|
381
|
+
eventType=run_event_type,
|
|
382
|
+
eventTime=datetime.now(timezone.utc).isoformat(),
|
|
383
|
+
run=run,
|
|
384
|
+
job=job,
|
|
385
|
+
)
|
|
386
|
+
self.client.emit(run_event)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# if __name__ == "__main__":
|
|
390
|
+
# from openlineage.client import OpenLineageClient
|
|
391
|
+
# from openlineage.client.transport.file import FileConfig, FileTransport
|
|
392
|
+
#
|
|
393
|
+
# file_config = FileConfig(
|
|
394
|
+
# log_file_path="/path/to/your/file",
|
|
395
|
+
# append=False,
|
|
396
|
+
# )
|
|
397
|
+
#
|
|
398
|
+
# client = OpenLineageClient(transport=FileTransport(file_config))
|
|
399
|
+
# namespace = "my_namespace"
|
|
400
|
+
# db_datset = Dataset(namespace, name, facets)
|
|
@@ -0,0 +1,167 @@
|
|
|
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 json
|
|
19
|
+
import logging
|
|
20
|
+
from collections.abc import Collection
|
|
21
|
+
from contextvars import ContextVar
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from opentelemetry import context, trace
|
|
29
|
+
from opentelemetry.sdk.trace import Span
|
|
30
|
+
except ImportError as e:
|
|
31
|
+
raise ImportError(
|
|
32
|
+
"Failed to import `opentelemetry` "
|
|
33
|
+
"Use `pip install sf-hamilton[opentelemetry]` to install "
|
|
34
|
+
"dependencies for the `h_opentelemetry` plugin."
|
|
35
|
+
) from e
|
|
36
|
+
|
|
37
|
+
from hamilton.graph_types import HamiltonGraph, HamiltonNode
|
|
38
|
+
from hamilton.lifecycle import GraphExecutionHook, NodeExecutionHook, TaskExecutionHook
|
|
39
|
+
|
|
40
|
+
# We have to keep track of tokens for the span
|
|
41
|
+
# As OpenTel has some weird behavior around context managers, we have to account for the latest ones we started
|
|
42
|
+
# This way we can pop one off and know where to set the current one (as the parent, when the next one ends)
|
|
43
|
+
token_stack = ContextVar[list[tuple[object, Span]] | None]("token_stack", default=None)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _exit_span(exc: Exception | None = None):
|
|
47
|
+
"""Ditto with _enter_span, but for exiting the span. Pops the token off the stack and detaches the context."""
|
|
48
|
+
stack = token_stack.get()[:]
|
|
49
|
+
token, span = stack.pop()
|
|
50
|
+
token_stack.set(stack)
|
|
51
|
+
context.detach(token)
|
|
52
|
+
if exc:
|
|
53
|
+
span.set_status(trace.Status(trace.StatusCode.ERROR, str(exc)))
|
|
54
|
+
else:
|
|
55
|
+
span.set_status(trace.Status(trace.StatusCode.OK))
|
|
56
|
+
span.end()
|
|
57
|
+
return span
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _enter_span(name: str, tracer: trace.Tracer):
|
|
61
|
+
"""Utility function to enter a span. Starts, sets the current context, and adds it to the token stack.
|
|
62
|
+
|
|
63
|
+
See this for some background on why start_span doesn't really work. We could use start_as_current_span,
|
|
64
|
+
but this is a bit more explicit.
|
|
65
|
+
"""
|
|
66
|
+
span = tracer.start_span(
|
|
67
|
+
name=name,
|
|
68
|
+
record_exception=False, # we'll handle this ourselves
|
|
69
|
+
set_status_on_exception=False,
|
|
70
|
+
)
|
|
71
|
+
ctx = trace.set_span_in_context(span)
|
|
72
|
+
token = context.attach(ctx)
|
|
73
|
+
stack = (token_stack.get() or [])[:]
|
|
74
|
+
stack.append((token, span))
|
|
75
|
+
token_stack.set(stack)
|
|
76
|
+
return span
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class OpenTelemetryTracer(NodeExecutionHook, GraphExecutionHook, TaskExecutionHook):
|
|
80
|
+
"""Adapter to log Hamilton execution to OpenTelemetry. At a high level, this works as follows:
|
|
81
|
+
1. On any of the start/pre hooks (run_before_graph, run_before_node, run_before_task), we start a new span
|
|
82
|
+
2. On any of the post ones we exit the span, accounting for the error (setting it if needed)
|
|
83
|
+
|
|
84
|
+
This works by logging to OpenTelemetry, and setting the span processor to be the right one (that knows about the tracker).
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, tracer_name: str | None = None, tracer: trace.Tracer | None = None):
|
|
88
|
+
if tracer_name and tracer:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"Only pass in one of tracer_name or tracer, not both, got: tracer_name={tracer_name} and tracer={tracer}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if tracer:
|
|
94
|
+
self.tracer = tracer
|
|
95
|
+
elif tracer_name:
|
|
96
|
+
self.tracer = trace.get_tracer(tracer_name)
|
|
97
|
+
else:
|
|
98
|
+
self.tracer = trace.get_tracer(__name__)
|
|
99
|
+
|
|
100
|
+
self.graph = None
|
|
101
|
+
|
|
102
|
+
def run_before_graph_execution(
|
|
103
|
+
self,
|
|
104
|
+
*,
|
|
105
|
+
graph: HamiltonGraph,
|
|
106
|
+
final_vars: list[str],
|
|
107
|
+
inputs: dict,
|
|
108
|
+
overrides: dict,
|
|
109
|
+
execution_path: Collection[str],
|
|
110
|
+
run_id: str,
|
|
111
|
+
**kwargs,
|
|
112
|
+
):
|
|
113
|
+
self.graph = graph
|
|
114
|
+
|
|
115
|
+
attributes = {
|
|
116
|
+
"graph_version": graph.version,
|
|
117
|
+
"final_vars": final_vars,
|
|
118
|
+
"inputs": list(inputs.keys()) if inputs else [],
|
|
119
|
+
"overrides": list(overrides.keys()) if overrides else [],
|
|
120
|
+
"execution_path": list(execution_path),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
graph_span = _enter_span(run_id, self.tracer)
|
|
124
|
+
graph_span.set_attributes(attributes)
|
|
125
|
+
|
|
126
|
+
def run_before_node_execution(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
node_name: str,
|
|
130
|
+
node_tags: dict,
|
|
131
|
+
node_return_type: type,
|
|
132
|
+
**kwargs: Any,
|
|
133
|
+
):
|
|
134
|
+
attributes = {
|
|
135
|
+
"type": str(node_return_type),
|
|
136
|
+
"node_version": self.graph[node_name].version,
|
|
137
|
+
"tags": json.dumps(node_tags),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
node_span = _enter_span(node_name, self.tracer)
|
|
141
|
+
node_span.set_attributes(attributes)
|
|
142
|
+
|
|
143
|
+
def run_before_task_execution(
|
|
144
|
+
self,
|
|
145
|
+
*,
|
|
146
|
+
task_id: str,
|
|
147
|
+
nodes: list[HamiltonNode],
|
|
148
|
+
inputs: dict[str, Any],
|
|
149
|
+
overrides: dict[str, Any],
|
|
150
|
+
**kwargs,
|
|
151
|
+
):
|
|
152
|
+
attributes = {
|
|
153
|
+
"nodes": [n.name for n in nodes],
|
|
154
|
+
"inputs": list(inputs.keys()) if inputs else [],
|
|
155
|
+
"overrides": list(overrides.keys()) if overrides else [],
|
|
156
|
+
}
|
|
157
|
+
task_span = _enter_span(task_id, self.tracer)
|
|
158
|
+
task_span.set_attributes(attributes)
|
|
159
|
+
|
|
160
|
+
def run_after_task_execution(self, *, error: Exception | None, **kwargs):
|
|
161
|
+
_exit_span(error)
|
|
162
|
+
|
|
163
|
+
def run_after_node_execution(self, *, error: Exception | None, **kwargs):
|
|
164
|
+
_exit_span(error)
|
|
165
|
+
|
|
166
|
+
def run_after_graph_execution(self, *, error: Exception | None, **kwargs):
|
|
167
|
+
_exit_span(error)
|