Coverage for bioimageio/spec/_internal/types.py: 96%
80 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-12 17:44 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-12 17:44 +0000
1from __future__ import annotations
3from datetime import datetime, timezone
4from keyword import iskeyword
5from typing import Any, ClassVar, Sequence, Type, TypeVar, Union
7import annotated_types
8from dateutil.parser import isoparse
9from pydantic import PlainSerializer, RootModel, StringConstraints
10from typing_extensions import Annotated, Literal
12from .constants import DOI_REGEX, SI_UNIT_REGEX
13from .field_warning import AfterWarner
14from .io import FileSource, PermissiveFileSource, RelativeFilePath
15from .io_basics import AbsoluteDirectory, AbsoluteFilePath, FileName, FilePath, Sha256
16from .io_packaging import FileSource_
17from .license_id import DeprecatedLicenseId, LicenseId
18from .type_guards import is_sequence
19from .url import HttpUrl
20from .validated_string import ValidatedString
21from .validator_annotations import AfterValidator, BeforeValidator
22from .version_type import Version
23from .warning_levels import ALERT
25UTC = timezone.utc
27__all__ = [
28 "AbsoluteDirectory",
29 "AbsoluteFilePath",
30 "Datetime",
31 "DeprecatedLicenseId",
32 "Doi",
33 "FileName",
34 "FilePath",
35 "FileSource",
36 "FileSource_",
37 "ImportantFileSource",
38 "HttpUrl",
39 "Identifier",
40 "LicenseId",
41 "LowerCaseIdentifier",
42 "NotEmpty",
43 "OrcidId",
44 "PermissiveFileSource",
45 "RelativeFilePath",
46 "Sha256",
47 "SiUnit",
48 "Version",
49]
50S = TypeVar("S", bound=Sequence[Any])
51A = TypeVar("A", bound=Any)
52NotEmpty = Annotated[S, annotated_types.MinLen(1)]
55def _validate_fair(value: Any) -> Any:
56 """Raise trivial values."""
57 if value is None or (is_sequence(value) and not value):
58 raise ValueError("Needs to be filled for FAIR compliance")
60 return value
63FAIR = Annotated[
64 A,
65 AfterWarner(_validate_fair, severity=ALERT),
66]
68ImportantFileSource = FileSource_
69"""DEPRECATED alias, use `FileSource` instead"""
72def _validate_identifier(s: str) -> str:
73 if not s.isidentifier():
74 raise ValueError(
75 f"'{s}' is not a valid (Python) identifier, see"
76 + " https://docs.python.org/3/reference/lexical_analysis.html#identifiers"
77 + " for details."
78 )
80 return s
83def _validate_is_not_keyword(s: str) -> str:
84 if iskeyword(s):
85 raise ValueError(f"'{s}' is a Python keyword and not allowed here.")
87 return s
90def _validate_datetime(dt: Union[datetime, str, Any]) -> datetime:
91 if isinstance(dt, datetime):
92 return dt
93 elif isinstance(dt, str):
94 return isoparse(dt).astimezone(UTC)
96 raise ValueError(f"'{dt}' not a string or datetime.")
99def _validate_orcid_id(orcid_id: str):
100 if len(orcid_id) == 19 and all(orcid_id[idx] == "-" for idx in [4, 9, 14]):
101 check = 0
102 for n in orcid_id[:4] + orcid_id[5:9] + orcid_id[10:14] + orcid_id[15:]:
103 # adapted from stdnum.iso7064.mod_11_2.checksum()
104 check = (2 * check + int(10 if n == "X" else n)) % 11
105 if check == 1:
106 return orcid_id # valid
108 raise ValueError(
109 f"'{orcid_id} is not a valid ORCID iD in hyphenated groups of 4 digits."
110 )
113# TODO follow up on https://github.com/pydantic/pydantic/issues/8964
114# to remove _serialize_datetime
115def _serialize_datetime_json(dt: datetime) -> str:
116 return dt.astimezone(UTC).isoformat(timespec="seconds")
119class Datetime(
120 RootModel[
121 Annotated[
122 datetime,
123 BeforeValidator(_validate_datetime),
124 PlainSerializer(_serialize_datetime_json, when_used="json-unless-none"),
125 ]
126 ]
127):
128 """Timestamp in [ISO 8601](#https://en.wikipedia.org/wiki/ISO_8601) format
129 with a few restrictions listed [here](https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat).
130 """
132 @classmethod
133 def now(cls):
134 return cls(datetime.now(UTC))
137class Doi(ValidatedString):
138 """A digital object identifier, see https://www.doi.org/"""
140 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[
141 Annotated[str, StringConstraints(pattern=DOI_REGEX)]
142 ]
145FormatVersionPlaceholder = Literal["latest", "discover"]
146IdentifierAnno = Annotated[
147 NotEmpty[str],
148 AfterValidator(_validate_identifier),
149 AfterValidator(_validate_is_not_keyword),
150]
153class Identifier(ValidatedString):
154 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[IdentifierAnno]
157LowerCaseIdentifierAnno = Annotated[IdentifierAnno, annotated_types.LowerCase]
160class LowerCaseIdentifier(ValidatedString):
161 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[LowerCaseIdentifierAnno]
164class OrcidId(ValidatedString):
165 """An ORCID identifier, see https://orcid.org/"""
167 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[
168 Annotated[str, AfterValidator(_validate_orcid_id)]
169 ]
172def _normalize_multiplication(si_unit: Union[Any, str]) -> Union[Any, str]:
173 if isinstance(si_unit, str):
174 return si_unit.replace("×", "·").replace("*", "·").replace(" ", "·")
175 else:
176 return si_unit
179class SiUnit(ValidatedString):
180 """An SI unit"""
182 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[
183 Annotated[
184 str,
185 StringConstraints(min_length=1, pattern=SI_UNIT_REGEX),
186 BeforeValidator(_normalize_multiplication),
187 ]
188 ]
191RelativeTolerance = Annotated[float, annotated_types.Interval(ge=0, le=1e-2)]
192AbsoluteTolerance = Annotated[float, annotated_types.Interval(ge=0)]
193MismatchedElementsPerMillion = Annotated[int, annotated_types.Interval(ge=0, le=1000)]