Coverage for bioimageio/spec/_internal/types.py: 96%
71 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-18 12:47 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-18 12:47 +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 .io import FileSource, PermissiveFileSource, RelativeFilePath
14from .io_basics import AbsoluteDirectory, AbsoluteFilePath, FileName, FilePath, Sha256
15from .io_packaging import FileSource_
16from .license_id import DeprecatedLicenseId, LicenseId
17from .url import HttpUrl
18from .validated_string import ValidatedString
19from .validator_annotations import AfterValidator, BeforeValidator
20from .version_type import Version
22UTC = timezone.utc
24__all__ = [
25 "AbsoluteDirectory",
26 "AbsoluteFilePath",
27 "Datetime",
28 "DeprecatedLicenseId",
29 "Doi",
30 "FileName",
31 "FilePath",
32 "FileSource",
33 "FileSource_",
34 "ImportantFileSource",
35 "HttpUrl",
36 "Identifier",
37 "LicenseId",
38 "LowerCaseIdentifier",
39 "NotEmpty",
40 "OrcidId",
41 "PermissiveFileSource",
42 "RelativeFilePath",
43 "Sha256",
44 "SiUnit",
45 "Version",
46]
47S = TypeVar("S", bound=Sequence[Any])
48NotEmpty = Annotated[S, annotated_types.MinLen(1)]
50ImportantFileSource = FileSource_
51"""DEPRECATED alias, use `FileSource` instead"""
54def _validate_identifier(s: str) -> str:
55 if not s.isidentifier():
56 raise ValueError(
57 f"'{s}' is not a valid (Python) identifier, see"
58 + " https://docs.python.org/3/reference/lexical_analysis.html#identifiers"
59 + " for details."
60 )
62 return s
65def _validate_is_not_keyword(s: str) -> str:
66 if iskeyword(s):
67 raise ValueError(f"'{s}' is a Python keyword and not allowed here.")
69 return s
72def _validate_datetime(dt: Union[datetime, str, Any]) -> datetime:
73 if isinstance(dt, datetime):
74 return dt
75 elif isinstance(dt, str):
76 return isoparse(dt).astimezone(UTC)
78 raise ValueError(f"'{dt}' not a string or datetime.")
81def _validate_orcid_id(orcid_id: str):
82 if len(orcid_id) == 19 and all(orcid_id[idx] == "-" for idx in [4, 9, 14]):
83 check = 0
84 for n in orcid_id[:4] + orcid_id[5:9] + orcid_id[10:14] + orcid_id[15:]:
85 # adapted from stdnum.iso7064.mod_11_2.checksum()
86 check = (2 * check + int(10 if n == "X" else n)) % 11
87 if check == 1:
88 return orcid_id # valid
90 raise ValueError(
91 f"'{orcid_id} is not a valid ORCID iD in hyphenated groups of 4 digits."
92 )
95# TODO follow up on https://github.com/pydantic/pydantic/issues/8964
96# to remove _serialize_datetime
97def _serialize_datetime_json(dt: datetime) -> str:
98 return dt.astimezone(UTC).isoformat(timespec="seconds")
101class Datetime(
102 RootModel[
103 Annotated[
104 datetime,
105 BeforeValidator(_validate_datetime),
106 PlainSerializer(_serialize_datetime_json, when_used="json-unless-none"),
107 ]
108 ]
109):
110 """Timestamp in [ISO 8601](#https://en.wikipedia.org/wiki/ISO_8601) format
111 with a few restrictions listed [here](https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat).
112 """
114 @classmethod
115 def now(cls):
116 return cls(datetime.now(UTC))
119class Doi(ValidatedString):
120 """A digital object identifier, see https://www.doi.org/"""
122 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[
123 Annotated[str, StringConstraints(pattern=DOI_REGEX)]
124 ]
127FormatVersionPlaceholder = Literal["latest", "discover"]
128IdentifierAnno = Annotated[
129 NotEmpty[str],
130 AfterValidator(_validate_identifier),
131 AfterValidator(_validate_is_not_keyword),
132]
135class Identifier(ValidatedString):
136 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[IdentifierAnno]
139LowerCaseIdentifierAnno = Annotated[IdentifierAnno, annotated_types.LowerCase]
142class LowerCaseIdentifier(ValidatedString):
143 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[LowerCaseIdentifierAnno]
146class OrcidId(ValidatedString):
147 """An ORCID identifier, see https://orcid.org/"""
149 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[
150 Annotated[str, AfterValidator(_validate_orcid_id)]
151 ]
154def _normalize_multiplication(si_unit: Union[Any, str]) -> Union[Any, str]:
155 if isinstance(si_unit, str):
156 return si_unit.replace("×", "·").replace("*", "·").replace(" ", "·")
157 else:
158 return si_unit
161class SiUnit(ValidatedString):
162 """An SI unit"""
164 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[
165 Annotated[
166 str,
167 StringConstraints(min_length=1, pattern=SI_UNIT_REGEX),
168 BeforeValidator(_normalize_multiplication),
169 ]
170 ]
173RelativeTolerance = Annotated[float, annotated_types.Interval(ge=0, le=1e-2)]
174AbsoluteTolerance = Annotated[float, annotated_types.Interval(ge=0)]
175MismatchedElementsPerMillion = Annotated[int, annotated_types.Interval(ge=0, le=1000)]