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,282 @@
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 pathlib
19
+ import sqlite3
20
+ import threading
21
+
22
+ from hamilton.caching.cache_key import decode_key
23
+ from hamilton.caching.stores.base import MetadataStore
24
+
25
+
26
+ class SQLiteMetadataStore(MetadataStore):
27
+ def __init__(
28
+ self,
29
+ path: str,
30
+ connection_kwargs: dict | None = None,
31
+ ) -> None:
32
+ self._directory = pathlib.Path(path).resolve()
33
+ self._directory.mkdir(parents=True, exist_ok=True)
34
+ self._path = self._directory.joinpath("metadata_store").with_suffix(".db")
35
+ self.connection_kwargs: dict = connection_kwargs or {}
36
+
37
+ self._thread_local = threading.local()
38
+
39
+ # creating tables at `__init__` prevents other methods from encountering
40
+ # `sqlite3.OperationalError` because tables are missing.
41
+ self._create_tables_if_not_exists()
42
+
43
+ def __getstate__(self) -> dict:
44
+ """Serialized `__init__` arguments required to initialize the
45
+ MetadataStore in a new thread or process.
46
+ """
47
+ state = {}
48
+ # NOTE kwarg `path` is not equivalent to `self._path`
49
+ state["path"] = self._directory
50
+ state["connection_kwargs"] = self.connection_kwargs
51
+ return state
52
+
53
+ def _get_connection(self) -> sqlite3.Connection:
54
+ if not hasattr(self._thread_local, "connection"):
55
+ self._thread_local.connection = sqlite3.connect(
56
+ str(self._path), check_same_thread=False, **self.connection_kwargs
57
+ )
58
+ return self._thread_local.connection
59
+
60
+ def _close_connection(self) -> None:
61
+ if hasattr(self._thread_local, "connection"):
62
+ self._thread_local.connection.close()
63
+ del self._thread_local.connection
64
+
65
+ @property
66
+ def connection(self) -> sqlite3.Connection:
67
+ """Connection to the SQLite database."""
68
+ return self._get_connection()
69
+
70
+ def __del__(self):
71
+ """Close the SQLite connection when the object is deleted"""
72
+ self._close_connection()
73
+
74
+ def _create_tables_if_not_exists(self) -> None:
75
+ """Create the tables necessary for the cache:
76
+
77
+ run_ids: queue of run_ids, ordered by start time.
78
+ history: queue of executed node; allows to query "latest" execution of a node
79
+ cache_metadata: information to determine if a node needs to be computed or not
80
+
81
+ In the table ``cache_metadata``, the ``cache_key`` is unique whereas
82
+ ``history`` allows duplicate.
83
+ """
84
+ cur = self.connection.cursor()
85
+
86
+ cur.execute(
87
+ """\
88
+ CREATE TABLE IF NOT EXISTS run_ids (
89
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
90
+ run_id TEXT,
91
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
92
+ )
93
+ """
94
+ )
95
+ cur.execute(
96
+ """\
97
+ CREATE TABLE IF NOT EXISTS history (
98
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
99
+ cache_key TEXT,
100
+ run_id TEXT,
101
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
102
+
103
+ FOREIGN KEY (cache_key) REFERENCES cache_metadata(cache_key)
104
+ );
105
+ """
106
+ )
107
+ cur.execute(
108
+ """\
109
+ CREATE TABLE IF NOT EXISTS cache_metadata (
110
+ cache_key TEXT PRIMARY KEY,
111
+ data_version TEXT NOT NULL,
112
+ node_name TEXT NOT NULL,
113
+ code_version TEXT NOT NULL,
114
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
115
+
116
+ FOREIGN KEY (cache_key) REFERENCES history(cache_key)
117
+ );
118
+ """
119
+ )
120
+ self.connection.commit()
121
+
122
+ def initialize(self, run_id) -> None:
123
+ """Call initialize when starting a run. This will create database tables
124
+ if necessary.
125
+ """
126
+ cur = self.connection.cursor()
127
+ cur.execute("INSERT INTO run_ids (run_id) VALUES (?)", (run_id,))
128
+ self.connection.commit()
129
+
130
+ def __len__(self) -> int:
131
+ """Number of entries in cache_metadata"""
132
+ cur = self.connection.cursor()
133
+ cur.execute("SELECT COUNT(*) FROM cache_metadata")
134
+ return cur.fetchone()[0]
135
+
136
+ def set(
137
+ self,
138
+ *,
139
+ cache_key: str,
140
+ data_version: str,
141
+ run_id: str,
142
+ node_name: str = None,
143
+ code_version: str = None,
144
+ **kwargs,
145
+ ) -> None:
146
+ cur = self.connection.cursor()
147
+
148
+ # if the caller of ``.set()`` directly provides the ``node_name`` and ``code_version``,
149
+ # we can skip the decoding step.
150
+ if (node_name is None) or (code_version is None):
151
+ try:
152
+ decoded_key = decode_key(cache_key)
153
+ except BaseException as e:
154
+ raise ValueError(
155
+ f"Failed decoding the cache_key: {cache_key}.\n",
156
+ "The `cache_key` must be created by `hamilton.caching.cache_key.create_cache_key()` ",
157
+ "if `node_name` and `code_version` are not provided.",
158
+ ) from e
159
+
160
+ node_name = decoded_key["node_name"]
161
+ code_version = decoded_key["code_version"]
162
+
163
+ cur.execute("INSERT INTO history (cache_key, run_id) VALUES (?, ?)", (cache_key, run_id))
164
+ cur.execute(
165
+ """\
166
+ INSERT OR IGNORE INTO cache_metadata (
167
+ cache_key, node_name, code_version, data_version
168
+ ) VALUES (?, ?, ?, ?)
169
+ """,
170
+ (cache_key, node_name, code_version, data_version),
171
+ )
172
+
173
+ self.connection.commit()
174
+
175
+ def get(self, cache_key: str) -> str | None:
176
+ cur = self.connection.cursor()
177
+ cur.execute(
178
+ """\
179
+ SELECT data_version
180
+ FROM cache_metadata
181
+ WHERE cache_key = ?
182
+ """,
183
+ (cache_key,),
184
+ )
185
+ result = cur.fetchone()
186
+
187
+ if result is None:
188
+ data_version = None
189
+ else:
190
+ data_version = result[0]
191
+
192
+ return data_version
193
+
194
+ def delete(self, cache_key: str) -> None:
195
+ """Delete metadata associated with ``cache_key``."""
196
+ cur = self.connection.cursor()
197
+ cur.execute("DELETE FROM cache_metadata WHERE cache_key = ?", (cache_key,))
198
+ self.connection.commit()
199
+
200
+ def delete_all(self) -> None:
201
+ """Delete all existing tables from the database"""
202
+ cur = self.connection.cursor()
203
+
204
+ for table_name in ["run_ids", "history", "cache_metadata"]:
205
+ cur.execute(f"DROP TABLE IF EXISTS {table_name};")
206
+
207
+ self.connection.commit()
208
+
209
+ def exists(self, cache_key: str) -> bool:
210
+ """boolean check if a ``data_version`` is found for ``cache_key``
211
+ If True, ``.get()`` should successfully retrieve the ``data_version``.
212
+ """
213
+ cur = self.connection.cursor()
214
+ cur.execute("SELECT cache_key FROM cache_metadata WHERE cache_key = ?", (cache_key,))
215
+ result = cur.fetchone()
216
+
217
+ return result is not None
218
+
219
+ def get_run_ids(self) -> list[str]:
220
+ """Return a list of run ids, sorted from oldest to newest start time."""
221
+ cur = self.connection.cursor()
222
+ cur.execute("SELECT run_id FROM run_ids ORDER BY id")
223
+ result = cur.fetchall()
224
+
225
+ return [r[0] for r in result]
226
+
227
+ def _run_exists(self, run_id: str) -> bool:
228
+ """Returns True if a run was initialized with ``run_id``, even
229
+ if the run recorded no node executions.
230
+ """
231
+ cur = self.connection.cursor()
232
+ cur.execute(
233
+ """\
234
+ SELECT EXISTS(
235
+ SELECT 1
236
+ FROM run_ids
237
+ WHERE run_id = ?
238
+ )
239
+ """,
240
+ (run_id,),
241
+ )
242
+ result = cur.fetchone()
243
+ # SELECT EXISTS returns 1 for True, i.e., `run_id` is found
244
+ return result[0] == 1
245
+
246
+ def get_run(self, run_id: str) -> list[dict]:
247
+ """Return a list of node metadata associated with a run.
248
+
249
+ :param run_id: ID of the run to retrieve
250
+ :return: List of node metadata which includes ``cache_key``, ``data_version``,
251
+ ``node_name``, and ``code_version``. The list can be empty if a run was initialized
252
+ but no nodes were executed.
253
+
254
+ :raises IndexError: if the ``run_id`` is not found in metadata store.
255
+ """
256
+ cur = self.connection.cursor()
257
+ if self._run_exists(run_id) is False:
258
+ raise IndexError(f"`run_id` not found in table `run_ids`: {run_id}")
259
+
260
+ cur.execute(
261
+ """\
262
+ SELECT
263
+ cache_metadata.cache_key,
264
+ cache_metadata.data_version,
265
+ cache_metadata.node_name,
266
+ cache_metadata.code_version
267
+ FROM history
268
+ JOIN cache_metadata ON history.cache_key = cache_metadata.cache_key
269
+ WHERE history.run_id = ?
270
+ """,
271
+ (run_id,),
272
+ )
273
+ results = cur.fetchall()
274
+ return [
275
+ dict(
276
+ cache_key=cache_key,
277
+ data_version=data_version,
278
+ node_name=node_name,
279
+ code_version=code_version,
280
+ )
281
+ for cache_key, data_version, node_name, code_version in results
282
+ ]
@@ -0,0 +1,40 @@
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 pathlib
19
+
20
+
21
+ def get_directory_size(directory: str) -> float:
22
+ """Get the size of the content of a directory in bytes."""
23
+ total_size = 0
24
+ for p in pathlib.Path(directory).rglob("*"):
25
+ if p.is_file():
26
+ total_size += p.stat().st_size
27
+
28
+ return total_size
29
+
30
+
31
+ def readable_bytes_size(n_bytes: float) -> str:
32
+ """Convert a number of bytes to a human-readable unit."""
33
+ labels = ["B", "KB", "MB", "GB", "TB"]
34
+ exponent = 0
35
+
36
+ while n_bytes > 1024.0:
37
+ n_bytes /= 1024.0
38
+ exponent += 1
39
+
40
+ return f"{n_bytes:.2f} {labels[exponent]}"
@@ -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.
@@ -0,0 +1,328 @@
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 dataclasses
19
+ import json
20
+ import logging
21
+ import os
22
+ import warnings
23
+ from collections.abc import Callable
24
+ from pathlib import Path
25
+ from pprint import pprint
26
+ from typing import Annotated, Any
27
+
28
+ import typer
29
+
30
+ # silence UserWarning: 'PYARROW_IGNORE_TIMEZONE'
31
+ with warnings.catch_warnings():
32
+ warnings.filterwarnings("ignore", category=UserWarning)
33
+ from hamilton import driver
34
+
35
+ from hamilton.cli import commands
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ @dataclasses.dataclass
41
+ class Response:
42
+ command: str
43
+ success: bool
44
+ message: Any
45
+
46
+
47
+ class CliState:
48
+ verbose: bool | None = None
49
+ json_out: bool | None = None
50
+ dr: driver.Driver | None = None
51
+ name: str | None = None
52
+
53
+
54
+ cli = typer.Typer(rich_markup_mode="rich")
55
+ state = CliState()
56
+
57
+ MODULES_ANNOTATIONS = Annotated[
58
+ list[Path],
59
+ typer.Argument(
60
+ help="Paths to Hamilton modules",
61
+ exists=True,
62
+ dir_okay=False,
63
+ readable=True,
64
+ resolve_path=True,
65
+ ),
66
+ ]
67
+
68
+ NAME_ANNOTATIONS = Annotated[
69
+ str | None,
70
+ typer.Option("--name", "-n", help="Name of the dataflow. Default: Derived from MODULES."),
71
+ ]
72
+
73
+ CONTEXT_ANNOTATIONS = Annotated[
74
+ Path | None,
75
+ typer.Option(
76
+ "--context",
77
+ "-ctx",
78
+ help="Path to Driver context file [.json, .py]",
79
+ exists=True,
80
+ dir_okay=False,
81
+ readable=True,
82
+ resolve_path=True,
83
+ ),
84
+ ]
85
+
86
+ VIZ_OUTPUT_ANNOTATIONS = Annotated[
87
+ Path,
88
+ typer.Option(
89
+ "--output",
90
+ "-o",
91
+ help="Output path of visualization. If path is a directory, use NAME for file name.",
92
+ dir_okay=True,
93
+ writable=True,
94
+ resolve_path=True,
95
+ ),
96
+ ]
97
+
98
+
99
+ # TODO add `experiments` for `hamilton.plugins.h_experiments`
100
+ # TODO add `dataflows` submenu to manage locally installed dataflows
101
+ # TODO add `init` to load project template
102
+ # callback() creates entrypoint for `hamilton` without command
103
+ @cli.callback()
104
+ def main(
105
+ ctx: typer.Context,
106
+ verbose: Annotated[
107
+ bool,
108
+ typer.Option(
109
+ help="Output all intermediary commands",
110
+ rich_help_panel="Output format",
111
+ ),
112
+ ] = False,
113
+ json_out: Annotated[
114
+ bool,
115
+ typer.Option(
116
+ help="Output JSON for programmatic use (e.g., CI)",
117
+ rich_help_panel="Output format",
118
+ ),
119
+ ] = False,
120
+ ):
121
+ """Hamilton CLI"""
122
+ state.verbose = verbose
123
+ state.json_out = json_out
124
+ logger.debug(f"verbose set to {verbose}")
125
+ logger.debug(f"json_out set to {json_out}")
126
+
127
+
128
+ def _try_command(cmd: Callable, **cmd_kwargs) -> Any:
129
+ """Try a command and raise errors to Typer and exit CLI"""
130
+ cmd_name = cmd.__name__
131
+ try:
132
+ logger.debug(f"calling commands.{cmd_name}")
133
+ result = cmd(**cmd_kwargs)
134
+ except Exception as e:
135
+ response = Response(
136
+ command=cmd_name, success=False, message={"error": str(type(e)), "details": str(e)}
137
+ )
138
+ logger.error(dataclasses.asdict(response))
139
+ raise typer.Exit(code=1) from e
140
+
141
+ return result
142
+
143
+
144
+ def _response_handler(ctx: typer.Context, response: Response) -> None:
145
+ """Handle how to display response"""
146
+ if (ctx.info_name == response.command) or state.verbose:
147
+ if state.json_out is True:
148
+ print(json.dumps(dataclasses.asdict(response)))
149
+ else:
150
+ pprint(response.message)
151
+
152
+
153
+ @cli.command()
154
+ def build(
155
+ ctx: typer.Context,
156
+ modules: MODULES_ANNOTATIONS,
157
+ name: NAME_ANNOTATIONS = None,
158
+ context_path: CONTEXT_ANNOTATIONS = None,
159
+ ):
160
+ """Build a single Driver with MODULES"""
161
+ state.dr = _try_command(cmd=commands.build, modules=modules, context_path=context_path)
162
+
163
+ if name:
164
+ state.name = name
165
+ else:
166
+ state.name = "_".join([str(Path(m).stem) for m in modules])[:40]
167
+
168
+ _response_handler(
169
+ ctx=ctx,
170
+ response=Response(
171
+ command="build",
172
+ success=True,
173
+ message={"modules": [p.stem for p in modules]},
174
+ ),
175
+ )
176
+
177
+
178
+ @cli.command()
179
+ def diff(
180
+ ctx: typer.Context,
181
+ modules: MODULES_ANNOTATIONS,
182
+ name: NAME_ANNOTATIONS = None,
183
+ context_path: CONTEXT_ANNOTATIONS = None,
184
+ output_file_path: VIZ_OUTPUT_ANNOTATIONS = Path("./"),
185
+ git_reference: Annotated[
186
+ str,
187
+ typer.Option(
188
+ help="[link=https://git-scm.com/book/en/v2/Git-Internals-Git-References]git reference[/link] to compare to"
189
+ ),
190
+ ] = "HEAD",
191
+ view: Annotated[
192
+ bool,
193
+ typer.Option(
194
+ "--view",
195
+ "-v",
196
+ help="Generate a dataflow diff visualization",
197
+ ),
198
+ ] = False,
199
+ ):
200
+ """Diff between the current MODULES and their specified GIT_REFERENCE"""
201
+ if state.dr is None:
202
+ ctx.invoke(version, ctx=ctx, modules=modules, name=name, context_path=context_path)
203
+
204
+ # default value isn't set to None to let Typer properly resolve the path
205
+ # then, we change the file name
206
+ if output_file_path.is_dir():
207
+ output_file_path.mkdir(parents=True, exist_ok=True)
208
+ output_file_path = output_file_path.joinpath(f"diff_{state.name}.png")
209
+
210
+ diff = _try_command(
211
+ cmd=commands.diff,
212
+ current_dr=state.dr,
213
+ modules=modules,
214
+ git_reference=git_reference,
215
+ view=view,
216
+ output_file_path=output_file_path,
217
+ context_path=context_path,
218
+ )
219
+ _response_handler(
220
+ ctx=ctx,
221
+ response=Response(
222
+ command="diff",
223
+ success=True,
224
+ message=diff,
225
+ ),
226
+ )
227
+
228
+
229
+ @cli.command()
230
+ def validate(
231
+ ctx: typer.Context,
232
+ modules: MODULES_ANNOTATIONS,
233
+ context_path: CONTEXT_ANNOTATIONS,
234
+ name: NAME_ANNOTATIONS = None,
235
+ ):
236
+ """Validate DATAFLOW execution for the given CONTEXT"""
237
+ if state.dr is None:
238
+ ctx.invoke(build, ctx=ctx, modules=modules, name=name, context_path=context_path)
239
+
240
+ validated_context = _try_command(commands.validate, dr=state.dr, context_path=context_path)
241
+ _response_handler(
242
+ ctx=ctx,
243
+ response=Response(
244
+ command="validate",
245
+ success=True,
246
+ message=validated_context,
247
+ ),
248
+ )
249
+
250
+
251
+ @cli.command()
252
+ def version(
253
+ ctx: typer.Context,
254
+ modules: MODULES_ANNOTATIONS,
255
+ name: NAME_ANNOTATIONS = None,
256
+ context_path: CONTEXT_ANNOTATIONS = None,
257
+ ):
258
+ """Version NODES and DATAFLOW from dataflow with MODULES"""
259
+ if state.dr is None:
260
+ ctx.invoke(build, ctx=ctx, modules=modules, name=name, context_path=context_path)
261
+
262
+ dataflow_version = _try_command(cmd=commands.version, dr=state.dr)
263
+ _response_handler(
264
+ ctx=ctx,
265
+ response=Response(
266
+ command="version",
267
+ success=True,
268
+ message=dataflow_version,
269
+ ),
270
+ )
271
+
272
+
273
+ @cli.command()
274
+ def view(
275
+ ctx: typer.Context,
276
+ modules: MODULES_ANNOTATIONS,
277
+ name: NAME_ANNOTATIONS = None,
278
+ context_path: CONTEXT_ANNOTATIONS = None,
279
+ output_file_path: VIZ_OUTPUT_ANNOTATIONS = Path("./"),
280
+ ):
281
+ """Build and visualize dataflow with MODULES"""
282
+ if state.dr is None:
283
+ ctx.invoke(build, ctx=ctx, modules=modules, name=name, context_path=context_path)
284
+
285
+ if output_file_path.is_dir():
286
+ output_file_path.mkdir(parents=True, exist_ok=True)
287
+ output_file_path = output_file_path.joinpath(f"dag_{state.name}.png")
288
+
289
+ _try_command(cmd=commands.view, dr=state.dr, output_file_path=output_file_path)
290
+ _response_handler(
291
+ ctx=ctx,
292
+ response=Response(command="view", success=True, message={"path": str(output_file_path)}),
293
+ )
294
+
295
+
296
+ @cli.command()
297
+ def ui(
298
+ ctx: typer.Context,
299
+ port: int = 8241,
300
+ base_dir: str = os.path.join(Path.home(), ".hamilton", "db"),
301
+ no_migration: bool = False,
302
+ no_open: bool = False,
303
+ settings_file: str = "mini",
304
+ config_file: str | None = None,
305
+ ):
306
+ """Runs the Hamilton UI on sqllite in port 8241"""
307
+ try:
308
+ from hamilton_ui import commands
309
+ except ImportError as e:
310
+ logger.error(
311
+ "hamilton[ui] not installed -- you have to install this to run the UI. "
312
+ 'Run `pip install "sf-hamilton[ui]"` to install and get started with the UI!'
313
+ )
314
+ raise typer.Exit(code=1) from e
315
+
316
+ ctx.invoke(
317
+ commands.run,
318
+ port=port,
319
+ base_dir=base_dir,
320
+ no_migration=no_migration,
321
+ no_open=no_open,
322
+ settings_file=settings_file,
323
+ config_file=config_file,
324
+ )
325
+
326
+
327
+ if __name__ == "__main__":
328
+ cli()