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/cli/commands.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from hamilton import ad_hoc_utils, driver
|
|
21
|
+
from hamilton.cli import logic
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build(modules: list[Path], context_path: Path | None = None):
|
|
25
|
+
"""Build a Hamilton driver from the passed modules, and
|
|
26
|
+
load the Driver config from the context file.
|
|
27
|
+
|
|
28
|
+
Dynamic execution is enabled by default to support dataflow
|
|
29
|
+
using Parallelizable/Collect. This only matters if we are to
|
|
30
|
+
execute code.
|
|
31
|
+
"""
|
|
32
|
+
context = logic.load_context(context_path) if context_path else {}
|
|
33
|
+
module_objects = [ad_hoc_utils.module_from_source(p.read_text()) for p in modules]
|
|
34
|
+
return (
|
|
35
|
+
driver.Builder()
|
|
36
|
+
.enable_dynamic_execution(allow_experimental_mode=True)
|
|
37
|
+
.with_modules(*module_objects)
|
|
38
|
+
.with_config(context.get("HAMILTON_CONFIG", {}))
|
|
39
|
+
.build()
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def diff(
|
|
44
|
+
current_dr: driver.Driver,
|
|
45
|
+
modules: list[Path],
|
|
46
|
+
git_reference: str | None = "HEAD",
|
|
47
|
+
view: bool = False,
|
|
48
|
+
output_file_path: Path = Path("./diff.png"),
|
|
49
|
+
context_path: Path | None = None,
|
|
50
|
+
) -> dict:
|
|
51
|
+
"""Get the diff of"""
|
|
52
|
+
context = logic.load_context(context_path) if context_path else {}
|
|
53
|
+
|
|
54
|
+
current_version = logic.hash_hamilton_nodes(current_dr)
|
|
55
|
+
current_node_to_func = logic.map_nodes_to_functions(current_dr)
|
|
56
|
+
|
|
57
|
+
reference_modules = logic.load_modules_from_git(modules, git_reference)
|
|
58
|
+
reference_dr = (
|
|
59
|
+
driver.Builder()
|
|
60
|
+
.enable_dynamic_execution(allow_experimental_mode=True)
|
|
61
|
+
.with_modules(*reference_modules)
|
|
62
|
+
.with_config(context.get("HAMILTON_CONFIG", {}))
|
|
63
|
+
.build()
|
|
64
|
+
)
|
|
65
|
+
reference_version = logic.hash_hamilton_nodes(reference_dr)
|
|
66
|
+
reference_node_to_func = logic.map_nodes_to_functions(reference_dr)
|
|
67
|
+
|
|
68
|
+
nodes_diff = logic.diff_versions(
|
|
69
|
+
reference_map=reference_version,
|
|
70
|
+
current_map=current_version,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
full_diff = dict()
|
|
74
|
+
for status, node_names in nodes_diff.items():
|
|
75
|
+
full_diff[status] = dict()
|
|
76
|
+
for node_name in node_names:
|
|
77
|
+
if current_node_to_func.get(node_name):
|
|
78
|
+
func_name = current_node_to_func.get(node_name)
|
|
79
|
+
else:
|
|
80
|
+
func_name = reference_node_to_func.get(node_name)
|
|
81
|
+
|
|
82
|
+
full_diff[status][node_name] = func_name
|
|
83
|
+
|
|
84
|
+
if view:
|
|
85
|
+
dot = logic.visualize_diff(current_dr=current_dr, reference_dr=reference_dr, **nodes_diff)
|
|
86
|
+
|
|
87
|
+
# simplified logic from hamilton.graph.display()
|
|
88
|
+
output_format = "png"
|
|
89
|
+
if output_file_path: # infer format from path
|
|
90
|
+
if output_file_path.suffix != "":
|
|
91
|
+
output_format = output_file_path.suffix.partition(".")[-1]
|
|
92
|
+
|
|
93
|
+
dot.render(output_file_path.with_suffix(""), format=output_format)
|
|
94
|
+
|
|
95
|
+
return full_diff
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def validate(dr: driver.Driver, context_path: Path) -> dict:
|
|
99
|
+
"""Use driver.validate_execution() with values from the context file"""
|
|
100
|
+
context = logic.load_context(context_path)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
dr.validate_execution(
|
|
104
|
+
final_vars=context["HAMILTON_FINAL_VARS"],
|
|
105
|
+
inputs=context["HAMILTON_INPUTS"],
|
|
106
|
+
overrides=context["HAMILTON_OVERRIDES"],
|
|
107
|
+
)
|
|
108
|
+
except ValueError as e:
|
|
109
|
+
raise e
|
|
110
|
+
|
|
111
|
+
return context
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def version(dr: driver.Driver) -> dict:
|
|
115
|
+
"""Get the node and dataflow versions from the instantiated Driver"""
|
|
116
|
+
nodes_hash = logic.hash_hamilton_nodes(dr)
|
|
117
|
+
dataflow_hash = logic.hash_dataflow(nodes_hash)
|
|
118
|
+
return dict(
|
|
119
|
+
nodes_hash=nodes_hash,
|
|
120
|
+
dataflow_hash=dataflow_hash,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def view(dr: driver.Driver, output_file_path: Path = Path("dag.png")) -> None:
|
|
125
|
+
"""Display all functions of the instantiated Driver"""
|
|
126
|
+
dr.display_all_functions(output_file_path)
|
hamilton/cli/logic.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from types import ModuleType
|
|
20
|
+
|
|
21
|
+
from hamilton import driver
|
|
22
|
+
|
|
23
|
+
CONFIG_HEADER = "HAMILTON_CONFIG"
|
|
24
|
+
FINAL_VARS_HEADER = "HAMILTON_FINAL_VARS"
|
|
25
|
+
INPUTS_HEADER = "HAMILTON_INPUTS"
|
|
26
|
+
OVERRIDES_HEADER = "HAMILTON_OVERRIDES"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_git_base_directory() -> str:
|
|
30
|
+
"""Get the base path of the current git directory"""
|
|
31
|
+
import subprocess
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
36
|
+
stdout=subprocess.PIPE,
|
|
37
|
+
stderr=subprocess.PIPE,
|
|
38
|
+
text=True,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if result.returncode == 0:
|
|
42
|
+
return result.stdout.strip()
|
|
43
|
+
else:
|
|
44
|
+
print("Error:", result.stderr.strip())
|
|
45
|
+
raise OSError(f"{result.stderr.strip()}")
|
|
46
|
+
except FileNotFoundError as e:
|
|
47
|
+
raise FileNotFoundError("Git command not found. Please make sure Git is installed.") from e
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_git_reference(git_relative_path: str | Path, git_reference: str) -> str:
|
|
51
|
+
"""Get the source code from the specified file and git reference"""
|
|
52
|
+
import subprocess
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
["git", "show", f"{git_reference}:{git_relative_path}"],
|
|
57
|
+
stdout=subprocess.PIPE,
|
|
58
|
+
stderr=subprocess.PIPE,
|
|
59
|
+
text=True,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if result.returncode == 0:
|
|
63
|
+
return result.stdout.strip()
|
|
64
|
+
elif result.returncode == 128:
|
|
65
|
+
# TODO catch the following error:
|
|
66
|
+
# fatal: Path '{git_relative_path}' exists on disk, but not in '{git_revision}'.
|
|
67
|
+
return
|
|
68
|
+
else:
|
|
69
|
+
return
|
|
70
|
+
except FileNotFoundError as e:
|
|
71
|
+
raise FileNotFoundError("Git command not found. Please make sure Git is installed.") from e
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def version_hamilton_functions(module: ModuleType) -> dict[str, str]:
|
|
75
|
+
"""Hash the source code of Hamilton functions from a module"""
|
|
76
|
+
from hamilton import graph_types, graph_utils
|
|
77
|
+
|
|
78
|
+
origins_version: dict[str, str] = dict()
|
|
79
|
+
|
|
80
|
+
for origin_name, _ in graph_utils.find_functions(module):
|
|
81
|
+
origin_callable = getattr(module, origin_name)
|
|
82
|
+
origins_version[origin_name] = graph_types.hash_source_code(origin_callable, strip=True)
|
|
83
|
+
|
|
84
|
+
return origins_version
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def hash_hamilton_nodes(dr: driver.Driver) -> dict[str, str]:
|
|
88
|
+
"""Hash the source code of Hamilton functions from nodes in a Driver"""
|
|
89
|
+
from hamilton import graph_types
|
|
90
|
+
|
|
91
|
+
graph = graph_types.HamiltonGraph.from_graph(dr.graph)
|
|
92
|
+
return {n.name: n.version for n in graph.nodes}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def map_nodes_to_functions(dr: driver.Driver) -> dict[str, str]:
|
|
96
|
+
"""Get a mapping from node name to Hamilton function name"""
|
|
97
|
+
from hamilton import graph_types
|
|
98
|
+
|
|
99
|
+
graph = graph_types.HamiltonGraph.from_graph(dr.graph)
|
|
100
|
+
|
|
101
|
+
node_to_function = dict()
|
|
102
|
+
for n in graph.nodes:
|
|
103
|
+
# is None for config nodes
|
|
104
|
+
if n.originating_functions is None:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
node_callable = n.originating_functions[0]
|
|
108
|
+
node_to_function[n.name] = node_callable.__name__
|
|
109
|
+
|
|
110
|
+
return node_to_function
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def hash_dataflow(nodes_version: dict[str, str]) -> str:
|
|
114
|
+
"""Create a dataflow hash from the hashes of its nodes"""
|
|
115
|
+
import hashlib
|
|
116
|
+
|
|
117
|
+
sorted_nodes = sorted(nodes_version.values())
|
|
118
|
+
return hashlib.sha256(str(sorted_nodes).encode()).hexdigest()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def load_modules_from_git(
|
|
122
|
+
module_paths: list[Path], git_reference: str = "HEAD"
|
|
123
|
+
) -> list[ModuleType]:
|
|
124
|
+
"""Dynamically import modules for a git reference"""
|
|
125
|
+
from hamilton import ad_hoc_utils
|
|
126
|
+
|
|
127
|
+
git_base_dir = Path(get_git_base_directory())
|
|
128
|
+
|
|
129
|
+
modules = []
|
|
130
|
+
for module_path in module_paths:
|
|
131
|
+
relative_to_git = module_path.relative_to(git_base_dir)
|
|
132
|
+
previous_revision = get_git_reference(relative_to_git, git_reference)
|
|
133
|
+
module = ad_hoc_utils.module_from_source(previous_revision)
|
|
134
|
+
modules.append(module)
|
|
135
|
+
|
|
136
|
+
return modules
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def diff_nodes_against_functions(
|
|
140
|
+
nodes_version: dict[str, str],
|
|
141
|
+
origins_version: dict[str, str],
|
|
142
|
+
node_to_origin: dict[str, str],
|
|
143
|
+
) -> dict:
|
|
144
|
+
"""Compare the nodes version from a built Driver to
|
|
145
|
+
the origins version from module source code when a second
|
|
146
|
+
Driver can't be instantiated
|
|
147
|
+
|
|
148
|
+
:nodes_version: mapping from node name to its origin function hash
|
|
149
|
+
:origins_version: mapping from origin function name to origin function hash
|
|
150
|
+
:node_to_origin: mapping from node name to origin name
|
|
151
|
+
"""
|
|
152
|
+
node_added = []
|
|
153
|
+
code_diff = []
|
|
154
|
+
origins_removed = []
|
|
155
|
+
|
|
156
|
+
# iterate over nodes from built Driver
|
|
157
|
+
for node_name, node_version in nodes_version.items():
|
|
158
|
+
origin_name = node_to_origin[node_name]
|
|
159
|
+
origin_version = origins_version.get(origin_name)
|
|
160
|
+
|
|
161
|
+
# if origin_version is None, the node is new
|
|
162
|
+
if origin_version is None:
|
|
163
|
+
node_added.append(node_name)
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
# if node_version != origin_version, the current node's origin
|
|
167
|
+
# function existed before, but was edited since origin_version
|
|
168
|
+
if node_version != origin_version:
|
|
169
|
+
code_diff.append(node_name)
|
|
170
|
+
|
|
171
|
+
nodes_origin_version = set(nodes_version.values())
|
|
172
|
+
# iterate over origin functions from source code
|
|
173
|
+
for origin_name, origin_version in origins_version.items():
|
|
174
|
+
# if origin_version not found in nodes_version, the
|
|
175
|
+
# origin function is not used by the built Driver
|
|
176
|
+
if origin_version not in nodes_origin_version:
|
|
177
|
+
origins_removed.append(origin_name)
|
|
178
|
+
|
|
179
|
+
return dict(
|
|
180
|
+
nodes_added=node_added,
|
|
181
|
+
code_diff=code_diff,
|
|
182
|
+
origins_removed=origins_removed,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def diff_versions(current_map: dict[str, str], reference_map: dict[str, str]) -> dict:
|
|
187
|
+
"""Generic diff of two {name: hash} mappings (can be node or origin name)
|
|
188
|
+
|
|
189
|
+
:mapping_v1: mapping from node (or function) name to its function hash
|
|
190
|
+
:mapping_v2: mapping from node (or function) name to its function hash
|
|
191
|
+
"""
|
|
192
|
+
current_only, reference_only, edit = [], [], []
|
|
193
|
+
|
|
194
|
+
for node_name, v1 in current_map.items():
|
|
195
|
+
v2 = reference_map.get(node_name)
|
|
196
|
+
if v2 is None:
|
|
197
|
+
current_only.append(node_name)
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
if v1 != v2:
|
|
201
|
+
edit.append(node_name)
|
|
202
|
+
|
|
203
|
+
for node_name, _ in reference_map.items():
|
|
204
|
+
v1 = current_map.get(node_name)
|
|
205
|
+
if v1 is None:
|
|
206
|
+
reference_only.append(node_name)
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
return dict(current_only=current_only, reference_only=reference_only, edit=edit)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _custom_diff_style(
|
|
213
|
+
*,
|
|
214
|
+
node,
|
|
215
|
+
node_class,
|
|
216
|
+
current_only: list[str],
|
|
217
|
+
reference_only: list[str],
|
|
218
|
+
edit: list[str],
|
|
219
|
+
):
|
|
220
|
+
"""Custom visualization style for the diff of 2 dataflows"""
|
|
221
|
+
if node.name in current_only:
|
|
222
|
+
style = ({"fillcolor": "greenyellow"}, node_class, "Current only")
|
|
223
|
+
|
|
224
|
+
elif node.name in reference_only:
|
|
225
|
+
style = ({"fillcolor": "tomato1"}, node_class, "Reference only")
|
|
226
|
+
|
|
227
|
+
elif node.name in edit:
|
|
228
|
+
style = ({"fillcolor": "yellow2"}, node_class, "Edited")
|
|
229
|
+
|
|
230
|
+
else:
|
|
231
|
+
style = ({}, node_class, None)
|
|
232
|
+
|
|
233
|
+
return style
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def visualize_diff(
|
|
237
|
+
current_dr: driver.Driver,
|
|
238
|
+
reference_dr: driver.Driver,
|
|
239
|
+
current_only: list[str],
|
|
240
|
+
reference_only: list[str],
|
|
241
|
+
edit: list[str],
|
|
242
|
+
):
|
|
243
|
+
"""Visualize the diff of 2 dataflows.
|
|
244
|
+
|
|
245
|
+
Uses the union of sets of nodes from driver 1 and driver 2.
|
|
246
|
+
"""
|
|
247
|
+
import functools
|
|
248
|
+
|
|
249
|
+
from hamilton import graph
|
|
250
|
+
|
|
251
|
+
all_nodes = set(reference_dr.graph.get_nodes()).union(set(current_dr.graph.get_nodes()))
|
|
252
|
+
|
|
253
|
+
diff_style = functools.partial(
|
|
254
|
+
_custom_diff_style,
|
|
255
|
+
current_only=current_only,
|
|
256
|
+
reference_only=reference_only,
|
|
257
|
+
edit=edit,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return graph.create_graphviz_graph(
|
|
261
|
+
nodes=all_nodes,
|
|
262
|
+
comment="Diff viz",
|
|
263
|
+
graphviz_kwargs=dict(),
|
|
264
|
+
custom_style_function=diff_style,
|
|
265
|
+
node_modifiers=dict(),
|
|
266
|
+
strictly_display_only_nodes_passed_in=True,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# TODO refactor ContextLoader to a class
|
|
271
|
+
# TODO support loading from pyproject.toml
|
|
272
|
+
def load_context(file_path: Path) -> dict:
|
|
273
|
+
if not file_path.exists():
|
|
274
|
+
raise FileNotFoundError(f"`{file_path}` doesn't exist.")
|
|
275
|
+
|
|
276
|
+
extension = file_path.suffix
|
|
277
|
+
if extension == ".json":
|
|
278
|
+
context = _read_json_context(file_path)
|
|
279
|
+
elif extension == ".py":
|
|
280
|
+
context = _read_py_context(file_path)
|
|
281
|
+
else:
|
|
282
|
+
raise ValueError(f"Received extension `{extension}` is unsupported.")
|
|
283
|
+
|
|
284
|
+
context = _validate_context(context)
|
|
285
|
+
return context
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _validate_context(context: dict) -> dict:
|
|
289
|
+
if context[CONFIG_HEADER] is None:
|
|
290
|
+
context[CONFIG_HEADER] = {}
|
|
291
|
+
|
|
292
|
+
if context[FINAL_VARS_HEADER] is None:
|
|
293
|
+
context[FINAL_VARS_HEADER] = []
|
|
294
|
+
|
|
295
|
+
if context[INPUTS_HEADER] is None:
|
|
296
|
+
context[INPUTS_HEADER] = {}
|
|
297
|
+
|
|
298
|
+
if context[OVERRIDES_HEADER] is None:
|
|
299
|
+
context[OVERRIDES_HEADER] = {}
|
|
300
|
+
|
|
301
|
+
return context
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _read_json_context(file_path: Path) -> dict:
|
|
305
|
+
""""""
|
|
306
|
+
import json
|
|
307
|
+
|
|
308
|
+
data = json.load(file_path.open())
|
|
309
|
+
|
|
310
|
+
context = {}
|
|
311
|
+
for k in [
|
|
312
|
+
CONFIG_HEADER,
|
|
313
|
+
FINAL_VARS_HEADER,
|
|
314
|
+
INPUTS_HEADER,
|
|
315
|
+
OVERRIDES_HEADER,
|
|
316
|
+
]:
|
|
317
|
+
context[k] = data.get(k, None)
|
|
318
|
+
|
|
319
|
+
return context
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _read_py_context(file_path: Path) -> dict:
|
|
323
|
+
import importlib
|
|
324
|
+
|
|
325
|
+
spec = importlib.util.spec_from_file_location("cli_config", file_path)
|
|
326
|
+
module = importlib.util.module_from_spec(spec)
|
|
327
|
+
spec.loader.exec_module(module)
|
|
328
|
+
|
|
329
|
+
context = {}
|
|
330
|
+
for k in [
|
|
331
|
+
CONFIG_HEADER,
|
|
332
|
+
FINAL_VARS_HEADER,
|
|
333
|
+
INPUTS_HEADER,
|
|
334
|
+
OVERRIDES_HEADER,
|
|
335
|
+
]:
|
|
336
|
+
context[k] = getattr(module, k, None)
|
|
337
|
+
|
|
338
|
+
return context
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
# code in this module should no depend on much
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from typing import Any, List, Optional, Set, Tuple, Union
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def convert_output_value(
|
|
24
|
+
output_value: str | Callable | Any, module_set: set[str]
|
|
25
|
+
) -> tuple[str | None, str | None]:
|
|
26
|
+
"""Converts output values that one can request into strings.
|
|
27
|
+
|
|
28
|
+
It checks that if it's a function, it's in the passed in module set.
|
|
29
|
+
|
|
30
|
+
:param output_value: the value we want to convert into a string. We don't annotate driver.Variable here for
|
|
31
|
+
import reasons.
|
|
32
|
+
:param module_set: the set of modules functions could come from.
|
|
33
|
+
:return: a tuple, (string value, string error). One or the other is returned, never both.
|
|
34
|
+
"""
|
|
35
|
+
if isinstance(output_value, str):
|
|
36
|
+
return output_value, None
|
|
37
|
+
elif hasattr(output_value, "name"):
|
|
38
|
+
return output_value.name, None
|
|
39
|
+
elif isinstance(output_value, Callable):
|
|
40
|
+
if output_value.__module__ in module_set:
|
|
41
|
+
return output_value.__name__, None
|
|
42
|
+
else:
|
|
43
|
+
return None, (
|
|
44
|
+
f"Function {output_value.__module__}.{output_value.__name__} is a function not "
|
|
45
|
+
f"in a "
|
|
46
|
+
f"module given to the materializer. Valid choices are {module_set}."
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
return None, (
|
|
50
|
+
f"Materializer dependency {output_value} is not a string, a function, or a driver.Variable."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def convert_output_values(
|
|
55
|
+
output_values: list[str | Callable | Any], module_set: set[str]
|
|
56
|
+
) -> list[str]:
|
|
57
|
+
"""Checks & converts outputs values to strings. This is used in building dependencies for the DAG.
|
|
58
|
+
|
|
59
|
+
:param output_values: the values to convert.
|
|
60
|
+
:param module_set: the modules any functions could come from.
|
|
61
|
+
:return: the final values
|
|
62
|
+
:raises ValueError: if there are values that can't be used/converted.
|
|
63
|
+
"""
|
|
64
|
+
final_values = []
|
|
65
|
+
errors = []
|
|
66
|
+
for final_var in output_values:
|
|
67
|
+
_val, _error = convert_output_value(final_var, module_set)
|
|
68
|
+
if _val:
|
|
69
|
+
final_values.append(_val)
|
|
70
|
+
if _error:
|
|
71
|
+
errors.append(_error)
|
|
72
|
+
if errors:
|
|
73
|
+
errors.sort()
|
|
74
|
+
error_str = f"{len(errors)} errors encountered:\n " + "\n ".join(errors)
|
|
75
|
+
raise ValueError(error_str)
|
|
76
|
+
return final_values
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
"""This module exists so that people can download dataflows without the sf-hamilton-contrib package.
|
|
19
|
+
|
|
20
|
+
It will get clobbered when sf-hamilton-contrib is installed, which is good.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from contextlib import contextmanager
|
|
25
|
+
|
|
26
|
+
__version__ = "__unknown__" # this will be overwritten once sf-hamilton-contrib is installed.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@contextmanager
|
|
30
|
+
def catch_import_errors(module_name: str, file_location: str, logger: logging.Logger):
|
|
31
|
+
try:
|
|
32
|
+
# Yield control to the inner block which will have the import statements.
|
|
33
|
+
yield
|
|
34
|
+
except ImportError as e:
|
|
35
|
+
location = file_location[: file_location.rfind("/")]
|
|
36
|
+
logger.error("ImportError: %s", e)
|
|
37
|
+
logger.error(
|
|
38
|
+
"Please install the required packages. Options:\n"
|
|
39
|
+
f"(1): with `pip install -r {location}/requirements.txt`\n"
|
|
40
|
+
)
|
|
41
|
+
raise e
|
|
@@ -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.
|