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
|
@@ -0,0 +1,622 @@
|
|
|
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 argparse
|
|
19
|
+
import ast
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
from collections import defaultdict
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from IPython.core.magic import Magics, cell_magic, line_magic, magics_class
|
|
27
|
+
from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring
|
|
28
|
+
from IPython.core.shellapp import InteractiveShellApp
|
|
29
|
+
from IPython.display import HTML, Code, display
|
|
30
|
+
from IPython.utils.process import arg_split
|
|
31
|
+
|
|
32
|
+
from hamilton import ad_hoc_utils, driver
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_assigned_variables(module_node: ast.Module) -> set[str]:
|
|
36
|
+
"""Get the set of variable names assigned in a AST Module"""
|
|
37
|
+
assigned_vars = set()
|
|
38
|
+
|
|
39
|
+
def visit_node(ast_node):
|
|
40
|
+
"""Recursive function looking for assigned variable names"""
|
|
41
|
+
if isinstance(ast_node, ast.Assign):
|
|
42
|
+
for target in ast_node.targets:
|
|
43
|
+
if isinstance(target, ast.Name):
|
|
44
|
+
assigned_vars.add(target.id)
|
|
45
|
+
|
|
46
|
+
for child_node in ast.iter_child_nodes(ast_node):
|
|
47
|
+
visit_node(child_node)
|
|
48
|
+
|
|
49
|
+
visit_node(module_node)
|
|
50
|
+
return assigned_vars
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def execute_and_get_assigned_values(shell: InteractiveShellApp, cell: str) -> dict[str, Any]:
|
|
54
|
+
"""Execute source code from a cell in the user namespace and collect
|
|
55
|
+
the values of all assigned variables into a dictionary.
|
|
56
|
+
"""
|
|
57
|
+
shell.ex(cell)
|
|
58
|
+
expr = shell.input_transformer_manager.transform_cell(cell)
|
|
59
|
+
expr_ast = shell.compile.ast_parse(expr)
|
|
60
|
+
return {name: shell.user_ns[name] for name in get_assigned_variables(expr_ast)}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def topological_sort(nodes):
|
|
64
|
+
"""Sort the nodes for nice output display"""
|
|
65
|
+
|
|
66
|
+
def dfs(node, visited, stack):
|
|
67
|
+
visited.add(node)
|
|
68
|
+
for neighbor in graph.get(node, []):
|
|
69
|
+
if neighbor not in visited:
|
|
70
|
+
dfs(neighbor, visited, stack)
|
|
71
|
+
stack.append(node)
|
|
72
|
+
|
|
73
|
+
graph = {n.name: set([*n.required_dependencies, *n.optional_dependencies]) for n in nodes}
|
|
74
|
+
visited = set()
|
|
75
|
+
stack = []
|
|
76
|
+
|
|
77
|
+
for node in graph:
|
|
78
|
+
if node not in visited:
|
|
79
|
+
dfs(node, visited, stack)
|
|
80
|
+
|
|
81
|
+
return stack
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _normalize_result_names(node_name: str) -> str:
|
|
85
|
+
"""Remove periods from the name of dynamically generated Hamilton nodes"""
|
|
86
|
+
return node_name.replace(".", "__")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def display_in_databricks(dot):
|
|
90
|
+
try:
|
|
91
|
+
display(HTML(dot.pipe(format="svg").decode("utf-8")))
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(
|
|
94
|
+
f"Failed to display graph: {e}\n"
|
|
95
|
+
"Please ensure graphviz is installed via `%sh apt install -y graphviz`"
|
|
96
|
+
)
|
|
97
|
+
return
|
|
98
|
+
return dot
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def insert_cell_with_content():
|
|
102
|
+
"""Calling this function before .set_next_input() will output text content to
|
|
103
|
+
the next code cell.
|
|
104
|
+
|
|
105
|
+
This works by printing JavaScript in the current cell's HTML output
|
|
106
|
+
|
|
107
|
+
adapted from: https://stackoverflow.com/questions/65379879/define-a-ipython-magic-which-replaces-the-content-of-the-next-cell
|
|
108
|
+
"""
|
|
109
|
+
js_script = r"""<script>
|
|
110
|
+
|
|
111
|
+
if (document.getElementById('notebook-container')) {
|
|
112
|
+
//console.log('Jupyter Notebook');
|
|
113
|
+
allCells = document.getElementById('notebook-container').children;
|
|
114
|
+
selectionClass = /\bselected\b/;
|
|
115
|
+
jupyter = 'notebook';
|
|
116
|
+
}
|
|
117
|
+
else if (document.getElementsByClassName('jp-Notebook-cell').length){
|
|
118
|
+
//console.log('Jupyter Lab');
|
|
119
|
+
allCells = document.getElementsByClassName('jp-Notebook-cell');
|
|
120
|
+
selectionClass = /\bjp-mod-selected\b/;
|
|
121
|
+
jupyter = 'lab';
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log('Unknown Environment');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (typeof allCells !== 'undefined') {
|
|
128
|
+
for (i = 0; i < allCells.length - 1; i++) {
|
|
129
|
+
if(selectionClass.test(allCells[i].getAttribute('class'))){
|
|
130
|
+
allCells[i + 1].remove();
|
|
131
|
+
|
|
132
|
+
// remove output indicators of current cell
|
|
133
|
+
window.setTimeout(function(){
|
|
134
|
+
if(jupyter === 'lab') {
|
|
135
|
+
allCells[i].setAttribute('class', allCells[i].getAttribute('class') + ' jp-mod-noOutputs');
|
|
136
|
+
allCells[i].getElementsByClassName('jp-OutputArea jp-Cell-outputArea')[0].innerHTML = '';
|
|
137
|
+
} else if(jupyter === 'notebook'){
|
|
138
|
+
allCells[i].getElementsByClassName('output')[0].innerHTML = '';
|
|
139
|
+
}
|
|
140
|
+
}, 20);
|
|
141
|
+
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
</script>"""
|
|
147
|
+
|
|
148
|
+
# remove next cell
|
|
149
|
+
display(HTML(js_script))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def determine_notebook_type() -> str:
|
|
153
|
+
if "DATABRICKS_RUNTIME_VERSION" in os.environ:
|
|
154
|
+
return "databricks"
|
|
155
|
+
return "default"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def parse_known_argstring(magic_func, argstring) -> tuple[argparse.Namespace, list[str]]:
|
|
159
|
+
"""IPython magic arguments parsing doesn't allow unknown args.
|
|
160
|
+
Used instead of IPython.core.magic_arguments.parse_argstring
|
|
161
|
+
|
|
162
|
+
IPython ref: https://github.com/ipython/ipython/blob/43781b39a67f02ff4e9ae63484387f654dd045d4/IPython/core/magic_arguments.py#L164
|
|
163
|
+
argparse ref: https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.parse_known_args
|
|
164
|
+
"""
|
|
165
|
+
argv = arg_split(argstring)
|
|
166
|
+
# magic_func.parser is an argparse.ArgumentParser subclass
|
|
167
|
+
known, unknown = magic_func.parser.parse_known_args(argv)
|
|
168
|
+
return known, unknown
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def parse_key_value_config(config_string):
|
|
172
|
+
config = {}
|
|
173
|
+
for item in config_string.split():
|
|
174
|
+
key, value = item.split("=")
|
|
175
|
+
config[key] = value
|
|
176
|
+
return config
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@magics_class
|
|
180
|
+
class HamiltonMagics(Magics):
|
|
181
|
+
"""Magics to facilitate interactive Hamilton development in notebooks."""
|
|
182
|
+
|
|
183
|
+
def __init__(self, **kwargs):
|
|
184
|
+
super().__init__(**kwargs)
|
|
185
|
+
self.builder = None
|
|
186
|
+
self.notebook_env = determine_notebook_type()
|
|
187
|
+
self.incremental_cells_state = defaultdict(dict)
|
|
188
|
+
|
|
189
|
+
def resolve_unknown_args_cell_to_module(self, unknown: list[str]):
|
|
190
|
+
"""Handle unknown arguments. It won't make the magic execution fail."""
|
|
191
|
+
|
|
192
|
+
# deprecated in V2 because it's less useful since `%%cell_to_module` can execute itself
|
|
193
|
+
if any(arg in ("-r", "--rebuilder-drivers") for arg in unknown):
|
|
194
|
+
print(
|
|
195
|
+
"DeprecationWarning: -r/--rebuilder-drivers no long does anything and will be removed in future releases."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# deprecated in V2 because it relates to the deprecated -r/--rebuild-drivers
|
|
199
|
+
if any(arg in ("-v", "--verbose") for arg in unknown):
|
|
200
|
+
print(
|
|
201
|
+
"DeprecationWarning: -v/--verbose no long does anything and will be removed in future releases."
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if any(arg == "--hide_results" for arg in unknown):
|
|
205
|
+
print(
|
|
206
|
+
"DeprecationWarning: `--hide_results` is no longer required when using `--execute`. Now, results are hidden by default. Use `--show_results` if you want them automatically printed to cell output."
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# there for backwards compatibility. Equivalent to calling `%%cell_to_module?`
|
|
210
|
+
# not included as @argument because it's not really a function arg to %%cell_to_module
|
|
211
|
+
if any(arg in ("-h", "--help") for arg in unknown):
|
|
212
|
+
print(help(self.cell_to_module))
|
|
213
|
+
|
|
214
|
+
def resolve_config_arg(self, config_arg) -> bool | dict:
|
|
215
|
+
# default case: didn't receive `-c/--config`. Set an empty dict
|
|
216
|
+
if config_arg is None:
|
|
217
|
+
config = {}
|
|
218
|
+
# case 1, 2, 3: `-c/--config` is specified
|
|
219
|
+
# case 1: -c/--config refers to variable in the user namespace
|
|
220
|
+
elif self.shell.user_ns.get(config_arg):
|
|
221
|
+
config = self.shell.user_ns.get(config_arg)
|
|
222
|
+
# case 2: parse using key=value
|
|
223
|
+
elif "=" in config_arg:
|
|
224
|
+
config = parse_key_value_config(config_arg)
|
|
225
|
+
# case 3: parse as JSON
|
|
226
|
+
elif ":" in config_arg:
|
|
227
|
+
try:
|
|
228
|
+
# strip quotation marks added by IPython and avoid mutating `args`
|
|
229
|
+
config_str = config_arg.strip("'\"")
|
|
230
|
+
config = json.loads(config_str)
|
|
231
|
+
except json.JSONDecodeError:
|
|
232
|
+
print(f"JSONDecodeError: Failed to parse `config` as JSON. Received {config_arg}")
|
|
233
|
+
return False
|
|
234
|
+
return config
|
|
235
|
+
|
|
236
|
+
@magic_arguments() # needed on top to enable parsing
|
|
237
|
+
@argument("name", nargs="?", help="Name for the module defined in this cell.")
|
|
238
|
+
@argument(
|
|
239
|
+
"-m",
|
|
240
|
+
"--module_name",
|
|
241
|
+
nargs="?",
|
|
242
|
+
const=True,
|
|
243
|
+
help="Alias for positional argument `module_name`. There for backwards compatibility. Prefer the position arg.",
|
|
244
|
+
)
|
|
245
|
+
@argument(
|
|
246
|
+
"-d",
|
|
247
|
+
"--display",
|
|
248
|
+
nargs="?",
|
|
249
|
+
const=True,
|
|
250
|
+
help="Display the dataflow. The argument is the variable name of a dictionary of visualization kwargs; else {}.",
|
|
251
|
+
)
|
|
252
|
+
@argument(
|
|
253
|
+
"--display_cache",
|
|
254
|
+
action="store_true",
|
|
255
|
+
help="After execution, display the retrieved results. This uses `dr.cache.view_run()`.",
|
|
256
|
+
)
|
|
257
|
+
@argument(
|
|
258
|
+
"-x",
|
|
259
|
+
"--execute",
|
|
260
|
+
nargs="?",
|
|
261
|
+
const=True,
|
|
262
|
+
help="Execute the dataflow. The argument is the variable name of a list of nodes; else execute all nodes.",
|
|
263
|
+
)
|
|
264
|
+
@argument(
|
|
265
|
+
"-b",
|
|
266
|
+
"--builder",
|
|
267
|
+
help="Builder to which the module will be added and used for execution. Allows to pass Config and Adapters",
|
|
268
|
+
)
|
|
269
|
+
@argument(
|
|
270
|
+
"-c",
|
|
271
|
+
"--config",
|
|
272
|
+
help="Config to build a Driver. Passing -c/--config at the same time as a Builder -b/--builder with a config will raise an exception.",
|
|
273
|
+
)
|
|
274
|
+
@argument(
|
|
275
|
+
"-i",
|
|
276
|
+
"--inputs",
|
|
277
|
+
help="Execution inputs. The argument is the variable name of a dict of inputs; else {}.",
|
|
278
|
+
)
|
|
279
|
+
@argument(
|
|
280
|
+
"-o",
|
|
281
|
+
"--overrides",
|
|
282
|
+
help="Execution overrides. The argument is the variable name of a dict of overrides; else {}.",
|
|
283
|
+
)
|
|
284
|
+
@argument(
|
|
285
|
+
"--show_results",
|
|
286
|
+
action="store_true",
|
|
287
|
+
help="Print node values in the output cell after each node is executed.",
|
|
288
|
+
)
|
|
289
|
+
@argument(
|
|
290
|
+
"-w",
|
|
291
|
+
"--write_to_file",
|
|
292
|
+
nargs="?",
|
|
293
|
+
const=True,
|
|
294
|
+
help="Write cell content to a file. The argument is the file path; else write to {module_name}.py",
|
|
295
|
+
)
|
|
296
|
+
@cell_magic
|
|
297
|
+
def cell_to_module(self, line, cell):
|
|
298
|
+
"""Turn a notebook cell into a Hamilton module definition. This allows you to define
|
|
299
|
+
and execute a dataflow from a single cell.
|
|
300
|
+
|
|
301
|
+
For example:
|
|
302
|
+
```
|
|
303
|
+
%%cell_to_module dataflow --display --execute
|
|
304
|
+
def A() -> int:
|
|
305
|
+
return 37
|
|
306
|
+
|
|
307
|
+
def B(A: int) -> bool:
|
|
308
|
+
return (A % 3) > 2
|
|
309
|
+
```
|
|
310
|
+
"""
|
|
311
|
+
# shell.ex() is equivalent to exec(), but in the user namespace (i.e. notebook context).
|
|
312
|
+
# This allows imports and functions defined in the magic cell %%cell_to_module to be
|
|
313
|
+
# directly accessed from the notebook
|
|
314
|
+
self.shell.ex(cell)
|
|
315
|
+
|
|
316
|
+
args, unknown_args = parse_known_argstring(
|
|
317
|
+
self.cell_to_module, line
|
|
318
|
+
) # specify how to parse by passing method
|
|
319
|
+
self.resolve_unknown_args_cell_to_module(unknown_args)
|
|
320
|
+
|
|
321
|
+
# validate variables exist in the user namespace expect `config` because it's a special case
|
|
322
|
+
# will exit using `return` in case of error
|
|
323
|
+
args_that_read_user_namespace = ["display", "builder", "final_vars", "inputs", "overrides"]
|
|
324
|
+
for name, value in vars(args).items():
|
|
325
|
+
if name not in args_that_read_user_namespace:
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
# special case: args that can be passed as a flag (=True) without values
|
|
329
|
+
if name in ["display", "final_vars"] and value is True:
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
# main case: exit if variable is not in user namespace
|
|
333
|
+
if value and self.shell.user_ns.get(value) is None:
|
|
334
|
+
print(f"KeyError: Received `--{name} {value}` but variable not found.")
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
# parse config; exit if config is invalid
|
|
338
|
+
config = self.resolve_config_arg(args.config)
|
|
339
|
+
if config is False:
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
# check if string instance because module_name has default `True`
|
|
343
|
+
if isinstance(args.name, str) and isinstance(args.module_name, str):
|
|
344
|
+
print(
|
|
345
|
+
f"ValueError: Received both positional arg name={args.name} and named arg module_name={args.module_name}. Pass either one."
|
|
346
|
+
)
|
|
347
|
+
return
|
|
348
|
+
|
|
349
|
+
# merged the positional arg `name` with named arg `module_name` for backwards compatibility
|
|
350
|
+
module_name = args.module_name if isinstance(args.module_name, str) else args.name
|
|
351
|
+
base_builder = self.shell.user_ns[args.builder] if args.builder else driver.Builder()
|
|
352
|
+
inputs = self.shell.user_ns[args.inputs] if args.inputs else {}
|
|
353
|
+
overrides = self.shell.user_ns[args.overrides] if args.overrides else {}
|
|
354
|
+
display_config = (
|
|
355
|
+
self.shell.user_ns[args.display] if args.display not in [True, None] else {}
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# determine the Driver config
|
|
359
|
+
# can't check from args.builder because it might be None
|
|
360
|
+
if config and base_builder.config:
|
|
361
|
+
print(
|
|
362
|
+
"AssertionError: Received a config -c/--config and a Builder -b/--builder with an existing config. Pass either one."
|
|
363
|
+
)
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
# Decision: write to file before trying to build and execute Driver
|
|
367
|
+
# See argument `help` for behavior details
|
|
368
|
+
if args.write_to_file:
|
|
369
|
+
if isinstance(args.write_to_file, str):
|
|
370
|
+
file_path = Path(args.write_to_file)
|
|
371
|
+
else:
|
|
372
|
+
file_path = Path(f"{module_name}.py")
|
|
373
|
+
file_path.write_text(cell)
|
|
374
|
+
|
|
375
|
+
# create_module() is preferred over module_from_source() to simplify
|
|
376
|
+
# the integration with the Hamilton UI which assumes physical Python modules
|
|
377
|
+
try:
|
|
378
|
+
cell_module = ad_hoc_utils.create_module(cell, module_name)
|
|
379
|
+
except BaseException as e:
|
|
380
|
+
print("Failed to build the module. Stack trace:")
|
|
381
|
+
raise e
|
|
382
|
+
self.shell.push({module_name: cell_module})
|
|
383
|
+
|
|
384
|
+
# build the Driver. the Builder is copied to avoid conflict with the user namespace
|
|
385
|
+
builder = base_builder.copy()
|
|
386
|
+
dr = builder.with_config(config).with_modules(cell_module).build()
|
|
387
|
+
|
|
388
|
+
# determine final vars
|
|
389
|
+
if args.execute not in [True, None]:
|
|
390
|
+
final_vars = self.shell.user_ns[args.execute]
|
|
391
|
+
else:
|
|
392
|
+
nodes = [n for n in dr.list_available_variables() if not n.is_external_input]
|
|
393
|
+
final_vars = topological_sort(nodes)
|
|
394
|
+
|
|
395
|
+
# visualize
|
|
396
|
+
if args.display:
|
|
397
|
+
# try/except `display_config` or inputs/overrides may be invalid
|
|
398
|
+
try:
|
|
399
|
+
if args.execute:
|
|
400
|
+
dot = dr.visualize_execution(
|
|
401
|
+
final_vars=final_vars,
|
|
402
|
+
inputs=inputs,
|
|
403
|
+
overrides=overrides,
|
|
404
|
+
**display_config,
|
|
405
|
+
)
|
|
406
|
+
else:
|
|
407
|
+
dot = dr.display_all_functions(**display_config)
|
|
408
|
+
except Exception as e:
|
|
409
|
+
print(f"Failed to display {e}.\n\nThe display config was: {display_config}")
|
|
410
|
+
dot = dr.display_all_functions()
|
|
411
|
+
|
|
412
|
+
# handle output environment
|
|
413
|
+
if self.notebook_env == "databricks":
|
|
414
|
+
display_in_databricks(dot)
|
|
415
|
+
else:
|
|
416
|
+
display(dot)
|
|
417
|
+
|
|
418
|
+
# execute
|
|
419
|
+
if args.execute:
|
|
420
|
+
results = dr.execute(
|
|
421
|
+
final_vars=final_vars,
|
|
422
|
+
inputs=inputs,
|
|
423
|
+
overrides=overrides,
|
|
424
|
+
)
|
|
425
|
+
# normalize variable names that contain a `.` character like @pipe(step())
|
|
426
|
+
results = {_normalize_result_names(name): value for name, value in results.items()}
|
|
427
|
+
self.shell.push(results)
|
|
428
|
+
|
|
429
|
+
if args.show_results:
|
|
430
|
+
# results will follow the order of `final_vars` or topologically sorted if all vars
|
|
431
|
+
display(*(results[n] for n in final_vars))
|
|
432
|
+
|
|
433
|
+
if args.display_cache:
|
|
434
|
+
dot = dr.cache.view_run()
|
|
435
|
+
if self.notebook_env == "databricks":
|
|
436
|
+
display_in_databricks(dot)
|
|
437
|
+
else:
|
|
438
|
+
display(dot)
|
|
439
|
+
|
|
440
|
+
# TODO unify the API and logic of `%%cell_to_module` and `%%incr_cell_to_module`
|
|
441
|
+
@magic_arguments()
|
|
442
|
+
@argument("module_name", nargs="?", help="Name for the module defined in this cell.")
|
|
443
|
+
@argument(
|
|
444
|
+
"identifier", type=int, help="Identifier for this cell w.r.t. the module being created."
|
|
445
|
+
)
|
|
446
|
+
@argument(
|
|
447
|
+
"-c",
|
|
448
|
+
"--config",
|
|
449
|
+
help="Config to build a Driver. Passing -c/--config at the same time as a Builder -b/--builder with a config will raise an exception.",
|
|
450
|
+
)
|
|
451
|
+
@argument(
|
|
452
|
+
"-b",
|
|
453
|
+
"--builder",
|
|
454
|
+
help="Builder to which the module will be added and used for execution. Allows to pass Config and Adapters",
|
|
455
|
+
)
|
|
456
|
+
@argument(
|
|
457
|
+
"-d",
|
|
458
|
+
"--display",
|
|
459
|
+
nargs="?",
|
|
460
|
+
const=True,
|
|
461
|
+
help="Display the dataflow. The argument is the variable name of a dictionary of visualization kwargs; else {}.",
|
|
462
|
+
)
|
|
463
|
+
@argument(
|
|
464
|
+
"-w",
|
|
465
|
+
"--write_to_file",
|
|
466
|
+
nargs="?",
|
|
467
|
+
const=True,
|
|
468
|
+
help="Write cell content to a file. The argument is the file path; else write to {module_name}.py",
|
|
469
|
+
)
|
|
470
|
+
@cell_magic
|
|
471
|
+
def incr_cell_to_module(self, line, cell):
|
|
472
|
+
"""Incrementally build a module. This executes the cell and dynamically creates a Python module from its content.
|
|
473
|
+
A Hamilton Driver is automatically instantiated with that module for variable `{MODULE_NAME}_dr`.
|
|
474
|
+
|
|
475
|
+
> %%incr_cell_to_module MODULE_NAME -i IDENTIFIER --display
|
|
476
|
+
"""
|
|
477
|
+
# This function mimics the logic of `.cell_to_module()`. Find more comments there.
|
|
478
|
+
# Start by trying to execute the code cell.
|
|
479
|
+
self.shell.ex(cell)
|
|
480
|
+
|
|
481
|
+
# parse user inputs
|
|
482
|
+
args, unknown_args = parse_known_argstring(self.incr_cell_to_module, line)
|
|
483
|
+
self.resolve_unknown_args_cell_to_module(unknown_args)
|
|
484
|
+
|
|
485
|
+
# check user inputs pointing to variables in user namespace
|
|
486
|
+
args_that_read_user_namespace = ["display", "builder"]
|
|
487
|
+
for name, value in vars(args).items():
|
|
488
|
+
if name not in args_that_read_user_namespace:
|
|
489
|
+
continue
|
|
490
|
+
|
|
491
|
+
# special case: `display` can be passed as a flag (=True), without a config
|
|
492
|
+
if name == "display" and value is True:
|
|
493
|
+
continue
|
|
494
|
+
|
|
495
|
+
# main case: exit if variable is not in user namespace
|
|
496
|
+
if value and self.shell.user_ns.get(value) is None:
|
|
497
|
+
return f"KeyError: Received `--{name} {value}` but variable not found."
|
|
498
|
+
|
|
499
|
+
# TODO convert -i to -id
|
|
500
|
+
if args.identifier is None:
|
|
501
|
+
raise ValueError("`-id/--identifier` is required. Please provide an id for this cell.")
|
|
502
|
+
|
|
503
|
+
# parse config; exit if config is invalid
|
|
504
|
+
config = self.resolve_config_arg(args.config)
|
|
505
|
+
if config is False:
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
# set parsed arguments
|
|
509
|
+
module_name = args.module_name
|
|
510
|
+
base_builder = self.shell.user_ns[args.builder] if args.builder else driver.Builder()
|
|
511
|
+
display_config = (
|
|
512
|
+
self.shell.user_ns[args.display] if args.display not in [True, None] else {}
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# determine the Driver config
|
|
516
|
+
# can't check from args.builder because it might be None
|
|
517
|
+
if config and base_builder.config:
|
|
518
|
+
return "AssertionError: Received a config -c/--config and a Builder -b/--builder with an existing config. Pass either one."
|
|
519
|
+
|
|
520
|
+
# store current cell in state
|
|
521
|
+
self.incremental_cells_state[module_name][args.identifier] = cell
|
|
522
|
+
|
|
523
|
+
# build module source from multiple cells
|
|
524
|
+
module_dict = self.incremental_cells_state[module_name]
|
|
525
|
+
sorted_module_keys = sorted(list(module_dict.keys()))
|
|
526
|
+
module_source = "\n\n".join([module_dict[k] for k in sorted_module_keys])
|
|
527
|
+
multi_cell_module = ad_hoc_utils.create_module(module_source, module_name)
|
|
528
|
+
self.shell.push({module_name: multi_cell_module})
|
|
529
|
+
|
|
530
|
+
# Decision: write to file before trying to build and execute Driver
|
|
531
|
+
# See argument `help` for behavior details
|
|
532
|
+
if args.write_to_file:
|
|
533
|
+
if isinstance(args.write_to_file, str):
|
|
534
|
+
file_path = Path(args.write_to_file)
|
|
535
|
+
else:
|
|
536
|
+
file_path = Path(f"{module_name}.py")
|
|
537
|
+
file_path.write_text(module_source)
|
|
538
|
+
|
|
539
|
+
# build Driver
|
|
540
|
+
builder = base_builder.copy()
|
|
541
|
+
dr = builder.with_config(config).with_modules(multi_cell_module).build()
|
|
542
|
+
|
|
543
|
+
# visualize
|
|
544
|
+
if args.display:
|
|
545
|
+
# try/except `display_config` or inputs/overrides may be invalid
|
|
546
|
+
try:
|
|
547
|
+
dot = dr.display_all_functions(**display_config)
|
|
548
|
+
except Exception as e:
|
|
549
|
+
print(f"Failed to display {e}.\n\nThe display config was: {display_config}")
|
|
550
|
+
dot = dr.display_all_functions()
|
|
551
|
+
|
|
552
|
+
# handle output environment
|
|
553
|
+
if self.notebook_env == "databricks":
|
|
554
|
+
display_in_databricks(dot)
|
|
555
|
+
else:
|
|
556
|
+
display(dot)
|
|
557
|
+
|
|
558
|
+
@magic_arguments() # needed on top to enable parsing
|
|
559
|
+
@argument("module_name", help="Module name to print.") # required argument
|
|
560
|
+
@line_magic
|
|
561
|
+
def print_module(self, line):
|
|
562
|
+
"""Prints the contents of a dynamic module we've been creating."""
|
|
563
|
+
args = parse_argstring(self.incr_cell_to_module, line)
|
|
564
|
+
if args.module_name in self.incremental_cells_state:
|
|
565
|
+
module_dict = self.incremental_cells_state[args.module_name]
|
|
566
|
+
sorted_module_keys = sorted(list(module_dict[args.module_name].keys()))
|
|
567
|
+
module_source = "\n\n".join([module_dict[k] for k in sorted_module_keys])
|
|
568
|
+
display(Code(module_source))
|
|
569
|
+
else:
|
|
570
|
+
print(f"KeyError: `{args.module_name}` not found.")
|
|
571
|
+
|
|
572
|
+
@magic_arguments() # needed on top to enable parsing
|
|
573
|
+
@argument("module_name", help="Module to print.") # required argument
|
|
574
|
+
@line_magic
|
|
575
|
+
def reset_module(self, line):
|
|
576
|
+
args = parse_argstring(self.incr_cell_to_module, line)
|
|
577
|
+
if args.module_name in self.incremental_cells_state:
|
|
578
|
+
del self.incremental_cells_state[args.module_name]
|
|
579
|
+
print(f"Reset `{args.module_name}`")
|
|
580
|
+
else:
|
|
581
|
+
print(f"KeyError: `{args.module_name}` not found.")
|
|
582
|
+
|
|
583
|
+
@magic_arguments()
|
|
584
|
+
@argument("name", type=str, help="Creates a dictionary from the cell's content.")
|
|
585
|
+
@cell_magic
|
|
586
|
+
def set_dict(self, line: str, cell: str):
|
|
587
|
+
"""Execute the cell and store all assigned variables as inputs"""
|
|
588
|
+
args = parse_argstring(self.set_dict, line)
|
|
589
|
+
self.shell.user_ns[args.name] = execute_and_get_assigned_values(self.shell, cell)
|
|
590
|
+
|
|
591
|
+
@line_magic
|
|
592
|
+
def insert_module(self, line):
|
|
593
|
+
"""Alias for `%module_to_cell`."""
|
|
594
|
+
self.module_to_cell(line)
|
|
595
|
+
|
|
596
|
+
@line_magic
|
|
597
|
+
def module_to_cell(self, line):
|
|
598
|
+
"""Insert in the next cell the source code from the module (.py)
|
|
599
|
+
at the path specified by `line`.
|
|
600
|
+
|
|
601
|
+
This is useful if you have an existing Hamilton module and want to pull in the contents
|
|
602
|
+
for further development in the notebook.
|
|
603
|
+
"""
|
|
604
|
+
|
|
605
|
+
# JavaScript magic to generate a new cell
|
|
606
|
+
insert_cell_with_content()
|
|
607
|
+
|
|
608
|
+
module_path = Path(line)
|
|
609
|
+
# insert our custom %%with_functions magic at the top of the cell
|
|
610
|
+
header = f"%%cell_to_module {module_path.stem}\n"
|
|
611
|
+
module_source = module_path.read_text()
|
|
612
|
+
# insert source code as text in the next cell
|
|
613
|
+
self.shell.set_next_input(header + module_source, replace=False)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def load_ipython_extension(ipython: InteractiveShellApp):
|
|
617
|
+
"""
|
|
618
|
+
Any module file that define a function named `load_ipython_extension`
|
|
619
|
+
can be loaded via `%load_ext module.path` or be configured to be
|
|
620
|
+
autoloaded by IPython at startup time.
|
|
621
|
+
"""
|
|
622
|
+
ipython.register_magics(HamiltonMagics)
|