apache-hamilton 1.90.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. apache_hamilton-1.90.0.dev0.dist-info/METADATA +407 -0
  2. apache_hamilton-1.90.0.dev0.dist-info/RECORD +151 -0
  3. apache_hamilton-1.90.0.dev0.dist-info/WHEEL +4 -0
  4. apache_hamilton-1.90.0.dev0.dist-info/entry_points.txt +9 -0
  5. apache_hamilton-1.90.0.dev0.dist-info/licenses/DISCLAIMER +10 -0
  6. apache_hamilton-1.90.0.dev0.dist-info/licenses/LICENSE +228 -0
  7. apache_hamilton-1.90.0.dev0.dist-info/licenses/NOTICE +5 -0
  8. hamilton/__init__.py +24 -0
  9. hamilton/ad_hoc_utils.py +132 -0
  10. hamilton/async_driver.py +465 -0
  11. hamilton/base.py +466 -0
  12. hamilton/caching/__init__.py +16 -0
  13. hamilton/caching/adapter.py +1475 -0
  14. hamilton/caching/cache_key.py +70 -0
  15. hamilton/caching/fingerprinting.py +287 -0
  16. hamilton/caching/stores/__init__.py +16 -0
  17. hamilton/caching/stores/base.py +242 -0
  18. hamilton/caching/stores/file.py +140 -0
  19. hamilton/caching/stores/memory.py +297 -0
  20. hamilton/caching/stores/sqlite.py +282 -0
  21. hamilton/caching/stores/utils.py +40 -0
  22. hamilton/cli/__init__.py +16 -0
  23. hamilton/cli/__main__.py +328 -0
  24. hamilton/cli/commands.py +126 -0
  25. hamilton/cli/logic.py +338 -0
  26. hamilton/common/__init__.py +76 -0
  27. hamilton/contrib/__init__.py +41 -0
  28. hamilton/data_quality/__init__.py +16 -0
  29. hamilton/data_quality/base.py +198 -0
  30. hamilton/data_quality/default_validators.py +560 -0
  31. hamilton/data_quality/pandera_validators.py +121 -0
  32. hamilton/dataflows/__init__.py +726 -0
  33. hamilton/dataflows/template/README.md +25 -0
  34. hamilton/dataflows/template/__init__.py +54 -0
  35. hamilton/dataflows/template/author.md +28 -0
  36. hamilton/dataflows/template/requirements.txt +0 -0
  37. hamilton/dataflows/template/tags.json +7 -0
  38. hamilton/dataflows/template/valid_configs.jsonl +1 -0
  39. hamilton/dev_utils/__init__.py +16 -0
  40. hamilton/dev_utils/deprecation.py +204 -0
  41. hamilton/driver.py +2112 -0
  42. hamilton/execution/__init__.py +16 -0
  43. hamilton/execution/debugging_utils.py +56 -0
  44. hamilton/execution/executors.py +502 -0
  45. hamilton/execution/graph_functions.py +421 -0
  46. hamilton/execution/grouping.py +430 -0
  47. hamilton/execution/state.py +539 -0
  48. hamilton/experimental/__init__.py +27 -0
  49. hamilton/experimental/databackend.py +61 -0
  50. hamilton/experimental/decorators/__init__.py +16 -0
  51. hamilton/experimental/decorators/parameterize_frame.py +233 -0
  52. hamilton/experimental/h_async.py +29 -0
  53. hamilton/experimental/h_cache.py +413 -0
  54. hamilton/experimental/h_dask.py +28 -0
  55. hamilton/experimental/h_databackends.py +174 -0
  56. hamilton/experimental/h_ray.py +28 -0
  57. hamilton/experimental/h_spark.py +32 -0
  58. hamilton/function_modifiers/README +40 -0
  59. hamilton/function_modifiers/__init__.py +121 -0
  60. hamilton/function_modifiers/adapters.py +900 -0
  61. hamilton/function_modifiers/base.py +859 -0
  62. hamilton/function_modifiers/configuration.py +310 -0
  63. hamilton/function_modifiers/delayed.py +202 -0
  64. hamilton/function_modifiers/dependencies.py +246 -0
  65. hamilton/function_modifiers/expanders.py +1230 -0
  66. hamilton/function_modifiers/macros.py +1634 -0
  67. hamilton/function_modifiers/metadata.py +434 -0
  68. hamilton/function_modifiers/recursive.py +908 -0
  69. hamilton/function_modifiers/validation.py +289 -0
  70. hamilton/function_modifiers_base.py +31 -0
  71. hamilton/graph.py +1153 -0
  72. hamilton/graph_types.py +264 -0
  73. hamilton/graph_utils.py +41 -0
  74. hamilton/htypes.py +450 -0
  75. hamilton/io/__init__.py +32 -0
  76. hamilton/io/data_adapters.py +216 -0
  77. hamilton/io/default_data_loaders.py +224 -0
  78. hamilton/io/materialization.py +500 -0
  79. hamilton/io/utils.py +158 -0
  80. hamilton/lifecycle/__init__.py +67 -0
  81. hamilton/lifecycle/api.py +833 -0
  82. hamilton/lifecycle/base.py +1130 -0
  83. hamilton/lifecycle/default.py +802 -0
  84. hamilton/log_setup.py +47 -0
  85. hamilton/models.py +92 -0
  86. hamilton/node.py +449 -0
  87. hamilton/plugins/README.md +48 -0
  88. hamilton/plugins/__init__.py +16 -0
  89. hamilton/plugins/dask_extensions.py +47 -0
  90. hamilton/plugins/dlt_extensions.py +161 -0
  91. hamilton/plugins/geopandas_extensions.py +49 -0
  92. hamilton/plugins/h_dask.py +331 -0
  93. hamilton/plugins/h_ddog.py +522 -0
  94. hamilton/plugins/h_diskcache.py +163 -0
  95. hamilton/plugins/h_experiments/__init__.py +22 -0
  96. hamilton/plugins/h_experiments/__main__.py +62 -0
  97. hamilton/plugins/h_experiments/cache.py +39 -0
  98. hamilton/plugins/h_experiments/data_model.py +68 -0
  99. hamilton/plugins/h_experiments/hook.py +219 -0
  100. hamilton/plugins/h_experiments/server.py +375 -0
  101. hamilton/plugins/h_kedro.py +152 -0
  102. hamilton/plugins/h_logging.py +454 -0
  103. hamilton/plugins/h_mcp/__init__.py +28 -0
  104. hamilton/plugins/h_mcp/__main__.py +33 -0
  105. hamilton/plugins/h_mcp/_helpers.py +129 -0
  106. hamilton/plugins/h_mcp/_templates.py +417 -0
  107. hamilton/plugins/h_mcp/server.py +328 -0
  108. hamilton/plugins/h_mlflow.py +335 -0
  109. hamilton/plugins/h_narwhals.py +134 -0
  110. hamilton/plugins/h_openlineage.py +400 -0
  111. hamilton/plugins/h_opentelemetry.py +167 -0
  112. hamilton/plugins/h_pandas.py +257 -0
  113. hamilton/plugins/h_pandera.py +117 -0
  114. hamilton/plugins/h_polars.py +304 -0
  115. hamilton/plugins/h_polars_lazyframe.py +282 -0
  116. hamilton/plugins/h_pyarrow.py +56 -0
  117. hamilton/plugins/h_pydantic.py +127 -0
  118. hamilton/plugins/h_ray.py +242 -0
  119. hamilton/plugins/h_rich.py +142 -0
  120. hamilton/plugins/h_schema.py +493 -0
  121. hamilton/plugins/h_slack.py +100 -0
  122. hamilton/plugins/h_spark.py +1380 -0
  123. hamilton/plugins/h_threadpool.py +125 -0
  124. hamilton/plugins/h_tqdm.py +122 -0
  125. hamilton/plugins/h_vaex.py +129 -0
  126. hamilton/plugins/huggingface_extensions.py +236 -0
  127. hamilton/plugins/ibis_extensions.py +93 -0
  128. hamilton/plugins/jupyter_magic.py +622 -0
  129. hamilton/plugins/kedro_extensions.py +117 -0
  130. hamilton/plugins/lightgbm_extensions.py +99 -0
  131. hamilton/plugins/matplotlib_extensions.py +108 -0
  132. hamilton/plugins/mlflow_extensions.py +216 -0
  133. hamilton/plugins/numpy_extensions.py +105 -0
  134. hamilton/plugins/pandas_extensions.py +1763 -0
  135. hamilton/plugins/plotly_extensions.py +150 -0
  136. hamilton/plugins/polars_extensions.py +71 -0
  137. hamilton/plugins/polars_implementations.py +25 -0
  138. hamilton/plugins/polars_lazyframe_extensions.py +302 -0
  139. hamilton/plugins/polars_post_1_0_0_extensions.py +877 -0
  140. hamilton/plugins/polars_pre_1_0_0_extension.py +836 -0
  141. hamilton/plugins/pydantic_extensions.py +98 -0
  142. hamilton/plugins/pyspark_pandas_extensions.py +47 -0
  143. hamilton/plugins/sklearn_plot_extensions.py +129 -0
  144. hamilton/plugins/spark_extensions.py +105 -0
  145. hamilton/plugins/vaex_extensions.py +51 -0
  146. hamilton/plugins/xgboost_extensions.py +91 -0
  147. hamilton/plugins/yaml_extensions.py +89 -0
  148. hamilton/registry.py +254 -0
  149. hamilton/settings.py +18 -0
  150. hamilton/telemetry.py +50 -0
  151. hamilton/version.py +18 -0
@@ -0,0 +1,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)