apache-hamilton 1.90.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. apache_hamilton-1.90.0.dev0.dist-info/METADATA +407 -0
  2. apache_hamilton-1.90.0.dev0.dist-info/RECORD +151 -0
  3. apache_hamilton-1.90.0.dev0.dist-info/WHEEL +4 -0
  4. apache_hamilton-1.90.0.dev0.dist-info/entry_points.txt +9 -0
  5. apache_hamilton-1.90.0.dev0.dist-info/licenses/DISCLAIMER +10 -0
  6. apache_hamilton-1.90.0.dev0.dist-info/licenses/LICENSE +228 -0
  7. apache_hamilton-1.90.0.dev0.dist-info/licenses/NOTICE +5 -0
  8. hamilton/__init__.py +24 -0
  9. hamilton/ad_hoc_utils.py +132 -0
  10. hamilton/async_driver.py +465 -0
  11. hamilton/base.py +466 -0
  12. hamilton/caching/__init__.py +16 -0
  13. hamilton/caching/adapter.py +1475 -0
  14. hamilton/caching/cache_key.py +70 -0
  15. hamilton/caching/fingerprinting.py +287 -0
  16. hamilton/caching/stores/__init__.py +16 -0
  17. hamilton/caching/stores/base.py +242 -0
  18. hamilton/caching/stores/file.py +140 -0
  19. hamilton/caching/stores/memory.py +297 -0
  20. hamilton/caching/stores/sqlite.py +282 -0
  21. hamilton/caching/stores/utils.py +40 -0
  22. hamilton/cli/__init__.py +16 -0
  23. hamilton/cli/__main__.py +328 -0
  24. hamilton/cli/commands.py +126 -0
  25. hamilton/cli/logic.py +338 -0
  26. hamilton/common/__init__.py +76 -0
  27. hamilton/contrib/__init__.py +41 -0
  28. hamilton/data_quality/__init__.py +16 -0
  29. hamilton/data_quality/base.py +198 -0
  30. hamilton/data_quality/default_validators.py +560 -0
  31. hamilton/data_quality/pandera_validators.py +121 -0
  32. hamilton/dataflows/__init__.py +726 -0
  33. hamilton/dataflows/template/README.md +25 -0
  34. hamilton/dataflows/template/__init__.py +54 -0
  35. hamilton/dataflows/template/author.md +28 -0
  36. hamilton/dataflows/template/requirements.txt +0 -0
  37. hamilton/dataflows/template/tags.json +7 -0
  38. hamilton/dataflows/template/valid_configs.jsonl +1 -0
  39. hamilton/dev_utils/__init__.py +16 -0
  40. hamilton/dev_utils/deprecation.py +204 -0
  41. hamilton/driver.py +2112 -0
  42. hamilton/execution/__init__.py +16 -0
  43. hamilton/execution/debugging_utils.py +56 -0
  44. hamilton/execution/executors.py +502 -0
  45. hamilton/execution/graph_functions.py +421 -0
  46. hamilton/execution/grouping.py +430 -0
  47. hamilton/execution/state.py +539 -0
  48. hamilton/experimental/__init__.py +27 -0
  49. hamilton/experimental/databackend.py +61 -0
  50. hamilton/experimental/decorators/__init__.py +16 -0
  51. hamilton/experimental/decorators/parameterize_frame.py +233 -0
  52. hamilton/experimental/h_async.py +29 -0
  53. hamilton/experimental/h_cache.py +413 -0
  54. hamilton/experimental/h_dask.py +28 -0
  55. hamilton/experimental/h_databackends.py +174 -0
  56. hamilton/experimental/h_ray.py +28 -0
  57. hamilton/experimental/h_spark.py +32 -0
  58. hamilton/function_modifiers/README +40 -0
  59. hamilton/function_modifiers/__init__.py +121 -0
  60. hamilton/function_modifiers/adapters.py +900 -0
  61. hamilton/function_modifiers/base.py +859 -0
  62. hamilton/function_modifiers/configuration.py +310 -0
  63. hamilton/function_modifiers/delayed.py +202 -0
  64. hamilton/function_modifiers/dependencies.py +246 -0
  65. hamilton/function_modifiers/expanders.py +1230 -0
  66. hamilton/function_modifiers/macros.py +1634 -0
  67. hamilton/function_modifiers/metadata.py +434 -0
  68. hamilton/function_modifiers/recursive.py +908 -0
  69. hamilton/function_modifiers/validation.py +289 -0
  70. hamilton/function_modifiers_base.py +31 -0
  71. hamilton/graph.py +1153 -0
  72. hamilton/graph_types.py +264 -0
  73. hamilton/graph_utils.py +41 -0
  74. hamilton/htypes.py +450 -0
  75. hamilton/io/__init__.py +32 -0
  76. hamilton/io/data_adapters.py +216 -0
  77. hamilton/io/default_data_loaders.py +224 -0
  78. hamilton/io/materialization.py +500 -0
  79. hamilton/io/utils.py +158 -0
  80. hamilton/lifecycle/__init__.py +67 -0
  81. hamilton/lifecycle/api.py +833 -0
  82. hamilton/lifecycle/base.py +1130 -0
  83. hamilton/lifecycle/default.py +802 -0
  84. hamilton/log_setup.py +47 -0
  85. hamilton/models.py +92 -0
  86. hamilton/node.py +449 -0
  87. hamilton/plugins/README.md +48 -0
  88. hamilton/plugins/__init__.py +16 -0
  89. hamilton/plugins/dask_extensions.py +47 -0
  90. hamilton/plugins/dlt_extensions.py +161 -0
  91. hamilton/plugins/geopandas_extensions.py +49 -0
  92. hamilton/plugins/h_dask.py +331 -0
  93. hamilton/plugins/h_ddog.py +522 -0
  94. hamilton/plugins/h_diskcache.py +163 -0
  95. hamilton/plugins/h_experiments/__init__.py +22 -0
  96. hamilton/plugins/h_experiments/__main__.py +62 -0
  97. hamilton/plugins/h_experiments/cache.py +39 -0
  98. hamilton/plugins/h_experiments/data_model.py +68 -0
  99. hamilton/plugins/h_experiments/hook.py +219 -0
  100. hamilton/plugins/h_experiments/server.py +375 -0
  101. hamilton/plugins/h_kedro.py +152 -0
  102. hamilton/plugins/h_logging.py +454 -0
  103. hamilton/plugins/h_mcp/__init__.py +28 -0
  104. hamilton/plugins/h_mcp/__main__.py +33 -0
  105. hamilton/plugins/h_mcp/_helpers.py +129 -0
  106. hamilton/plugins/h_mcp/_templates.py +417 -0
  107. hamilton/plugins/h_mcp/server.py +328 -0
  108. hamilton/plugins/h_mlflow.py +335 -0
  109. hamilton/plugins/h_narwhals.py +134 -0
  110. hamilton/plugins/h_openlineage.py +400 -0
  111. hamilton/plugins/h_opentelemetry.py +167 -0
  112. hamilton/plugins/h_pandas.py +257 -0
  113. hamilton/plugins/h_pandera.py +117 -0
  114. hamilton/plugins/h_polars.py +304 -0
  115. hamilton/plugins/h_polars_lazyframe.py +282 -0
  116. hamilton/plugins/h_pyarrow.py +56 -0
  117. hamilton/plugins/h_pydantic.py +127 -0
  118. hamilton/plugins/h_ray.py +242 -0
  119. hamilton/plugins/h_rich.py +142 -0
  120. hamilton/plugins/h_schema.py +493 -0
  121. hamilton/plugins/h_slack.py +100 -0
  122. hamilton/plugins/h_spark.py +1380 -0
  123. hamilton/plugins/h_threadpool.py +125 -0
  124. hamilton/plugins/h_tqdm.py +122 -0
  125. hamilton/plugins/h_vaex.py +129 -0
  126. hamilton/plugins/huggingface_extensions.py +236 -0
  127. hamilton/plugins/ibis_extensions.py +93 -0
  128. hamilton/plugins/jupyter_magic.py +622 -0
  129. hamilton/plugins/kedro_extensions.py +117 -0
  130. hamilton/plugins/lightgbm_extensions.py +99 -0
  131. hamilton/plugins/matplotlib_extensions.py +108 -0
  132. hamilton/plugins/mlflow_extensions.py +216 -0
  133. hamilton/plugins/numpy_extensions.py +105 -0
  134. hamilton/plugins/pandas_extensions.py +1763 -0
  135. hamilton/plugins/plotly_extensions.py +150 -0
  136. hamilton/plugins/polars_extensions.py +71 -0
  137. hamilton/plugins/polars_implementations.py +25 -0
  138. hamilton/plugins/polars_lazyframe_extensions.py +302 -0
  139. hamilton/plugins/polars_post_1_0_0_extensions.py +877 -0
  140. hamilton/plugins/polars_pre_1_0_0_extension.py +836 -0
  141. hamilton/plugins/pydantic_extensions.py +98 -0
  142. hamilton/plugins/pyspark_pandas_extensions.py +47 -0
  143. hamilton/plugins/sklearn_plot_extensions.py +129 -0
  144. hamilton/plugins/spark_extensions.py +105 -0
  145. hamilton/plugins/vaex_extensions.py +51 -0
  146. hamilton/plugins/xgboost_extensions.py +91 -0
  147. hamilton/plugins/yaml_extensions.py +89 -0
  148. hamilton/registry.py +254 -0
  149. hamilton/settings.py +18 -0
  150. hamilton/telemetry.py +50 -0
  151. hamilton/version.py +18 -0
@@ -0,0 +1,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