Coverage for src/bioimageio/spec/_internal/types.py: 96%
78 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 15:08 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 15:08 +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 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 .license_id import DeprecatedLicenseId, LicenseId
17from .type_guards import is_sequence
18from .url import HttpUrl
19from .utils import PrettyPlainSerializer
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 "HttpUrl",
37 "Identifier",
38 "LicenseId",
39 "LowerCaseIdentifier",
40 "NotEmpty",
41 "OrcidId",
42 "PermissiveFileSource",
43 "RelativeFilePath",
44 "Sha256",
45 "SiUnit",
46 "Version",
47]
48S = TypeVar("S", bound=Sequence[Any])
49A = TypeVar("A", bound=Any)
50NotEmpty = Annotated[S, annotated_types.MinLen(1)]
53def _validate_fair(value: Any) -> Any:
54 """Raise trivial values."""
55 if value is None or (is_sequence(value) and not value):
56 raise ValueError("Needs to be filled for FAIR compliance")
58 return value
61FAIR = Annotated[
62 A,
63 AfterWarner(_validate_fair, severity=ALERT),
64]
67def _validate_identifier(s: str) -> str:
68 if not s.isidentifier():
69 raise ValueError(
70 f"'{s}' is not a valid (Python) identifier, see"
71 + " https://docs.python.org/3/reference/lexical_analysis.html#identifiers"
72 + " for details."
73 )
75 return s
78def _validate_is_not_keyword(s: str) -> str:
79 if iskeyword(s):
80 raise ValueError(f"'{s}' is a Python keyword and not allowed here.")
82 return s
85def _validate_datetime(dt: Union[datetime, str, Any]) -> datetime:
86 if isinstance(dt, datetime):
87 return dt
88 elif isinstance(dt, str):
89 return isoparse(dt).astimezone(UTC)
91 raise ValueError(f"'{dt}' not a string or datetime.")
94def _validate_orcid_id(orcid_id: str):
95 if len(orcid_id) == 19 and all(orcid_id[idx] == "-" for idx in [4, 9, 14]):
96 check = 0
97 for n in orcid_id[:4] + orcid_id[5:9] + orcid_id[10:14] + orcid_id[15:]:
98 # adapted from stdnum.iso7064.mod_11_2.checksum()
99 check = (2 * check + int(10 if n == "X" else n)) % 11
100 if check == 1:
101 return orcid_id # valid
103 raise ValueError(
104 f"'{orcid_id} is not a valid ORCID iD in hyphenated groups of 4 digits."
105 )
108# TODO follow up on https://github.com/pydantic/pydantic/issues/8964
109# to remove _serialize_datetime
110def _serialize_datetime_json(dt: datetime) -> str:
111 return dt.astimezone(UTC).isoformat(timespec="seconds")
114class Datetime(
115 RootModel[
116 Annotated[
117 datetime,
118 BeforeValidator(_validate_datetime),
119 PrettyPlainSerializer(
120 _serialize_datetime_json, when_used="json-unless-none"
121 ),
122 ]
123 ]
124):
125 """Timestamp in [ISO 8601](#https://en.wikipedia.org/wiki/ISO_8601) format
126 with a few restrictions listed [here](https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat).
127 """
129 @classmethod
130 def now(cls):
131 return cls(datetime.now(UTC))
134class Doi(ValidatedString):
135 """A digital object identifier, see https://www.doi.org/"""
137 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[
138 Annotated[str, StringConstraints(pattern=DOI_REGEX)]
139 ]
142FormatVersionPlaceholder = Literal["latest", "discover"]
143IdentifierAnno = Annotated[
144 NotEmpty[str],
145 AfterValidator(_validate_identifier),
146 AfterValidator(_validate_is_not_keyword),
147]
150class Identifier(ValidatedString):
151 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[IdentifierAnno]
154LowerCaseIdentifierAnno = Annotated[IdentifierAnno, annotated_types.LowerCase]
157class LowerCaseIdentifier(ValidatedString):
158 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[LowerCaseIdentifierAnno]
161class OrcidId(ValidatedString):
162 """An ORCID identifier, see https://orcid.org/"""
164 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[
165 Annotated[str, AfterValidator(_validate_orcid_id)]
166 ]
169def _normalize_multiplication(si_unit: Union[Any, str]) -> Union[Any, str]:
170 if isinstance(si_unit, str):
171 return si_unit.replace("×", "·").replace("*", "·").replace(" ", "·")
172 else:
173 return si_unit
176class SiUnit(ValidatedString):
177 """An SI unit"""
179 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[
180 Annotated[
181 str,
182 StringConstraints(min_length=1, pattern=SI_UNIT_REGEX),
183 BeforeValidator(_normalize_multiplication),
184 ]
185 ]
188RelativeTolerance = Annotated[float, annotated_types.Interval(ge=0, le=1e-2)]
189AbsoluteTolerance = Annotated[float, annotated_types.Interval(ge=0)]
190MismatchedElementsPerMillion = Annotated[int, annotated_types.Interval(ge=0, le=5000)]