Coverage for extension_catalog_model/model.py: 96%

92 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-24 11:13 +0000

1from pydantic import BaseModel, HttpUrl 

2from pydantic.functional_validators import field_validator, model_validator 

3from packaging.version import Version 

4 

5from typing import List, Optional 

6import re 

7 

8class VersionRange(BaseModel): 

9 """ 

10 A specification of the minimum and maximum versions that an extension supports. Versions should be specified in the form "v[MAJOR].[MINOR].[PATCH]" corresponding to semantic versions, although trailing release candidate qualifiers (eg, "-rc1") are also allowed. 

11 

12 :param min: The minimum/lowest version (inclusive) that this extension is known to be compatible with. 

13 :param max: The maximum/highest version (inclusive) that this extension is known to be compatible with. 

14 :param excludes: Any specific versions within the minimum and maximum range (inclusive) that are not compatible. 

15 """ 

16 min: str = "v0.1.0" 

17 max: Optional[str] = None 

18 excludes: Optional[List[str]] = None 

19 

20 @field_validator("min") 

21 @classmethod 

22 def _validate_min(cls, min): 

23 return cls._validate_version(min) 

24 

25 @field_validator("max") 

26 @classmethod 

27 def _validate_min(cls, max): 

28 if max is None: 

29 return max 

30 return cls._validate_version(max) 

31 

32 @model_validator(mode="after") 

33 def _validate_version_range(self): 

34 if (self.max is not None): 

35 vmin = Version(self.min) 

36 vmax = Version(self.max) 

37 assert vmin <= vmax, "Maximum version should be greater than or equal to minimum version." 

38 if self.excludes is not None: 

39 assert all([Version(x) <= vmax for x in self.excludes]), "All excludes entries should be between the minimum and maximum version." 

40 if self.excludes is not None: 

41 assert all([vmin <= Version(x) for x in self.excludes]), "All excludes entries should be between the minimum and maximum version." 

42 return self 

43 

44 @classmethod 

45 def _validate_version(cls, version): 

46 return _validate_version(version) 

47 

48 

49 @field_validator("excludes") 

50 def _validate_excludes(cls, excludes): 

51 if excludes is None: 

52 return excludes 

53 return [cls._validate_version(v) for v in excludes] 

54 

55def _validate_version(version): 

56 assert re.match(r"^v[0-9]+(\.[0-9]+)?(\.[0-9]+)?(-rc[0-9]+)?$", version), "Versions should be specified in the form v[MAJOR].[MINOR].[PATCH] and may include pre-releases, eg v0.6.0-rc1." 

57 return version 

58 

59 

60class Release(BaseModel): 

61 """ 

62 A description of an extension release hosted on GitHub. 

63 

64 :param name: The name of the release. This should be a valid semantic version. 

65 :param main_url: The GitHub URL where the main extension jar or zip file can be downloaded. 

66 :param required_dependency_urls: SciJava Maven, Maven Central, or GitHub URLs where required dependency jars or zip files can be downloaded. 

67 :param optional_dependency_urls: SciJava Maven, Maven Central, or GitHub URLs where optional dependency jars or zip files can be downloaded. 

68 :param javadoc_urls: SciJava Maven, Maven Central, or GitHub URLs where javadoc jars or zip files can be downloaded. 

69 :param version_range: A specification of minimum and maximum compatible versions. 

70 """ 

71 name: str 

72 main_url: HttpUrl 

73 required_dependency_urls: Optional[List[HttpUrl]] = None 

74 optional_dependency_urls: Optional[List[HttpUrl]] = None 

75 javadoc_urls: Optional[List[HttpUrl]] = None 

76 version_range: VersionRange 

77 

78 @field_validator("name") 

79 @classmethod 

80 def _validate_name(cls, name): 

81 return _validate_version(name) 

82 

83 @field_validator("main_url") 

84 @classmethod 

85 def _check_main_url(cls, main_url: HttpUrl): 

86 return _validate_primary_url(main_url) 

87 

88 @field_validator("required_dependency_urls", 

89 "optional_dependency_urls", "javadoc_urls") 

90 @classmethod 

91 def _check_urls(cls, urls): 

92 if urls is None: 

93 return None 

94 return [_validate_dependency_url(url) for url in urls] 

95 

96 

97class Extension(BaseModel): 

98 """ 

99 A description of an extension. 

100 

101 :param name: The extension's name. 

102 :param description: A short (one sentence or so) description of what the extension is and what it does. 

103 :param author: The author or group responsible for the extension. 

104 :param homepage: A link to the GitHub repository associated with the extension. 

105 :param starred: Whether the extension is generally useful or recommended for most users. 

106 :param releases: A list of available releases of the extension. 

107 """ 

108 name: str 

109 description: str 

110 author: str 

111 homepage: HttpUrl 

112 starred: Optional[bool] = False 

113 releases: List[Release] 

114 

115 @field_validator("homepage") 

116 @classmethod 

117 def _validate_homepage(cls, url): 

118 return _validate_primary_url(url) 

119 ## todo: should we check that the download links' owner/repo matches the homepage...? 

120 

121class Catalog(BaseModel): 

122 """ 

123 A catalog describing a collection of extensions. 

124 

125 :param name: The name of the catalog. 

126 :param description: A short (one sentence or so) description of what the catalog contains and what its purpose is. 

127 :param extensions: The collection of extensions that the catalog describes. 

128 """ 

129 name: str 

130 description: str 

131 extensions: List[Extension] 

132 

133 @field_validator("extensions") 

134 @classmethod 

135 def _validate_extension_list(cls, extensions): 

136 names = [ext.name for ext in extensions] 

137 assert len(names) == len(set(names)), "Duplicated extension names not allowed in extension catalog." 

138 return extensions 

139 

140def _validate_primary_url(primary_url: HttpUrl): 

141 assert primary_url.scheme == "https", "URLs must use https" 

142 assert primary_url.host == "github.com", "Homepage and main download links must currently be hosted on github.com." 

143 assert re.match("^/[0-9a-zA-Z]+/[0-9a-zA-Z]+/?", primary_url.path) is not None, "Homepage and main download links must currently point to a valid github repo." 

144 return primary_url 

145 

146def _validate_dependency_url(url): 

147 assert url.scheme == "https", "URLs must use https" 

148 assert url.host in ["github.com", "maven.scijava.org", "repo1.maven.org"], "Dependency and javadoc download links must currently be hosted on github.com, SciJava Maven, or Maven Central." 

149 return url