Coverage for extension_catalog_model/model.py: 96%

97 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2026-04-28 15:58 +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 

7import requests 

8 

9class VersionRange(BaseModel): 

10 """ 

11 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. 

12 

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

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

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

16 """ 

17 min: str = "v0.1.0" 

18 max: Optional[str] = None 

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

20 

21 @field_validator("min") 

22 @classmethod 

23 def _validate_min(cls, min): 

24 return cls._validate_version(min) 

25 

26 @field_validator("max") 

27 @classmethod 

28 def _validate_min(cls, max): 

29 if max is None: 

30 return max 

31 return cls._validate_version(max) 

32 

33 @model_validator(mode="after") 

34 def _validate_version_range(self): 

35 if (self.max is not None): 

36 vmin = Version(self.min) 

37 vmax = Version(self.max) 

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

39 if self.excludes is not None: 

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

41 if self.excludes is not None: 

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

43 return self 

44 

45 @classmethod 

46 def _validate_version(cls, version): 

47 return _validate_version(version) 

48 

49 

50 @field_validator("excludes") 

51 def _validate_excludes(cls, excludes): 

52 if excludes is None: 

53 return excludes 

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

55 

56def _validate_version(version): 

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

58 return version 

59 

60 

61class Release(BaseModel): 

62 """ 

63 A description of an extension release hosted on GitHub. 

64 

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

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

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

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

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

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

71 """ 

72 name: str 

73 main_url: HttpUrl 

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

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

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

77 version_range: VersionRange 

78 

79 @field_validator("name") 

80 @classmethod 

81 def _validate_name(cls, name): 

82 return _validate_version(name) 

83 

84 @field_validator("main_url") 

85 @classmethod 

86 def _check_main_url(cls, main_url: HttpUrl): 

87 return _validate_primary_url(main_url) 

88 

89 @field_validator("required_dependency_urls", 

90 "optional_dependency_urls", "javadoc_urls") 

91 @classmethod 

92 def _check_urls(cls, urls): 

93 if urls is None: 

94 return None 

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

96 

97 

98class Extension(BaseModel): 

99 """ 

100 A description of an extension. 

101 

102 :param name: The extension's name. 

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

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

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

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

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

108 """ 

109 name: str 

110 description: str 

111 author: str 

112 homepage: HttpUrl 

113 starred: Optional[bool] = False 

114 releases: List[Release] 

115 

116 @field_validator("homepage") 

117 @classmethod 

118 def _validate_homepage(cls, url): 

119 return _validate_primary_url(url) 

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

121 

122class Catalog(BaseModel): 

123 """ 

124 A catalog describing a collection of extensions. 

125 

126 :param name: The name of the catalog. 

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

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

129 """ 

130 name: str 

131 description: str 

132 extensions: List[Extension] 

133 

134 @field_validator("extensions") 

135 @classmethod 

136 def _validate_extension_list(cls, extensions): 

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

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

139 return extensions 

140 

141def _validate_primary_url(primary_url: HttpUrl): 

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

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

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

145 retcode = requests.get(primary_url).status_code 

146 assert retcode == 200, f"URL request returns {retcode}" 

147 return primary_url 

148 

149def _validate_dependency_url(url: HttpUrl): 

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

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

152 retcode = requests.get(url).status_code 

153 assert retcode == 200, f"URL request returns {retcode}" 

154 return url 

155