apache-hamilton 1.90.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apache_hamilton-1.90.0.dev0.dist-info/METADATA +407 -0
- apache_hamilton-1.90.0.dev0.dist-info/RECORD +151 -0
- apache_hamilton-1.90.0.dev0.dist-info/WHEEL +4 -0
- apache_hamilton-1.90.0.dev0.dist-info/entry_points.txt +9 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/DISCLAIMER +10 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/LICENSE +228 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/NOTICE +5 -0
- hamilton/__init__.py +24 -0
- hamilton/ad_hoc_utils.py +132 -0
- hamilton/async_driver.py +465 -0
- hamilton/base.py +466 -0
- hamilton/caching/__init__.py +16 -0
- hamilton/caching/adapter.py +1475 -0
- hamilton/caching/cache_key.py +70 -0
- hamilton/caching/fingerprinting.py +287 -0
- hamilton/caching/stores/__init__.py +16 -0
- hamilton/caching/stores/base.py +242 -0
- hamilton/caching/stores/file.py +140 -0
- hamilton/caching/stores/memory.py +297 -0
- hamilton/caching/stores/sqlite.py +282 -0
- hamilton/caching/stores/utils.py +40 -0
- hamilton/cli/__init__.py +16 -0
- hamilton/cli/__main__.py +328 -0
- hamilton/cli/commands.py +126 -0
- hamilton/cli/logic.py +338 -0
- hamilton/common/__init__.py +76 -0
- hamilton/contrib/__init__.py +41 -0
- hamilton/data_quality/__init__.py +16 -0
- hamilton/data_quality/base.py +198 -0
- hamilton/data_quality/default_validators.py +560 -0
- hamilton/data_quality/pandera_validators.py +121 -0
- hamilton/dataflows/__init__.py +726 -0
- hamilton/dataflows/template/README.md +25 -0
- hamilton/dataflows/template/__init__.py +54 -0
- hamilton/dataflows/template/author.md +28 -0
- hamilton/dataflows/template/requirements.txt +0 -0
- hamilton/dataflows/template/tags.json +7 -0
- hamilton/dataflows/template/valid_configs.jsonl +1 -0
- hamilton/dev_utils/__init__.py +16 -0
- hamilton/dev_utils/deprecation.py +204 -0
- hamilton/driver.py +2112 -0
- hamilton/execution/__init__.py +16 -0
- hamilton/execution/debugging_utils.py +56 -0
- hamilton/execution/executors.py +502 -0
- hamilton/execution/graph_functions.py +421 -0
- hamilton/execution/grouping.py +430 -0
- hamilton/execution/state.py +539 -0
- hamilton/experimental/__init__.py +27 -0
- hamilton/experimental/databackend.py +61 -0
- hamilton/experimental/decorators/__init__.py +16 -0
- hamilton/experimental/decorators/parameterize_frame.py +233 -0
- hamilton/experimental/h_async.py +29 -0
- hamilton/experimental/h_cache.py +413 -0
- hamilton/experimental/h_dask.py +28 -0
- hamilton/experimental/h_databackends.py +174 -0
- hamilton/experimental/h_ray.py +28 -0
- hamilton/experimental/h_spark.py +32 -0
- hamilton/function_modifiers/README +40 -0
- hamilton/function_modifiers/__init__.py +121 -0
- hamilton/function_modifiers/adapters.py +900 -0
- hamilton/function_modifiers/base.py +859 -0
- hamilton/function_modifiers/configuration.py +310 -0
- hamilton/function_modifiers/delayed.py +202 -0
- hamilton/function_modifiers/dependencies.py +246 -0
- hamilton/function_modifiers/expanders.py +1230 -0
- hamilton/function_modifiers/macros.py +1634 -0
- hamilton/function_modifiers/metadata.py +434 -0
- hamilton/function_modifiers/recursive.py +908 -0
- hamilton/function_modifiers/validation.py +289 -0
- hamilton/function_modifiers_base.py +31 -0
- hamilton/graph.py +1153 -0
- hamilton/graph_types.py +264 -0
- hamilton/graph_utils.py +41 -0
- hamilton/htypes.py +450 -0
- hamilton/io/__init__.py +32 -0
- hamilton/io/data_adapters.py +216 -0
- hamilton/io/default_data_loaders.py +224 -0
- hamilton/io/materialization.py +500 -0
- hamilton/io/utils.py +158 -0
- hamilton/lifecycle/__init__.py +67 -0
- hamilton/lifecycle/api.py +833 -0
- hamilton/lifecycle/base.py +1130 -0
- hamilton/lifecycle/default.py +802 -0
- hamilton/log_setup.py +47 -0
- hamilton/models.py +92 -0
- hamilton/node.py +449 -0
- hamilton/plugins/README.md +48 -0
- hamilton/plugins/__init__.py +16 -0
- hamilton/plugins/dask_extensions.py +47 -0
- hamilton/plugins/dlt_extensions.py +161 -0
- hamilton/plugins/geopandas_extensions.py +49 -0
- hamilton/plugins/h_dask.py +331 -0
- hamilton/plugins/h_ddog.py +522 -0
- hamilton/plugins/h_diskcache.py +163 -0
- hamilton/plugins/h_experiments/__init__.py +22 -0
- hamilton/plugins/h_experiments/__main__.py +62 -0
- hamilton/plugins/h_experiments/cache.py +39 -0
- hamilton/plugins/h_experiments/data_model.py +68 -0
- hamilton/plugins/h_experiments/hook.py +219 -0
- hamilton/plugins/h_experiments/server.py +375 -0
- hamilton/plugins/h_kedro.py +152 -0
- hamilton/plugins/h_logging.py +454 -0
- hamilton/plugins/h_mcp/__init__.py +28 -0
- hamilton/plugins/h_mcp/__main__.py +33 -0
- hamilton/plugins/h_mcp/_helpers.py +129 -0
- hamilton/plugins/h_mcp/_templates.py +417 -0
- hamilton/plugins/h_mcp/server.py +328 -0
- hamilton/plugins/h_mlflow.py +335 -0
- hamilton/plugins/h_narwhals.py +134 -0
- hamilton/plugins/h_openlineage.py +400 -0
- hamilton/plugins/h_opentelemetry.py +167 -0
- hamilton/plugins/h_pandas.py +257 -0
- hamilton/plugins/h_pandera.py +117 -0
- hamilton/plugins/h_polars.py +304 -0
- hamilton/plugins/h_polars_lazyframe.py +282 -0
- hamilton/plugins/h_pyarrow.py +56 -0
- hamilton/plugins/h_pydantic.py +127 -0
- hamilton/plugins/h_ray.py +242 -0
- hamilton/plugins/h_rich.py +142 -0
- hamilton/plugins/h_schema.py +493 -0
- hamilton/plugins/h_slack.py +100 -0
- hamilton/plugins/h_spark.py +1380 -0
- hamilton/plugins/h_threadpool.py +125 -0
- hamilton/plugins/h_tqdm.py +122 -0
- hamilton/plugins/h_vaex.py +129 -0
- hamilton/plugins/huggingface_extensions.py +236 -0
- hamilton/plugins/ibis_extensions.py +93 -0
- hamilton/plugins/jupyter_magic.py +622 -0
- hamilton/plugins/kedro_extensions.py +117 -0
- hamilton/plugins/lightgbm_extensions.py +99 -0
- hamilton/plugins/matplotlib_extensions.py +108 -0
- hamilton/plugins/mlflow_extensions.py +216 -0
- hamilton/plugins/numpy_extensions.py +105 -0
- hamilton/plugins/pandas_extensions.py +1763 -0
- hamilton/plugins/plotly_extensions.py +150 -0
- hamilton/plugins/polars_extensions.py +71 -0
- hamilton/plugins/polars_implementations.py +25 -0
- hamilton/plugins/polars_lazyframe_extensions.py +302 -0
- hamilton/plugins/polars_post_1_0_0_extensions.py +877 -0
- hamilton/plugins/polars_pre_1_0_0_extension.py +836 -0
- hamilton/plugins/pydantic_extensions.py +98 -0
- hamilton/plugins/pyspark_pandas_extensions.py +47 -0
- hamilton/plugins/sklearn_plot_extensions.py +129 -0
- hamilton/plugins/spark_extensions.py +105 -0
- hamilton/plugins/vaex_extensions.py +51 -0
- hamilton/plugins/xgboost_extensions.py +91 -0
- hamilton/plugins/yaml_extensions.py +89 -0
- hamilton/registry.py +254 -0
- hamilton/settings.py +18 -0
- hamilton/telemetry.py +50 -0
- hamilton/version.py +18 -0
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.
|