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,454 @@
|
|
|
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
|
+
"""Synchronous/asynchronous adapter and functions for context-aware logging with Hamilton."""
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import sys
|
|
22
|
+
from collections.abc import Mapping, MutableMapping
|
|
23
|
+
from contextvars import ContextVar
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from typing import (
|
|
26
|
+
TYPE_CHECKING,
|
|
27
|
+
Any,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from hamilton.graph_types import HamiltonNode
|
|
31
|
+
from hamilton.lifecycle.api import (
|
|
32
|
+
GraphExecutionHook,
|
|
33
|
+
NodeExecutionHook,
|
|
34
|
+
TaskExecutionHook,
|
|
35
|
+
TaskGroupingHook,
|
|
36
|
+
TaskReturnHook,
|
|
37
|
+
TaskSubmissionHook,
|
|
38
|
+
)
|
|
39
|
+
from hamilton.lifecycle.base import BasePostNodeExecuteAsync, BasePreNodeExecute
|
|
40
|
+
from hamilton.node import Node
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
from typing import override
|
|
44
|
+
except ImportError:
|
|
45
|
+
override = lambda x: x # noqa E731
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if sys.version_info >= (3, 12):
|
|
49
|
+
LoggerAdapter = logging.LoggerAdapter[logging.Logger]
|
|
50
|
+
else:
|
|
51
|
+
if TYPE_CHECKING:
|
|
52
|
+
LoggerAdapter = logging.LoggerAdapter[logging.Logger]
|
|
53
|
+
else:
|
|
54
|
+
LoggerAdapter = logging.LoggerAdapter
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class _LoggingContext:
|
|
59
|
+
"""Represents the current logging context."""
|
|
60
|
+
|
|
61
|
+
graph: str | None = None
|
|
62
|
+
node: str | None = None
|
|
63
|
+
task: str | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Context variables for context-aware logging
|
|
67
|
+
_local_context = ContextVar("context", default=_LoggingContext()) # noqa: B039
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_logger(name: str | None = None) -> "ContextLogger":
|
|
71
|
+
"""Returns a context-aware logger for the specified name (created if necessary).
|
|
72
|
+
|
|
73
|
+
:param name: Name of the logger, defaults to root logger if not provided.
|
|
74
|
+
"""
|
|
75
|
+
logger = logging.getLogger(name)
|
|
76
|
+
return ContextLogger(logger, extra={})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ContextLogger(LoggerAdapter):
|
|
80
|
+
"""Custom logger adapter for Hamilton that adds context to log messages.
|
|
81
|
+
|
|
82
|
+
This logger adds context-aware prefix to log messages based on the current execution. The
|
|
83
|
+
logger is intended to be used with hamilton the `LoggingAdapter` lifecycle adapter. The context
|
|
84
|
+
is both thread-safe and async-safe. Context includes the following details:
|
|
85
|
+
- Graph run
|
|
86
|
+
- Task ID
|
|
87
|
+
- Node ID
|
|
88
|
+
|
|
89
|
+
The adapter also supports the following extra fields:
|
|
90
|
+
- `override_context`: Overrides the current context with the specified context.
|
|
91
|
+
- `skip_context`: Skips the context for the current log message.
|
|
92
|
+
- `node_context`: Includes additional node information for task-based log messages.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
@override
|
|
96
|
+
def process(
|
|
97
|
+
self, msg: str, kwargs: MutableMapping[str, Any]
|
|
98
|
+
) -> tuple[str, MutableMapping[str, Any]]:
|
|
99
|
+
# Ensure that the extra fields are passed through correctly
|
|
100
|
+
kwargs["extra"] = {**(self.extra or {}), **(kwargs.get("extra") or {})}
|
|
101
|
+
|
|
102
|
+
# Add the current prefix to the log message
|
|
103
|
+
prefix = self._get_current_context(kwargs["extra"])
|
|
104
|
+
msg = f"{prefix}{msg}"
|
|
105
|
+
|
|
106
|
+
return (msg, kwargs)
|
|
107
|
+
|
|
108
|
+
def _get_current_context(self, extra: Mapping[str, Any]) -> str:
|
|
109
|
+
"""Returns the current context."""
|
|
110
|
+
|
|
111
|
+
# Extra option to override context
|
|
112
|
+
context = extra.get("override_context", None)
|
|
113
|
+
if not isinstance(context, _LoggingContext):
|
|
114
|
+
context = _local_context.get()
|
|
115
|
+
|
|
116
|
+
# Extra option to skip context
|
|
117
|
+
if "skip_context" in extra:
|
|
118
|
+
return ""
|
|
119
|
+
|
|
120
|
+
if context.task:
|
|
121
|
+
# Extra option to include node information on task-based log messages
|
|
122
|
+
if context.node and "node_context" in extra:
|
|
123
|
+
return f"Task '{context.task}' - Node '{context.node}' - "
|
|
124
|
+
return f"Task '{context.task}' - "
|
|
125
|
+
|
|
126
|
+
if context.node:
|
|
127
|
+
return f"Node '{context.node}' - "
|
|
128
|
+
|
|
129
|
+
if context.graph:
|
|
130
|
+
return f"Graph run '{context.graph}' - "
|
|
131
|
+
|
|
132
|
+
return ""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class LoggingAdapter(
|
|
136
|
+
GraphExecutionHook,
|
|
137
|
+
NodeExecutionHook,
|
|
138
|
+
TaskGroupingHook,
|
|
139
|
+
TaskSubmissionHook,
|
|
140
|
+
TaskExecutionHook,
|
|
141
|
+
TaskReturnHook,
|
|
142
|
+
):
|
|
143
|
+
"""Hamilton lifecycle adapter that logs runtime execution events.
|
|
144
|
+
|
|
145
|
+
This adapter logs the following hamilton events:
|
|
146
|
+
- Graph start (`GraphExecutionHook`)
|
|
147
|
+
- Task grouping (`TaskGroupingHook`)
|
|
148
|
+
- Task submission (`TaskSubmissionHook`)
|
|
149
|
+
- Task pre-execution (`TaskExecutionHook`))
|
|
150
|
+
- Node pre-execution (`NodeExecutionHook`)
|
|
151
|
+
- Node post-execution (`NodeExecutionHook`)
|
|
152
|
+
- Task post-execution (`TaskExecutionHook`))
|
|
153
|
+
- Task resolution (`TaskReturnHook`)
|
|
154
|
+
- Graph completion (`GraphExecutionHook`)
|
|
155
|
+
|
|
156
|
+
This adapter can be run with both node-based and task-based execution (using the V2 executor).
|
|
157
|
+
When run with a node-based executor, the adapter logs the execution of each *node* as `INFO`.
|
|
158
|
+
When run with a task-based executor, the adapter logs the execution of each *task* as `INFO`
|
|
159
|
+
and the execution of each *node* as `DEBUG`.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def __init__(self, logger: str | logging.Logger | None = None) -> None:
|
|
163
|
+
# Precompute or overridden nodes
|
|
164
|
+
self._inputs_nodes: set[str] = set()
|
|
165
|
+
self._override_nodes: set[str] = set()
|
|
166
|
+
|
|
167
|
+
if logger is None:
|
|
168
|
+
self.logger = logging.getLogger(__name__)
|
|
169
|
+
elif isinstance(logger, logging.Logger):
|
|
170
|
+
self.logger = logger
|
|
171
|
+
else:
|
|
172
|
+
self.logger = logging.getLogger(logger)
|
|
173
|
+
|
|
174
|
+
if not isinstance(self.logger, ContextLogger):
|
|
175
|
+
self.logger = ContextLogger(self.logger, extra={})
|
|
176
|
+
|
|
177
|
+
self._exception_logged = False # For tracking remote exceptions
|
|
178
|
+
|
|
179
|
+
@override
|
|
180
|
+
def run_before_graph_execution(
|
|
181
|
+
self,
|
|
182
|
+
*,
|
|
183
|
+
inputs: dict[str, Any] | None,
|
|
184
|
+
overrides: dict[str, Any] | None,
|
|
185
|
+
run_id: str,
|
|
186
|
+
**future_kwargs: Any,
|
|
187
|
+
):
|
|
188
|
+
# Set context before logging
|
|
189
|
+
_local_context.set(_LoggingContext(graph=run_id))
|
|
190
|
+
|
|
191
|
+
self._inputs_nodes.update(inputs or [])
|
|
192
|
+
self._override_nodes.update(overrides or [])
|
|
193
|
+
|
|
194
|
+
message = "Starting graph execution"
|
|
195
|
+
self.logger.info(message)
|
|
196
|
+
|
|
197
|
+
if inputs:
|
|
198
|
+
names = ", ".join(f"'{key}'" for key in inputs)
|
|
199
|
+
self.logger.info("Using inputs %s", names)
|
|
200
|
+
|
|
201
|
+
if overrides:
|
|
202
|
+
names = ", ".join(f"'{key}'" for key in overrides)
|
|
203
|
+
self.logger.info("Using overrides %s", names)
|
|
204
|
+
|
|
205
|
+
@override
|
|
206
|
+
def run_after_task_grouping(self, *, run_id: str, task_ids: list[str], **future_kwargs):
|
|
207
|
+
self.logger.info("Dynamic DAG detected; task-based logging is enabled")
|
|
208
|
+
|
|
209
|
+
@override
|
|
210
|
+
def run_after_task_expansion(self, **future_kwargs):
|
|
211
|
+
pass # Note currently uses; required for TaskGroupingHook
|
|
212
|
+
|
|
213
|
+
@override
|
|
214
|
+
def run_before_task_submission(
|
|
215
|
+
self,
|
|
216
|
+
*,
|
|
217
|
+
run_id: str,
|
|
218
|
+
task_id: str,
|
|
219
|
+
spawning_task_id: str | None,
|
|
220
|
+
**future_kwargs,
|
|
221
|
+
):
|
|
222
|
+
# Set context before logging
|
|
223
|
+
_local_context.set(_LoggingContext(graph=run_id, task=task_id))
|
|
224
|
+
|
|
225
|
+
if spawning_task_id:
|
|
226
|
+
self.logger.debug("Spawning task and submitting to executor")
|
|
227
|
+
else:
|
|
228
|
+
self.logger.debug("Initializing new task and submitting to executor")
|
|
229
|
+
|
|
230
|
+
@override
|
|
231
|
+
def run_before_task_execution(
|
|
232
|
+
self,
|
|
233
|
+
*,
|
|
234
|
+
task_id: str,
|
|
235
|
+
run_id: str,
|
|
236
|
+
nodes: list[HamiltonNode],
|
|
237
|
+
**future_kwargs,
|
|
238
|
+
):
|
|
239
|
+
# Set context before logging
|
|
240
|
+
_local_context.set(_LoggingContext(graph=run_id, task=task_id))
|
|
241
|
+
|
|
242
|
+
# Do not log if the task matches the inputs or overrides
|
|
243
|
+
if task_id in self._inputs_nodes or task_id in self._override_nodes:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
message = "Starting execution"
|
|
247
|
+
if len(nodes) == 1 and nodes[0].name == task_id: # single node task
|
|
248
|
+
self.logger.debug(message)
|
|
249
|
+
else:
|
|
250
|
+
message += " of nodes %s"
|
|
251
|
+
names = ", ".join(f"'{node.name}'" for node in nodes)
|
|
252
|
+
self.logger.debug(message, names)
|
|
253
|
+
|
|
254
|
+
@override
|
|
255
|
+
def run_before_node_execution(
|
|
256
|
+
self,
|
|
257
|
+
*,
|
|
258
|
+
node_name: str,
|
|
259
|
+
node_kwargs: dict[str, Any],
|
|
260
|
+
task_id: str | None,
|
|
261
|
+
run_id: str,
|
|
262
|
+
**future_kwargs: Any,
|
|
263
|
+
):
|
|
264
|
+
# Set context before logging
|
|
265
|
+
_local_context.set(_LoggingContext(graph=run_id, task=task_id, node=node_name))
|
|
266
|
+
|
|
267
|
+
message = "Starting execution"
|
|
268
|
+
extra = {"include_task_node": True}
|
|
269
|
+
if node_kwargs:
|
|
270
|
+
message += " with dependencies %s"
|
|
271
|
+
params = ", ".join(f"'{key}'" for key in node_kwargs)
|
|
272
|
+
self.logger.debug(message, params, extra=extra)
|
|
273
|
+
else:
|
|
274
|
+
message += " without dependencies"
|
|
275
|
+
self.logger.debug(message, extra=extra)
|
|
276
|
+
|
|
277
|
+
@override
|
|
278
|
+
def run_after_node_execution(
|
|
279
|
+
self,
|
|
280
|
+
*,
|
|
281
|
+
node_name: str,
|
|
282
|
+
error: Exception | None,
|
|
283
|
+
success: bool,
|
|
284
|
+
task_id: str | None,
|
|
285
|
+
run_id: str,
|
|
286
|
+
**future_kwargs: Any,
|
|
287
|
+
):
|
|
288
|
+
# Reset context before logging via the token
|
|
289
|
+
_local_context.set(_LoggingContext(graph=run_id, task=task_id))
|
|
290
|
+
|
|
291
|
+
# Logger should use previous context and include node information for this method
|
|
292
|
+
extra = {
|
|
293
|
+
"override_context": _LoggingContext(graph=run_id, task=task_id, node=node_name),
|
|
294
|
+
"node_context": True,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if success:
|
|
298
|
+
log_func = self.logger.debug if task_id else self.logger.info
|
|
299
|
+
log_func("Finished execution [OK]", extra=extra)
|
|
300
|
+
elif error:
|
|
301
|
+
self.logger.exception("Encountered error", extra=extra)
|
|
302
|
+
self._exception_logged = True
|
|
303
|
+
|
|
304
|
+
@override
|
|
305
|
+
def run_after_task_execution(
|
|
306
|
+
self,
|
|
307
|
+
*,
|
|
308
|
+
task_id: str,
|
|
309
|
+
run_id: str,
|
|
310
|
+
success: bool,
|
|
311
|
+
error: Exception,
|
|
312
|
+
**future_kwargs,
|
|
313
|
+
):
|
|
314
|
+
# Reset context before logging
|
|
315
|
+
_local_context.set(_LoggingContext(graph=run_id))
|
|
316
|
+
|
|
317
|
+
# Logger should use previous context for this method
|
|
318
|
+
extra = {"override_context": _LoggingContext(graph=run_id, task=task_id)}
|
|
319
|
+
|
|
320
|
+
# Do not log if the task matches the inputs or overrides
|
|
321
|
+
if task_id in self._inputs_nodes or task_id in self._override_nodes:
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
if success:
|
|
325
|
+
self.logger.debug("Finished execution [OK]", extra=extra)
|
|
326
|
+
elif error:
|
|
327
|
+
self.logger.error("Execution failed due to errors", extra=extra)
|
|
328
|
+
|
|
329
|
+
@override
|
|
330
|
+
def run_after_task_return(
|
|
331
|
+
self,
|
|
332
|
+
*,
|
|
333
|
+
run_id: str,
|
|
334
|
+
task_id: str,
|
|
335
|
+
nodes: list[Node],
|
|
336
|
+
success: bool,
|
|
337
|
+
error: Exception | None,
|
|
338
|
+
**future_kwargs,
|
|
339
|
+
):
|
|
340
|
+
# Hard reset context before logging
|
|
341
|
+
_local_context.set(_LoggingContext(graph=run_id))
|
|
342
|
+
|
|
343
|
+
# Logger should use previous context for this method
|
|
344
|
+
extra = {"override_context": _LoggingContext(graph=run_id, task=task_id)}
|
|
345
|
+
|
|
346
|
+
if success:
|
|
347
|
+
# Input and override tasks should be logged as debug
|
|
348
|
+
if task_id in self._inputs_nodes or task_id in self._override_nodes:
|
|
349
|
+
log_func = self.logger.debug
|
|
350
|
+
else:
|
|
351
|
+
log_func = self.logger.info
|
|
352
|
+
log_func("Task completed [OK]", extra=extra)
|
|
353
|
+
elif error and not self._exception_logged:
|
|
354
|
+
# NOTE: _exception_logged is used to prevent duplicate exception logging
|
|
355
|
+
self.logger.exception("Task completion failed due to errors", extra=extra)
|
|
356
|
+
self._exception_logged = True
|
|
357
|
+
|
|
358
|
+
@override
|
|
359
|
+
def run_after_graph_execution(
|
|
360
|
+
self,
|
|
361
|
+
*,
|
|
362
|
+
success: bool,
|
|
363
|
+
run_id: str,
|
|
364
|
+
**future_kwargs: Any,
|
|
365
|
+
):
|
|
366
|
+
# Hard reset context before logging
|
|
367
|
+
_local_context.set(_LoggingContext())
|
|
368
|
+
|
|
369
|
+
# Logger should use previous context for this method
|
|
370
|
+
extra = {"override_context": _LoggingContext(graph=run_id)}
|
|
371
|
+
|
|
372
|
+
if success:
|
|
373
|
+
self.logger.info("Finished graph execution [OK]", extra=extra)
|
|
374
|
+
else:
|
|
375
|
+
self.logger.error("Graph execution failed due to errors", extra=extra)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class AsyncLoggingAdapter(GraphExecutionHook, BasePreNodeExecute, BasePostNodeExecuteAsync):
|
|
379
|
+
"""Async version of the `LoggingAdapter`.
|
|
380
|
+
|
|
381
|
+
This adapter logs the following hamilton events:
|
|
382
|
+
- Graph start (`GraphExecutionHook`)
|
|
383
|
+
- Node pre-execution (`BasePreNodeExecute`)
|
|
384
|
+
- Node post-execution (`BasePostNodeExecuteAsync`)
|
|
385
|
+
- Graph completion (`GraphExecutionHook`)
|
|
386
|
+
|
|
387
|
+
Note that this adapter is intended to be used with the async driver. Due to current limitations
|
|
388
|
+
with the async driver, this adapter is only able to approximate when the async node has been
|
|
389
|
+
submitted. It cannot currently log the exact moment the async node begins execution.
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
def __init__(self, logger: str | logging.Logger | None = None) -> None:
|
|
393
|
+
self._impl = LoggingAdapter(logger)
|
|
394
|
+
|
|
395
|
+
@override
|
|
396
|
+
def run_before_graph_execution(
|
|
397
|
+
self,
|
|
398
|
+
*,
|
|
399
|
+
inputs: dict[str, Any],
|
|
400
|
+
overrides: dict[str, Any],
|
|
401
|
+
run_id: str,
|
|
402
|
+
**future_kwargs: Any,
|
|
403
|
+
):
|
|
404
|
+
self._impl.run_before_graph_execution(inputs=inputs, overrides=overrides, run_id=run_id)
|
|
405
|
+
|
|
406
|
+
@override
|
|
407
|
+
def pre_node_execute(
|
|
408
|
+
self, *, run_id: str, node_: Node, kwargs: dict[str, Any], task_id: str | None = None
|
|
409
|
+
):
|
|
410
|
+
# NOTE: We call the base synchronous method here in order to approximate when the async task
|
|
411
|
+
# has bee submitted. This is a workaround until further work is done on the async adapter.
|
|
412
|
+
|
|
413
|
+
# Set context before logging
|
|
414
|
+
_local_context.set(_LoggingContext(graph=run_id, task=None, node=node_.name))
|
|
415
|
+
|
|
416
|
+
message = "Submitting async node"
|
|
417
|
+
extra = {"include_task_node": True}
|
|
418
|
+
if kwargs:
|
|
419
|
+
message += " with dependencies %s"
|
|
420
|
+
params = ", ".join(f"'{key}'" for key in kwargs)
|
|
421
|
+
self._impl.logger.debug(message, params, extra=extra)
|
|
422
|
+
else:
|
|
423
|
+
message += " without dependencies"
|
|
424
|
+
self._impl.logger.debug(message, extra=extra)
|
|
425
|
+
|
|
426
|
+
@override
|
|
427
|
+
async def post_node_execute(
|
|
428
|
+
self,
|
|
429
|
+
*,
|
|
430
|
+
run_id: str,
|
|
431
|
+
node_: Node,
|
|
432
|
+
kwargs: dict[str, Any],
|
|
433
|
+
success: bool,
|
|
434
|
+
error: Exception | None,
|
|
435
|
+
result: Any,
|
|
436
|
+
task_id: str | None = None,
|
|
437
|
+
):
|
|
438
|
+
self._impl.run_after_node_execution(
|
|
439
|
+
node_name=node_.name,
|
|
440
|
+
error=error,
|
|
441
|
+
success=success,
|
|
442
|
+
task_id=task_id,
|
|
443
|
+
run_id=run_id,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
@override
|
|
447
|
+
def run_after_graph_execution(
|
|
448
|
+
self,
|
|
449
|
+
*,
|
|
450
|
+
success: bool,
|
|
451
|
+
run_id: str,
|
|
452
|
+
**future_kwargs: Any,
|
|
453
|
+
):
|
|
454
|
+
self._impl.run_after_graph_execution(success=success, run_id=run_id)
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
"""Hamilton MCP server plugin -- exposes DAG validation as interactive tools."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_mcp_server():
|
|
22
|
+
"""Get the Hamilton MCP server instance.
|
|
23
|
+
|
|
24
|
+
Requires fastmcp: pip install "apache-hamilton[mcp]"
|
|
25
|
+
"""
|
|
26
|
+
from hamilton.plugins.h_mcp.server import mcp
|
|
27
|
+
|
|
28
|
+
return mcp
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
|
|
19
|
+
def main():
|
|
20
|
+
try:
|
|
21
|
+
from fastmcp import FastMCP # noqa: F401
|
|
22
|
+
except ModuleNotFoundError as e:
|
|
23
|
+
raise ModuleNotFoundError(
|
|
24
|
+
"FastMCP is required. Install with: pip install 'apache-hamilton[mcp]'"
|
|
25
|
+
) from e
|
|
26
|
+
|
|
27
|
+
from hamilton.plugins.h_mcp.server import mcp
|
|
28
|
+
|
|
29
|
+
mcp.run()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
main()
|
|
@@ -0,0 +1,129 @@
|
|
|
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 linecache
|
|
21
|
+
import sys
|
|
22
|
+
import traceback
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
from uuid import uuid4
|
|
25
|
+
|
|
26
|
+
from hamilton import ad_hoc_utils, driver
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from types import ModuleType
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_driver_from_code(
|
|
33
|
+
code: str, config: dict | None = None
|
|
34
|
+
) -> tuple[driver.Driver, ModuleType]:
|
|
35
|
+
"""Build a Hamilton Driver from a code string.
|
|
36
|
+
|
|
37
|
+
Uses ad_hoc_utils.module_from_source to create a temp module,
|
|
38
|
+
then builds the Driver with dynamic execution enabled.
|
|
39
|
+
|
|
40
|
+
:param code: Python source code defining Hamilton functions.
|
|
41
|
+
:param config: Optional Hamilton config dict.
|
|
42
|
+
:return: Tuple of (Driver, temp_module). Caller must clean up the module.
|
|
43
|
+
"""
|
|
44
|
+
module_name = f"mcp_temp_{uuid4().hex}"
|
|
45
|
+
module = ad_hoc_utils.module_from_source(code, module_name=module_name)
|
|
46
|
+
dr = (
|
|
47
|
+
driver.Builder()
|
|
48
|
+
.enable_dynamic_execution(allow_experimental_mode=True)
|
|
49
|
+
.with_modules(module)
|
|
50
|
+
.with_config(config or {})
|
|
51
|
+
.build()
|
|
52
|
+
)
|
|
53
|
+
return dr, module
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def cleanup_temp_module(module: ModuleType) -> None:
|
|
57
|
+
"""Remove a temporary module from sys.modules and linecache."""
|
|
58
|
+
name = module.__name__
|
|
59
|
+
sys.modules.pop(name, None)
|
|
60
|
+
linecache.cache.pop(name, None)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def format_validation_errors(exc: Exception) -> list[dict]:
|
|
64
|
+
"""Parse a Hamilton or Python exception into structured error dicts.
|
|
65
|
+
|
|
66
|
+
Returns a list of ``{"type": ..., "message": ..., "detail": ...}`` dicts.
|
|
67
|
+
"""
|
|
68
|
+
error_type = type(exc).__name__
|
|
69
|
+
message = str(exc)
|
|
70
|
+
|
|
71
|
+
if isinstance(exc, SyntaxError):
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
"type": error_type,
|
|
75
|
+
"message": message,
|
|
76
|
+
"detail": f"line {exc.lineno}" if exc.lineno else None,
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
return [{"type": error_type, "message": message, "detail": None}]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def format_exception_chain(exc: Exception) -> list[dict]:
|
|
84
|
+
"""Format an exception including its full traceback as structured errors."""
|
|
85
|
+
errors = format_validation_errors(exc)
|
|
86
|
+
tb = traceback.format_exception(type(exc), exc, exc.__traceback__)
|
|
87
|
+
errors[0]["traceback"] = "".join(tb)
|
|
88
|
+
return errors
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def serialize_results(results: dict) -> dict[str, str]:
|
|
92
|
+
"""Convert Hamilton execution results to JSON-safe string representations.
|
|
93
|
+
|
|
94
|
+
Handles pandas DataFrames/Series via .to_dict(), falls back to str().
|
|
95
|
+
"""
|
|
96
|
+
serialized = {}
|
|
97
|
+
for key, val in results.items():
|
|
98
|
+
try:
|
|
99
|
+
import pandas as pd
|
|
100
|
+
|
|
101
|
+
if isinstance(val, pd.DataFrame):
|
|
102
|
+
serialized[key] = val.to_dict()
|
|
103
|
+
continue
|
|
104
|
+
if isinstance(val, pd.Series):
|
|
105
|
+
serialized[key] = val.to_dict()
|
|
106
|
+
continue
|
|
107
|
+
except ImportError:
|
|
108
|
+
pass
|
|
109
|
+
try:
|
|
110
|
+
import numpy as np
|
|
111
|
+
|
|
112
|
+
if isinstance(val, np.ndarray):
|
|
113
|
+
serialized[key] = val.tolist()
|
|
114
|
+
continue
|
|
115
|
+
except ImportError:
|
|
116
|
+
pass
|
|
117
|
+
try:
|
|
118
|
+
import polars as pl
|
|
119
|
+
|
|
120
|
+
if isinstance(val, pl.DataFrame):
|
|
121
|
+
serialized[key] = val.to_dicts()
|
|
122
|
+
continue
|
|
123
|
+
if isinstance(val, pl.Series):
|
|
124
|
+
serialized[key] = val.to_list()
|
|
125
|
+
continue
|
|
126
|
+
except ImportError:
|
|
127
|
+
pass
|
|
128
|
+
serialized[key] = str(val)
|
|
129
|
+
return serialized
|