Coverage for bioimageio/spec/_internal/types.py: 74%
61 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-05 13:53 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-05 13:53 +0000
1from __future__ import annotations
3from datetime import datetime
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
21__all__ = [
22 "AbsoluteDirectory",
23 "AbsoluteFilePath",
24 "Datetime",
25 "DeprecatedLicenseId",
26 "Doi",
27 "FileName",
28 "FilePath",
29 "FileSource",
30 "HttpUrl",
31 "Identifier",
32 "ImportantFileSource",
33 "LicenseId",
34 "LowerCaseIdentifier",
35 "NotEmpty",
36 "OrcidId",
37 "PermissiveFileSource",
38 "RelativeFilePath",
39 "Sha256",
40 "SiUnit",
41 "Version",
42]
43S = TypeVar("S", bound=Sequence[Any])
44NotEmpty = Annotated[S, annotated_types.MinLen(1)]
47def _validate_identifier(s: str) -> str:
48 if not s.isidentifier():
49 raise ValueError(
50 f"'{s}' is not a valid (Python) identifier, see"
51 + " https://docs.python.org/3/reference/lexical_analysis.html#identifiers"
52 + " for details."
53 )
55 return s
58def _validate_is_not_keyword(s: str) -> str:
59 if iskeyword(s):
60 raise ValueError(f"'{s}' is a Python keyword and not allowed here.")
62 return s
65def _validate_datetime(dt: Union[datetime, str, Any]) -> datetime:
66 if isinstance(dt, datetime):
67 return dt
68 elif isinstance(dt, str):
69 return isoparse(dt)
71 raise ValueError(f"'{dt}' not a string or datetime.")
74def _validate_orcid_id(orcid_id: str):
75 if len(orcid_id) == 19 and all(orcid_id[idx] == "-" for idx in [4, 9, 14]):
76 check = 0
77 for n in orcid_id[:4] + orcid_id[5:9] + orcid_id[10:14] + orcid_id[15:]:
78 # adapted from stdnum.iso7064.mod_11_2.checksum()
79 check = (2 * check + int(10 if n == "X" else n)) % 11
80 if check == 1:
81 return orcid_id # valid
83 raise ValueError(
84 f"'{orcid_id} is not a valid ORCID iD in hyphenated groups of 4 digits."
85 )
88# TODO follow up on https://github.com/pydantic/pydantic/issues/8964
89# to remove _serialize_datetime
90def _serialize_datetime_json(dt: datetime) -> str:
91 return dt.isoformat()
94class Datetime(
95 RootModel[
96 Annotated[
97 datetime,
98 BeforeValidator(_validate_datetime),
99 PlainSerializer(_serialize_datetime_json, when_used="json-unless-none"),
100 ]
101 ]
102):
103 """Timestamp in [ISO 8601](#https://en.wikipedia.org/wiki/ISO_8601) format
104 with a few restrictions listed [here](https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat).
105 """
108class Doi(ValidatedString):
109 """A digital object identifier, see https://www.doi.org/"""
111 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[
112 Annotated[str, StringConstraints(pattern=DOI_REGEX)]
113 ]
116FormatVersionPlaceholder = Literal["latest", "discover"]
117IdentifierAnno = Annotated[
118 NotEmpty[str],
119 AfterValidator(_validate_identifier),
120 AfterValidator(_validate_is_not_keyword),
121]
124class Identifier(ValidatedString):
125 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[IdentifierAnno]
128LowerCaseIdentifierAnno = Annotated[IdentifierAnno, annotated_types.LowerCase]
131class LowerCaseIdentifier(ValidatedString):
132 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[LowerCaseIdentifierAnno]
135class OrcidId(ValidatedString):
136 """An ORCID identifier, see https://orcid.org/"""
138 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[
139 Annotated[str, AfterValidator(_validate_orcid_id)]
140 ]
143def _normalize_multiplication(si_unit: Union[Any, str]) -> Union[Any, str]:
144 if isinstance(si_unit, str):
145 return si_unit.replace("×", "·").replace("*", "·").replace(" ", "·")
146 else:
147 return si_unit
150class SiUnit(ValidatedString):
151 """An SI unit"""
153 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[
154 Annotated[
155 str,
156 StringConstraints(min_length=1, pattern=SI_UNIT_REGEX),
157 BeforeValidator(_normalize_multiplication),
158 ]
159 ]