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