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

1from __future__ import annotations 

2 

3from datetime import datetime, timezone 

4from keyword import iskeyword 

5from typing import Any, ClassVar, Sequence, Type, TypeVar, Union 

6 

7import annotated_types 

8from dateutil.parser import isoparse 

9from pydantic import RootModel, StringConstraints 

10from typing_extensions import Annotated, Literal 

11 

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 

25 

26UTC = timezone.utc 

27 

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)] 

54 

55 

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") 

60 

61 return value 

62 

63 

64FAIR = Annotated[ 

65 A, 

66 AfterWarner(_validate_fair, severity=ALERT), 

67] 

68 

69ImportantFileSource = FileSource_ 

70"""DEPRECATED alias, use `FileSource` instead""" 

71 

72 

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 ) 

80 

81 return s 

82 

83 

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.") 

87 

88 return s 

89 

90 

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) 

96 

97 raise ValueError(f"'{dt}' not a string or datetime.") 

98 

99 

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 

108 

109 raise ValueError( 

110 f"'{orcid_id} is not a valid ORCID iD in hyphenated groups of 4 digits." 

111 ) 

112 

113 

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") 

118 

119 

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 """ 

134 

135 @classmethod 

136 def now(cls): 

137 return cls(datetime.now(UTC)) 

138 

139 

140class Doi(ValidatedString): 

141 """A digital object identifier, see https://www.doi.org/""" 

142 

143 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[ 

144 Annotated[str, StringConstraints(pattern=DOI_REGEX)] 

145 ] 

146 

147 

148FormatVersionPlaceholder = Literal["latest", "discover"] 

149IdentifierAnno = Annotated[ 

150 NotEmpty[str], 

151 AfterValidator(_validate_identifier), 

152 AfterValidator(_validate_is_not_keyword), 

153] 

154 

155 

156class Identifier(ValidatedString): 

157 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[IdentifierAnno] 

158 

159 

160LowerCaseIdentifierAnno = Annotated[IdentifierAnno, annotated_types.LowerCase] 

161 

162 

163class LowerCaseIdentifier(ValidatedString): 

164 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[LowerCaseIdentifierAnno] 

165 

166 

167class OrcidId(ValidatedString): 

168 """An ORCID identifier, see https://orcid.org/""" 

169 

170 root_model: ClassVar[Type[RootModel[Any]]] = RootModel[ 

171 Annotated[str, AfterValidator(_validate_orcid_id)] 

172 ] 

173 

174 

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 

180 

181 

182class SiUnit(ValidatedString): 

183 """An SI unit""" 

184 

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 ] 

192 

193 

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)]