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,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]}"
|
hamilton/cli/__init__.py
ADDED
|
@@ -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.
|
hamilton/cli/__main__.py
ADDED
|
@@ -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()
|