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,522 @@
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
+ from types import ModuleType
20
+ from typing import Any
21
+
22
+ from hamilton import graph as h_graph
23
+ from hamilton import lifecycle, node
24
+ from hamilton.lifecycle import base
25
+
26
+ logger = logging.getLogger(__name__)
27
+ try:
28
+ # TODO: this works for ddtrace < 3.0; Span was deprecated in 3.0
29
+ # See https://github.com/DataDog/dd-trace-py/pull/12186
30
+ from ddtrace import Span, context, tracer
31
+ except ImportError as e:
32
+ logger.error("ImportError: %s", e)
33
+ logger.error(
34
+ "To use the h_ddog plugin, please install sf-hamilton[datadog] using "
35
+ "`pip install sf-hamilton[datadog]` (or use your favorite package manager)."
36
+ "Remember to use quotes around the package name if using zsh!"
37
+ )
38
+ raise
39
+
40
+
41
+ class _DDOGTracerImpl:
42
+ """Implementation class for DDOGTracer and AsyncDDOGTracer functionality.
43
+
44
+ This class encapsulates the core logic for Datadog tracing within Hamilton's lifecycle hooks.
45
+ It provides methods to handle the tracing operations required before and after the execution
46
+ of graphs, nodes, and tasks. The DDOGTracer and AsyncDDOGTracer classes are composed of this implementation class,
47
+ due to the differences in their base lifecycle classes (sync vs async). This class allows sharing logic between the
48
+ two without duplicating code or interfering with the base classes they inherit from.
49
+ """
50
+
51
+ def __init__(self, root_name: str, include_causal_links: bool = False, service: str = None):
52
+ self.root_name = root_name
53
+ self.service = service
54
+ self.include_causal_links = include_causal_links
55
+ self.run_span_cache = {} # Cache of run_id -> span tuples
56
+ self.task_span_cache = {} # cache of run_iod -> task_id -> span. Note that we will prune this after task execution
57
+ self.node_span_cache = {} # Cache of run_id -> [task_id, node_id] -> span. We use this to open/close general traces
58
+
59
+ @staticmethod
60
+ def _serialize_span_dict(span_dict: dict[str, Span]):
61
+ """Serializes to a readable format. We're not propogating span links (see note above on causal links),
62
+ but that's fine (for now). We have to do this as passing spans back and forth is frowned upon.
63
+
64
+ :param span_dict: A key -> span dictionary
65
+ :return: The serialized representation.
66
+ """
67
+ # For some reason this doesn't use the right ser/deser for dask
68
+ # Or for some reason it has contexts instead of spans. Well, we can serialize them both!
69
+ return {
70
+ key: {
71
+ "trace_id": span.context.trace_id if isinstance(span, Span) else span.trace_id,
72
+ "span_id": span.context.span_id if isinstance(span, Span) else span.span_id,
73
+ }
74
+ for key, span in span_dict.items()
75
+ }
76
+
77
+ @staticmethod
78
+ def _deserialize_span_dict(serialized_repr: dict[str, dict]) -> dict[str, context.Context]:
79
+ """Note that we deserialize as contexts, as passing spans is not supported
80
+ (the child should never terminate the parent span).
81
+
82
+ :param span_dict: Dict of str -> dict params for contexts
83
+ :return: A dictionary of contexts
84
+ """
85
+ return {key: context.Context(**params) for key, params in serialized_repr.items()}
86
+
87
+ def __getstate__(self):
88
+ """Gets the state for serialization"""
89
+ return dict(
90
+ root_trace_name=self.root_name,
91
+ service=self.service,
92
+ include_causal_links=self.include_causal_links,
93
+ run_span_cache=self._serialize_span_dict(self.run_span_cache),
94
+ task_span_cache={
95
+ key: self._serialize_span_dict(value) for key, value in self.task_span_cache.items()
96
+ },
97
+ # this is unnecessary, but leaving it here for now
98
+ # to remove it, we need to add a default check in the one that adds to the nodes
99
+ node_span_cache={
100
+ key: self._serialize_span_dict(value) for key, value in self.task_span_cache.items()
101
+ }, # Nothing here, we can just wipe it for a new task
102
+ )
103
+
104
+ def __setstate__(self, state):
105
+ """Sets the state for serialization"""
106
+ self.service = state["service"]
107
+ self.root_name = state["root_trace_name"]
108
+ self.include_causal_links = state["include_causal_links"]
109
+ # TODO -- move this out/consider doing it to the others
110
+ self.run_span_cache = self._deserialize_span_dict(state["run_span_cache"])
111
+ # We only really need this if we log the stuff before submitting...
112
+ # This shouldn't happen but it leaves flexibility for the future
113
+ self.task_span_cache = {
114
+ key: self._deserialize_span_dict(value)
115
+ for key, value in state["task_span_cache"].items()
116
+ }
117
+ self.node_span_cache = {
118
+ key: self._deserialize_span_dict(value)
119
+ for key, value in state["node_span_cache"].items()
120
+ }
121
+
122
+ @staticmethod
123
+ def _sanitize_tags(tags: dict[str, Any]) -> dict[str, str]:
124
+ """Sanitizes tags to be strings, just in case.
125
+
126
+ :param tags: Node tags.
127
+ :return: The string -> string representation of tags
128
+ """
129
+ return {f"hamilton.{key}": str(value) for key, value in tags.items()}
130
+
131
+ def run_before_graph_execution(self, *, run_id: str, **future_kwargs: Any):
132
+ """Runs before graph execution -- sets the state so future ones can reference it.
133
+
134
+ :param run_id: ID of the run
135
+ :param future_kwargs: reserved for future keyword arguments/backwards compatibility.
136
+ """
137
+ # This returns None if there's no active context and works as a no-op, otherwise we tie this span to a parent.
138
+ current_context = tracer.current_trace_context()
139
+ span = tracer.start_span(
140
+ name=self.root_name, child_of=current_context, activate=True, service=self.service
141
+ )
142
+ self.run_span_cache[run_id] = span # we save this as a root span
143
+ self.node_span_cache[run_id] = {}
144
+ self.task_span_cache[run_id] = {}
145
+
146
+ def run_before_node_execution(
147
+ self,
148
+ *,
149
+ node_name: str,
150
+ node_kwargs: dict[str, Any],
151
+ node_tags: dict[str, Any],
152
+ task_id: str | None,
153
+ run_id: str,
154
+ **future_kwargs: Any,
155
+ ):
156
+ """Runs before a node's execution. Sets up/stores spans.
157
+
158
+ :param node_name: Name of the node.
159
+ :param node_kwargs: Keyword arguments of the node.
160
+ :param node_tags: Tags of the node (they'll get stored as datadog tags)
161
+ :param task_id: Task ID that spawned the node
162
+ :param run_id: ID of the run.
163
+ :param future_kwargs: reserved for future keyword arguments/backwards compatibility.
164
+ """
165
+ # We need to do this on launching tasks and we have not yet exposed it.
166
+ # TODO -- do pre-task and post-task execution.
167
+ parent_span = self.task_span_cache[run_id].get(task_id) or self.run_span_cache[run_id]
168
+ new_span_name = f"{task_id}:" if task_id is not None else ""
169
+ new_span_name += node_name
170
+ new_span = tracer.start_span(
171
+ name=new_span_name, child_of=parent_span, activate=True, service=self.service
172
+ )
173
+ if self.include_causal_links:
174
+ prior_spans = {
175
+ key: self.node_span_cache[run_id].get((task_id, key)) for key in node_kwargs
176
+ }
177
+ for input_node, span in prior_spans.items():
178
+ if span is not None:
179
+ new_span.link_span(
180
+ context=span.context,
181
+ attributes={
182
+ "link.name": f"{input_node}_to_{node_name}",
183
+ },
184
+ )
185
+ tags = node_tags.copy()
186
+ tags["hamilton.node_name"] = node_name
187
+ new_span.set_tags(self._sanitize_tags(tags=tags))
188
+ self.node_span_cache[run_id][(task_id, node_name)] = new_span
189
+
190
+ def run_after_node_execution(
191
+ self,
192
+ *,
193
+ node_name: str,
194
+ error: Exception | None,
195
+ task_id: str | None,
196
+ run_id: str,
197
+ **future_kwargs: Any,
198
+ ):
199
+ """Runs after a node's execution -- completes the span.
200
+
201
+ :param node_name: Name of the node
202
+ :param error: Error that the node raised, if any
203
+ :param task_id: Task ID that spawned the node
204
+ :param run_id: ID of the run.
205
+ :param future_kwargs: reserved for future keyword arguments/backwards compatibility.
206
+ """
207
+ span = self.node_span_cache[run_id][(task_id, node_name)]
208
+ exc_type = None
209
+ exc_value = None
210
+ tb = None
211
+ if error is not None:
212
+ exc_type = type(error)
213
+ exc_value = error
214
+ tb = error.__traceback__
215
+ span.__exit__(exc_type, exc_value, tb)
216
+
217
+ def run_after_graph_execution(
218
+ self, *, error: Exception | None, run_id: str, **future_kwargs: Any
219
+ ):
220
+ """Runs after graph execution. Garbage collects + finishes the root span.
221
+
222
+ :param error: Error the graph raised when running, if any
223
+ :param run_id: ID of the run
224
+ :param future_kwargs: reserved for future keyword arguments/backwards compatibility.
225
+ """
226
+ span = self.run_span_cache[run_id]
227
+ exc_type = None
228
+ exc_value = None
229
+ tb = None
230
+ if error is not None:
231
+ exc_type = type(error)
232
+ exc_value = error
233
+ tb = error.__traceback__
234
+ span.__exit__(exc_type, exc_value, tb)
235
+ del self.run_span_cache[run_id]
236
+ del self.node_span_cache[run_id]
237
+ del self.task_span_cache[run_id]
238
+
239
+ def run_before_task_execution(self, *, task_id: str, run_id: str, **future_kwargs):
240
+ """Runs before task execution. Sets up the task span.
241
+
242
+ :param task_id: ID of the task
243
+ :param run_id: ID of the run,
244
+ :param future_kwargs: reserved for future keyword arguments/backwards compatibility.
245
+ """
246
+ parent_span = self.run_span_cache[run_id]
247
+ self.task_span_cache[run_id][task_id] = tracer.start_span(
248
+ name=task_id,
249
+ child_of=parent_span, # span or context both work
250
+ activate=True,
251
+ service=self.service,
252
+ )
253
+
254
+ def run_after_task_execution(
255
+ self,
256
+ *,
257
+ task_id: str,
258
+ run_id: str,
259
+ error: Exception,
260
+ **future_kwargs,
261
+ ):
262
+ """Rusn after task execution. Finishes task-level spans.
263
+
264
+ :param task_id: ID of the task, ID of the run.
265
+ :param run_id: ID of the run
266
+ :param error: Error the graph raised when running, if any
267
+ :param future_kwargs: Future keyword arguments for backwards compatibility
268
+ """
269
+ span = self.task_span_cache[run_id][task_id]
270
+ exc_type = None
271
+ exc_value = None
272
+ tb = None
273
+ if error is not None:
274
+ exc_type = type(error)
275
+ exc_value = error
276
+ tb = error.__traceback__
277
+ span.__exit__(exc_type, exc_value, tb)
278
+
279
+
280
+ class DDOGTracer(
281
+ lifecycle.NodeExecutionHook, lifecycle.GraphExecutionHook, lifecycle.TaskExecutionHook
282
+ ):
283
+ """Lifecycle adapter to use datadog to run tracing on node execution. This works with the following execution environments:
284
+ 1. Vanilla Hamilton -- no task-based computation, just nodes
285
+ 2. Task-based, synchronous
286
+ 3. Task-based with Multithreading, Ray, and Dask
287
+ It will likely work with others, although we have not yet tested them. This does not work with async (yet).
288
+
289
+ Note that this is not a typical use of Datadog if you're not using hamilton for a microservice. It does work quite nicely, however!
290
+ Monitoring ETLs is not a typical datadog case (you can't see relationships between nodes/tasks or data summaries),
291
+ but it is easy enough to work with and gives some basic information.
292
+
293
+ This tracer bypasses context management so we can more accurately track relationships between nodes/tags. Also, we plan to
294
+ get this working with OpenTelemetry, and use that for datadog integration.
295
+
296
+ To use this, you'll want to run `pip install sf-hamilton[ddog]` (or `pip install "sf-hamilton[ddog]"` if using zsh)
297
+ """
298
+
299
+ def __init__(self, root_name: str, include_causal_links: bool = False, service: str = None):
300
+ """Creates a DDOGTracer. This has the option to specify some parameters.
301
+
302
+ :param root_name: Name of the root trace/span. Due to the way datadog inherits, this will inherit an active span.
303
+ :param include_causal_links: Whether or not to include span causal links. Note that there are some edge-cases here, and
304
+ This is in beta for datadog, and actually broken in the current client, but it has been fixed and will be released shortly:
305
+ https://github.com/DataDog/dd-trace-py/issues/8049. Furthermore, the query on datadog is slow for displaying causal links.
306
+ We've disabled this by default, but feel free to test it out -- its likely they'll be improving the docum
307
+ :param service: Service name -- will pick it up from the environment through DDOG if not available.
308
+ """
309
+ self._impl = _DDOGTracerImpl(
310
+ root_name=root_name, include_causal_links=include_causal_links, service=service
311
+ )
312
+
313
+ def run_before_graph_execution(self, *, run_id: str, **future_kwargs: Any):
314
+ """Runs before graph execution -- sets the state so future ones can reference it.
315
+
316
+ :param run_id: ID of the run
317
+ :param future_kwargs: reserved for future keyword arguments/backwards compatibility.
318
+ """
319
+ self._impl.run_before_graph_execution(run_id=run_id, **future_kwargs)
320
+
321
+ def run_before_node_execution(
322
+ self,
323
+ *,
324
+ node_name: str,
325
+ node_kwargs: dict[str, Any],
326
+ node_tags: dict[str, Any],
327
+ task_id: str | None,
328
+ run_id: str,
329
+ **future_kwargs: Any,
330
+ ):
331
+ """Runs before a node's execution. Sets up/stores spans.
332
+
333
+ :param node_name: Name of the node.
334
+ :param node_kwargs: Keyword arguments of the node.
335
+ :param node_tags: Tags of the node (they'll get stored as datadog tags)
336
+ :param task_id: Task ID that spawned the node
337
+ :param run_id: ID of the run.
338
+ :param future_kwargs: reserved for future keyword arguments/backwards compatibility.
339
+ """
340
+ self._impl.run_before_node_execution(
341
+ node_name=node_name,
342
+ node_kwargs=node_kwargs,
343
+ node_tags=node_tags,
344
+ task_id=task_id,
345
+ run_id=run_id,
346
+ **future_kwargs,
347
+ )
348
+
349
+ def run_after_node_execution(
350
+ self,
351
+ *,
352
+ node_name: str,
353
+ error: Exception | None,
354
+ task_id: str | None,
355
+ run_id: str,
356
+ **future_kwargs: Any,
357
+ ):
358
+ """Runs after a node's execution -- completes the span.
359
+
360
+ :param node_name: Name of the node
361
+ :param error: Error that the node raised, if any
362
+ :param task_id: Task ID that spawned the node
363
+ :param run_id: ID of the run.
364
+ :param future_kwargs: reserved for future keyword arguments/backwards compatibility.
365
+ """
366
+ self._impl.run_after_node_execution(
367
+ node_name=node_name, error=error, task_id=task_id, run_id=run_id, **future_kwargs
368
+ )
369
+
370
+ def run_after_graph_execution(
371
+ self, *, error: Exception | None, run_id: str, **future_kwargs: Any
372
+ ):
373
+ """Runs after graph execution. Garbage collects + finishes the root span.
374
+
375
+ :param error: Error the graph raised when running, if any
376
+ :param run_id: ID of the run
377
+ :param future_kwargs: reserved for future keyword arguments/backwards compatibility.
378
+ """
379
+ self._impl.run_after_graph_execution(error=error, run_id=run_id, **future_kwargs)
380
+
381
+ def run_before_task_execution(self, *, task_id: str, run_id: str, **future_kwargs):
382
+ """Runs before task execution. Sets up the task span.
383
+
384
+ :param task_id: ID of the task
385
+ :param run_id: ID of the run,
386
+ :param future_kwargs: reserved for future keyword arguments/backwards compatibility.
387
+ """
388
+ self._impl.run_before_task_execution(task_id=task_id, run_id=run_id, **future_kwargs)
389
+
390
+ def run_after_task_execution(
391
+ self,
392
+ *,
393
+ task_id: str,
394
+ run_id: str,
395
+ error: Exception,
396
+ **future_kwargs,
397
+ ):
398
+ """Rusn after task execution. Finishes task-level spans.
399
+
400
+ :param task_id: ID of the task, ID of the run.
401
+ :param run_id: ID of the run
402
+ :param error: Error the graph raised when running, if any
403
+ :param future_kwargs: Future keyword arguments for backwards compatibility
404
+ """
405
+ self._impl.run_after_task_execution(
406
+ task_id=task_id, run_id=run_id, error=error, **future_kwargs
407
+ )
408
+
409
+
410
+ class AsyncDDOGTracer(
411
+ base.BasePostGraphConstructAsync,
412
+ base.BasePreGraphExecuteAsync,
413
+ base.BasePreNodeExecuteAsync,
414
+ base.BasePostNodeExecuteAsync,
415
+ base.BasePostGraphExecuteAsync,
416
+ ):
417
+ def __init__(
418
+ self, root_name: str, include_causal_links: bool = False, service: str | None = None
419
+ ):
420
+ """Creates a AsyncDDOGTracer, the asyncio-friendly version of DDOGTracer.
421
+
422
+ This has the option to specify some parameters:
423
+
424
+ :param root_name: Name of the root trace/span. Due to the way datadog inherits, this will inherit an active span.
425
+ :param include_causal_links: Whether or not to include span causal links. Note that there are some edge-cases here, and
426
+ This is in beta for datadog, and actually broken in the current client, but it has been fixed and will be released shortly:
427
+ https://github.com/DataDog/dd-trace-py/issues/8049. Furthermore, the query on datadog is slow for displaying causal links.
428
+ We've disabled this by default, but feel free to test it out -- its likely they'll be improving the docum
429
+ :param service: Service name -- will pick it up from the environment through DDOG if not available.
430
+ """
431
+ self._impl = _DDOGTracerImpl(
432
+ root_name=root_name, include_causal_links=include_causal_links, service=service
433
+ )
434
+
435
+ async def post_graph_construct(
436
+ self, graph: h_graph.FunctionGraph, modules: list[ModuleType], config: dict[str, Any]
437
+ ) -> None:
438
+ """Runs after graph construction. This is a no-op for this plugin.
439
+
440
+ :param graph: Graph that has been constructed.
441
+ :param modules: Modules passed into the graph
442
+ :param config: Config passed into the graph
443
+ """
444
+ pass
445
+
446
+ async def pre_graph_execute(
447
+ self,
448
+ run_id: str,
449
+ graph: h_graph.FunctionGraph,
450
+ final_vars: list[str],
451
+ inputs: dict[str, Any],
452
+ overrides: dict[str, Any],
453
+ ) -> None:
454
+ """Runs before graph execution -- sets the state so future ones can reference it.
455
+
456
+ :param run_id: ID of the run, unique in scope of the driver.
457
+ :param graph: Graph that is being executed
458
+ :param final_vars: Variables we are extracting from the graph
459
+ :param inputs: Inputs to the graph
460
+ :param overrides: Overrides to graph execution
461
+ """
462
+ self._impl.run_before_graph_execution(run_id=run_id)
463
+
464
+ async def pre_node_execute(
465
+ self, run_id: str, node_: node.Node, kwargs: dict[str, Any], task_id: str | None = None
466
+ ) -> None:
467
+ """Runs before a node's execution. Sets up/stores spans.
468
+
469
+ :param run_id: ID of the run, unique in scope of the driver.
470
+ :param node_: Node that is being executed
471
+ :param kwargs: Keyword arguments that are being passed into the node
472
+ :param task_id: ID of the task, defaults to None if not in a task setting
473
+ """
474
+ self._impl.run_before_node_execution(
475
+ node_name=node_.name,
476
+ node_kwargs=kwargs,
477
+ node_tags=node_.tags,
478
+ task_id=task_id,
479
+ run_id=run_id,
480
+ )
481
+
482
+ async def post_node_execute(
483
+ self,
484
+ run_id: str,
485
+ node_: node.Node,
486
+ success: bool,
487
+ error: Exception | None,
488
+ result: Any,
489
+ task_id: str | None = None,
490
+ **future_kwargs: dict,
491
+ ) -> None:
492
+ """Runs after a node's execution -- completes the span.
493
+
494
+ :param run_id: ID of the run, unique in scope of the driver.
495
+ :param node_: Node that is being executed
496
+ :param kwargs: Keyword arguments that are being passed into the node
497
+ :param success: Whether or not the node executed successfully
498
+ :param error: The error that was raised, if any
499
+ :param result: The result of the node execution, if no error was raised
500
+ :param task_id: ID of the task, defaults to None if not in a task-based execution
501
+ """
502
+ self._impl.run_after_node_execution(
503
+ node_name=node_.name, error=error, task_id=task_id, run_id=run_id
504
+ )
505
+
506
+ async def post_graph_execute(
507
+ self,
508
+ run_id: str,
509
+ graph: h_graph.FunctionGraph,
510
+ success: bool,
511
+ error: Exception | None,
512
+ results: dict[str, Any] | None,
513
+ ) -> None:
514
+ """Runs after graph execution. Garbage collects + finishes the root span.
515
+
516
+ :param run_id: ID of the run, unique in scope of the driver.
517
+ :param graph: Graph that was executed
518
+ :param success: Whether or not the graph executed successfully
519
+ :param error: Error that was raised, if any
520
+ :param results: Results of the graph execution
521
+ """
522
+ self._impl.run_after_graph_execution(error=error, run_id=run_id)
@@ -0,0 +1,163 @@
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
+ from typing import Any
20
+
21
+ import diskcache
22
+
23
+ from hamilton import driver, graph_types, lifecycle, node
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def _bytes_to_mb(kb: int) -> float:
29
+ return kb / (1024**2)
30
+
31
+
32
+ def evict_all_except(nodes_to_keep: dict[str, node.Node], cache: diskcache.Cache) -> int:
33
+ """Evicts all nodes and node version except those passed.
34
+ Remaining nodes may have multiple entries for different input values
35
+ """
36
+ nodes_history: dict[str, list[str]] = cache.get(key=DiskCacheAdapter.nodes_history_key) # type: ignore
37
+
38
+ new_nodes_history = dict()
39
+ eviction_counter = 0
40
+ for node_name, history in nodes_history.items():
41
+ if len(history) < 1:
42
+ continue
43
+
44
+ if node_name in nodes_to_keep.keys():
45
+ node_to_keep = nodes_to_keep[node_name]
46
+ hash_to_keep = graph_types.hash_source_code(node_to_keep.callable, strip=True)
47
+ history.remove(hash_to_keep)
48
+ new_nodes_history[node_name] = [hash_to_keep]
49
+
50
+ for hash_to_evict in history:
51
+ cache.evict(tag=f"{node_name}.{hash_to_evict}")
52
+ eviction_counter += 1
53
+
54
+ cache.set(key=DiskCacheAdapter.nodes_history_key, value=new_nodes_history)
55
+ return eviction_counter
56
+
57
+
58
+ def evict_all_except_driver(dr: driver.Driver) -> dict:
59
+ """Wrap the utility `evict_all_except` to receive a driver.Driver object"""
60
+ cache_hooks = [
61
+ adapter for adapter in dr.adapter.adapters if isinstance(adapter, DiskCacheAdapter)
62
+ ]
63
+
64
+ if len(cache_hooks) == 0:
65
+ raise AssertionError("0 `h_diskcache.CacheHook` defined for this Driver")
66
+ elif len(cache_hooks) > 1:
67
+ raise AssertionError(">1 `h_diskcache.CacheHook` defined for this Driver")
68
+
69
+ cache: diskcache.Cache = cache_hooks[0].cache
70
+ volume_before = cache.volume()
71
+ eviction_counter = evict_all_except(nodes_to_keep=dr.graph.nodes, cache=cache)
72
+ volume_after = cache.volume()
73
+ volume_difference = volume_before - volume_after
74
+
75
+ logger.info(f"Evicted: {_bytes_to_mb(volume_difference):.2f} MB")
76
+ logger.debug(f"Evicted {eviction_counter} entries")
77
+ logger.debug(f"Cache size after: {_bytes_to_mb(volume_after):.2f} MB")
78
+
79
+ return dict(
80
+ evicted_size_mb=_bytes_to_mb(volume_difference),
81
+ eviction_counter=eviction_counter,
82
+ size_after=_bytes_to_mb(volume_after),
83
+ )
84
+
85
+
86
+ MAX_KWARGS_REPR_LENGTH = 200
87
+
88
+
89
+ class DiskCacheAdapter(
90
+ lifecycle.NodeExecutionHook,
91
+ lifecycle.GraphExecutionHook,
92
+ lifecycle.NodeExecutionMethod,
93
+ ):
94
+ nodes_history_key: str = "_nodes_history"
95
+
96
+ def __init__(
97
+ self, cache_vars: list[str] | None = None, cache_path: str = ".", **cache_settings
98
+ ):
99
+ self.cache_vars = cache_vars or []
100
+ self.cache_path = cache_path
101
+ self.cache = diskcache.Cache(directory=cache_path, **cache_settings)
102
+ self.nodes_history: dict[str, list[str]] = self.cache.get(
103
+ key=DiskCacheAdapter.nodes_history_key, default=dict()
104
+ ) # type: ignore
105
+ self.used_nodes_hash: dict[str, str] = dict()
106
+
107
+ logger.warning(
108
+ "The `DiskCacheAdapter` is deprecated and will be removed in Hamilton 2.0. "
109
+ "Consider enabling the core caching feature via `Builder.with_cache()`. "
110
+ "This might not be 1-to-1 replacement, so please reach out if there are missing features. "
111
+ "See https://hamilton.apache.org/concepts/caching/ to learn more."
112
+ )
113
+
114
+ def run_before_graph_execution(self, *, graph: graph_types.HamiltonGraph, **kwargs):
115
+ """Set cache_vars to all nodes if not specified"""
116
+ if self.cache_vars == []:
117
+ self.cache_vars = [n.name for n in graph.nodes]
118
+
119
+ def run_to_execute_node(
120
+ self, *, node_name: str, node_callable: Any, node_kwargs: dict[str, Any], **kwargs
121
+ ):
122
+ """Create hash key then use cached value if exist"""
123
+ if node_name not in self.cache_vars:
124
+ return node_callable(**node_kwargs)
125
+
126
+ node_hash = graph_types.hash_source_code(node_callable, strip=True)
127
+ self.used_nodes_hash[node_name] = node_hash
128
+ cache_key = (node_hash, *node_kwargs.values())
129
+
130
+ from_cache = self.cache.get(key=cache_key, default=None)
131
+ if from_cache is not None:
132
+ if logger.isEnabledFor(logging.DEBUG):
133
+ node_kwargs_string = repr(node_kwargs)
134
+ if len(node_kwargs_string) > MAX_KWARGS_REPR_LENGTH: # limit size of log
135
+ node_kwargs_string = node_kwargs_string[0:MAX_KWARGS_REPR_LENGTH] + "..."
136
+ logger.debug(f"{node_name} {node_kwargs_string}: from cache")
137
+ return from_cache
138
+
139
+ if logger.isEnabledFor(logging.DEBUG):
140
+ node_kwargs_string = repr(node_kwargs)
141
+ if len(node_kwargs_string) > MAX_KWARGS_REPR_LENGTH: # limit size of log
142
+ node_kwargs_string = node_kwargs_string[0:MAX_KWARGS_REPR_LENGTH] + "..."
143
+ logger.debug(f"{node_name} {node_kwargs_string}: executed")
144
+ self.nodes_history[node_name] = self.nodes_history.get(node_name, []) + [node_hash]
145
+ return node_callable(**node_kwargs)
146
+
147
+ def run_after_node_execution(self, *, node_name: str, node_kwargs: dict, result: Any, **kwargs):
148
+ if node_name not in self.cache_vars:
149
+ return
150
+
151
+ node_hash = self.used_nodes_hash[node_name]
152
+ cache_key = (node_hash, *node_kwargs.values())
153
+ cache_tag = f"{node_name}.{node_hash}"
154
+ # only adds if key doesn't exist
155
+ self.cache.add(key=cache_key, value=result, tag=cache_tag)
156
+
157
+ def run_after_graph_execution(self, *args, **kwargs):
158
+ self.cache.set(key=DiskCacheAdapter.nodes_history_key, value=self.nodes_history)
159
+ logger.info(f"Cache size: {_bytes_to_mb(self.cache.volume()):.2f} MB")
160
+ self.cache.close()
161
+
162
+ def run_before_node_execution(self, *args, **kwargs):
163
+ pass