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,22 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
|
|
18
|
+
from hamilton.plugins.h_experiments.hook import ExperimentTracker
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"ExperimentTracker",
|
|
22
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
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 os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from hamilton.plugins.h_experiments.cache import JsonCache
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main():
|
|
26
|
+
try:
|
|
27
|
+
import fastapi # noqa: F401
|
|
28
|
+
import fastui # noqa: F401
|
|
29
|
+
import uvicorn
|
|
30
|
+
except ModuleNotFoundError as e:
|
|
31
|
+
raise ModuleNotFoundError(
|
|
32
|
+
"Some dependencies are missing. Make sure to `pip install sf-hamilton[experiments]`"
|
|
33
|
+
) from e
|
|
34
|
+
parser = argparse.ArgumentParser(prog="hamilton-experiments")
|
|
35
|
+
parser.description = "Hamilton Experiment Server launcher"
|
|
36
|
+
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"path",
|
|
39
|
+
metavar="path",
|
|
40
|
+
type=str,
|
|
41
|
+
default="./experiments",
|
|
42
|
+
nargs="?",
|
|
43
|
+
help="Set HAMILTON_EXPERIMENTS_PATH environment variable",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument("--host", default="127.0.0.1", type=str, help="Bind to this address")
|
|
46
|
+
parser.add_argument("--port", default=8123, type=int, help="Bind to this port")
|
|
47
|
+
|
|
48
|
+
args = parser.parse_args()
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
JsonCache(args.path)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
raise ValueError(f"Server failed to launch. No metadata cache found at {args.path}") from e
|
|
54
|
+
|
|
55
|
+
# set environment variable that FastAPI will use
|
|
56
|
+
os.environ["HAMILTON_EXPERIMENTS_PATH"] = str(Path(args.path).resolve())
|
|
57
|
+
|
|
58
|
+
uvicorn.run("hamilton.plugins.h_experiments.server:app", host=args.host, port=args.port)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
main()
|
|
@@ -0,0 +1,39 @@
|
|
|
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 dbm
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class JsonCache:
|
|
22
|
+
def __init__(self, cache_path: str):
|
|
23
|
+
self.cache_path = f"{cache_path}/json_cache"
|
|
24
|
+
|
|
25
|
+
def keys(self) -> list:
|
|
26
|
+
with dbm.open(self.cache_path, "r") as db:
|
|
27
|
+
return list(db.keys())
|
|
28
|
+
|
|
29
|
+
def write(self, data: str, id_: str) -> None:
|
|
30
|
+
with dbm.open(self.cache_path, "c") as db:
|
|
31
|
+
db[id_] = data
|
|
32
|
+
|
|
33
|
+
def read(self, id_: str) -> str:
|
|
34
|
+
with dbm.open(self.cache_path, "r") as db:
|
|
35
|
+
return db[id_].decode()
|
|
36
|
+
|
|
37
|
+
def delete(self, id_: str) -> None:
|
|
38
|
+
with dbm.open(self.cache_path, "r") as db:
|
|
39
|
+
del db[id_]
|
|
@@ -0,0 +1,68 @@
|
|
|
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 datetime
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, create_model, model_validator
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def model_from_values(name: str, specs: dict[str, Any]) -> dict[str, tuple[type, ...]]:
|
|
25
|
+
return create_model(name, **{str(k): (type(v), ...) for k, v in specs.items()})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NodeMaterializer(BaseModel):
|
|
29
|
+
source_nodes: list[str]
|
|
30
|
+
path: str
|
|
31
|
+
sink: str
|
|
32
|
+
data_saver: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def denormalize_node_list(nodes: list) -> dict:
|
|
36
|
+
denorm = dict()
|
|
37
|
+
for node in nodes:
|
|
38
|
+
name = node["name"]
|
|
39
|
+
del node["name"]
|
|
40
|
+
denorm[name] = node
|
|
41
|
+
return denorm
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RunMetadata(BaseModel):
|
|
45
|
+
experiment: str
|
|
46
|
+
run_id: str
|
|
47
|
+
run_dir: str
|
|
48
|
+
success: bool
|
|
49
|
+
date_completed: datetime.datetime
|
|
50
|
+
graph_hash: str
|
|
51
|
+
modules: list[str]
|
|
52
|
+
config: dict
|
|
53
|
+
inputs: dict
|
|
54
|
+
overrides: dict
|
|
55
|
+
materialized: list[NodeMaterializer]
|
|
56
|
+
graph_version: int | None = None
|
|
57
|
+
|
|
58
|
+
@model_validator(mode="before")
|
|
59
|
+
def pre_root(cls, v: dict[str, Any]):
|
|
60
|
+
dt = datetime.datetime.fromisoformat(v["date_completed"])
|
|
61
|
+
res = datetime.timedelta(seconds=1)
|
|
62
|
+
nsecs = dt.hour * 3600 + dt.minute * 60 + dt.second + dt.microsecond * 1e-6
|
|
63
|
+
delta = nsecs % res.seconds
|
|
64
|
+
v["date_completed"] = dt - datetime.timedelta(seconds=delta)
|
|
65
|
+
|
|
66
|
+
for field in ["inputs", "overrides"]:
|
|
67
|
+
v[field] = denormalize_node_list(v[field])
|
|
68
|
+
return v
|
|
@@ -0,0 +1,219 @@
|
|
|
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 datetime
|
|
19
|
+
import hashlib
|
|
20
|
+
import inspect
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
import string
|
|
25
|
+
import uuid
|
|
26
|
+
from dataclasses import asdict, dataclass
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from hamilton import graph_types, lifecycle
|
|
31
|
+
from hamilton.plugins.h_experiments.cache import JsonCache
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def validate_string_input(user_input):
|
|
37
|
+
"""Validate the experiment name will make a valid directory name"""
|
|
38
|
+
allowed = set(string.ascii_letters + string.digits + "_" + "-")
|
|
39
|
+
for char in user_input:
|
|
40
|
+
if char not in allowed:
|
|
41
|
+
raise ValueError(f"`{char}` from `{user_input}` is an invalid character.")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_default_input(node) -> Any:
|
|
45
|
+
"""Get default input node value from originating function signature"""
|
|
46
|
+
param_name = node.name
|
|
47
|
+
origin_function = node.originating_functions[0]
|
|
48
|
+
param = inspect.signature(origin_function).parameters[param_name]
|
|
49
|
+
return None if param.default is inspect._empty else param.default
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def json_encoder(obj: Any):
|
|
53
|
+
"""convert non JSON-serializable objects to a serializable format
|
|
54
|
+
|
|
55
|
+
set[T] -> list[T]
|
|
56
|
+
else -> dict[type: str, byte_hash: str]
|
|
57
|
+
"""
|
|
58
|
+
if isinstance(obj, set):
|
|
59
|
+
serialized = list(obj)
|
|
60
|
+
else:
|
|
61
|
+
obj_hash = hashlib.sha256()
|
|
62
|
+
obj_hash.update(obj)
|
|
63
|
+
serialized = dict(
|
|
64
|
+
dtype=type(obj).__name__,
|
|
65
|
+
obj_hash=obj_hash.hexdigest(),
|
|
66
|
+
)
|
|
67
|
+
return serialized
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class NodeImplementation:
|
|
72
|
+
name: str
|
|
73
|
+
source_code: str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class NodeInput:
|
|
78
|
+
name: str
|
|
79
|
+
value: Any
|
|
80
|
+
default_value: Any | None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class NodeOverride:
|
|
85
|
+
name: str
|
|
86
|
+
value: Any
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class NodeMaterializer:
|
|
91
|
+
source_nodes: list[str]
|
|
92
|
+
path: str
|
|
93
|
+
sink: str
|
|
94
|
+
data_saver: str
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class RunMetadata:
|
|
99
|
+
"""Metadata about an Hamilton to store in cache"""
|
|
100
|
+
|
|
101
|
+
experiment: str
|
|
102
|
+
run_id: str
|
|
103
|
+
run_dir: str
|
|
104
|
+
success: bool
|
|
105
|
+
date_completed: datetime.datetime
|
|
106
|
+
graph_hash: str
|
|
107
|
+
modules: list[str]
|
|
108
|
+
config: dict
|
|
109
|
+
inputs: list[NodeInput]
|
|
110
|
+
overrides: list[NodeOverride]
|
|
111
|
+
materialized: list[NodeMaterializer]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ExperimentTracker(
|
|
115
|
+
lifecycle.NodeExecutionHook,
|
|
116
|
+
lifecycle.GraphExecutionHook,
|
|
117
|
+
lifecycle.GraphConstructionHook,
|
|
118
|
+
):
|
|
119
|
+
def __init__(self, experiment_name: str, base_directory: str = "./experiments"):
|
|
120
|
+
validate_string_input(experiment_name)
|
|
121
|
+
|
|
122
|
+
self.experiment_name = experiment_name
|
|
123
|
+
self.cache = JsonCache(cache_path=base_directory)
|
|
124
|
+
self.run_id = str(uuid.uuid4())
|
|
125
|
+
|
|
126
|
+
self.init_directory = Path.cwd()
|
|
127
|
+
self.run_directory = (
|
|
128
|
+
Path(base_directory).resolve().joinpath(self.experiment_name, self.run_id)
|
|
129
|
+
)
|
|
130
|
+
self.run_directory.mkdir(exist_ok=True, parents=True)
|
|
131
|
+
|
|
132
|
+
self.graph_hash: str = ""
|
|
133
|
+
self.modules: set[str] = set()
|
|
134
|
+
self.config = dict()
|
|
135
|
+
self.inputs: list[NodeInput] = list()
|
|
136
|
+
self.overrides: list[NodeOverride] = list()
|
|
137
|
+
self.materializers: list[NodeMaterializer] = list()
|
|
138
|
+
|
|
139
|
+
def run_after_graph_construction(self, *, config: dict[str, Any], **kwargs):
|
|
140
|
+
"""Store the Driver config before creating the graph"""
|
|
141
|
+
self.config = config
|
|
142
|
+
|
|
143
|
+
def run_before_graph_execution(
|
|
144
|
+
self,
|
|
145
|
+
*,
|
|
146
|
+
graph: graph_types.HamiltonGraph,
|
|
147
|
+
inputs: dict[str, Any],
|
|
148
|
+
overrides: dict[str, Any],
|
|
149
|
+
**kwargs,
|
|
150
|
+
):
|
|
151
|
+
"""Store execution metadata: graph hash, inputs, overrides"""
|
|
152
|
+
self.graph_hash = graph.version
|
|
153
|
+
|
|
154
|
+
for node in graph.nodes:
|
|
155
|
+
if node.tags.get("module"):
|
|
156
|
+
self.modules.add(node.tags["module"])
|
|
157
|
+
|
|
158
|
+
# filter out config nodes
|
|
159
|
+
elif node.is_external_input and node.originating_functions:
|
|
160
|
+
self.inputs.append(
|
|
161
|
+
NodeInput(
|
|
162
|
+
name=node.name,
|
|
163
|
+
value=inputs.get(node.name),
|
|
164
|
+
default_value=_get_default_input(node),
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if overrides:
|
|
169
|
+
self.overrides = [NodeOverride(name=k, value=v) for k, v in overrides.items()]
|
|
170
|
+
|
|
171
|
+
def run_before_node_execution(self, *args, node_tags: dict, **kwargs):
|
|
172
|
+
"""Move to run directory before executing materializer"""
|
|
173
|
+
if node_tags.get("hamilton.data_saver") is True:
|
|
174
|
+
os.chdir(self.run_directory) # before materialization
|
|
175
|
+
|
|
176
|
+
def run_after_node_execution(
|
|
177
|
+
self, *, node_name: str, node_tags: dict, node_kwargs: dict, result: Any, **kwargs
|
|
178
|
+
):
|
|
179
|
+
"""Move back to init directory after executing materializer.
|
|
180
|
+
Then, save materialization metadata
|
|
181
|
+
"""
|
|
182
|
+
if node_tags.get("hamilton.data_saver") is True:
|
|
183
|
+
if "path" in result:
|
|
184
|
+
path = result["path"]
|
|
185
|
+
elif "file_metadata" in result:
|
|
186
|
+
path = result["file_metadata"]["path"]
|
|
187
|
+
else:
|
|
188
|
+
logger.warning(
|
|
189
|
+
f"Materialization result from node={node_name} has no recordable path: {result}. Materializer must have either "
|
|
190
|
+
f"'path' or 'file_metadata' keys."
|
|
191
|
+
)
|
|
192
|
+
self.materializers.append(
|
|
193
|
+
NodeMaterializer(
|
|
194
|
+
source_nodes=list(node_kwargs.keys()),
|
|
195
|
+
path=str(Path(path).resolve()),
|
|
196
|
+
sink=node_tags["hamilton.data_saver.sink"],
|
|
197
|
+
data_saver=node_tags["hamilton.data_saver.classname"],
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
os.chdir(self.init_directory) # after materialization
|
|
201
|
+
|
|
202
|
+
def run_after_graph_execution(self, *, success: bool, **kwargs):
|
|
203
|
+
"""Encode run metadata as JSON and store in cache"""
|
|
204
|
+
run_data = dict(
|
|
205
|
+
experiment=self.experiment_name,
|
|
206
|
+
run_id=self.run_id,
|
|
207
|
+
run_dir=str(self.run_directory),
|
|
208
|
+
date_completed=datetime.datetime.now().isoformat(),
|
|
209
|
+
success=success,
|
|
210
|
+
graph_hash=self.graph_hash,
|
|
211
|
+
modules=list(self.modules),
|
|
212
|
+
config=self.config,
|
|
213
|
+
inputs=[] if len(self.inputs) == 0 else [asdict(i) for i in self.inputs],
|
|
214
|
+
overrides=[] if len(self.overrides) == 0 else [asdict(o) for o in self.overrides],
|
|
215
|
+
materialized=[asdict(m) for m in self.materializers],
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
run_json_string = json.dumps(run_data, default=str, sort_keys=True)
|
|
219
|
+
self.cache.write(run_json_string, self.run_id)
|