hafnia 0.1.25__py3-none-any.whl → 0.1.27__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.
- cli/__main__.py +4 -9
- cli/config.py +20 -27
- cli/consts.py +3 -1
- cli/experiment_cmds.py +3 -17
- cli/profile_cmds.py +16 -2
- cli/recipe_cmds.py +2 -6
- cli/runc_cmds.py +50 -49
- hafnia/data/factory.py +3 -3
- hafnia/experiment/hafnia_logger.py +5 -5
- hafnia/http.py +2 -2
- hafnia/log.py +15 -24
- hafnia/platform/__init__.py +0 -2
- hafnia/platform/builder.py +110 -144
- hafnia/platform/download.py +8 -8
- hafnia/platform/experiment.py +31 -25
- hafnia/utils.py +67 -65
- {hafnia-0.1.25.dist-info → hafnia-0.1.27.dist-info}/METADATA +157 -12
- hafnia-0.1.27.dist-info/RECORD +27 -0
- hafnia/platform/api.py +0 -12
- hafnia/platform/executor.py +0 -111
- hafnia-0.1.25.dist-info/RECORD +0 -29
- {hafnia-0.1.25.dist-info → hafnia-0.1.27.dist-info}/WHEEL +0 -0
- {hafnia-0.1.25.dist-info → hafnia-0.1.27.dist-info}/entry_points.txt +0 -0
- {hafnia-0.1.25.dist-info → hafnia-0.1.27.dist-info}/licenses/LICENSE +0 -0
cli/__main__.py
CHANGED
|
@@ -10,6 +10,7 @@ from cli.config import Config, ConfigSchema
|
|
|
10
10
|
def main(ctx: click.Context) -> None:
|
|
11
11
|
"""Hafnia CLI."""
|
|
12
12
|
ctx.obj = Config()
|
|
13
|
+
ctx.max_content_width = 120
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
@main.command("configure")
|
|
@@ -17,9 +18,7 @@ def main(ctx: click.Context) -> None:
|
|
|
17
18
|
def configure(cfg: Config) -> None:
|
|
18
19
|
"""Configure Hafnia CLI settings."""
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
profile_name = click.prompt("Profile Name", type=str, default="default")
|
|
21
|
+
profile_name = click.prompt("Profile Name", type=str, default=consts.DEFAULT_PROFILE_NAME)
|
|
23
22
|
profile_name = profile_name.strip()
|
|
24
23
|
try:
|
|
25
24
|
cfg.add_profile(profile_name, ConfigSchema(), set_active=True)
|
|
@@ -32,12 +31,8 @@ def configure(cfg: Config) -> None:
|
|
|
32
31
|
except ValueError as e:
|
|
33
32
|
click.echo(f"Error: {str(e)}", err=True)
|
|
34
33
|
return
|
|
35
|
-
platform_url = click.prompt("Hafnia Platform URL", type=str, default=
|
|
34
|
+
platform_url = click.prompt("Hafnia Platform URL", type=str, default=consts.DEFAULT_API_URL)
|
|
36
35
|
cfg.platform_url = platform_url.strip()
|
|
37
|
-
try:
|
|
38
|
-
cfg.organization_id = get_organization_id(cfg.get_platform_endpoint("organizations"), cfg.api_key)
|
|
39
|
-
except Exception:
|
|
40
|
-
raise click.ClickException(consts.ERROR_ORG_ID)
|
|
41
36
|
cfg.save_config()
|
|
42
37
|
profile_cmds.profile_show(cfg)
|
|
43
38
|
|
|
@@ -57,4 +52,4 @@ main.add_command(experiment_cmds.experiment)
|
|
|
57
52
|
main.add_command(recipe_cmds.recipe)
|
|
58
53
|
|
|
59
54
|
if __name__ == "__main__":
|
|
60
|
-
main()
|
|
55
|
+
main(max_content_width=120)
|
cli/config.py
CHANGED
|
@@ -6,14 +6,21 @@ from typing import Dict, List, Optional
|
|
|
6
6
|
from pydantic import BaseModel, field_validator
|
|
7
7
|
|
|
8
8
|
import cli.consts as consts
|
|
9
|
-
from hafnia.log import
|
|
9
|
+
from hafnia.log import user_logger
|
|
10
|
+
|
|
11
|
+
PLATFORM_API_MAPPING = {
|
|
12
|
+
"recipes": "/api/v1/recipes",
|
|
13
|
+
"experiments": "/api/v1/experiments",
|
|
14
|
+
"experiment_environments": "/api/v1/experiment-environments",
|
|
15
|
+
"experiment_runs": "/api/v1/experiment-runs",
|
|
16
|
+
"runs": "/api/v1/experiments-runs",
|
|
17
|
+
"datasets": "/api/v1/datasets",
|
|
18
|
+
}
|
|
10
19
|
|
|
11
20
|
|
|
12
21
|
class ConfigSchema(BaseModel):
|
|
13
|
-
organization_id: str = ""
|
|
14
22
|
platform_url: str = ""
|
|
15
23
|
api_key: Optional[str] = None
|
|
16
|
-
api_mapping: Optional[Dict[str, str]] = None
|
|
17
24
|
|
|
18
25
|
@field_validator("api_key")
|
|
19
26
|
def validate_api_key(cls, value: str) -> str:
|
|
@@ -61,14 +68,6 @@ class Config:
|
|
|
61
68
|
def api_key(self, value: str) -> None:
|
|
62
69
|
self.config.api_key = value
|
|
63
70
|
|
|
64
|
-
@property
|
|
65
|
-
def organization_id(self) -> str:
|
|
66
|
-
return self.config.organization_id
|
|
67
|
-
|
|
68
|
-
@organization_id.setter
|
|
69
|
-
def organization_id(self, value: str) -> None:
|
|
70
|
-
self.config.organization_id = value
|
|
71
|
-
|
|
72
71
|
@property
|
|
73
72
|
def platform_url(self) -> str:
|
|
74
73
|
return self.config.platform_url
|
|
@@ -77,7 +76,6 @@ class Config:
|
|
|
77
76
|
def platform_url(self, value: str) -> None:
|
|
78
77
|
base_url = value.rstrip("/")
|
|
79
78
|
self.config.platform_url = base_url
|
|
80
|
-
self.config.api_mapping = self.get_api_mapping(base_url)
|
|
81
79
|
|
|
82
80
|
def __init__(self, config_path: Optional[Path] = None) -> None:
|
|
83
81
|
self.config_path = self.resolve_config_path(config_path)
|
|
@@ -96,27 +94,22 @@ class Config:
|
|
|
96
94
|
|
|
97
95
|
def add_profile(self, profile_name: str, profile: ConfigSchema, set_active: bool = False) -> None:
|
|
98
96
|
profile_name = profile_name.strip()
|
|
97
|
+
if profile_name in self.config_data.profiles:
|
|
98
|
+
user_logger.warning(
|
|
99
|
+
f"Profile with name '{profile_name}' already exists, it will be overwritten by the new one."
|
|
100
|
+
)
|
|
101
|
+
|
|
99
102
|
self.config_data.profiles[profile_name] = profile
|
|
100
103
|
if set_active:
|
|
101
104
|
self.config_data.active_profile = profile_name
|
|
102
105
|
self.save_config()
|
|
103
106
|
|
|
104
|
-
def get_api_mapping(self, base_url: str) -> Dict:
|
|
105
|
-
return {
|
|
106
|
-
"organizations": f"{base_url}/api/v1/organizations",
|
|
107
|
-
"recipes": f"{base_url}/api/v1/recipes",
|
|
108
|
-
"experiments": f"{base_url}/api/v1/experiments",
|
|
109
|
-
"experiment_environments": f"{base_url}/api/v1/experiment-environments",
|
|
110
|
-
"experiment_runs": f"{base_url}/api/v1/experiment-runs",
|
|
111
|
-
"runs": f"{base_url}/api/v1/experiments-runs",
|
|
112
|
-
"datasets": f"{base_url}/api/v1/datasets",
|
|
113
|
-
}
|
|
114
|
-
|
|
115
107
|
def get_platform_endpoint(self, method: str) -> str:
|
|
116
108
|
"""Get specific API endpoint"""
|
|
117
|
-
if
|
|
118
|
-
raise ValueError(f"{method} is not supported.")
|
|
119
|
-
|
|
109
|
+
if method not in PLATFORM_API_MAPPING:
|
|
110
|
+
raise ValueError(f"'{method}' is not supported.")
|
|
111
|
+
endpoint = self.config.platform_url + PLATFORM_API_MAPPING[method]
|
|
112
|
+
return endpoint
|
|
120
113
|
|
|
121
114
|
def load_config(self) -> ConfigFileSchema:
|
|
122
115
|
"""Load configuration from file."""
|
|
@@ -127,7 +120,7 @@ class Config:
|
|
|
127
120
|
data = json.load(f)
|
|
128
121
|
return ConfigFileSchema(**data)
|
|
129
122
|
except json.JSONDecodeError:
|
|
130
|
-
|
|
123
|
+
user_logger.error("Error decoding JSON file.")
|
|
131
124
|
raise ValueError("Failed to parse configuration file")
|
|
132
125
|
|
|
133
126
|
def save_config(self) -> None:
|
cli/consts.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
DEFAULT_API_URL = "https://api.mdi.milestonesys.com"
|
|
2
|
+
DEFAULT_PROFILE_NAME = "default"
|
|
3
|
+
|
|
1
4
|
ERROR_CONFIGURE: str = "Please configure the CLI with `hafnia configure`"
|
|
2
5
|
ERROR_PROFILE_NOT_EXIST: str = "No active profile configured. Please configure the CLI with `hafnia configure`"
|
|
3
6
|
ERROR_PROFILE_REMOVE_ACTIVE: str = "Cannot remove active profile. Please switch to another profile first."
|
|
4
7
|
ERROR_API_KEY_NOT_SET: str = "API key not set. Please configure the CLI with `hafnia configure`."
|
|
5
|
-
ERROR_ORG_ID: str = "Failed to fetch organization ID. Verify platform URL and API key."
|
|
6
8
|
ERROR_CREATE_PROFILE: str = "Failed to create profile. Profile name must be unique and not empty."
|
|
7
9
|
|
|
8
10
|
ERROR_GET_RESOURCE: str = "Failed to get the data from platform. Verify url or api key."
|
cli/experiment_cmds.py
CHANGED
|
@@ -22,25 +22,18 @@ def experiment() -> None:
|
|
|
22
22
|
@click.pass_obj
|
|
23
23
|
def create(cfg: Config, name: str, source_dir: Path, exec_cmd: str, dataset_name: str, env_name: str) -> None:
|
|
24
24
|
"""Create a new experiment run"""
|
|
25
|
-
from hafnia.platform import
|
|
26
|
-
create_experiment,
|
|
27
|
-
create_recipe,
|
|
28
|
-
get_dataset_id,
|
|
29
|
-
get_exp_environment_id,
|
|
30
|
-
)
|
|
25
|
+
from hafnia.platform import create_experiment, create_recipe, get_dataset_id, get_exp_environment_id
|
|
31
26
|
|
|
32
27
|
if not source_dir.exists():
|
|
33
28
|
raise click.ClickException(consts.ERROR_EXPERIMENT_DIR)
|
|
34
29
|
|
|
35
30
|
try:
|
|
36
31
|
dataset_id = get_dataset_id(dataset_name, cfg.get_platform_endpoint("datasets"), cfg.api_key)
|
|
37
|
-
except (IndexError, KeyError):
|
|
38
|
-
raise click.ClickException(f"Dataset '{dataset_name}' not found.")
|
|
39
32
|
except Exception:
|
|
40
33
|
raise click.ClickException(f"Error retrieving dataset '{dataset_name}'.")
|
|
41
34
|
|
|
42
35
|
try:
|
|
43
|
-
recipe_id = create_recipe(source_dir, cfg.get_platform_endpoint("recipes"), cfg.api_key
|
|
36
|
+
recipe_id = create_recipe(source_dir, cfg.get_platform_endpoint("recipes"), cfg.api_key)
|
|
44
37
|
except Exception:
|
|
45
38
|
raise click.ClickException(f"Failed to create recipe from '{source_dir}'")
|
|
46
39
|
|
|
@@ -51,14 +44,7 @@ def create(cfg: Config, name: str, source_dir: Path, exec_cmd: str, dataset_name
|
|
|
51
44
|
|
|
52
45
|
try:
|
|
53
46
|
experiment_id = create_experiment(
|
|
54
|
-
name,
|
|
55
|
-
dataset_id,
|
|
56
|
-
recipe_id,
|
|
57
|
-
exec_cmd,
|
|
58
|
-
env_id,
|
|
59
|
-
cfg.get_platform_endpoint("experiments"),
|
|
60
|
-
cfg.api_key,
|
|
61
|
-
cfg.organization_id,
|
|
47
|
+
name, dataset_id, recipe_id, exec_cmd, env_id, cfg.get_platform_endpoint("experiments"), cfg.api_key
|
|
62
48
|
)
|
|
63
49
|
except Exception:
|
|
64
50
|
raise click.ClickException(f"Failed to create experiment '{name}'")
|
cli/profile_cmds.py
CHANGED
|
@@ -3,7 +3,7 @@ from rich.console import Console
|
|
|
3
3
|
from rich.table import Table
|
|
4
4
|
|
|
5
5
|
import cli.consts as consts
|
|
6
|
-
from cli.config import Config
|
|
6
|
+
from cli.config import Config, ConfigSchema
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
@click.group()
|
|
@@ -43,6 +43,21 @@ def profile_use(cfg: Config, profile_name: str) -> None:
|
|
|
43
43
|
click.echo(f"{consts.PROFILE_SWITCHED_SUCCESS} {profile_name}")
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
@profile.command("create")
|
|
47
|
+
@click.argument("api-key", required=True)
|
|
48
|
+
@click.option("--name", help="Specify profile name", default=consts.DEFAULT_PROFILE_NAME, show_default=True)
|
|
49
|
+
@click.option("--api-url", help="API URL", default=consts.DEFAULT_API_URL, show_default=True)
|
|
50
|
+
@click.option(
|
|
51
|
+
"--activate/--no-activate", help="Activate the created profile after creation", default=True, show_default=True
|
|
52
|
+
)
|
|
53
|
+
@click.pass_obj
|
|
54
|
+
def profile_create(cfg: Config, name: str, api_url: str, api_key: str, activate: bool) -> None:
|
|
55
|
+
"""Create a new profile."""
|
|
56
|
+
cfg_profile = ConfigSchema(platform_url=api_url, api_key=api_key)
|
|
57
|
+
|
|
58
|
+
cfg.add_profile(profile_name=name, profile=cfg_profile, set_active=activate)
|
|
59
|
+
|
|
60
|
+
|
|
46
61
|
@profile.command("rm")
|
|
47
62
|
@click.argument("profile_name", required=True)
|
|
48
63
|
@click.pass_obj
|
|
@@ -80,7 +95,6 @@ def profile_show(cfg: Config) -> None:
|
|
|
80
95
|
table.add_column("Value")
|
|
81
96
|
|
|
82
97
|
table.add_row("API Key", masked_key)
|
|
83
|
-
table.add_row("Organization", cfg.organization_id)
|
|
84
98
|
table.add_row("Platform URL", cfg.platform_url)
|
|
85
99
|
table.add_row("Config File", cfg.config_path.as_posix())
|
|
86
100
|
console.print(table)
|
cli/recipe_cmds.py
CHANGED
|
@@ -19,7 +19,6 @@ def recipe() -> None:
|
|
|
19
19
|
def create(source: str, output: str) -> None:
|
|
20
20
|
"""Create HRF from local path"""
|
|
21
21
|
|
|
22
|
-
from hafnia.platform.builder import validate_recipe
|
|
23
22
|
from hafnia.utils import archive_dir
|
|
24
23
|
|
|
25
24
|
path_output_zip = Path(output)
|
|
@@ -28,7 +27,6 @@ def create(source: str, output: str) -> None:
|
|
|
28
27
|
|
|
29
28
|
path_source = Path(source)
|
|
30
29
|
path_output_zip = archive_dir(path_source, path_output_zip)
|
|
31
|
-
validate_recipe(path_output_zip)
|
|
32
30
|
|
|
33
31
|
|
|
34
32
|
@recipe.command(name="view")
|
|
@@ -36,7 +34,7 @@ def create(source: str, output: str) -> None:
|
|
|
36
34
|
@click.option("--depth-limit", type=int, default=3, help="Limit the depth of the tree view.", show_default=True)
|
|
37
35
|
def view(path: str, depth_limit: int) -> None:
|
|
38
36
|
"""View the content of a recipe zip file."""
|
|
39
|
-
from hafnia.utils import
|
|
37
|
+
from hafnia.utils import show_recipe_content
|
|
40
38
|
|
|
41
39
|
path_recipe = Path(path)
|
|
42
40
|
if not path_recipe.exists():
|
|
@@ -44,6 +42,4 @@ def view(path: str, depth_limit: int) -> None:
|
|
|
44
42
|
f"Recipe file '{path_recipe}' does not exist. Please provide a valid path. "
|
|
45
43
|
f"To create a recipe, use the 'hafnia recipe create' command."
|
|
46
44
|
)
|
|
47
|
-
|
|
48
|
-
tree_str = view_recipe_content(path_recipe, depth_limit=depth_limit)
|
|
49
|
-
click.echo(tree_str)
|
|
45
|
+
show_recipe_content(path_recipe, depth_limit=depth_limit)
|
cli/runc_cmds.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import subprocess
|
|
3
3
|
import zipfile
|
|
4
|
-
from hashlib import sha256
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
from tempfile import TemporaryDirectory
|
|
7
6
|
from typing import Optional
|
|
@@ -9,6 +8,7 @@ from typing import Optional
|
|
|
9
8
|
import click
|
|
10
9
|
|
|
11
10
|
from cli.config import Config
|
|
11
|
+
from hafnia.log import sys_logger, user_logger
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@click.group(name="runc")
|
|
@@ -17,15 +17,6 @@ def runc():
|
|
|
17
17
|
pass
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
@runc.command(name="launch")
|
|
21
|
-
@click.argument("task", required=True)
|
|
22
|
-
def launch(task: str) -> None:
|
|
23
|
-
"""Launch a job within the image."""
|
|
24
|
-
from hafnia.platform.executor import handle_launch
|
|
25
|
-
|
|
26
|
-
handle_launch(task)
|
|
27
|
-
|
|
28
|
-
|
|
29
20
|
@runc.command(name="launch-local")
|
|
30
21
|
@click.argument("exec_cmd", type=str)
|
|
31
22
|
@click.option(
|
|
@@ -65,9 +56,9 @@ def launch_local(cfg: Config, exec_cmd: str, dataset: str, image_name: str) -> N
|
|
|
65
56
|
if not path_state_file.exists():
|
|
66
57
|
raise click.ClickException("State file does not exist. Please build the image first.")
|
|
67
58
|
state_dict = json.loads(path_state_file.read_text())
|
|
68
|
-
if "
|
|
69
|
-
raise click.ClickException("
|
|
70
|
-
image_name = state_dict["
|
|
59
|
+
if "image_tag" not in state_dict:
|
|
60
|
+
raise click.ClickException("'image_tag' not found in state file. Please build the image first.")
|
|
61
|
+
image_name = state_dict["image_tag"]
|
|
71
62
|
|
|
72
63
|
docker_cmds = [
|
|
73
64
|
"docker",
|
|
@@ -94,50 +85,60 @@ def launch_local(cfg: Config, exec_cmd: str, dataset: str, image_name: str) -> N
|
|
|
94
85
|
|
|
95
86
|
@runc.command(name="build")
|
|
96
87
|
@click.argument("recipe_url")
|
|
97
|
-
@click.
|
|
98
|
-
@click.
|
|
99
|
-
@click.argument("image_name", default="recipe")
|
|
88
|
+
@click.option("--state_file", "--st", type=str, default="state.json")
|
|
89
|
+
@click.option("--repo", type=str, default="localhost", help="Docker repository")
|
|
100
90
|
@click.pass_obj
|
|
101
|
-
def build(cfg: Config, recipe_url: str, state_file: str,
|
|
91
|
+
def build(cfg: Config, recipe_url: str, state_file: str, repo: str) -> None:
|
|
102
92
|
"""Build docker image with a given recipe."""
|
|
103
93
|
from hafnia.platform.builder import build_image, prepare_recipe
|
|
104
94
|
|
|
105
95
|
with TemporaryDirectory() as temp_dir:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
build_image(image_info, ecr_repository, state_file)
|
|
96
|
+
metadata = prepare_recipe(recipe_url, Path(temp_dir), cfg.api_key)
|
|
97
|
+
build_image(metadata, repo, state_file=state_file)
|
|
109
98
|
|
|
110
99
|
|
|
111
100
|
@runc.command(name="build-local")
|
|
112
101
|
@click.argument("recipe")
|
|
113
|
-
@click.
|
|
114
|
-
@click.
|
|
115
|
-
def build_local(recipe:
|
|
102
|
+
@click.option("--state_file", "--st", type=str, default="state.json")
|
|
103
|
+
@click.option("--repo", type=str, default="localhost", help="Docker repository")
|
|
104
|
+
def build_local(recipe: Path, state_file: str, repo: str) -> None:
|
|
116
105
|
"""Build recipe from local path as image with prefix - localhost"""
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
106
|
+
import shutil
|
|
107
|
+
import uuid
|
|
108
|
+
|
|
109
|
+
import seedir
|
|
110
|
+
|
|
111
|
+
from hafnia.platform.builder import build_image
|
|
112
|
+
from hafnia.utils import filter_recipe_files
|
|
113
|
+
|
|
114
|
+
recipe = Path(recipe)
|
|
115
|
+
|
|
116
|
+
with TemporaryDirectory() as d:
|
|
117
|
+
tmp_dir = Path(d)
|
|
118
|
+
recipe_dir = tmp_dir / "recipe"
|
|
119
|
+
recipe_dir.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
|
|
121
|
+
if recipe.suffix == ".zip":
|
|
122
|
+
user_logger.info("Extracting recipe for processing.")
|
|
123
|
+
with zipfile.ZipFile(recipe.as_posix(), "r") as zip_ref:
|
|
124
|
+
zip_ref.extractall(recipe_dir)
|
|
125
|
+
elif recipe.is_dir():
|
|
126
|
+
for rf in filter_recipe_files(recipe):
|
|
127
|
+
src_path = (recipe / rf).absolute()
|
|
128
|
+
target_path = recipe_dir / rf
|
|
129
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
shutil.copyfile(src_path, target_path)
|
|
131
|
+
|
|
132
|
+
user_logger.info(
|
|
133
|
+
seedir.seedir(recipe_dir, sort=True, first="folders", style="emoji", printout=False, depthlimit=2)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
metadata = {
|
|
137
|
+
"dockerfile": (recipe_dir / "Dockerfile").as_posix(),
|
|
138
|
+
"docker_context": recipe_dir.as_posix(),
|
|
139
|
+
"digest": uuid.uuid4().hex[:8],
|
|
139
140
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
|
|
142
|
+
user_logger.info("Start building image.")
|
|
143
|
+
sys_logger.debug(metadata)
|
|
144
|
+
build_image(metadata, repo, state_file=state_file)
|
hafnia/data/factory.py
CHANGED
|
@@ -7,7 +7,7 @@ from datasets import Dataset, DatasetDict, load_from_disk
|
|
|
7
7
|
|
|
8
8
|
from cli.config import Config
|
|
9
9
|
from hafnia import utils
|
|
10
|
-
from hafnia.log import
|
|
10
|
+
from hafnia.log import user_logger
|
|
11
11
|
from hafnia.platform import download_resource, get_dataset_id
|
|
12
12
|
|
|
13
13
|
|
|
@@ -15,7 +15,7 @@ def load_local(dataset_path: Path) -> Union[Dataset, DatasetDict]:
|
|
|
15
15
|
"""Load a Hugging Face dataset from a local directory path."""
|
|
16
16
|
if not dataset_path.exists():
|
|
17
17
|
raise ValueError(f"Can not load dataset, directory does not exist -- {dataset_path}")
|
|
18
|
-
|
|
18
|
+
user_logger.info(f"Loading data from {dataset_path.as_posix()}")
|
|
19
19
|
return load_from_disk(dataset_path.as_posix())
|
|
20
20
|
|
|
21
21
|
|
|
@@ -37,7 +37,7 @@ def download_or_get_dataset_path(
|
|
|
37
37
|
dataset_path_sample = dataset_path_base / "sample"
|
|
38
38
|
|
|
39
39
|
if dataset_path_sample.exists() and not force_redownload:
|
|
40
|
-
|
|
40
|
+
user_logger.info("Dataset found locally. Set 'force=True' or add `--force` flag with cli to re-download")
|
|
41
41
|
return dataset_path_sample
|
|
42
42
|
|
|
43
43
|
dataset_id = get_dataset_id(dataset_name, endpoint_dataset, api_key)
|
|
@@ -13,7 +13,7 @@ from datasets import DatasetDict
|
|
|
13
13
|
from pydantic import BaseModel, field_validator
|
|
14
14
|
|
|
15
15
|
from hafnia.data.factory import load_dataset
|
|
16
|
-
from hafnia.log import
|
|
16
|
+
from hafnia.log import sys_logger, user_logger
|
|
17
17
|
from hafnia.utils import is_remote_job, now_as_str
|
|
18
18
|
|
|
19
19
|
|
|
@@ -49,7 +49,7 @@ class Entity(BaseModel):
|
|
|
49
49
|
try:
|
|
50
50
|
return float(v)
|
|
51
51
|
except (ValueError, TypeError) as e:
|
|
52
|
-
|
|
52
|
+
user_logger.warning(f"Invalid value '{v}' provided, defaulting to -1.0: {e}")
|
|
53
53
|
return -1.0
|
|
54
54
|
|
|
55
55
|
@field_validator("ent_type", mode="before")
|
|
@@ -165,9 +165,9 @@ class HafniaLogger:
|
|
|
165
165
|
existing_params = {}
|
|
166
166
|
existing_params.update(params)
|
|
167
167
|
file_path.write_text(json.dumps(existing_params, indent=2))
|
|
168
|
-
|
|
168
|
+
user_logger.info(f"Saved parameters to {file_path}")
|
|
169
169
|
except Exception as e:
|
|
170
|
-
|
|
170
|
+
user_logger.error(f"Failed to save parameters to {file_path}: {e}")
|
|
171
171
|
|
|
172
172
|
def log_environment(self):
|
|
173
173
|
environment_info = {
|
|
@@ -201,4 +201,4 @@ class HafniaLogger:
|
|
|
201
201
|
next_table = pa.concat_tables([prev, log_batch])
|
|
202
202
|
pq.write_table(next_table, self.log_file)
|
|
203
203
|
except Exception as e:
|
|
204
|
-
|
|
204
|
+
sys_logger.error(f"Failed to flush logs: {e}")
|
hafnia/http.py
CHANGED
|
@@ -20,7 +20,7 @@ def fetch(endpoint: str, headers: Dict, params: Optional[Dict] = None) -> Dict:
|
|
|
20
20
|
json.JSONDecodeError: On invalid JSON response
|
|
21
21
|
"""
|
|
22
22
|
params = {} if params is None else params
|
|
23
|
-
http = urllib3.PoolManager(
|
|
23
|
+
http = urllib3.PoolManager(retries=urllib3.Retry(3))
|
|
24
24
|
try:
|
|
25
25
|
response = http.request("GET", endpoint, fields=params, headers=headers)
|
|
26
26
|
if response.status != 200:
|
|
@@ -48,7 +48,7 @@ def post(endpoint: str, headers: Dict, data: Union[Path, Dict, bytes], multipart
|
|
|
48
48
|
json.JSONDecodeError: If response isn't valid JSON
|
|
49
49
|
ValueError: If data type is unsupported
|
|
50
50
|
"""
|
|
51
|
-
http = urllib3.PoolManager(
|
|
51
|
+
http = urllib3.PoolManager(retries=urllib3.Retry(3))
|
|
52
52
|
try:
|
|
53
53
|
if multipart:
|
|
54
54
|
# Remove content-type header if present as urllib3 will set it
|
hafnia/log.py
CHANGED
|
@@ -1,32 +1,23 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import os
|
|
2
3
|
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class CustomFormatter(logging.Formatter):
|
|
7
|
-
log_format = "%(asctime)s - %(name)s:%(filename)s @ %(lineno)d - %(levelname)s - %(message)s"
|
|
4
|
+
from rich.logging import RichHandler
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
formatter = logging.Formatter(self.log_format)
|
|
11
|
-
return formatter.format(record)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def create_logger() -> logging.Logger:
|
|
15
|
-
root_logger = logging.getLogger(__package_name__)
|
|
16
|
-
if root_logger.hasHandlers():
|
|
17
|
-
return root_logger
|
|
6
|
+
from hafnia import __package_name__
|
|
18
7
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
ch.setFormatter(CustomFormatter())
|
|
8
|
+
system_handler = RichHandler(rich_tracebacks=True, show_path=True, show_level=True)
|
|
9
|
+
user_handler = RichHandler(rich_tracebacks=False, show_path=False, show_level=False, log_time_format="[%X]")
|
|
22
10
|
|
|
23
|
-
root_logger.propagate = False
|
|
24
|
-
for handler in root_logger.handlers:
|
|
25
|
-
root_logger.removeHandler(handler)
|
|
26
11
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
12
|
+
def create_logger(handler: RichHandler, name: str, log_level: str) -> logging.Logger:
|
|
13
|
+
logger = logging.getLogger(name)
|
|
14
|
+
if logger.hasHandlers():
|
|
15
|
+
logger.handlers.clear()
|
|
16
|
+
logger.addHandler(handler)
|
|
17
|
+
logger.setLevel(log_level)
|
|
18
|
+
logger.propagate = False
|
|
19
|
+
return logger
|
|
30
20
|
|
|
31
21
|
|
|
32
|
-
|
|
22
|
+
sys_logger = create_logger(system_handler, f"{__package_name__}.system", os.getenv("HAFNIA_LOG", "INFO").upper())
|
|
23
|
+
user_logger = create_logger(user_handler, f"{__package_name__}.user", "DEBUG")
|
hafnia/platform/__init__.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from hafnia.platform.api import get_organization_id
|
|
2
1
|
from hafnia.platform.download import (
|
|
3
2
|
download_resource,
|
|
4
3
|
download_single_object,
|
|
@@ -12,7 +11,6 @@ from hafnia.platform.experiment import (
|
|
|
12
11
|
)
|
|
13
12
|
|
|
14
13
|
__all__ = [
|
|
15
|
-
"get_organization_id",
|
|
16
14
|
"get_dataset_id",
|
|
17
15
|
"create_recipe",
|
|
18
16
|
"get_exp_environment_id",
|