Coverage for src/bioimageio/spec/_hf.py: 98%
40 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-18 09:17 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-18 09:17 +0000
1from __future__ import annotations
3import os
4import tempfile
5import warnings
6from contextlib import nullcontext
7from functools import cache
8from pathlib import Path
10from loguru import logger
12from bioimageio.spec import save_bioimageio_package_as_folder
13from bioimageio.spec._internal.validation_context import get_validation_context
14from bioimageio.spec.model.v0_5 import ModelDescr
16from ._hf_card import create_huggingface_model_card
17from ._version import VERSION
20@cache
21def get_huggingface_api(): # pragma: no cover
22 from huggingface_hub import HfApi
24 return HfApi(library_name="bioimageio.spec", library_version=VERSION)
27def push_to_hub(
28 descr: ModelDescr,
29 username_or_org: str,
30 *,
31 prep_dir: os.PathLike[str] | str | None = None,
32 prep_only_no_upload: bool = False,
33 create_pr: bool | None = None,
34):
35 """Push the model package described by `descr` to the Hugging Face Hub.
37 Note:
38 - Uses `descr.id` as the repository name under the provided `username_or_org`.
39 - If `descr.version` is set, the model package is uploaded to the 'main' branch
40 and tagged with the version.
41 - If `descr.version` is `None`, the model package is uploaded to the 'draft' branch.
43 Args:
44 descr: The model description to be pushed to the Hugging Face Hub.
45 username_or_org: The Hugging Face username or organization under which the model package will be uploaded.
46 The model ID from `descr.id` will be used as the repository name.
47 prep_dir: Optional path to an empty directory where the model package will be prepared before uploading.
48 prep_only_no_upload: If `True`, only prepare the model package in `prep_dir` without uploading it
49 to the Hugging Face Hub.
50 create_pr: If `False` commit directly to the 'main'/'draft' branch.
51 If `True`, create a pull request targeting 'main'/'draft'.
52 Defaults to `True` if uploading to a model description with version (to the main branch),
53 and `False` if uploading a model description without version (to the 'draft' branch).
55 Examples:
56 Upload a model description as a new version to the main branch
57 (`descr.id` and `descr.version` must be set):
59 >>> descr = ModelDescr(id="my-model-id", version="1.0", create_pr=False, ...) # doctest: +SKIP
60 >>> push_to_hub(descr, "my_hf_username") # doctest: +SKIP
62 Upload a model description as a draft to the 'draft' branch
63 (`descr.id` must be set; `descr.version` must be `None`):
65 >>> descr = ModelDescr(id="my-model-id", version=None, ...) # doctest: +SKIP
66 >>> push_to_hub(descr, "my_hf_username") # doctest: +SKIP
68 See what would be uploaded without actually uploading:
70 >>> push_to_hub(..., prep_dir="empty_local_folder", prep_only_no_upload=True) # doctest: +SKIP
72 """
74 if descr.id is None:
75 raise ValueError("descr.id must be set to push to Hugging Face Hub.")
76 repo_id = f"{username_or_org}/{descr.id}"
78 if prep_dir is None:
79 ctxt = tempfile.TemporaryDirectory(suffix="_" + repo_id.replace("/", "_"))
80 elif Path(prep_dir).exists() and any(Path(prep_dir).iterdir()):
81 raise ValueError("Provided `prep_dir` is not empty.")
82 # TODO: implement resuming upload
83 # prep_dir: If a non-empty folder is provided, it will be attempted to continue an interrupted upload.
84 # logger.info(f"Continuing upload from {prep_dir}")
85 # if prep_only_no_upload:
86 # raise ValueError("`prep_only_no_upload` is True but `prep_dir` is non-empty.")
87 else:
88 ctxt = nullcontext(prep_dir)
90 with ctxt as pdir:
91 _push_to_hub_impl(
92 descr,
93 repo_id=repo_id,
94 prep_dir=Path(pdir),
95 prep_only=prep_only_no_upload,
96 create_pr=create_pr,
97 )
100def _push_to_hub_impl(
101 descr: ModelDescr,
102 *,
103 repo_id: str,
104 prep_dir: Path,
105 prep_only: bool,
106 create_pr: bool | None,
107):
108 readme, referenced_files = create_huggingface_model_card(descr, repo_id=repo_id)
109 referenced_files_subfolders = {"images"}
110 assert not (
111 unexpected := [
112 rf
113 for rf in referenced_files
114 if not any(rf.startswith(f"{sf}/") for sf in referenced_files_subfolders)
115 ]
116 ), f"unexpected folder of referenced files: {unexpected}"
118 logger.info(f"Preparing model for upload at {prep_dir}.")
119 prep_dir.mkdir(parents=True, exist_ok=True)
120 _ = (prep_dir / "README.md").write_text(readme, encoding="utf-8")
121 for img_name, img_data in referenced_files.items():
122 image_path = prep_dir / img_name
123 image_path.parent.mkdir(parents=True, exist_ok=True)
124 _ = image_path.write_bytes(img_data)
126 with get_validation_context().replace(file_name="bioimageio.yaml"):
127 _ = save_bioimageio_package_as_folder(descr, output_path=prep_dir / "package")
129 logger.info(f"Prepared model for upload at {prep_dir}")
131 commit_message = f"Upload {descr.version or 'draft'} with bioimageio.spec {VERSION}"
132 commit_description = (
133 f"Version comment: {descr.version_comment}" if descr.version_comment else None
134 )
136 if not prep_only: # pragma: no cover
137 logger.info(f"Pushing model '{descr.id}' to Hugging Face Hub")
139 api = get_huggingface_api()
140 repo_url = api.create_repo(repo_id=repo_id, exist_ok=True, repo_type="model")
141 logger.info(f"Created repository at {repo_url}")
143 existing_refs = api.list_repo_refs(
144 repo_id=repo_id, repo_type="model", include_pull_requests=True
145 )
146 has_draft_ref = False
147 has_tag = False
148 for ref in existing_refs.branches + existing_refs.tags:
149 if ref.name == str(descr.version):
150 has_tag = True
151 if ref.name == "draft":
152 has_draft_ref = True
154 if descr.version is None:
155 revision = "draft"
156 if not has_draft_ref:
157 api.create_branch(repo_id=repo_id, branch="draft", repo_type="model")
158 else:
159 revision = None
161 if create_pr is None:
162 # default to creating a PR if commiting to main branch,
163 # commit directly to 'draft' branch
164 create_pr = revision is None
166 commit_info = api.upload_folder(
167 repo_id=repo_id,
168 revision=revision,
169 folder_path=prep_dir,
170 delete_patterns=[f"{sf}/*" for sf in referenced_files_subfolders]
171 + ["package/*"],
172 commit_message=commit_message,
173 commit_description=commit_description,
174 create_pr=create_pr,
175 )
176 logger.info(f"Created commit {commit_info.commit_url}")
177 if descr.version is not None:
178 if has_tag:
179 warnings.warn(f"Moving existing version tag {descr.version}.")
181 api.create_tag(
182 repo_id=repo_id,
183 tag=str(descr.version),
184 revision=commit_info.oid,
185 tag_message=descr.version_comment,
186 exist_ok=True,
187 )