Coverage for src/bioimageio/spec/generic/v0_2.py: 93%
187 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 10:20 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 10:20 +0000
1from __future__ import annotations
3import string
4from typing import (
5 TYPE_CHECKING,
6 Any,
7 Callable,
8 ClassVar,
9 List,
10 Literal,
11 Mapping,
12 Sequence,
13 TypeVar,
14 cast,
15)
17import annotated_types
18import pydantic
19from annotated_types import Len, LowerCase, MaxLen
20from pydantic import (
21 EmailStr,
22 Field,
23 RootModel,
24 ValidationInfo,
25 field_validator,
26 model_validator,
27)
28from typing_extensions import Annotated, Self, assert_never
30from .._internal.common_nodes import Node, ResourceDescrBase
31from .._internal.constants import TAG_CATEGORIES
32from .._internal.field_warning import as_warning, issue_warning, warn
33from .._internal.io import (
34 BioimageioYamlContent,
35 WithSuffix,
36 YamlValue,
37 wo_special_file_name,
38)
39from .._internal.io_packaging import FileSource_package, include_in_package
40from .._internal.type_guards import is_sequence
41from .._internal.types import (
42 DeprecatedLicenseId,
43 FilePath,
44 FileSource,
45 LicenseId,
46 NotEmpty,
47)
48from .._internal.types import Doi as Doi
49from .._internal.types import OrcidId as OrcidId
50from .._internal.types import RelativeFilePath as RelativeFilePath
51from .._internal.url import HttpUrl as HttpUrl
52from .._internal.validated_string import ValidatedString
53from .._internal.validator_annotations import AfterValidator, RestrictCharacters
54from .._internal.version_type import Version as Version
55from ._v0_2_converter import convert_from_older_format as _convert_from_older_format
58class ResourceId(ValidatedString):
59 root_model: ClassVar[type[RootModel[Any]]] = RootModel[
60 Annotated[
61 NotEmpty[str],
62 AfterValidator(str.lower), # convert upper case on the fly
63 RestrictCharacters(string.ascii_lowercase + string.digits + "_-/."),
64 annotated_types.Predicate(
65 lambda s: not (s.startswith("/") or s.endswith("/"))
66 ),
67 ]
68 ]
71KNOWN_SPECIFIC_RESOURCE_TYPES = (
72 "application",
73 "collection",
74 "dataset",
75 "model",
76 "notebook",
77)
79VALID_COVER_IMAGE_EXTENSIONS = (
80 ".gif",
81 ".jpeg",
82 ".jpg",
83 ".png",
84 ".svg",
85 ".tif",
86 ".tiff",
87)
90_FileSource_cover = Annotated[
91 FileSource_package,
92 WithSuffix(VALID_COVER_IMAGE_EXTENSIONS, case_sensitive=False),
93]
96class AttachmentsDescr(Node, extra="allow"):
97 files: list[FileSource_package] = Field(
98 default_factory=cast(Callable[[], List[FileSource]], list)
99 )
100 """File attachments"""
103def _remove_slashes(s: str):
104 return s.replace("/", "").replace("\\", "")
107class Uploader(Node):
108 email: EmailStr
109 """Email"""
110 name: Annotated[str, AfterValidator(_remove_slashes)] | None = None
111 """name"""
114class _Person(Node):
115 affiliation: str | None = None
116 """Affiliation"""
118 email: EmailStr | None = None
119 """Email"""
121 orcid: Annotated[OrcidId | None, Field(examples=["0000-0001-2345-6789"])] = None
122 """An [ORCID iD](https://support.orcid.org/hc/en-us/sections/360001495313-What-is-ORCID
123 ) in hyphenated groups of 4 digits, (and [valid](
124 https://support.orcid.org/hc/en-us/articles/360006897674-Structure-of-the-ORCID-Identifier
125 ) as per ISO 7064 11,2.)
126 """
129class Author(_Person):
130 name: Annotated[str, AfterValidator(_remove_slashes)]
131 github_user: str | None = None # TODO: validate github_user
134class Maintainer(_Person):
135 name: Annotated[str, AfterValidator(_remove_slashes)] | None = None
136 github_user: str
139class BadgeDescr(Node):
140 """A custom badge"""
142 label: Annotated[str, Field(examples=["Open in Colab"])]
143 """badge label to display on hover"""
145 icon: Annotated[
146 Annotated[
147 FilePath | RelativeFilePath,
148 AfterValidator(wo_special_file_name),
149 include_in_package,
150 ]
151 | HttpUrl
152 | pydantic.HttpUrl
153 | None,
154 Field(examples=["https://colab.research.google.com/assets/colab-badge.svg"]),
155 ] = None
156 """badge icon (included in bioimage.io package if not a URL)"""
158 url: Annotated[
159 HttpUrl,
160 Field(
161 examples=[
162 "https://colab.research.google.com/github/HenriquesLab/ZeroCostDL4Mic/blob/master/Colab_notebooks/U-net_2D_ZeroCostDL4Mic.ipynb"
163 ]
164 ),
165 ]
166 """target URL"""
169class CiteEntry(Node):
170 text: str
171 """free text description"""
173 doi: Doi | None = None
174 """A digital object identifier (DOI) is the prefered citation reference.
175 See https://www.doi.org/ for details. (alternatively specify `url`)"""
177 @field_validator("doi", mode="before")
178 @classmethod
179 def accept_prefixed_doi(cls, doi: Any) -> Any:
180 if isinstance(doi, str):
181 for doi_prefix in ("https://doi.org/", "http://dx.doi.org/"):
182 if doi.startswith(doi_prefix):
183 doi = doi[len(doi_prefix) :]
184 break
186 return doi
188 url: str | None = None
189 """URL to cite (preferably specify a `doi` instead)"""
191 @model_validator(mode="after")
192 def _check_doi_or_url(self) -> Self:
193 if not self.doi and not self.url:
194 raise ValueError("Either 'doi' or 'url' is required")
196 return self
199class LinkedResource(Node):
200 """Reference to a bioimage.io resource"""
202 id: ResourceId
203 """A valid resource `id` from the bioimage.io collection."""
205 version_number: int | None = None
206 """version number (n-th published version, not the semantic version) of linked resource"""
209class GenericModelDescrBase(ResourceDescrBase):
210 """Base for all resource descriptions including of model descriptions"""
212 name: Annotated[
213 NotEmpty[str], warn(Annotated[str, MaxLen(128)], "Longer than 128 characters.")
214 ]
215 """A human-friendly name of the resource description"""
217 description: str
219 covers: list[_FileSource_cover] = Field(
220 default_factory=cast(Callable[[], List[_FileSource_cover]], list),
221 examples=[["cover.png"]],
222 description=(
223 "Cover images. Please use an image smaller than 500KB and an aspect"
224 " ratio width to height of 2:1.\nThe supported image formats are:"
225 f" {VALID_COVER_IMAGE_EXTENSIONS}"
226 ),
227 )
228 """Cover images. Please use an image smaller than 500KB and an aspect ratio width to height of 2:1."""
230 id_emoji: (
231 Annotated[str, Len(min_length=1, max_length=1), Field(examples=["🦈", "🦥"])]
232 | None
233 ) = None
234 """UTF-8 emoji for display alongside the `id`."""
236 authors: list[Author] = Field( # pyright: ignore[reportUnknownVariableType]
237 default_factory=list
238 )
239 """The authors are the creators of the RDF and the primary points of contact."""
241 @field_validator("authors", mode="before")
242 @classmethod
243 def accept_author_strings(cls, authors: Any | Sequence[Any]) -> Any:
244 """we unofficially accept strings as author entries"""
245 if is_sequence(authors):
246 authors = [{"name": a} if isinstance(a, str) else a for a in authors]
248 if not authors:
249 issue_warning("missing", value=authors, field="authors")
251 return authors
253 attachments: AttachmentsDescr | None = None
254 """file and other attachments"""
256 cite: list[CiteEntry] = Field( # pyright: ignore[reportUnknownVariableType]
257 default_factory=list
258 )
259 """citations"""
261 @field_validator("cite", mode="after")
262 @classmethod
263 def _warn_empty_cite(cls, value: Any):
264 if not value:
265 issue_warning("missing", value=value, field="cite")
267 return value
269 config: Annotated[
270 dict[str, YamlValue],
271 Field(
272 examples=[
273 {
274 "bioimageio": {
275 "my_custom_key": 3837283,
276 "another_key": {"nested": "value"},
277 },
278 "imagej": {"macro_dir": "path/to/macro/file"},
279 }
280 ],
281 ),
282 ] = Field(default_factory=dict)
283 """A field for custom configuration that can contain any keys not present in the RDF spec.
284 This means you should not store, for example, a github repo URL in `config` since we already have the
285 `git_repo` field defined in the spec.
286 Keys in `config` may be very specific to a tool or consumer software. To avoid conflicting definitions,
287 it is recommended to wrap added configuration into a sub-field named with the specific domain or tool name,
288 for example:
289 ```yaml
290 config:
291 bioimageio: # here is the domain name
292 my_custom_key: 3837283
293 another_key:
294 nested: value
295 imagej: # config specific to ImageJ
296 macro_dir: path/to/macro/file
297 ```
298 If possible, please use [`snake_case`](https://en.wikipedia.org/wiki/Snake_case) for keys in `config`.
299 You may want to list linked files additionally under `attachments` to include them when packaging a resource
300 (packaging a resource means downloading/copying important linked files and creating a ZIP archive that contains
301 an altered rdf.yaml file with local references to the downloaded files)"""
303 download_url: HttpUrl | None = None
304 """URL to download the resource from (deprecated)"""
306 git_repo: Annotated[
307 str | None,
308 Field(
309 examples=[
310 "https://github.com/bioimage-io/spec-bioimage-io/tree/main/example_descriptions/models/unet2d_nuclei_broad"
311 ],
312 ),
313 ] = None
314 """A URL to the Git repository where the resource is being developed."""
316 icon: Annotated[str, Len(min_length=1, max_length=2)] | FileSource | None = None
317 """An icon for illustration"""
319 links: Annotated[
320 list[str],
321 Field(
322 examples=[
323 (
324 "ilastik/ilastik",
325 "deepimagej/deepimagej",
326 "zero/notebook_u-net_3d_zerocostdl4mic",
327 )
328 ],
329 ),
330 ] = Field(default_factory=list)
331 """IDs of other bioimage.io resources"""
333 uploader: Uploader | None = None
334 """The person who uploaded the model (e.g. to bioimage.io)"""
336 # TODO: (py>3.8) remove pyright ignore
337 maintainers: list[Maintainer] = Field( # pyright: ignore[reportUnknownVariableType]
338 default_factory=list
339 )
340 """Maintainers of this resource.
341 If not specified `authors` are maintainers and at least some of them should specify their `github_user` name"""
343 rdf_source: FileSource | None = None
344 """Resource description file (RDF) source; used to keep track of where an rdf.yaml was loaded from.
345 Do not set this field in a YAML file."""
347 tags: Annotated[
348 list[str],
349 Field(examples=[("unet2d", "pytorch", "nucleus", "segmentation", "dsb2018")]),
350 ] = Field(default_factory=list)
351 """Associated tags"""
353 @as_warning
354 @field_validator("tags")
355 @classmethod
356 def warn_about_tag_categories(
357 cls, value: list[str], info: ValidationInfo
358 ) -> list[str]:
359 categories = TAG_CATEGORIES.get(info.data["type"], {})
360 missing_categories: list[Mapping[str, Sequence[str]]] = []
361 for cat, entries in categories.items():
362 if not any(e in value for e in entries):
363 missing_categories.append({cat: entries})
365 if missing_categories:
366 raise ValueError(
367 "Missing tags from bioimage.io categories: {missing_categories}"
368 )
370 return value
372 version: Version | None = None
373 """The version of the resource following SemVer 2.0."""
375 version_number: int | None = None
376 """version number (n-th published version, not the semantic version)"""
379class GenericDescrBase(GenericModelDescrBase):
380 """Base for all resource descriptions except for the model descriptions"""
382 implemented_format_version: ClassVar[Literal["0.2.4"]] = "0.2.4"
383 if TYPE_CHECKING:
384 format_version: Literal["0.2.4"] = "0.2.4"
385 else:
386 format_version: Literal["0.2.4"]
387 """The format version of this resource specification
388 (not the `version` of the resource description)
389 When creating a new resource always use the latest micro/patch version described here.
390 The `format_version` is important for any consumer software to understand how to parse the fields.
391 """
393 @model_validator(mode="before")
394 @classmethod
395 def _convert_from_older_format(
396 cls, data: BioimageioYamlContent, /
397 ) -> BioimageioYamlContent:
398 _convert_from_older_format(data)
399 return data
401 badges: list[BadgeDescr] = Field( # pyright: ignore[reportUnknownVariableType]
402 default_factory=list
403 )
404 """badges associated with this resource"""
406 documentation: Annotated[
407 FileSource | None,
408 Field(
409 examples=[
410 "https://raw.githubusercontent.com/bioimage-io/spec-bioimage-io/main/example_descriptions/models/unet2d_nuclei_broad/README.md",
411 "README.md",
412 ],
413 ),
414 ] = None
415 """URL or relative path to a markdown file with additional documentation.
416 The recommended documentation file name is `README.md`. An `.md` suffix is mandatory."""
418 license: Annotated[
419 LicenseId | DeprecatedLicenseId | str | None,
420 Field(union_mode="left_to_right", examples=["CC0-1.0", "MIT", "BSD-2-Clause"]),
421 ] = None
422 """A [SPDX license identifier](https://spdx.org/licenses/).
423 We do not support custom license beyond the SPDX license list, if you need that please
424 [open a GitHub issue](https://github.com/bioimage-io/spec-bioimage-io/issues/new/choose
425 ) to discuss your intentions with the community."""
427 @field_validator("license", mode="after")
428 @classmethod
429 def deprecated_spdx_license(
430 cls, value: LicenseId | DeprecatedLicenseId | str | None
431 ):
432 if isinstance(value, LicenseId):
433 pass
434 elif value is None:
435 issue_warning("missing", value=value, field="license")
436 elif isinstance(value, DeprecatedLicenseId):
437 issue_warning(
438 "'{value}' is a deprecated license identifier.",
439 value=value,
440 field="license",
441 )
442 elif isinstance(value, str):
443 issue_warning(
444 "'{value}' is an unknown license identifier.",
445 value=value,
446 field="license",
447 )
448 else:
449 assert_never(value)
451 return value
454ResourceDescrType = TypeVar("ResourceDescrType", bound=GenericDescrBase)
457class GenericDescr(GenericDescrBase, extra="ignore"):
458 """Specification of the fields used in a generic bioimage.io-compliant resource description file (RDF).
460 An RDF is a YAML file that describes a resource such as a model, a dataset, or a notebook.
461 Note that those resources are described with a type-specific RDF.
462 Use this generic resource description, if none of the known specific types matches your resource.
463 """
465 type: Annotated[str, LowerCase, Field(frozen=True)] = "generic"
466 """The resource type assigns a broad category to the resource."""
468 id: (
469 Annotated[ResourceId, Field(examples=["affable-shark", "ambitious-sloth"])]
470 | None
471 ) = None
472 """bioimage.io-wide unique resource identifier
473 assigned by bioimage.io; version **un**specific."""
475 source: HttpUrl | None = None
476 """The primary source of the resource"""
478 @field_validator("type", mode="after")
479 @classmethod
480 def check_specific_types(cls, value: str) -> str:
481 if value in KNOWN_SPECIFIC_RESOURCE_TYPES:
482 raise ValueError(
483 f"Use the {value} description instead of this generic description for"
484 + f" your '{value}' resource."
485 )
487 return value