modelcar-maker 0.4.0__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.
@@ -0,0 +1,154 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ from .download import hf_download
6
+ from .image.types import Backend
7
+ from .util import normalize
8
+ from .util import settings
9
+
10
+
11
+ @dataclass
12
+ class ProcessResult:
13
+ """Represents the state of the process workflow."""
14
+
15
+ skipped: bool = False
16
+ downloaded_to: Optional[Path] = None
17
+ image: Optional[str] = None
18
+ image_built: bool = False
19
+ image_pushed: bool = False
20
+ image_cleaned_up: bool = False
21
+ model_cleaned_up: bool = False
22
+
23
+
24
+ def cleanup(path: Path, skip: list[str] = ["Containerfile"]) -> bool:
25
+ """Remove all children of the provided path, except for top-level files provided in skip."""
26
+ changed = False
27
+ for subpath in path.iterdir():
28
+ if subpath.name in skip:
29
+ continue
30
+ if subpath.is_dir():
31
+ _ = cleanup(subpath, skip=[])
32
+ subpath.rmdir()
33
+ changed = True
34
+ else:
35
+ subpath.unlink()
36
+ changed = True
37
+ return changed
38
+
39
+
40
+ def process(
41
+ model: str,
42
+ image_repo: str = f"{settings.image.registry}/{settings.image.repository}",
43
+ backend: Backend | str = settings.image.backend,
44
+ base_image: str = settings.image.base_image,
45
+ architectures: list[str] = settings.image.architectures,
46
+ authfile: Path | None = settings.image.get("authfile"),
47
+ push: bool = settings.image.push,
48
+ image_cleanup: bool = settings.image.cleanup,
49
+ model_cleanup: bool = settings.models.cleanup,
50
+ skip_if_exists: bool = settings.image.skip_if_exists,
51
+ pull: bool = settings.image.pull,
52
+ ) -> ProcessResult:
53
+ """Run through the entire process of downloading, packaging, and publishing a Model Car image."""
54
+ result = ProcessResult()
55
+
56
+ if isinstance(backend, str):
57
+ try:
58
+ backend = Backend(backend)
59
+ except ValueError:
60
+ raise NotImplementedError(f"Backend {backend!r} is not supported") from None
61
+
62
+ from .image.types import BuildArgs
63
+ from .image.types import PushArgs
64
+ from .image.types import RmArgs
65
+
66
+ if backend is Backend.PODMAN:
67
+ from .image.podman import do_build
68
+ from .image.podman import do_image_rm
69
+ from .image.podman import do_push
70
+ from .image.podman import image_exists
71
+ else:
72
+ from .image.olot import do_build
73
+ from .image.olot import do_image_rm
74
+ from .image.olot import do_push
75
+ from .image.olot import image_exists
76
+
77
+ if skip_if_exists:
78
+ if image_exists(model, image_repo):
79
+ # It was requested that we skip the build, and the image exists.
80
+ # We should still check if a cleanup is called for.
81
+ result.skipped = True
82
+ if image_cleanup:
83
+ oci_layout_dir = Path("tmp").joinpath(normalize(model)) if backend is not Backend.PODMAN else None
84
+ if do_image_rm(
85
+ RmArgs(model=model, repo=image_repo, oci_layout_dir=oci_layout_dir, architectures=architectures)
86
+ ):
87
+ result.image_cleaned_up = True
88
+ if model_cleanup:
89
+ download_dir = Path("models").joinpath(normalize(model))
90
+ result.model_cleaned_up = cleanup(download_dir)
91
+ return result
92
+
93
+ # Ensure that the model is downloaded before the build
94
+ download_dir, commit = hf_download(model)
95
+ result.downloaded_to = download_dir
96
+
97
+ # Build the image
98
+ build_result = do_build(
99
+ BuildArgs(
100
+ model=model,
101
+ repo=image_repo,
102
+ model_dir=download_dir,
103
+ base_image=base_image,
104
+ commit=commit,
105
+ pull=pull,
106
+ architectures=architectures,
107
+ )
108
+ )
109
+ result.image = build_result.image
110
+ result.image_built = True
111
+
112
+ if push:
113
+ do_push(
114
+ PushArgs(
115
+ model=model,
116
+ repo=image_repo,
117
+ authfile=authfile,
118
+ oci_layout_dir=build_result.oci_layout_dir,
119
+ architectures=architectures,
120
+ manifest_list=build_result.manifest_list,
121
+ )
122
+ )
123
+ result.image_pushed = True
124
+
125
+ if image_cleanup:
126
+ if backend is Backend.PODMAN:
127
+ if image_exists(model, image_repo):
128
+ if do_image_rm(
129
+ RmArgs(
130
+ model=model,
131
+ repo=image_repo,
132
+ oci_layout_dir=build_result.oci_layout_dir,
133
+ architectures=architectures,
134
+ manifest_list=build_result.manifest_list,
135
+ )
136
+ ):
137
+ result.image_cleaned_up = True
138
+ else:
139
+ if do_image_rm(
140
+ RmArgs(
141
+ model=model,
142
+ repo=image_repo,
143
+ oci_layout_dir=build_result.oci_layout_dir,
144
+ architectures=architectures,
145
+ manifest_list=build_result.manifest_list,
146
+ )
147
+ ):
148
+ result.image_cleaned_up = True
149
+
150
+ if model_cleanup and result.image_built:
151
+ # Only clean up the files if it was skipped (above) or an image definitely got built
152
+ result.model_cleaned_up = cleanup(download_dir)
153
+
154
+ return result
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.4.0'
22
+ __version_tuple__ = version_tuple = (0, 4, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,3 @@
1
+ from .cli import cli
2
+
3
+ __all__ = ["cli"]
@@ -0,0 +1,177 @@
1
+ from pathlib import Path
2
+ from typing import Optional
3
+
4
+ import typer
5
+ from rich import print as rprint
6
+ from typing_extensions import Annotated
7
+
8
+ from .. import process
9
+ from ..image.types import Backend
10
+ from ..util import make_logger
11
+ from ..util import settings
12
+
13
+
14
+ def version_callback(value: bool):
15
+ if value:
16
+ from ..__version__ import version
17
+
18
+ rprint(version)
19
+ raise typer.Exit()
20
+
21
+
22
+ cli = typer.Typer(
23
+ context_settings={"help_option_names": ["-h", "--help"]},
24
+ pretty_exceptions_show_locals=False,
25
+ add_completion=False,
26
+ )
27
+
28
+
29
+ @cli.command()
30
+ def build(
31
+ ctx: typer.Context,
32
+ model: Annotated[
33
+ Optional[str],
34
+ typer.Argument(help="The model to download (otherwise ensure all defaults are downloaded)"),
35
+ ] = None,
36
+ registry: Annotated[
37
+ str,
38
+ typer.Option(
39
+ "--registry",
40
+ help="The registry to include in the image reference",
41
+ ),
42
+ ] = settings.image.registry,
43
+ repository: Annotated[
44
+ str,
45
+ typer.Option(
46
+ "-r",
47
+ "--repository",
48
+ help="The repository, within the registry, to include in the image reference",
49
+ ),
50
+ ] = settings.image.repository,
51
+ backend: Annotated[
52
+ Backend,
53
+ typer.Option(
54
+ "--backend",
55
+ help="Build backend to use: olot (default) or podman",
56
+ ),
57
+ ] = Backend(settings.image.backend),
58
+ base_image: Annotated[
59
+ str,
60
+ typer.Option(
61
+ "--base-image",
62
+ help="Base image to use for the modelcar (applies to both backends)",
63
+ ),
64
+ ] = settings.image.base_image,
65
+ arch: Annotated[
66
+ list[str],
67
+ typer.Option(
68
+ "--arch",
69
+ help="Target architecture(s) to build for (repeat for multiple)",
70
+ ),
71
+ ] = settings.image.architectures,
72
+ pull: Annotated[
73
+ bool,
74
+ typer.Option(
75
+ "--pull/--no-pull",
76
+ help="Pull the base image before building if a newer version is available",
77
+ ),
78
+ ] = settings.image.pull,
79
+ push: Annotated[
80
+ bool,
81
+ typer.Option(
82
+ "--push/--no-push",
83
+ help="Push the image(s) after building",
84
+ ),
85
+ ] = settings.image.push,
86
+ authfile: Annotated[
87
+ Optional[Path],
88
+ typer.Option(
89
+ "-a",
90
+ "--authfile",
91
+ help="The authfile to use for the push",
92
+ ),
93
+ ] = settings.image.get("authfile"),
94
+ image_cleanup: Annotated[
95
+ bool,
96
+ typer.Option(
97
+ "--image-clean-up/--no-image-clean-up",
98
+ help="Clean up the container image after push to free up space",
99
+ ),
100
+ ] = settings.image.cleanup,
101
+ model_cleanup: Annotated[
102
+ bool,
103
+ typer.Option(
104
+ "--model-clean-up/--no-model-clean-up",
105
+ help="Clean up the downloaded model images after build to free up space",
106
+ ),
107
+ ] = settings.models.cleanup,
108
+ skip_if_exists: Annotated[
109
+ bool,
110
+ typer.Option(
111
+ "--skip-if-exists/--no-skip-if-exists",
112
+ help="Skip downloading and publishing the image if it already exists",
113
+ ),
114
+ ] = settings.image.skip_if_exists,
115
+ verbose: Annotated[
116
+ int,
117
+ typer.Option(
118
+ "--verbose",
119
+ "-v",
120
+ count=True,
121
+ help="Increase logging verbosity (repeat for more)",
122
+ show_default=False,
123
+ metavar="",
124
+ ),
125
+ ] = settings.meta.verbosity,
126
+ _: Annotated[
127
+ bool,
128
+ typer.Option(
129
+ "--version",
130
+ "-V",
131
+ callback=version_callback,
132
+ help="Print the version and exit",
133
+ is_eager=True,
134
+ show_default=False,
135
+ ),
136
+ ] = False,
137
+ ):
138
+ """Modelcar Maker model download, build, and push"""
139
+
140
+ logger = make_logger(verbose)
141
+ image_repo = f"{registry}/{repository}"
142
+ logger.debug(f"Backend: {backend}, Architectures: {arch}, Push {image_repo}: {push}")
143
+ if model is None:
144
+ if not isinstance(settings.models.default, list):
145
+ ctx.fail(f"Default models should be a list, not {type(settings.models.default)}")
146
+ if len(settings.models.default) < 1:
147
+ ctx.fail("No model provided, default models list is empty")
148
+ models = settings.models.default
149
+ else:
150
+ models = [model]
151
+
152
+ for model in models:
153
+ result = process(
154
+ str(model),
155
+ image_repo,
156
+ backend=backend,
157
+ base_image=base_image,
158
+ architectures=arch,
159
+ authfile=authfile,
160
+ push=push,
161
+ image_cleanup=image_cleanup,
162
+ model_cleanup=model_cleanup,
163
+ skip_if_exists=skip_if_exists,
164
+ pull=pull,
165
+ )
166
+ cleanup_str = f"Cleanup: {'✅' if result.image_cleaned_up else '❌'} Image, {'✅' if result.model_cleaned_up else '❌'} Model"
167
+
168
+ if result.skipped:
169
+ rprint(f"{model} was skipped as it already exists at {image_repo} - {cleanup_str}.")
170
+ if result.image_pushed:
171
+ rprint(
172
+ f"{model} was downloaded to {result.downloaded_to}, built as {result.image}, and pushed - {cleanup_str}."
173
+ )
174
+ elif result.image_built:
175
+ rprint(
176
+ f"{model} was downloaded to {result.downloaded_to} and built as {result.image}, but not pushed - {cleanup_str}."
177
+ )
@@ -0,0 +1,3 @@
1
+ from .hf_download import hf_download
2
+
3
+ __all__ = ["hf_download"]
@@ -0,0 +1,35 @@
1
+ import warnings
2
+ from pathlib import Path
3
+
4
+ from huggingface_hub import snapshot_download
5
+ from rich import print as rprint
6
+ from tqdm import TqdmExperimentalWarning
7
+ from tqdm.rich import tqdm_rich
8
+
9
+ from ..util import logger
10
+ from ..util import normalize
11
+
12
+ warnings.filterwarnings("ignore", category=TqdmExperimentalWarning)
13
+
14
+
15
+ def hf_download(repo_id: str) -> tuple[Path, str]:
16
+ """Download the given model repo_id into the models/ directory, returning the path."""
17
+ from huggingface_hub import HfApi
18
+
19
+ normalized = normalize(repo_id)
20
+ download_dir = Path(f"models/{normalized}")
21
+ logger.info(f"Downloading {repo_id} to {download_dir}")
22
+ if not download_dir.is_dir():
23
+ download_dir.mkdir(parents=True, exist_ok=True)
24
+
25
+ rprint(f"Downloading {repo_id} to {download_dir}")
26
+ snapshot_download(repo_id, local_dir=download_dir, tqdm_class=tqdm_rich) # type: ignore[arg-type]
27
+
28
+ # Get the commit hash from the public API instead of internal cache metadata.
29
+ sha = HfApi().model_info(repo_id).sha
30
+ assert sha is not None
31
+
32
+ return (
33
+ download_dir,
34
+ sha,
35
+ )
@@ -0,0 +1,17 @@
1
+ from .common import _image
2
+ from .common import list_model_files
3
+ from .template import render
4
+ from .types import BuildArgs
5
+ from .types import BuildResult
6
+ from .types import PushArgs
7
+ from .types import RmArgs
8
+
9
+ __all__ = [
10
+ "_image",
11
+ "list_model_files",
12
+ "render",
13
+ "BuildArgs",
14
+ "BuildResult",
15
+ "PushArgs",
16
+ "RmArgs",
17
+ ]
@@ -0,0 +1,41 @@
1
+ from pathlib import Path
2
+
3
+ from ..util import normalize
4
+
5
+
6
+ def _image(model: str, repo: str) -> str:
7
+ """Given a model repo id and container image repository, render the full image name."""
8
+ tag = normalize(model) + "-modelcar"
9
+ return f"{repo}:{tag}"
10
+
11
+
12
+ def should_include(file: str | Path) -> bool:
13
+ """Simple filter function to determine if a file should be included in an image definition."""
14
+ if isinstance(file, Path):
15
+ file = file.name
16
+
17
+ if file.startswith("."):
18
+ return False
19
+ if file == "Containerfile":
20
+ return False
21
+ if file == "original":
22
+ return False
23
+ if file == "consolidated.safetensors":
24
+ return False
25
+ return True
26
+
27
+
28
+ def list_model_files(model_dir: Path) -> tuple[list[str], str | None]:
29
+ """List files to include in the modelcar and identify the modelcard source.
30
+
31
+ Returns:
32
+ A tuple of (sorted list of included filenames, modelcard_source or None).
33
+ """
34
+ model_files = [file.name for file in model_dir.iterdir() if should_include(file)]
35
+ model_files.sort()
36
+ modelcard_source = None
37
+ for file in model_files:
38
+ if file.startswith("README"):
39
+ modelcard_source = file
40
+ break
41
+ return model_files, modelcard_source