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,375 @@
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 functools
19
+ import itertools
20
+ import json
21
+ import os
22
+
23
+ import pandas as pd
24
+ from fastapi import FastAPI
25
+ from fastapi.responses import HTMLResponse, RedirectResponse
26
+ from fastapi.staticfiles import StaticFiles
27
+ from fastui import AnyComponent, FastUI, prebuilt_html
28
+ from fastui import components as c
29
+ from fastui.components.display import DisplayLookup, DisplayMode
30
+ from fastui.events import GoToEvent
31
+ from fastui.forms import SelectSearchResponse
32
+ from pydantic import BaseModel, Field
33
+
34
+ from hamilton.plugins.h_experiments.cache import JsonCache
35
+ from hamilton.plugins.h_experiments.data_model import (
36
+ NodeMaterializer,
37
+ RunMetadata,
38
+ model_from_values,
39
+ )
40
+
41
+
42
+ def convert_graph_hash_to_version(runs):
43
+ """Convert graph hash to incremental id, per experiment"""
44
+ versioned_runs = []
45
+ for _, group in itertools.groupby(runs, lambda r: r.experiment):
46
+ group = sorted(group, key=lambda run: run.date_completed)
47
+
48
+ current_version = 1
49
+ current_hash = None
50
+ for r in group:
51
+ if current_hash is None:
52
+ current_hash = r.graph_hash
53
+
54
+ if r.graph_hash != current_hash:
55
+ current_hash = r.graph_hash
56
+ current_version += 1
57
+
58
+ r.graph_version = current_version
59
+ versioned_runs.append(r)
60
+ return versioned_runs
61
+
62
+
63
+ def get_runs(metadata_cache_path: str) -> list[RunMetadata]:
64
+ """Create RunMetadata objects for all runs JSON found in cache"""
65
+ cache = JsonCache(cache_path=metadata_cache_path)
66
+ runs = [RunMetadata.model_validate_json(cache.read(run_id)) for run_id in cache.keys()]
67
+ runs = convert_graph_hash_to_version(runs)
68
+ runs = sorted(runs, key=lambda run: run.date_completed, reverse=True)
69
+ return runs
70
+
71
+
72
+ # environment variables are the most convenient approach
73
+ # to configure FastAPI application
74
+ base_directory = os.getenv("HAMILTON_EXPERIMENTS_PATH", "")
75
+
76
+ # disable Swagger UI /docs because they are currently bugged for FastUI
77
+ app = FastAPI(docs_url=None, redoc_url=None)
78
+ app.mount("/experiments", StaticFiles(directory=base_directory), name="experiments")
79
+ runs = get_runs(base_directory)
80
+
81
+
82
+ def base_page(*components: AnyComponent) -> list[AnyComponent]:
83
+ """Template applied to all pages. Includes: title, navigation, and footer"""
84
+ return [
85
+ c.PageTitle(text="📝 Hamilton Experiment Manager"),
86
+ c.Navbar(
87
+ title="📝 Hamilton Experiment Manager",
88
+ title_event=GoToEvent(url="/"),
89
+ class_name="+ mb-4",
90
+ ),
91
+ c.Page(components=[*components]),
92
+ c.Footer(
93
+ extra_text="Powered by Hamilton",
94
+ links=[
95
+ c.Link(
96
+ components=[c.Text(text="GitHub")],
97
+ on_click=GoToEvent(url="https://github.com/apache/hamilton"),
98
+ ),
99
+ ],
100
+ ),
101
+ ]
102
+
103
+
104
+ @functools.cache
105
+ def run_lookup():
106
+ """Cache a mapping of {run_id: run} to query runs"""
107
+ return {run.run_id: run for run in runs}
108
+
109
+
110
+ @app.get("/api/filter/{field}", response_model=SelectSearchResponse)
111
+ async def search_filter(field: str) -> SelectSearchResponse:
112
+ """Get all the unique values for a RunMetadata field to populate menus"""
113
+ options = {str(getattr(run, field)): getattr(run, field) for run in runs}
114
+ return SelectSearchResponse(options=[{"label": v, "value": v} for v in options])
115
+
116
+
117
+ class RunFilter(BaseModel):
118
+ """Filter the runs_overview() page using `experiment` and `graph_version`"""
119
+
120
+ experiment: str = Field(
121
+ json_schema_extra={
122
+ "search_url": "/api/filter/experiment",
123
+ "placeholder": "Select Experiment ...",
124
+ }
125
+ )
126
+ graph_version: str = Field(
127
+ json_schema_extra={
128
+ "search_url": "/api/filter/graph_version",
129
+ "placeholder": "Select Graph version ...",
130
+ },
131
+ )
132
+
133
+
134
+ @app.get("/api/runs", response_model=FastUI, response_model_exclude_none=True)
135
+ def runs_overview(
136
+ experiment: str | None = None,
137
+ graph_version: int | None = None,
138
+ ) -> list[AnyComponent]:
139
+ """RunOverview page with filters for the table"""
140
+
141
+ # refresh cache to display new runs
142
+ runs = get_runs(os.getenv("HAMILTON_EXPERIMENTS_PATH", ""))
143
+
144
+ selection = dict()
145
+
146
+ if experiment:
147
+ selection["experiment"] = dict(value=experiment, label=experiment)
148
+
149
+ if graph_version:
150
+ selection["graph_version"] = dict(value=graph_version, label=str(graph_version))
151
+
152
+ selected_runs = runs
153
+ if selection:
154
+ for k, v in selection.items():
155
+ selected_runs = list(filter(lambda r: getattr(r, k) == v["value"], selected_runs))
156
+
157
+ return base_page(
158
+ c.ModelForm(
159
+ model=RunFilter,
160
+ submit_url="/runs",
161
+ initial=selection,
162
+ method="GOTO",
163
+ submit_on_change=True,
164
+ display_mode="inline",
165
+ ),
166
+ c.Table(
167
+ data=selected_runs,
168
+ data_model=RunMetadata,
169
+ columns=[
170
+ DisplayLookup(field="date_completed", mode=DisplayMode.datetime),
171
+ DisplayLookup(field="experiment"),
172
+ DisplayLookup(field="graph_version"),
173
+ DisplayLookup(field="run_id", on_click=GoToEvent(url="/run/{run_id}/")),
174
+ ],
175
+ ),
176
+ )
177
+
178
+
179
+ def run_tabs(run_id) -> list[AnyComponent]:
180
+ """Create Metadata and Artifacts tabs for individual Run pages"""
181
+ return [
182
+ c.LinkList(
183
+ links=[
184
+ c.Link(
185
+ components=[c.Text(text="Metadata")],
186
+ on_click=GoToEvent(url=f"/run/{run_id}/"),
187
+ active="startswith:/run/",
188
+ ),
189
+ c.Link(
190
+ components=[c.Text(text="Artifacts")],
191
+ on_click=GoToEvent(url=f"/artifacts/{run_id}"),
192
+ active="startswith:/artifacts/",
193
+ ),
194
+ ],
195
+ mode="tabs",
196
+ class_name="+ mb-4",
197
+ )
198
+ ]
199
+
200
+
201
+ @app.get("/api/run/{run_id}/", response_model=FastUI, response_model_exclude_none=True)
202
+ def run_metadata(run_id: str) -> list[AnyComponent]:
203
+ """Individual Run > Metadata"""
204
+ run = run_lookup()[run_id]
205
+
206
+ return base_page(
207
+ c.Heading(text=run.experiment, level=2),
208
+ *run_tabs(run_id=run_id),
209
+ c.Details(
210
+ data=run,
211
+ fields=[
212
+ DisplayLookup(field="experiment"),
213
+ DisplayLookup(field="run_id"),
214
+ DisplayLookup(field="success"),
215
+ DisplayLookup(field="graph_hash"),
216
+ DisplayLookup(field="modules"),
217
+ ],
218
+ ),
219
+ c.Image(
220
+ src=f"/experiments/{run.experiment}/{run.run_id}/dag.png",
221
+ width="100%",
222
+ height="auto",
223
+ loading="lazy",
224
+ referrer_policy="no-referrer",
225
+ class_name="border rounded",
226
+ ),
227
+ c.Details(
228
+ data=run,
229
+ fields=[
230
+ DisplayLookup(field="config", mode=DisplayMode.json),
231
+ DisplayLookup(field="inputs", mode=DisplayMode.json),
232
+ DisplayLookup(field="overrides", mode=DisplayMode.json),
233
+ ],
234
+ ),
235
+ )
236
+
237
+
238
+ @functools.cache
239
+ def create_table_model(**kwargs):
240
+ """Cache the creation of a Pydantic model from a DataFrame row"""
241
+ return model_from_values("Table", specs=kwargs)
242
+
243
+
244
+ def dataframe_to_table(df: pd.DataFrame, **kwargs) -> AnyComponent:
245
+ """Populate a FastUI table with pagination from a pandas DataFrame"""
246
+ df = df.reset_index()
247
+ Table = create_table_model(**{str(k): v for k, v in df.iloc[0].to_dict().items()})
248
+
249
+ page: int = 1 if kwargs.get("page") is None else kwargs.get("page", 1)
250
+ page_size = min(20, df.shape[0])
251
+ page_df = df.iloc[(page - 1) * page_size : page * page_size]
252
+
253
+ # create a Pydantic objects for each row
254
+ data = [
255
+ Table.model_validate({str(k): v for k, v in row.items()})
256
+ for row in page_df.to_dict(orient="index").values()
257
+ ]
258
+
259
+ return [
260
+ c.Table(
261
+ data=data,
262
+ data_model=Table,
263
+ columns=[DisplayLookup(field=str(col)) for col in df.columns],
264
+ ),
265
+ c.Pagination(page=page, page_size=20, total=df.shape[0]),
266
+ ]
267
+
268
+
269
+ def artifact_components(materializer: NodeMaterializer, **kwargs) -> list[AnyComponent]:
270
+ """Create a FastUI component for an artifact based on the materializer type
271
+
272
+ Instead of mapping Python type -> component, the materializer/file type -> component
273
+ has a much lower and manageable cardinality
274
+ """
275
+ artifact_path = materializer.path.replace(base_directory, "/experiments")
276
+
277
+ if materializer.sink == "json":
278
+ with open(materializer.path, "r") as f:
279
+ data = json.load(f)
280
+ components = [c.Json(value=data)]
281
+
282
+ elif materializer.sink == "parquet":
283
+ data = pd.read_parquet(materializer.path)
284
+ components = dataframe_to_table(data, **kwargs)
285
+
286
+ elif materializer.sink == "csv":
287
+ data = pd.read_csv(materializer.path)
288
+ components = dataframe_to_table(data, **kwargs)
289
+
290
+ elif materializer.sink in ["plt", "plotly"]:
291
+ try:
292
+ components = [
293
+ c.Image(
294
+ src=artifact_path,
295
+ width="100%",
296
+ height="auto",
297
+ loading="lazy",
298
+ referrer_policy="no-referrer",
299
+ )
300
+ ]
301
+ # TODO refactor to each default else case
302
+ except Exception:
303
+ components = [
304
+ c.Json(
305
+ value={
306
+ k: v
307
+ for k, v in dict(materializer).items()
308
+ if k in ["path", "sink", "data_saver"]
309
+ }
310
+ )
311
+ ]
312
+
313
+ else:
314
+ components = [
315
+ c.Json(
316
+ value={
317
+ k: v
318
+ for k, v in dict(materializer).items()
319
+ if k in ["path", "sink", "data_saver"]
320
+ }
321
+ )
322
+ ]
323
+
324
+ return components
325
+
326
+
327
+ def artifact_tabs(run: RunMetadata) -> list[AnyComponent]:
328
+ """Create a tab for each artifact"""
329
+ links = []
330
+ for i, m in enumerate(run.materialized):
331
+ artifact_name = "-".join(m.source_nodes)
332
+ artifact_name, _, _ = artifact_name.partition("__")
333
+ links.append(
334
+ c.Link(
335
+ components=[c.Text(text=artifact_name)],
336
+ on_click=GoToEvent(
337
+ url=f"/artifacts/{run.run_id}",
338
+ query=dict(artifact_id=i),
339
+ ),
340
+ active=f"startswith:/artifacts/{run.run_id}?artifact_id={i}",
341
+ )
342
+ )
343
+
344
+ return [
345
+ c.LinkList(
346
+ links=links,
347
+ mode="tabs",
348
+ class_name="+ mb-4",
349
+ )
350
+ ]
351
+
352
+
353
+ @app.get("/api/artifacts/{run_id}", response_model=FastUI, response_model_exclude_none=True)
354
+ def run_artifacts(run_id: str, artifact_id: int = 0, page: int | None = None) -> list[AnyComponent]:
355
+ """Individual Run > Artifact"""
356
+ run = run_lookup()[run_id]
357
+
358
+ return base_page(
359
+ c.Heading(text=run.experiment, level=2),
360
+ *run_tabs(run_id=run_id),
361
+ *artifact_tabs(run=run),
362
+ *artifact_components(materializer=run.materialized[artifact_id], page=page),
363
+ )
364
+
365
+
366
+ @app.get("/api/")
367
+ def landing_page() -> RedirectResponse:
368
+ """Landing page redirects to the Run overview page"""
369
+ return RedirectResponse(url="/api/runs")
370
+
371
+
372
+ @app.get("{path:path}")
373
+ async def html_landing() -> HTMLResponse:
374
+ """Simple HTML page which serves the React app, comes last as it matches all paths."""
375
+ return HTMLResponse(prebuilt_html(title="Hamilton Experiment Manager"))
@@ -0,0 +1,152 @@
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 inspect
19
+ from typing import Any
20
+
21
+ from kedro.pipeline.node import Node as KNode
22
+ from kedro.pipeline.pipeline import Pipeline as KPipeline
23
+
24
+ from hamilton import driver, graph
25
+ from hamilton.function_modifiers.expanders import extract_fields
26
+ from hamilton.lifecycle import base as lifecycle_base
27
+ from hamilton.node import Node as HNode
28
+
29
+
30
+ def expand_k_node(base_node: HNode, outputs: list[str]) -> list[HNode]:
31
+ """Manually apply `@extract_fields()` on a Hamilton node.Node for a Kedro
32
+ node that specifies >1 `outputs`.
33
+
34
+ The number of nodes == len(outputs) + 1 because it includes the `base_node`
35
+ """
36
+
37
+ def _convert_output_from_tuple_to_dict(node_result: Any, node_kwargs: dict[str, Any]):
38
+ return {out: v for out, v in zip(outputs, node_result, strict=False)}
39
+
40
+ # NOTE isinstance(Any, type) is False for Python < 3.11
41
+ extractor = extract_fields(fields={out: Any for out in outputs})
42
+ func = base_node.originating_functions[0]
43
+ if issubclass(func.__annotations__["return"], tuple):
44
+ base_node = base_node.transform_output(_convert_output_from_tuple_to_dict, dict)
45
+ func.__annotations__["return"] = dict
46
+
47
+ extractor.validate(func)
48
+ return list(extractor.transform_node(base_node, {}, func))
49
+
50
+
51
+ def k_node_to_h_nodes(node: KNode) -> list[HNode]:
52
+ """Convert a Kedro node to a list of Hamilton nodes.
53
+ If the Kedro node specifies 1 output, generate 1 Hamilton node.
54
+ If it generate >1 output, generate len(outputs) + 1 to include the base node + extracted fields.
55
+ """
56
+ # determine if more than one output
57
+ node_names = []
58
+ if isinstance(node.outputs, list):
59
+ node_names.extend(node.outputs)
60
+ elif isinstance(node.outputs, dict):
61
+ node_names.extend(node.outputs.values())
62
+
63
+ # determine the base node name
64
+ if len(node_names) == 1:
65
+ base_node_name = node_names[0]
66
+ elif isinstance(node.outputs, str):
67
+ base_node_name = node.outputs
68
+ else:
69
+ base_node_name = node.func.__name__
70
+
71
+ func_sig = inspect.signature(node.func)
72
+ params = func_sig.parameters.values()
73
+ output_type = func_sig.return_annotation
74
+ if output_type is None:
75
+ # manually creating `hamilton.node.Node` doesn't accept `typ=None`
76
+ output_type = type[None] # NoneType is introduced in Python 3.10
77
+
78
+ base_node = HNode(
79
+ name=base_node_name,
80
+ typ=output_type,
81
+ doc_string=getattr(node.func, "__doc__", ""),
82
+ callabl=node.func,
83
+ originating_functions=(node.func,),
84
+ )
85
+
86
+ # if Kedro node defines multiple outputs, use `@extract_fields()`
87
+ if len(node_names) > 1:
88
+ h_nodes = expand_k_node(base_node, node_names)
89
+ else:
90
+ h_nodes = [base_node]
91
+
92
+ # remap the function parameters to the node `inputs` and clean Kedro `parameters` name
93
+ new_params = {}
94
+ for param, k_input in zip(params, node.inputs, strict=False):
95
+ if k_input.startswith("params:"):
96
+ k_input = k_input.partition("params:")[-1]
97
+
98
+ new_params[param.name] = k_input
99
+
100
+ h_nodes = [n.reassign_inputs(input_names=new_params) for n in h_nodes]
101
+
102
+ return h_nodes
103
+
104
+
105
+ def kedro_pipeline_to_driver(
106
+ *pipelines: KPipeline,
107
+ builder: driver.Builder | None = None,
108
+ ) -> driver.Driver:
109
+ """Convert one or mode Kedro `Pipeline` to a Hamilton `Driver`.
110
+ Pass a Hamilton `Builder` to include lifecycle adapters in your `Driver`.
111
+
112
+ :param pipelines: one or more Kedro `Pipeline` objects
113
+ :param builder: a Hamilton `Builder` to use when building the `Driver`
114
+ :return: the Hamilton `Driver` built from Kedro `Pipeline` objects.
115
+
116
+ .. code-block: python
117
+
118
+ from hamilton import driver
119
+ from hamilton.plugins import h_kedro
120
+
121
+ builder = driver.Builder().with_adapters(tracker)
122
+
123
+ dr = h_kedro.kedro_pipeline_to_driver(
124
+ data_science.create_pipeline(), # Kedro Pipeline
125
+ data_processing.create_pipeline(), # Kedro Pipeline
126
+ builder=builder
127
+ )
128
+ """
129
+ # generate nodes
130
+ h_nodes = []
131
+ for pipe in pipelines:
132
+ for node in pipe.nodes:
133
+ h_nodes.extend(k_node_to_h_nodes(node))
134
+
135
+ # resolve dependencies
136
+ h_nodes = graph.update_dependencies(
137
+ {n.name: n for n in h_nodes},
138
+ lifecycle_base.LifecycleAdapterSet(),
139
+ )
140
+
141
+ builder = builder or driver.Builder()
142
+ dr = builder.build()
143
+ # inject function graph in Driver
144
+ dr.graph = graph.FunctionGraph(
145
+ h_nodes, config={}, adapter=lifecycle_base.LifecycleAdapterSet(*builder.adapters)
146
+ )
147
+ # reapply lifecycle hooks
148
+ if dr.adapter.does_hook("post_graph_construct", is_async=False):
149
+ dr.adapter.call_all_lifecycle_hooks_sync(
150
+ "post_graph_construct", graph=dr.graph, modules=dr.graph_modules, config={}
151
+ )
152
+ return dr