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
hamilton/log_setup.py ADDED
@@ -0,0 +1,47 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ import logging
19
+ import sys
20
+
21
+ import numpy as np
22
+
23
+ LOG_LEVELS = {
24
+ "CRITICAL": logging.CRITICAL,
25
+ "ERROR": logging.ERROR,
26
+ "WARNING": logging.WARNING,
27
+ "INFO": logging.INFO,
28
+ "DEBUG": logging.DEBUG,
29
+ }
30
+
31
+
32
+ # this is suboptimal but python has no public mapping of log names to levels
33
+
34
+
35
+ def setup_logging(log_level: int = logging.INFO):
36
+ """Helper function to setup logging to console.
37
+ :param log_level: Log level to use when logging
38
+ """
39
+ root_logger = logging.getLogger("") # root logger
40
+ formatter = logging.Formatter("[%(levelname)s] %(asctime)s %(name)s(%(lineno)s): %(message)s")
41
+ stream_handler = logging.StreamHandler(sys.stdout)
42
+ stream_handler.setFormatter(formatter)
43
+ if not len(root_logger.handlers):
44
+ # assumes we have already been set up.
45
+ root_logger.addHandler(stream_handler)
46
+ root_logger.setLevel(log_level)
47
+ np.seterr(divide="ignore", invalid="ignore")
hamilton/models.py ADDED
@@ -0,0 +1,92 @@
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 abc
19
+ from typing import Any
20
+
21
+ import pandas as pd
22
+
23
+
24
+ class DynamicTransformBase(abc.ABC):
25
+ """Abstract class for a dynamic transform transform as seen by hamilton.
26
+ These are transforms that come from configuration parameters, and define the following:
27
+ 1. The nodes that they depend on
28
+ 2. What the transform does
29
+
30
+ Paired with the decorator @dynamic_transform(CLS, config_item, **extra_params) one can write incredibly powerful
31
+ DAGs that depend on dynamic transform configs.
32
+ """
33
+
34
+ def __init__(self, config_parameters: Any, name: str):
35
+ self._config_parameters = config_parameters
36
+ self._name = name
37
+
38
+ @abc.abstractmethod
39
+ def get_dependents(self) -> list[str]:
40
+ """Gets the names/types of the inputs to this transform.
41
+ :return: A list of columns on which this model depends.
42
+ """
43
+ pass
44
+
45
+ @abc.abstractmethod
46
+ def compute(self, **inputs: Any) -> Any:
47
+ """Runs a computation based on a set of input values.
48
+ :param inputs: data that are inputs to the transform
49
+ :return: The result of the transform.
50
+ """
51
+ pass
52
+
53
+ @property
54
+ def config_parameters(self) -> dict[str, Any]:
55
+ """Accessor for configuration parameters"""
56
+ return self._config_parameters
57
+
58
+ @property
59
+ def name(self) -> str:
60
+ return self._name
61
+
62
+
63
+ # Maintained for backwards compatibility
64
+ # Will likely keep this in some way, but might change the name or the approach
65
+ # with the 2.0 release
66
+ class BaseModel(DynamicTransformBase, abc.ABC):
67
+ """Base classes for models in hamilton
68
+ We define a model as anything whose inputs are configuration-driven.
69
+ The goal of this is to allow users to write something like:
70
+
71
+ @model(...)
72
+ def my_column():
73
+ #column that is some combinations of other columns using some previously trained model
74
+
75
+ Note that the model-training is not yet in scope."""
76
+
77
+ def compute(self, **inputs: Any) -> Any:
78
+ """Delegates to predict function to compute this model's return.
79
+
80
+ :param inputs: Inputs for the model
81
+ :return: The result of calling the model.
82
+ """
83
+ return self.predict(**inputs)
84
+
85
+ @abc.abstractmethod
86
+ def predict(self, **inputs: pd.Series) -> pd.Series:
87
+ """Runs the predict() function on the model, given the set of kwargs.
88
+ :param inputs: Inputs to the model.
89
+ :return: A series representing the output of the model.
90
+ """
91
+
92
+ pass
hamilton/node.py ADDED
@@ -0,0 +1,449 @@
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 builtins
19
+ import inspect
20
+ import sys
21
+ import typing
22
+ from collections.abc import Callable
23
+ from enum import Enum
24
+ from typing import Any
25
+
26
+ import typing_inspect
27
+
28
+ from hamilton.htypes import Collect, Parallelizable
29
+
30
+ """
31
+ Module that contains the primitive components of the graph.
32
+
33
+ These get their own file because we don't like circular dependencies.
34
+ """
35
+
36
+
37
+ class NodeType(Enum):
38
+ """
39
+ Specifies where this node's value originates.
40
+ This can be used by different adapters to flexibly execute a function graph.
41
+ """
42
+
43
+ STANDARD = 1 # standard dependencies
44
+ EXTERNAL = 2 # This node's value should be taken from cache
45
+ PRIOR_RUN = 3 # This node's value should be taken from a prior run.
46
+ EXPAND = 4
47
+ COLLECT = 5
48
+ # This is not used in a standard function graph, but it comes in handy for
49
+ # repeatedly running the same one.
50
+
51
+
52
+ class DependencyType(Enum):
53
+ REQUIRED = 1
54
+ OPTIONAL = 2
55
+
56
+ @staticmethod
57
+ def from_parameter(param: inspect.Parameter):
58
+ if param.default is inspect.Parameter.empty:
59
+ return DependencyType.REQUIRED
60
+ return DependencyType.OPTIONAL
61
+
62
+
63
+ class Node:
64
+ """Object representing a node of computation."""
65
+
66
+ def __init__(
67
+ self,
68
+ name: str,
69
+ typ: type,
70
+ doc_string: str = "",
71
+ callabl: Callable = None,
72
+ node_source: NodeType = NodeType.STANDARD,
73
+ input_types: dict[str, type | tuple[type, DependencyType]] = None,
74
+ tags: dict[str, Any] = None,
75
+ namespace: tuple[str, ...] = (),
76
+ originating_functions: tuple[Callable, ...] | None = None,
77
+ optional_values: dict[str, Any] | None = None,
78
+ ):
79
+ """Constructor for our Node object.
80
+
81
+ :param name: the name of the function.
82
+ :param typ: the output type of the function.
83
+ :param doc_string: the doc string for the function. Optional.
84
+ :param callabl: the actual function callable.
85
+ :param node_source: whether this is something someone has to pass in.
86
+ :param input_types: the input parameters and their types.
87
+ :param tags: the set of tags that this node contains.
88
+ """
89
+ if tags is None:
90
+ tags = dict()
91
+ self._tags = tags
92
+ self._name = name
93
+ self._type = typ
94
+ if typ is None or typ == inspect._empty:
95
+ raise ValueError(f"Missing type for hint for function {name}. Please add one to fix.")
96
+ self._callable = callabl
97
+ self._doc = doc_string
98
+ self._node_source = node_source
99
+ self._dependencies = []
100
+ self._depended_on_by = []
101
+ self._namespace = namespace
102
+ self._input_types = {}
103
+ self._originating_functions = originating_functions
104
+ self._default_parameter_values = {}
105
+
106
+ if self._node_source in (
107
+ NodeType.STANDARD,
108
+ NodeType.COLLECT,
109
+ NodeType.EXPAND,
110
+ ):
111
+ if input_types is not None:
112
+ for key, value in input_types.items():
113
+ if isinstance(value, tuple):
114
+ self._input_types[key] = value
115
+ else:
116
+ self._input_types = {
117
+ key: (value, DependencyType.REQUIRED)
118
+ for key, value in input_types.items()
119
+ }
120
+ # assume optional values passed
121
+ self._default_parameter_values = optional_values or {}
122
+ else:
123
+ type_hint_kwargs: dict[str, Any] = {"include_extras": True}
124
+ if sys.version_info >= (3, 13):
125
+ type_hint_kwargs["globalns"] = callabl.__globals__
126
+ input_types = typing.get_type_hints(callabl, **type_hint_kwargs)
127
+ signature = inspect.signature(callabl)
128
+ for key, value in signature.parameters.items():
129
+ if key not in input_types:
130
+ raise ValueError(
131
+ f"Missing type hint for {key} in function {name}. Please add one to fix."
132
+ )
133
+ dep_type = DependencyType.from_parameter(value)
134
+ self._input_types[key] = (
135
+ input_types[key],
136
+ dep_type,
137
+ )
138
+ if dep_type == DependencyType.OPTIONAL:
139
+ # capture optional value
140
+ self._default_parameter_values[key] = value.default
141
+ elif self.user_defined:
142
+ if len(self._input_types) > 0:
143
+ raise ValueError(
144
+ f"Input types cannot be provided for user-defined node {self.name}"
145
+ )
146
+
147
+ @property
148
+ def collect_dependency(self) -> str:
149
+ """Returns the name of the dependency that this node collects."""
150
+ if self._node_source != NodeType.COLLECT:
151
+ raise ValueError(f"Node {self.name} is not a collect node.")
152
+ # gets the dependency that gets collected
153
+ # This should be folded into the dependency type...
154
+ for key, (type_, _) in self._input_types.items():
155
+ if typing_inspect.get_origin(type_) == Collect:
156
+ return key
157
+
158
+ @property
159
+ def namespace(self) -> tuple[str, ...]:
160
+ return self._namespace
161
+
162
+ @property
163
+ def documentation(self) -> str:
164
+ return self._doc
165
+
166
+ @property
167
+ def input_types(self) -> dict[Any, tuple[Any, DependencyType]]:
168
+ return self._input_types
169
+
170
+ @property
171
+ def default_parameter_values(self) -> dict[str, Any]:
172
+ """Only returns parameters for which we have optional values."""
173
+ return self._default_parameter_values
174
+
175
+ def requires(self, dependency: str) -> bool:
176
+ """Returns whether or not this node requires the given dependency.
177
+
178
+ :param dependency: Dependency we may require
179
+ :return: True if it is an input *and* it is required
180
+ """
181
+ return (
182
+ dependency in self._input_types
183
+ and self._input_types[dependency][1] == DependencyType.REQUIRED
184
+ )
185
+
186
+ @property
187
+ def name(self) -> str:
188
+ return ".".join(self.namespace + (self._name,))
189
+
190
+ @property
191
+ def type(self) -> Any:
192
+ return self._type
193
+
194
+ def set_type(self, typ: Any):
195
+ """Sets the type of the node"""
196
+ assert self.user_defined is True, "Cannot reset type of non-user-defined node"
197
+ self._type = typ
198
+
199
+ @property
200
+ def callable(self):
201
+ return self._callable
202
+
203
+ # TODO - deprecate in favor of the node sources above
204
+ @property
205
+ def user_defined(self):
206
+ return self._node_source == NodeType.EXTERNAL
207
+
208
+ @property
209
+ def node_role(self):
210
+ return self._node_source
211
+
212
+ @property
213
+ def dependencies(self) -> list["Node"]:
214
+ return self._dependencies
215
+
216
+ @property
217
+ def depended_on_by(self) -> list["Node"]:
218
+ return self._depended_on_by
219
+
220
+ @property
221
+ def tags(self) -> dict[str, str]:
222
+ return self._tags
223
+
224
+ @property
225
+ def originating_functions(self) -> tuple[Callable, ...] | None:
226
+ """Gives all functions from which this node was created. None if the data
227
+ is not available (it is user-defined, or we have not added it yet). Note that this can be
228
+ multiple in the case of subdags (the subdag function + the other function). In that case,
229
+ it will be in order of creation (subdag function last).
230
+
231
+ Note that this is filled in in function_modifiers.base -- see note in from_fn
232
+
233
+ :return: A Tuple consisting of functions from which this node was created.
234
+ """
235
+ return self._originating_functions
236
+
237
+ def add_originating_function(self, fn: Callable):
238
+ """Adds a function to the list of originating functions.
239
+
240
+ This is used in the case to attach originating functions to user-defined (i.e. external/input nodes).
241
+ :param fn: Function to add
242
+ """
243
+ assert self.user_defined is True, "Cannot add originating function to non-user-defined node"
244
+ if self._originating_functions is None:
245
+ self._originating_functions = (fn,)
246
+ else:
247
+ self._originating_functions += (fn,)
248
+
249
+ def add_tag(self, tag_name: str, tag_value: str):
250
+ self._tags[tag_name] = tag_value
251
+
252
+ def __hash__(self):
253
+ return hash(self._name)
254
+
255
+ def __repr__(self):
256
+ return f"<{self.name} {self._tags}>"
257
+
258
+ def __eq__(self, other: "Node"):
259
+ """Want to deeply compare nodes in a custom way.
260
+
261
+ Current user is just unit tests. But you never know :)
262
+
263
+ Note: we only compare names of dependencies because we don't want infinite recursion.
264
+ """
265
+ return (
266
+ isinstance(other, Node)
267
+ and self._name == other.name
268
+ and self._type == other.type
269
+ and self._doc == other.documentation
270
+ and self._tags == other.tags
271
+ and self.user_defined == other.user_defined
272
+ and [n.name for n in self.dependencies] == [o.name for o in other.dependencies]
273
+ and [n.name for n in self.depended_on_by] == [o.name for o in other.depended_on_by]
274
+ and self.node_role == other.node_role
275
+ )
276
+
277
+ def __ne__(self, other: "Node"):
278
+ return not self.__eq__(other)
279
+
280
+ def __call__(self, *args, **kwargs):
281
+ """Call just delegates to the callable, purely for clean syntactic sugar"""
282
+ return self.callable(*args, **kwargs)
283
+
284
+ @staticmethod
285
+ def from_fn(fn: Callable, name: str = None) -> "Node":
286
+ """Generates a node from a function. Optionally overrides the name.
287
+
288
+ Note that currently, the `originating_function` is externally passed in -- this
289
+ happens in resolve_nodes in function_modifiers.base. TBD whether we'll want it to stay there.
290
+
291
+ :param fn: Function to generate the name from
292
+ :param name: Name to use for the node
293
+ :return: The node we generated
294
+ """
295
+ if name is None:
296
+ name = fn.__name__
297
+ type_hint_kwargs = {"include_extras": True}
298
+ return_type = typing.get_type_hints(fn, **type_hint_kwargs).get("return")
299
+ if return_type is None:
300
+ raise ValueError(f"Missing type hint for return value in function {fn.__qualname__}.")
301
+ module = inspect.getmodule(fn).__name__
302
+ tags = {"module": module}
303
+
304
+ node_source = NodeType.STANDARD
305
+ # TODO - extract this into a function + clean up!
306
+ if typing_inspect.is_generic_type(return_type):
307
+ if typing_inspect.get_origin(return_type) == Parallelizable:
308
+ node_source = NodeType.EXPAND
309
+ for hint in typing.get_type_hints(fn, **type_hint_kwargs).values():
310
+ if typing_inspect.is_generic_type(hint):
311
+ if typing_inspect.get_origin(hint) == Collect:
312
+ node_source = NodeType.COLLECT
313
+ break
314
+
315
+ if hasattr(fn, "__config_decorated__"):
316
+ tags["hamilton.config"] = ",".join(fn.__config_decorated__)
317
+ return Node(
318
+ name,
319
+ return_type,
320
+ fn.__doc__ or "",
321
+ callabl=fn,
322
+ tags=tags,
323
+ node_source=node_source,
324
+ )
325
+
326
+ def copy_with(self, include_refs: bool = True, **overrides) -> "Node":
327
+ """Copies a node with the specified overrides for the constructor arguments.
328
+ Utility function for creating a node -- useful for modifying it.
329
+
330
+ :param kwargs: kwargs to use in place of the node. Passed to the constructor.
331
+ :param include_refs: Whether or not to include dependencies and depended_on_by
332
+ :return: A node copied from self with the specified keyword arguments replaced.
333
+ """
334
+ constructor_args = dict(
335
+ name=self.name,
336
+ typ=self.type,
337
+ doc_string=self.documentation,
338
+ callabl=self.callable,
339
+ node_source=self.node_role,
340
+ input_types=self.input_types.copy(),
341
+ tags=self.tags.copy(),
342
+ originating_functions=self.originating_functions,
343
+ optional_values=self.default_parameter_values.copy()
344
+ if self.default_parameter_values
345
+ else {},
346
+ )
347
+ constructor_args.update(**overrides)
348
+ out = Node(**constructor_args)
349
+ if include_refs:
350
+ out._dependencies = self._dependencies
351
+ out._depended_on_by = self._depended_on_by
352
+ return out
353
+
354
+ def copy(self, include_refs: bool = True) -> "Node":
355
+ """Copies a node, not modifying anything (except for the references
356
+ /dependencies if specified).
357
+
358
+ :param include_refs: Whether or not to include dependencies and depended_on_by
359
+ :return: A copy of the node.
360
+ """
361
+ """Gives a copy of the node, so we can modify it without modifying the original.
362
+ :return: A copy of the node.
363
+ """
364
+ return self.copy_with(include_refs)
365
+
366
+ def reassign_inputs(
367
+ self, input_names: dict[str, Any] = None, input_values: dict[str, Any] = None
368
+ ) -> "Node":
369
+ """Reassigns the input names of a node. Useful for applying
370
+ a node to a separate input if needed. Note that things can get a
371
+ little strange if you have multiple inputs with the same name, so
372
+ be careful about how you use this.
373
+
374
+ :param input_names: Input name map to reassign
375
+ :return: A node with the input names reassigned
376
+ """
377
+ if input_names is None:
378
+ input_names = {}
379
+ if input_values is None:
380
+ input_values = {}
381
+
382
+ is_async = inspect.iscoroutinefunction(self.callable) # determine if its async
383
+
384
+ def new_callable(**kwargs) -> Any:
385
+ reverse_input_names = {v: k for k, v in input_names.items()}
386
+ kwargs = {**kwargs, **input_values}
387
+ return self.callable(**{reverse_input_names.get(k, k): v for k, v in kwargs.items()})
388
+
389
+ async def async_function(**kwargs):
390
+ return await new_callable(**kwargs)
391
+
392
+ fn_to_use = async_function if is_async else new_callable
393
+
394
+ new_input_types = {
395
+ input_names.get(k, k): v for k, v in self.input_types.items() if k not in input_values
396
+ }
397
+ # out = self.copy_with(callabl=new_callable, input_types=new_input_types)
398
+ out = self.copy_with(callabl=fn_to_use, input_types=new_input_types)
399
+ return out
400
+
401
+ def transform_output(
402
+ self, __transform: Callable[[dict[str, Any], Any], Any], __output_type: builtins.type[Any]
403
+ ) -> "Node":
404
+ """Applies a transformation on the output of the node, returning a new node.
405
+ Also modifies the type.
406
+
407
+ :param __transform: Transformation to apply. This is a function with two arguments:
408
+ (a) the kwargs passed to the node, and (b) the output of the node.
409
+ :param __output_type: Return type of the transformation
410
+ :return: A new node, with the right type/transformation
411
+ """
412
+
413
+ def new_callable(**kwargs) -> Any:
414
+ return __transform(self.callable(**kwargs), kwargs)
415
+
416
+ return self.copy_with(callabl=new_callable, typ=__output_type)
417
+
418
+
419
+ def matches_query(
420
+ tags: dict[str, str | list[str]], query_dict: dict[str, str | list[str] | None]
421
+ ) -> bool:
422
+ """Check whether a set of node tags matches the query based on tags.
423
+
424
+ An empty dict of a query matches all tags.
425
+
426
+ :param tags: the tags of the node.
427
+ :param query_dict: of tag to value. If value is None, we just check that the tag exists.
428
+ :return: True if we have tags that match all tag queries, False otherwise.
429
+ """
430
+ # it's an AND clause between each tag and value in the query dict.
431
+ for tag, value in query_dict.items():
432
+ # if tag not in node we can return False immediately.
433
+ if tag not in tags:
434
+ return False
435
+ # if value is None -- we don't care about the value, just that the tag exists.
436
+ if value is None:
437
+ continue
438
+ node_tag_value = tags[tag]
439
+ if not isinstance(node_tag_value, list):
440
+ node_tag_value = [node_tag_value]
441
+ if not isinstance(value, list):
442
+ value = [value]
443
+ if set(value).intersection(set(node_tag_value)):
444
+ # if there is some overlap, we're good.
445
+ continue
446
+ else:
447
+ # else, return False.
448
+ return False
449
+ return True
@@ -0,0 +1,48 @@
1
+ <!--
2
+ Licensed to the Apache Software Foundation (ASF) under one
3
+ or more contributor license agreements. See the NOTICE file
4
+ distributed with this work for additional information
5
+ regarding copyright ownership. The ASF licenses this file
6
+ to you under the Apache License, Version 2.0 (the
7
+ "License"); you may not use this file except in compliance
8
+ with the License. You may obtain a copy of the License at
9
+
10
+ http://www.apache.org/licenses/LICENSE-2.0
11
+
12
+ Unless required by applicable law or agreed to in writing,
13
+ software distributed under the License is distributed on an
14
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ KIND, either express or implied. See the License for the
16
+ specific language governing permissions and limitations
17
+ under the License.
18
+ -->
19
+
20
+ # Plugins
21
+
22
+ Apache Hamilton enables plugins -- the requirement is that the core library according to the plugin is installed, and the plugin will be registered automatically.
23
+ It is up to the user to install the plugin via the target, E.G. `hamilton[pyspark]`, which will install the correct depdendencies.
24
+
25
+
26
+ ## Structure
27
+
28
+ For each plugin, `foo`, there will be two files (likely, at some point, broken into modules), that will have the names:
29
+ 1. `foo_extensions.py` This refers to constructs that get automatically registered (dataframe/column types, data loaders, etc...) This never gets imported by the user directly.
30
+ 2. `h_foo.py` This refers to constructs that the user refers to directly, E.G. specific decorators, result builders, etc...
31
+
32
+ `h_foo` is just another module. `foo_extensions` has a few special properties.
33
+
34
+ 1. It can opt out of specifying dataframe/column types if it doesn't make sense, by containing the variable `COLUMN_FRIENDLY_DF_TYPE = False`. This defaults to true on import.
35
+ 2. If it is true, it must contain the following methods:
36
+ 1. `get_column_foo` -- a function to extract the column
37
+ 2. `register_types` -- a function to register the types for that extension
38
+ 3. `fill_with_scalar_foo` -- a function to fill a column with a scalar
39
+
40
+ `register_types` must be called within the module body, as must registering any data loaders. This restriction will likely be removed and the registering pulled out to import level.
41
+
42
+ Note this is not a public-facing API -- this is for managing internal plugins. If you want to add new capabilities, feel free to reach out to the team.
43
+
44
+
45
+ ## Adding a new plugin
46
+
47
+ Follow the rules above. Also add an install target with the same name in `setup.py`. Once you add this, you'll also want to update [registry.py](../registry.py) to include the new plugin, and update
48
+ the [docs](../../docs/data_adapters_extension.py) to document it.
@@ -0,0 +1,16 @@
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.