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