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,328 @@
|
|
|
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 __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import inspect
|
|
21
|
+
import textwrap
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
|
|
25
|
+
from fastmcp import FastMCP
|
|
26
|
+
|
|
27
|
+
from hamilton.plugins.h_mcp._helpers import (
|
|
28
|
+
build_driver_from_code,
|
|
29
|
+
cleanup_temp_module,
|
|
30
|
+
format_exception_chain,
|
|
31
|
+
serialize_results,
|
|
32
|
+
)
|
|
33
|
+
from hamilton.plugins.h_mcp._templates import get_available_templates, get_capabilities
|
|
34
|
+
|
|
35
|
+
mcp = FastMCP(
|
|
36
|
+
name="Hamilton",
|
|
37
|
+
instructions=(
|
|
38
|
+
"Hamilton is a Python micro-framework where functions define DAG nodes. "
|
|
39
|
+
"Ask the user which data libraries they use (e.g., pandas, numpy, polars) "
|
|
40
|
+
"then call hamilton_capabilities with their preferred_libraries. "
|
|
41
|
+
"Use hamilton_validate_dag to check code before execution. "
|
|
42
|
+
"Workflow: ask user -> capabilities -> scaffold -> validate -> visualize -> correct -> execute."
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@mcp.tool()
|
|
48
|
+
def hamilton_validate_dag(code: str, config: dict | None = None) -> dict:
|
|
49
|
+
"""Validate Hamilton DAG code by building the Driver.
|
|
50
|
+
|
|
51
|
+
Compiles Python source into a Hamilton DAG and checks for missing
|
|
52
|
+
dependencies, type mismatches, and circular references -- all without
|
|
53
|
+
executing the code.
|
|
54
|
+
|
|
55
|
+
Returns ``{"valid": true, "node_count": N, "nodes": [...], "inputs": [...]}``
|
|
56
|
+
on success or ``{"valid": false, "errors": [...]}`` on failure.
|
|
57
|
+
"""
|
|
58
|
+
module = None
|
|
59
|
+
try:
|
|
60
|
+
dr, module = build_driver_from_code(code, config)
|
|
61
|
+
variables = dr.list_available_variables()
|
|
62
|
+
nodes = [v.name for v in variables if not v.is_external_input]
|
|
63
|
+
inputs = [v.name for v in variables if v.is_external_input]
|
|
64
|
+
return {
|
|
65
|
+
"valid": True,
|
|
66
|
+
"node_count": len(nodes),
|
|
67
|
+
"nodes": sorted(nodes),
|
|
68
|
+
"inputs": sorted(inputs),
|
|
69
|
+
"errors": [],
|
|
70
|
+
}
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
return {
|
|
73
|
+
"valid": False,
|
|
74
|
+
"node_count": 0,
|
|
75
|
+
"nodes": [],
|
|
76
|
+
"inputs": [],
|
|
77
|
+
"errors": format_exception_chain(exc),
|
|
78
|
+
}
|
|
79
|
+
finally:
|
|
80
|
+
if module is not None:
|
|
81
|
+
cleanup_temp_module(module)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@mcp.tool()
|
|
85
|
+
def hamilton_list_nodes(code: str, config: dict | None = None) -> dict:
|
|
86
|
+
"""List all nodes in a Hamilton DAG with their types and dependencies.
|
|
87
|
+
|
|
88
|
+
Builds the DAG from source, then returns structured info for every node
|
|
89
|
+
including name, output type, tags, whether it is an external input,
|
|
90
|
+
and its required/optional dependencies.
|
|
91
|
+
"""
|
|
92
|
+
module = None
|
|
93
|
+
try:
|
|
94
|
+
dr, module = build_driver_from_code(code, config)
|
|
95
|
+
variables = dr.list_available_variables()
|
|
96
|
+
from hamilton.htypes import get_type_as_string
|
|
97
|
+
|
|
98
|
+
node_list = []
|
|
99
|
+
for v in variables:
|
|
100
|
+
node_list.append(
|
|
101
|
+
{
|
|
102
|
+
"name": v.name,
|
|
103
|
+
"type": get_type_as_string(v.type) or "",
|
|
104
|
+
"is_external_input": v.is_external_input,
|
|
105
|
+
"tags": v.tags,
|
|
106
|
+
"required_dependencies": sorted(v.required_dependencies),
|
|
107
|
+
"optional_dependencies": sorted(v.optional_dependencies),
|
|
108
|
+
"documentation": v.documentation,
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
return {"nodes": node_list, "errors": []}
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
return {"nodes": [], "errors": format_exception_chain(exc)}
|
|
114
|
+
finally:
|
|
115
|
+
if module is not None:
|
|
116
|
+
cleanup_temp_module(module)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@mcp.tool()
|
|
120
|
+
def hamilton_visualize(code: str, config: dict | None = None, output_format: str = "dot") -> str:
|
|
121
|
+
"""Visualize the Hamilton DAG as DOT graph source.
|
|
122
|
+
|
|
123
|
+
Builds the DAG and returns a Graphviz DOT-language string describing
|
|
124
|
+
the dependency graph. Requires ``graphviz`` (``pip install "apache-hamilton[visualization]"``).
|
|
125
|
+
"""
|
|
126
|
+
module = None
|
|
127
|
+
try:
|
|
128
|
+
dr, module = build_driver_from_code(code, config)
|
|
129
|
+
try:
|
|
130
|
+
dot = dr.display_all_functions(render_kwargs={"view": False})
|
|
131
|
+
except ImportError:
|
|
132
|
+
return (
|
|
133
|
+
"Error: graphviz is required for visualization. "
|
|
134
|
+
'Install with: pip install "apache-hamilton[visualization]"'
|
|
135
|
+
)
|
|
136
|
+
if dot is None:
|
|
137
|
+
return (
|
|
138
|
+
"Error: graphviz is required for visualization. "
|
|
139
|
+
'Install with: pip install "apache-hamilton[visualization]"'
|
|
140
|
+
)
|
|
141
|
+
return dot.source
|
|
142
|
+
except Exception as exc:
|
|
143
|
+
return f"Error: {exc}"
|
|
144
|
+
finally:
|
|
145
|
+
if module is not None:
|
|
146
|
+
cleanup_temp_module(module)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@mcp.tool()
|
|
150
|
+
def hamilton_execute(
|
|
151
|
+
code: str,
|
|
152
|
+
final_vars: list[str],
|
|
153
|
+
inputs: dict | None = None,
|
|
154
|
+
config: dict | None = None,
|
|
155
|
+
timeout_seconds: int = 30,
|
|
156
|
+
) -> dict:
|
|
157
|
+
"""Execute a Hamilton DAG and return the requested outputs.
|
|
158
|
+
|
|
159
|
+
Builds the DAG from source, then calls ``driver.execute()`` with the
|
|
160
|
+
given ``final_vars`` and ``inputs``. Results are serialized to JSON-safe
|
|
161
|
+
strings. A timeout (default 30s) guards against long-running code.
|
|
162
|
+
|
|
163
|
+
WARNING: This executes arbitrary Python code.
|
|
164
|
+
"""
|
|
165
|
+
module = None
|
|
166
|
+
result_container: dict = {}
|
|
167
|
+
error_container: dict = {}
|
|
168
|
+
|
|
169
|
+
def _run(dr, final_vars, inputs):
|
|
170
|
+
try:
|
|
171
|
+
result_container["results"] = dr.execute(final_vars=final_vars, inputs=inputs or {})
|
|
172
|
+
except Exception as exc:
|
|
173
|
+
error_container["error"] = exc
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
dr, module = build_driver_from_code(code, config)
|
|
177
|
+
|
|
178
|
+
start = time.monotonic()
|
|
179
|
+
worker = threading.Thread(target=_run, args=(dr, final_vars, inputs))
|
|
180
|
+
worker.start()
|
|
181
|
+
worker.join(timeout=timeout_seconds)
|
|
182
|
+
|
|
183
|
+
if worker.is_alive():
|
|
184
|
+
return {"error": f"Execution timed out after {timeout_seconds}s"}
|
|
185
|
+
|
|
186
|
+
elapsed_ms = round((time.monotonic() - start) * 1000, 1)
|
|
187
|
+
|
|
188
|
+
if "error" in error_container:
|
|
189
|
+
return {
|
|
190
|
+
"error": str(error_container["error"]),
|
|
191
|
+
"execution_time_ms": elapsed_ms,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
"results": serialize_results(result_container["results"]),
|
|
196
|
+
"execution_time_ms": elapsed_ms,
|
|
197
|
+
}
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
return {"error": str(exc)}
|
|
200
|
+
finally:
|
|
201
|
+
if module is not None:
|
|
202
|
+
cleanup_temp_module(module)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@mcp.tool()
|
|
206
|
+
def hamilton_get_docs(topic: str) -> str:
|
|
207
|
+
"""Get Hamilton documentation for a specific topic.
|
|
208
|
+
|
|
209
|
+
Supported topics: ``overview``, ``decorators``, ``driver``, ``builder``,
|
|
210
|
+
or any decorator name such as ``parameterize``, ``extract_columns``,
|
|
211
|
+
``config``, ``check_output``, ``tag``, ``pipe``, ``does``, ``subdag``, etc.
|
|
212
|
+
"""
|
|
213
|
+
topic = topic.strip().lower()
|
|
214
|
+
|
|
215
|
+
if topic == "overview":
|
|
216
|
+
return textwrap.dedent("""\
|
|
217
|
+
Hamilton -- Python micro-framework for dataflow DAGs
|
|
218
|
+
|
|
219
|
+
Core concepts:
|
|
220
|
+
- Each Python function defines a DAG node.
|
|
221
|
+
- The function name becomes the node name.
|
|
222
|
+
- Function parameters declare dependencies on other nodes.
|
|
223
|
+
- Return type annotations are required.
|
|
224
|
+
- The Driver compiles functions into a DAG, validates dependencies
|
|
225
|
+
and types at build time, then executes the graph.
|
|
226
|
+
|
|
227
|
+
Quick start:
|
|
228
|
+
1. Write functions in a module (my_functions.py).
|
|
229
|
+
2. Build a Driver:
|
|
230
|
+
from hamilton import driver
|
|
231
|
+
import my_functions
|
|
232
|
+
dr = driver.Builder().with_modules(my_functions).build()
|
|
233
|
+
3. Execute:
|
|
234
|
+
results = dr.execute(["output_node"], inputs={...})
|
|
235
|
+
|
|
236
|
+
Key decorators: @parameterize, @extract_columns, @config.when,
|
|
237
|
+
@check_output, @tag, @pipe, @does, @subdag
|
|
238
|
+
""")
|
|
239
|
+
|
|
240
|
+
if topic == "decorators":
|
|
241
|
+
from hamilton import function_modifiers
|
|
242
|
+
|
|
243
|
+
decorators = {
|
|
244
|
+
"parameterize": "Create multiple nodes from one function with different parameters.",
|
|
245
|
+
"parameterize_sources": "Parameterize by mapping different source nodes.",
|
|
246
|
+
"parameterize_values": "Parameterize by mapping different literal values.",
|
|
247
|
+
"extract_columns": "Expand a DataFrame-returning function into per-column nodes.",
|
|
248
|
+
"extract_fields": "Expand a dict-returning function into per-field nodes.",
|
|
249
|
+
"config.when": "Conditionally include a function based on config values.",
|
|
250
|
+
"check_output": "Attach data quality validators to a node's output.",
|
|
251
|
+
"tag": "Add metadata tags to a node.",
|
|
252
|
+
"tag_outputs": "Tag specific outputs of a multi-output function.",
|
|
253
|
+
"pipe": "Chain transforms: pass a node's output through a pipeline.",
|
|
254
|
+
"does": "Replace function body with another callable.",
|
|
255
|
+
"subdag": "Include an entire sub-DAG as a namespace.",
|
|
256
|
+
"inject": "Inject specific values or sources into function parameters.",
|
|
257
|
+
"schema": "Attach schema metadata to a node.",
|
|
258
|
+
"cache": "Mark a node for caching.",
|
|
259
|
+
"load_from": "Load data from an external source (data loader).",
|
|
260
|
+
"save_to": "Save data to an external destination (data saver).",
|
|
261
|
+
}
|
|
262
|
+
lines = ["Available Hamilton decorators:\n"]
|
|
263
|
+
for name, desc in decorators.items():
|
|
264
|
+
lines.append(f" @{name} -- {desc}")
|
|
265
|
+
lines.append("\nUse hamilton_get_docs('<decorator_name>') for full documentation.")
|
|
266
|
+
return "\n".join(lines)
|
|
267
|
+
|
|
268
|
+
if topic in ("driver", "builder"):
|
|
269
|
+
from hamilton import driver as driver_mod
|
|
270
|
+
|
|
271
|
+
doc = inspect.getdoc(driver_mod.Builder)
|
|
272
|
+
return doc or "No documentation found for Builder."
|
|
273
|
+
|
|
274
|
+
# Try to find a decorator by name in function_modifiers
|
|
275
|
+
from hamilton import function_modifiers
|
|
276
|
+
|
|
277
|
+
obj = getattr(function_modifiers, topic, None)
|
|
278
|
+
if obj is not None:
|
|
279
|
+
doc = inspect.getdoc(obj)
|
|
280
|
+
if doc:
|
|
281
|
+
return f"@{topic}\n\n{doc}"
|
|
282
|
+
# For class-based decorators, try the class itself
|
|
283
|
+
if isinstance(obj, type):
|
|
284
|
+
doc = inspect.getdoc(obj)
|
|
285
|
+
if doc:
|
|
286
|
+
return f"@{topic}\n\n{doc}"
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
f"Unknown topic '{topic}'. "
|
|
290
|
+
"Supported: overview, decorators, driver, builder, "
|
|
291
|
+
"or any decorator name (parameterize, extract_columns, config, "
|
|
292
|
+
"check_output, tag, pipe, does, subdag, etc.)"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@mcp.tool()
|
|
297
|
+
def hamilton_capabilities(preferred_libraries: list[str] | None = None) -> dict:
|
|
298
|
+
"""Report which optional libraries are installed and which features are available.
|
|
299
|
+
|
|
300
|
+
Call this first to discover the environment before generating code.
|
|
301
|
+
If the user specifies which libraries they use, pass them as
|
|
302
|
+
``preferred_libraries`` (e.g., ``["pandas", "numpy"]``) to filter
|
|
303
|
+
scaffold patterns accordingly.
|
|
304
|
+
"""
|
|
305
|
+
prefs = set(preferred_libraries) if preferred_libraries is not None else None
|
|
306
|
+
return get_capabilities(prefs)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@mcp.tool()
|
|
310
|
+
def hamilton_scaffold(
|
|
311
|
+
pattern: str,
|
|
312
|
+
preferred_libraries: list[str] | None = None,
|
|
313
|
+
) -> str:
|
|
314
|
+
"""Generate a starter Hamilton module for a given pattern.
|
|
315
|
+
|
|
316
|
+
Available patterns depend on installed or preferred libraries.
|
|
317
|
+
Pass ``preferred_libraries`` to filter to patterns matching the
|
|
318
|
+
user's environment (e.g., ``["pandas"]``).
|
|
319
|
+
"""
|
|
320
|
+
prefs = set(preferred_libraries) if preferred_libraries is not None else None
|
|
321
|
+
templates = get_available_templates(prefs)
|
|
322
|
+
pattern = pattern.strip().lower()
|
|
323
|
+
template = templates.get(pattern)
|
|
324
|
+
if template is None:
|
|
325
|
+
available = ", ".join(sorted(templates.keys()))
|
|
326
|
+
return f"Unknown pattern '{pattern}'. Available patterns: {available}"
|
|
327
|
+
|
|
328
|
+
return template.code
|
|
@@ -0,0 +1,335 @@
|
|
|
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 logging
|
|
19
|
+
import pickle
|
|
20
|
+
import warnings
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import mlflow
|
|
24
|
+
import mlflow.data
|
|
25
|
+
|
|
26
|
+
from hamilton import graph_types
|
|
27
|
+
from hamilton.lifecycle import GraphConstructionHook, GraphExecutionHook, NodeExecutionHook
|
|
28
|
+
|
|
29
|
+
# silence odd ongoing MLFlow issue that spams warnings
|
|
30
|
+
# GitHub Issue https://github.com/mlflow/mlflow/issues/8605
|
|
31
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
FIGURE_TYPES = []
|
|
35
|
+
try:
|
|
36
|
+
import matplotlib.figure
|
|
37
|
+
|
|
38
|
+
FIGURE_TYPES.append(matplotlib.figure.Figure)
|
|
39
|
+
except ImportError:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
import plotly.graph_objects
|
|
44
|
+
|
|
45
|
+
FIGURE_TYPES.append(plotly.graph_objects.Figure)
|
|
46
|
+
except ImportError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_path_from_metadata(metadata: dict) -> str | None:
|
|
54
|
+
"""Retrieve the `path` attribute from DataSaver output metadata"""
|
|
55
|
+
path = None
|
|
56
|
+
if "path" in metadata:
|
|
57
|
+
path = metadata["path"]
|
|
58
|
+
elif "file_metadata" in metadata:
|
|
59
|
+
path = metadata["file_metadata"]["path"]
|
|
60
|
+
|
|
61
|
+
return path
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# NOTE `mlflow.client.MLFlowClient` is preferred to top-level `mlflow.` methods in MLFlowTracker
|
|
65
|
+
# because the latter relies on hard-to-debug global variables. Yet, we set an `active_run` by using
|
|
66
|
+
# `mlflow.start_run()` in pre_graph_execution to ensure the user-specified MLFlow code
|
|
67
|
+
# and MLFlow materializers log metrics and models to the same run as the MLFlowTracker
|
|
68
|
+
class MLFlowTracker(
|
|
69
|
+
NodeExecutionHook,
|
|
70
|
+
GraphExecutionHook,
|
|
71
|
+
GraphConstructionHook,
|
|
72
|
+
):
|
|
73
|
+
"""Driver adapter logging Hamilton execution results to an MLFlow server."""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
tracking_uri: str | None = None,
|
|
78
|
+
registry_uri: str | None = None,
|
|
79
|
+
artifact_location: str | None = None,
|
|
80
|
+
experiment_name: str = "Hamilton",
|
|
81
|
+
experiment_tags: dict | None = None,
|
|
82
|
+
experiment_description: str | None = None,
|
|
83
|
+
run_id: str | None = None,
|
|
84
|
+
run_name: str | None = None,
|
|
85
|
+
run_tags: dict | None = None,
|
|
86
|
+
run_description: str | None = None,
|
|
87
|
+
log_system_metrics: bool = False,
|
|
88
|
+
):
|
|
89
|
+
"""Configure the MLFlow client and experiment for the lifetime of the tracker
|
|
90
|
+
|
|
91
|
+
:param tracking_uri: Destination of the logged artifacts and metadata. It can be a filesystem, database, or server. [reference](https://mlflow.org/docs/latest/getting-started/tracking-server-overview/index.html)
|
|
92
|
+
:param registry_uri: Destination of the registered models. By default it's the same as the tracking destination, but they can be different. [reference](https://mlflow.org/docs/latest/getting-started/registering-first-model/index.html)
|
|
93
|
+
:param artifact_location: Root path on tracking server where experiment is stored
|
|
94
|
+
:param experiment_name: MLFlow experiment name used to group runs.
|
|
95
|
+
:param experiment_tags: Tags to query experiments programmatically (not displayed).
|
|
96
|
+
:param experiment_description: Description of the experiment displayed
|
|
97
|
+
:param run_id: Run id to log to an existing run (every execution logs to the same run)
|
|
98
|
+
:param run_name: Run name displayed and used to query runs. You can have multiple runs with the same name but different run ids.
|
|
99
|
+
:param run_tags: Tags to query runs and appears as columns in the UI for filtering and grouping. It automatically includes serializable inputs and Driver config.
|
|
100
|
+
:param run_description: Description of the run displayed
|
|
101
|
+
:param log_system_metrics: Log system metrics to display (requires additonal dependencies)
|
|
102
|
+
"""
|
|
103
|
+
self.client = mlflow.client.MlflowClient(tracking_uri, registry_uri)
|
|
104
|
+
|
|
105
|
+
# experiment setup
|
|
106
|
+
experiment_tags = experiment_tags or {}
|
|
107
|
+
if experiment_description:
|
|
108
|
+
# mlflow.note.content is the description field
|
|
109
|
+
experiment_tags["mlflow.note.content"] = experiment_description
|
|
110
|
+
|
|
111
|
+
# TODO link HamiltonTracker project and MLFlowTracker experiment
|
|
112
|
+
experiment = self.client.get_experiment_by_name(experiment_name)
|
|
113
|
+
if experiment:
|
|
114
|
+
experiment_id = experiment.experiment_id
|
|
115
|
+
# update tags and description of an existing experiment
|
|
116
|
+
if experiment_tags:
|
|
117
|
+
for k, v in experiment_tags.items():
|
|
118
|
+
self.client.set_experiment_tag(experiment_id, key=k, value=v)
|
|
119
|
+
# create an experiment
|
|
120
|
+
else:
|
|
121
|
+
experiment_id = self.client.create_experiment(
|
|
122
|
+
name=experiment_name,
|
|
123
|
+
artifact_location=artifact_location,
|
|
124
|
+
tags=experiment_tags,
|
|
125
|
+
)
|
|
126
|
+
self.experiment_id = experiment_id
|
|
127
|
+
|
|
128
|
+
# run setup
|
|
129
|
+
# TODO link HamiltonTracker and MLFlowTracker run ids
|
|
130
|
+
self.mlflow_run_id = run_id
|
|
131
|
+
self.run_name = run_name
|
|
132
|
+
self.run_tags = run_tags or {}
|
|
133
|
+
if run_description:
|
|
134
|
+
# mlflow.note.content is the description field
|
|
135
|
+
self.run_tags["mlflow.note.content"] = run_description
|
|
136
|
+
|
|
137
|
+
self.log_system_metrics = log_system_metrics
|
|
138
|
+
|
|
139
|
+
def run_after_graph_construction(self, *, config: dict[str, Any], **kwargs):
|
|
140
|
+
"""Store the Driver config before creating the graph"""
|
|
141
|
+
self.config = config
|
|
142
|
+
|
|
143
|
+
def run_before_graph_execution(
|
|
144
|
+
self,
|
|
145
|
+
*,
|
|
146
|
+
run_id: str,
|
|
147
|
+
final_vars: list[str],
|
|
148
|
+
inputs: dict[str, Any],
|
|
149
|
+
graph: graph_types.HamiltonGraph,
|
|
150
|
+
**kwargs,
|
|
151
|
+
):
|
|
152
|
+
"""Create and start MLFlow run. Log graph version, run_id, inputs, overrides"""
|
|
153
|
+
# add Hamilton metadata to run tags
|
|
154
|
+
run_tags = self.run_tags
|
|
155
|
+
run_tags["hamilton_run_id"] = run_id # the Hamilton run_id
|
|
156
|
+
run_tags["code_version"] = graph.version
|
|
157
|
+
|
|
158
|
+
# create Hamilton run
|
|
159
|
+
self.run = self.client.create_run(
|
|
160
|
+
experiment_id=self.experiment_id,
|
|
161
|
+
tags=run_tags,
|
|
162
|
+
run_name=self.run_name,
|
|
163
|
+
)
|
|
164
|
+
self.run_id = self.run.info.run_id
|
|
165
|
+
# start run to set `active_run` and allow user-defined callbacks and materializers
|
|
166
|
+
# to log to the same run as the HamiltonTracker
|
|
167
|
+
mlflow.start_run(
|
|
168
|
+
run_id=self.run_id,
|
|
169
|
+
experiment_id=self.experiment_id,
|
|
170
|
+
tags=run_tags,
|
|
171
|
+
log_system_metrics=self.log_system_metrics,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# log config to artifacts
|
|
175
|
+
self.client.log_dict(self.run_id, self.config, "config.json")
|
|
176
|
+
|
|
177
|
+
# log HamiltonGraph to reproduce the run
|
|
178
|
+
self.graph = graph
|
|
179
|
+
graph_as_json = {n.name: n.as_dict() for n in graph.nodes}
|
|
180
|
+
self.client.log_dict(self.run_id, graph_as_json, "hamilton_graph.json")
|
|
181
|
+
|
|
182
|
+
# log config and inputs as `param` which creates columns in the UI to filter runs
|
|
183
|
+
# `log_param()` accepts `value: Any` and will stringify complex objects
|
|
184
|
+
for value_sets in [self.config, inputs]:
|
|
185
|
+
for node_name, value in value_sets.items():
|
|
186
|
+
self.client.log_param(self.run_id, key=node_name, value=value)
|
|
187
|
+
|
|
188
|
+
self.final_vars = final_vars
|
|
189
|
+
|
|
190
|
+
# TODO log DataLoaders as MLFlow datasets
|
|
191
|
+
def run_after_node_execution(
|
|
192
|
+
self,
|
|
193
|
+
*,
|
|
194
|
+
node_name: str,
|
|
195
|
+
node_return_type: type,
|
|
196
|
+
node_tags: dict,
|
|
197
|
+
node_kwargs: dict,
|
|
198
|
+
result: Any,
|
|
199
|
+
**kwargs,
|
|
200
|
+
):
|
|
201
|
+
"""Log materializers and final vars as artifacts"""
|
|
202
|
+
# log DataSavers as artifacts
|
|
203
|
+
# TODO refactor if/else as `handle_materializers()`
|
|
204
|
+
if node_tags.get("hamilton.data_saver") is True:
|
|
205
|
+
# don't log mlflow materializers as artifact since they already create models
|
|
206
|
+
# instead, use the Materializer metadata to add metadata to registered models
|
|
207
|
+
if node_tags["hamilton.data_saver.sink"] == "mlflow":
|
|
208
|
+
# skip if not registered model
|
|
209
|
+
if "registered_model" not in result.keys():
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
# get the registered model name (param of MLFlowModelSaver)
|
|
213
|
+
model_name = result["registered_model"]["name"]
|
|
214
|
+
version = result["registered_model"]["version"]
|
|
215
|
+
materializer_node = self.graph[node_name]
|
|
216
|
+
# get the "materialized node" defining the model
|
|
217
|
+
materialized_node = self.graph[materializer_node.required_dependencies.pop()]
|
|
218
|
+
# add the materialized node docstring as description
|
|
219
|
+
# registered models have multiple versions
|
|
220
|
+
self.client.update_registered_model(model_name, materialized_node.documentation)
|
|
221
|
+
self.client.update_model_version(
|
|
222
|
+
model_name, version, materialized_node.documentation
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# add node name as tag
|
|
226
|
+
self.client.set_registered_model_tag(
|
|
227
|
+
model_name, key="node_name", value=materialized_node.name
|
|
228
|
+
)
|
|
229
|
+
self.client.set_model_version_tag(
|
|
230
|
+
model_name, version, key="node_name", value=materialized_node.name
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# add origin function name as tag
|
|
234
|
+
self.client.set_registered_model_tag(
|
|
235
|
+
model_name,
|
|
236
|
+
key="function_name",
|
|
237
|
+
value=materialized_node.originating_functions[0].__name__,
|
|
238
|
+
)
|
|
239
|
+
self.client.set_model_version_tag(
|
|
240
|
+
model_name,
|
|
241
|
+
version,
|
|
242
|
+
key="function_name",
|
|
243
|
+
value=materialized_node.originating_functions[0].__name__,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# add the materialized node @tag values as tags
|
|
247
|
+
for k, v in materialized_node.tags.items():
|
|
248
|
+
# skip internal Hamilton tags
|
|
249
|
+
if "hamilton." in k:
|
|
250
|
+
continue
|
|
251
|
+
self.client.set_registered_model_tag(model_name, key=k, value=v)
|
|
252
|
+
self.client.set_model_version_tag(model_name, version, key=k, value=v)
|
|
253
|
+
# TODO automatically collect model input signature; maybe simpler from user code
|
|
254
|
+
|
|
255
|
+
# special case for matplotlib and plotly
|
|
256
|
+
# log materialized figure. Allows great degree of control over rendering format
|
|
257
|
+
# and also save interactive plotly visualization as HTML
|
|
258
|
+
elif node_tags["hamilton.data_saver.sink"] in ["plt", "plotly"]:
|
|
259
|
+
materializer_node = self.graph[node_name]
|
|
260
|
+
materialized_node = self.graph[materializer_node.required_dependencies.pop()]
|
|
261
|
+
figure = node_kwargs[materialized_node.name]
|
|
262
|
+
|
|
263
|
+
path = get_path_from_metadata(result)
|
|
264
|
+
if path:
|
|
265
|
+
self.client.log_figure(self.run_id, figure, path)
|
|
266
|
+
else:
|
|
267
|
+
logger.warning(
|
|
268
|
+
f"Materialization result from node={node_name} has no recordable path: {result}. Materializer must have either "
|
|
269
|
+
f"'path' or 'file_metadata' keys."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
else:
|
|
273
|
+
# log the materializer path as an artifact
|
|
274
|
+
path = get_path_from_metadata(result)
|
|
275
|
+
if path:
|
|
276
|
+
self.client.log_artifact(self.run_id, path, node_name)
|
|
277
|
+
else:
|
|
278
|
+
logger.warning(
|
|
279
|
+
f"Materialization result from node={node_name} has no recordable path: {result}. Materializer must have either "
|
|
280
|
+
f"'path' or 'file_metadata' keys."
|
|
281
|
+
)
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
# log final_vars as artifacts
|
|
285
|
+
if node_name not in self.final_vars:
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
# TODO refactor if/else as `handle_final_vars()`
|
|
289
|
+
# log float and int as metrics
|
|
290
|
+
if node_return_type in [float, int]:
|
|
291
|
+
self.client.log_metric(self.run_id, key=node_name, value=float(result))
|
|
292
|
+
|
|
293
|
+
# log str as text in .txt format
|
|
294
|
+
elif isinstance(node_return_type, str):
|
|
295
|
+
file_path = f"{node_name}.txt"
|
|
296
|
+
with open(file_path, "w") as f:
|
|
297
|
+
f.write(result)
|
|
298
|
+
self.client.log_text(self.run_id, result, file_path)
|
|
299
|
+
|
|
300
|
+
# log_dict (JSON) dictionary types; pickle if not json-serializable
|
|
301
|
+
elif isinstance(node_return_type, dict):
|
|
302
|
+
try:
|
|
303
|
+
file_path = f"{node_name}.json"
|
|
304
|
+
self.client.log_dict(self.run_id, result, file_path)
|
|
305
|
+
# not json-serializable
|
|
306
|
+
except TypeError:
|
|
307
|
+
file_path = f"{node_name}.pickle"
|
|
308
|
+
with open(file_path, "wb") as f:
|
|
309
|
+
pickle.dump(result, file=f)
|
|
310
|
+
self.client.log_dict(self.run_id, result, file_path)
|
|
311
|
+
|
|
312
|
+
# this puts less burden on users by not having to define materializers
|
|
313
|
+
# for viz, but less control over rendering format
|
|
314
|
+
elif node_return_type in FIGURE_TYPES:
|
|
315
|
+
file_path = f"{node_name}.png"
|
|
316
|
+
self.client.log_figure(self.run_id, result, file_path)
|
|
317
|
+
|
|
318
|
+
# default to log_artifact in .pickle format
|
|
319
|
+
else:
|
|
320
|
+
file_path = f"{node_name}.pickle"
|
|
321
|
+
with open(file_path, "wb") as f:
|
|
322
|
+
pickle.dump(result, f)
|
|
323
|
+
self.client.log_dict(self.run_id, result, file_path)
|
|
324
|
+
|
|
325
|
+
def run_after_graph_execution(self, success: bool, *args, **kwargs):
|
|
326
|
+
"""End the MLFlow run"""
|
|
327
|
+
# `status` is an enum value of mlflow.entities.RunStatus
|
|
328
|
+
if success:
|
|
329
|
+
self.client.set_terminated(self.run_id, status="FINISHED")
|
|
330
|
+
else:
|
|
331
|
+
self.client.set_terminated(self.run_id, status="FAILED")
|
|
332
|
+
mlflow.end_run()
|
|
333
|
+
|
|
334
|
+
def run_before_node_execution(self, *args, **kwargs):
|
|
335
|
+
"""Placeholder required to subclass NodeExecutionHook"""
|