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,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)