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