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

1from __future__ import annotations 

2 

3from datetime import datetime 

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 PlainSerializer, RootModel, StringConstraints 

10from typing_extensions import Annotated, Literal 

11 

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 

20 

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

45 

46 

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 ) 

54 

55 return s 

56 

57 

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

61 

62 return s 

63 

64 

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) 

70 

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

72 

73 

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 

82 

83 raise ValueError( 

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

85 ) 

86 

87 

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

92 

93 

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

106 

107 

108class Doi(ValidatedString): 

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

110 

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

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

113 ] 

114 

115 

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

117IdentifierAnno = Annotated[ 

118 NotEmpty[str], 

119 AfterValidator(_validate_identifier), 

120 AfterValidator(_validate_is_not_keyword), 

121] 

122 

123 

124class Identifier(ValidatedString): 

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

126 

127 

128LowerCaseIdentifierAnno = Annotated[IdentifierAnno, annotated_types.LowerCase] 

129 

130 

131class LowerCaseIdentifier(ValidatedString): 

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

133 

134 

135class OrcidId(ValidatedString): 

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

137 

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

139 Annotated[str, AfterValidator(_validate_orcid_id)] 

140 ] 

141 

142 

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 

148 

149 

150class SiUnit(ValidatedString): 

151 """An SI unit""" 

152 

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 ]