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,434 @@
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
+ """Decorators that attach metadata to nodes"""
19
+
20
+ import json
21
+ from collections.abc import Callable
22
+ from typing import Any, Literal
23
+
24
+ from hamilton import htypes, node, registry
25
+ from hamilton.function_modifiers import base
26
+
27
+ RAY_REMOTE_TAG_NAMESPACE = "ray_remote"
28
+
29
+
30
+ class tag(base.NodeDecorator):
31
+ """Decorator class that adds a tag to a node. Tags take the form of key/value pairings.
32
+ Tags can have dots to specify namespaces (keys with dots), but this is usually reserved for special cases
33
+ (E.G. subdecorators) that utilize them. Usually one will pass in tags as kwargs, so we expect tags to
34
+ be un-namespaced in most uses.
35
+
36
+ That is using:
37
+
38
+ .. code-block:: python
39
+
40
+ @tag(my_tag='tag_value')
41
+ def my_function(...) -> ...:
42
+
43
+ is un-namespaced because you cannot put a `.` in the keyword part (the part before the '=').
44
+
45
+ But using:
46
+
47
+ .. code-block:: python
48
+
49
+ @tag(**{'my.tag': 'tag_value'})
50
+ def my_function(...) -> ...:
51
+
52
+ allows you to add dots that allow you to namespace your tags.
53
+
54
+ Currently, tag values are restricted to allowing strings only, although we may consider changing the in the future
55
+ (E.G. thinking of lists).
56
+
57
+ Hamilton also reserves the right to change the following:
58
+ * adding purely positional arguments
59
+ * not allowing users to use a certain set of top-level prefixes (E.G. any tag where the top level is one of the \
60
+ values in RESERVED_TAG_PREFIX).
61
+
62
+ Example usage:
63
+
64
+ .. code-block:: python
65
+
66
+ @tag(foo='bar', a_tag_key='a_tag_value', **{'namespace.tag_key': 'tag_value'})
67
+ def my_function(...) -> ...:
68
+ ...
69
+ """
70
+
71
+ RESERVED_TAG_NAMESPACES = [
72
+ "hamilton",
73
+ "data_quality",
74
+ "gdpr",
75
+ "ccpa",
76
+ "dag",
77
+ "module",
78
+ RAY_REMOTE_TAG_NAMESPACE,
79
+ ] # Anything that starts with any of these is banned, the framework reserves the right to manage it
80
+
81
+ def __init__(
82
+ self,
83
+ *,
84
+ target_: base.TargetType = None,
85
+ bypass_reserved_namespaces_: bool = False,
86
+ **tags: str | list[str],
87
+ ):
88
+ """Constructor for adding tag annotations to a function.
89
+
90
+ :param bypass_reserved_namespaces\\_: Whether to bypass Reserved Namespace checking.
91
+ :param target\\_: Target nodes to decorate. This can be one of the following:
92
+
93
+ * **None**: tag all nodes outputted by this that are "final" (E.g. do not have a node\
94
+ outputted by this that depend on them)
95
+ * **Ellipsis (...)**: tag *all* nodes outputted by this
96
+ * **Collection[str]**: tag *only* the nodes with the specified names
97
+ * **str**: tag *only* the node with the specified name
98
+ :param tags: the keys are always going to be strings, so the type annotation here means the values are strings \
99
+ or lists of values. Implicitly this is `Dict[str, Union[str, List[str]]]` but the PEP guideline is to only
100
+ annotate it with the value `Union[str, List[str]]`.
101
+ """
102
+ super(tag, self).__init__(target=target_)
103
+ self.tags = tags
104
+ self.bypass_reserved_namespaces = bypass_reserved_namespaces_
105
+
106
+ def decorate_node(self, node_: node.Node) -> node.Node:
107
+ """Decorates the nodes produced by this with the specified tags
108
+
109
+ :param node_: Node to decorate
110
+ :return: Copy of the node, with tags assigned
111
+ """
112
+ node_tags = node_.tags.copy()
113
+ node_tags.update(self.tags)
114
+ return node_.copy_with(tags=node_tags)
115
+
116
+ def _key_allowed(self, key: str) -> bool:
117
+ """Validates that a tag key is allowed. Rules are:
118
+ 1. It must not be empty
119
+ 2. It can have dots, which specify a hierarchy of order
120
+ 3. All components, when split by dots, must be valid python identifiers
121
+ 4. It cannot utilize a reserved namespace
122
+
123
+ :param key: The key to validate
124
+ :return: True if it is valid, False if not
125
+ """
126
+ key_components = key.split(".")
127
+ if len(key_components) == 0:
128
+ # empty string...
129
+ return False
130
+ if not self.bypass_reserved_namespaces and key_components[0] in tag.RESERVED_TAG_NAMESPACES:
131
+ # Reserved prefixes
132
+ return False
133
+ for key in key_components:
134
+ if not key.isidentifier():
135
+ return False
136
+ return True
137
+
138
+ @staticmethod
139
+ def _value_allowed(value: Any) -> bool:
140
+ """Validates that a tag value is allowed. Rules are only that it must be a string.
141
+
142
+ :param value: Value to validate
143
+ :return: True if it is valid, False otherwise
144
+ """
145
+ if not isinstance(value, str) and not isinstance(value, list):
146
+ return False
147
+ elif isinstance(value, list):
148
+ if len(value) == 0:
149
+ return False # disallow empty lists
150
+ if not all([isinstance(v, str) for v in value]): # need values to be all strings
151
+ return False
152
+ if len(set(value)) != len(value): # need values to be unique
153
+ return False
154
+ return True
155
+
156
+ def validate(self, fn: Callable):
157
+ """Validates the decorator. In this case that the set of tags produced is final.
158
+
159
+ :param fn: Function that the decorator is called on.
160
+ :raises ValueError: if the specified tags contains invalid ones
161
+ """
162
+ bad_tags = set()
163
+ for key, value in self.tags.items():
164
+ if (not self._key_allowed(key)) or (not tag._value_allowed(value)):
165
+ if isinstance(value, list):
166
+ value = str(value)
167
+ bad_tags.add((key, value))
168
+ if bad_tags:
169
+ bad_tags_formatted = ",".join([f"{key}={value}" for key, value in bad_tags])
170
+ raise base.InvalidDecoratorException(
171
+ f"The following tags are invalid as tags: {bad_tags_formatted} "
172
+ "Tag keys can be split by ., to represent a hierarchy, "
173
+ "but each element of the hierarchy must be a valid python identifier. "
174
+ "Paths components also cannot be empty. "
175
+ "The value can only be a string, or a list of strings. "
176
+ "Note that the following top-level prefixes are "
177
+ f"reserved as well: {self.RESERVED_TAG_NAMESPACES}"
178
+ )
179
+
180
+
181
+ class tag_outputs(base.NodeDecorator):
182
+ def __init__(self, **tag_mapping: dict[str, str | list[str]]):
183
+ """Creates a tag_outputs decorator.
184
+
185
+ Note that this currently does not validate whether the nodes are spelled correctly as it takes in a superset of\
186
+ nodes.
187
+
188
+ :param tag_mapping: Mapping of output name to tags -- this is akin to applying @tag to individual outputs \
189
+ produced by the function.
190
+
191
+ Example usage:
192
+
193
+ .. code-block:: python
194
+
195
+ @tag_output(**{'a': {'a_tag': 'a_tag_value'}, 'b': {'b_tag': 'b_tag_value'}})
196
+ @extract_columns("a", "b")
197
+ def example_tag_outputs() -> pd.DataFrame:
198
+ return pd.DataFrame.from_records({"a": [1], "b": [2]})
199
+
200
+ """
201
+ super(base.NodeDecorator, self).__init__(target=...)
202
+ self.tag_mapping = tag_mapping
203
+
204
+ def decorate_node(self, node_: node.Node) -> node.Node:
205
+ """Decorates all final nodes with the specified tags."""
206
+ if node_.name not in self.tag_mapping:
207
+ return node_ # in this case we have no desire to update tags
208
+ new_tags = node_.tags.copy()
209
+ new_tags.update(self.tag_mapping.get(node_.name, {}))
210
+ return tag(**new_tags).decorate_node(node_)
211
+
212
+
213
+ # These represent a generic schema type -- E.G. one that will
214
+ # be supported across the entire set of usable dataframe/dataset types
215
+ # Eventually we'll be integrating mappings of these into the registry,
216
+ # but for now this serves largely as a placeholder/documentation
217
+ # GENERIC_SCHEMA_TYPES = (
218
+ # "int",
219
+ # "float",
220
+ # "str",
221
+ # "bool",
222
+ # "dict",
223
+ # "list",
224
+ # "object",
225
+ # "datetime",
226
+ # "date",
227
+ # )
228
+
229
+
230
+ class SchemaOutput(tag):
231
+ def __init__(self, *fields: tuple[str, str], target_: str | None = None):
232
+ """Initializes SchemaOutput. See docs for `@schema.output` for more details."""
233
+
234
+ tag_value = ",".join([f"{key}={value}" for key, value in fields])
235
+ super(SchemaOutput, self).__init__(
236
+ **{schema.INTERNAL_SCHEMA_OUTPUT_KEY: tag_value}, target_=target_
237
+ )
238
+
239
+ def validate_node(self, node_: node.Node):
240
+ """Validates that the node has a return type of a registered dataframe.
241
+
242
+ :param node_: Node to validate
243
+ :raises InvalidDecoratorException: if the node does not have a return type of a registered dataframe.
244
+ """
245
+ output_type = node_.type
246
+ available_types = registry.get_registered_dataframe_types()
247
+ for _, type_ in available_types.items():
248
+ if htypes.custom_subclass_check(output_type, type_):
249
+ return
250
+ raise base.InvalidDecoratorException(
251
+ f"Node {node_.name} has type {output_type} which is not a registered type for a dataset. "
252
+ f"Registered types are {available_types}. If you found this, either (a) ensure you have the "
253
+ f"right package installed, or (b) reach out to the team to figure out how to add yours."
254
+ )
255
+
256
+ @classmethod
257
+ def allows_multiple(cls) -> bool:
258
+ """Currently this only applies to a single output. If it is a set of nodes with multiple outputs,
259
+ it will apply to the "final" (sink) one. We can change this if there's need."""
260
+ return False
261
+
262
+ def validate(self, fn: Callable):
263
+ """Bypassed for now -- we have no function-level or class-level validations yet,
264
+ but this is done at `@tag`, which this inherits. We will be moving away from inheriting tag.
265
+ """
266
+ pass
267
+
268
+
269
+ class schema:
270
+ """Container class for schema stuff. This is purely so we can have a nice API for it -- E.G. Schema.output"""
271
+
272
+ INTERNAL_SCHEMA_OUTPUT_KEY = "hamilton.internal.schema_output"
273
+
274
+ @staticmethod
275
+ def output(*fields: tuple[str, str], target_: str | None = None) -> SchemaOutput:
276
+ """Initializes a `@schema.output` decorator. This takes in a list of fields, which are tuples of the form
277
+ `(field_name, field_type)`. The field type must be one of the function_modifiers.SchemaTypes types.
278
+
279
+ :param target_: Target node to decorate -- if `None` it'll decorate all final nodes (E.G. sinks in the subdag),
280
+ otherwise it will decorate the specified node.
281
+ :param fields: List of fields to add to the schema. Each field is a tuple of the form `(field_name, field_type)`
282
+
283
+ This is implemented using tags, but that might change. Thus you should not
284
+ rely on the tags created by this decorator (which is why they are prefixed with `internal`).
285
+
286
+ To use this, you should decorate a node with `@schema.output`
287
+
288
+ Example usage:
289
+
290
+ .. code-block:: python
291
+
292
+ @schema.output(
293
+ ("a", "int"),
294
+ ("b", "float"),
295
+ ("c", "str")
296
+ )
297
+ def example_schema() -> pd.DataFrame:
298
+ return pd.DataFrame.from_records({"a": [1], "b": [2.0], "c": ["3"]})
299
+
300
+ Then, when drawing the DAG, the schema will be displayed as sub-elements in the node for the DAG (if `display_schema` is selected).
301
+ """
302
+ return SchemaOutput(*fields, target_=target_)
303
+
304
+
305
+ class RayRemote(tag):
306
+ def __init__(self, **options: int | dict[str, int]):
307
+ """Initializes RayRemote. See docs for `@ray_remote_options` for more details."""
308
+
309
+ ray_tags = {f"ray_remote.{option}": json.dumps(value) for option, value in options.items()}
310
+
311
+ super(RayRemote, self).__init__(bypass_reserved_namespaces_=True, **ray_tags)
312
+
313
+
314
+ def ray_remote_options(**kwargs: int | dict[str, int]) -> RayRemote:
315
+ """Initializes a `@ray_remote_options` decorator. This takes in a list of options to pass to ray.remote().
316
+
317
+ Supported options include resources, as well as other options:
318
+ https://docs.ray.io/en/latest/ray-core/scheduling/resources.html
319
+
320
+ This is implemented using tags, but that might change. Thus you should not
321
+ rely on the tags created by this decorator (which is why they are on a reserved namespace).
322
+
323
+ To use this, you should decorate a node with `@ray_remote_options`
324
+
325
+ Example usage:
326
+
327
+ .. code-block:: python
328
+
329
+ @ray_remote_options(
330
+ num_gpus=1,
331
+ resources={"my_custom_resource": 1},
332
+ )
333
+ def example() -> pd.DataFrame: ...
334
+ """
335
+ return RayRemote(**kwargs)
336
+
337
+
338
+ # materializers that have a `path` kwarg and are part of the core Hamilton library
339
+ # parquet, csv, feather, orc, and excel are via the pandas extension because it's currently a Hamilton dependency
340
+ CACHE_MATERIALIZERS = Literal[
341
+ "json",
342
+ "file",
343
+ "pickle",
344
+ "parquet",
345
+ "csv",
346
+ "feather",
347
+ "orc",
348
+ "excel",
349
+ ]
350
+
351
+ # see hamilton.caching.adapter.CachingBehavior enum for details.
352
+ # default: caching is enabled
353
+ # recompute: always compute the node instead of retrieving
354
+ # ignore: the data version won't be part of downstream keys
355
+ # disable: act as if caching wasn't enabled.
356
+ CACHE_BEHAVIORS = Literal["default", "recompute", "ignore", "disable"]
357
+
358
+
359
+ class cache(base.NodeDecorator):
360
+ BEHAVIOR_KEY = "cache.behavior"
361
+ FORMAT_KEY = "cache.format"
362
+
363
+ def __init__(
364
+ self,
365
+ *,
366
+ behavior: CACHE_BEHAVIORS | None = None,
367
+ format: CACHE_MATERIALIZERS | str | None = None,
368
+ target_: base.TargetType = ...,
369
+ ):
370
+ """The ``@cache`` decorator can define the behavior and format of a specific node.
371
+
372
+ This feature is implemented via tags, but that could change. Thus you should not
373
+ rely on these tags for other purposes.
374
+
375
+ .. code-block:: python
376
+
377
+ @cache(behavior="recompute", format="parquet")
378
+ def raw_data() -> pd.DataFrame: ...
379
+
380
+
381
+ If the function uses other function modifiers and define multiple nodes, you can
382
+ set ``target_`` to specify which nodes to cache. The following only caches the ``performance`` node.
383
+
384
+ .. code-block:: python
385
+
386
+ @cache(format="json", target_="performance")
387
+ @extract_fields(trained_model=LinearRegression, performance: dict)
388
+ def model_training() -> dict:
389
+ # ...
390
+ performance = {"rmse": 0.1, "mae": 0.2}
391
+ return {"trained_model": trained_model, "performance": performance}
392
+
393
+
394
+ :param behavior: The behavior of the cache. This can be one of the following:
395
+ * **default**: caching is enabled
396
+ * **recompute**: always compute the node instead of retrieving
397
+ * **ignore**: the data version won't be part of downstream keys
398
+ * **disable**: act as if caching wasn't enabled.
399
+ :param format: The format of the cache. This can be one of the following:
400
+ * **json**: JSON format
401
+ * **file**: file format
402
+ * **pickle**: pickle format
403
+ * **parquet**: parquet format
404
+ * **csv**: csv format
405
+ * **feather**: feather format
406
+ * **orc**: orc format
407
+ * **excel**: excel format
408
+ :param target\\_: Target nodes to decorate. This can be one of the following:
409
+ * **None**: tag all nodes outputted by this that are "final" (E.g. do not have a node\
410
+ outputted by this that depend on them)
411
+ * **Ellipsis (...)**: tag *all* nodes outputted by this
412
+ * **Collection[str]**: tag *only* the nodes with the specified names
413
+ * **str**: tag *only* the node with the specified name
414
+ """
415
+ super(cache, self).__init__(target=target_)
416
+
417
+ # don't provide default value for behavior and format if not provided by user
418
+ # the SmartCacheAdapter expects the field to be empty if not set
419
+ self.cache_tags = {}
420
+ if behavior:
421
+ self.cache_tags[cache.BEHAVIOR_KEY] = behavior
422
+
423
+ if format:
424
+ self.cache_tags[cache.FORMAT_KEY] = format
425
+
426
+ def decorate_node(self, node_: node.Node) -> node.Node:
427
+ """Decorates the nodes with the cache tags.
428
+
429
+ :param node_: Node to decorate
430
+ :return: Copy of the node, with tags assigned
431
+ """
432
+ node_tags = node_.tags.copy()
433
+ node_tags.update(self.cache_tags)
434
+ return node_.copy_with(tags=node_tags)