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
« 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
5from typing import List, Optional
6import re
7import requests
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.
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
21 @field_validator("min")
22 @classmethod
23 def _validate_min(cls, min):
24 return cls._validate_version(min)
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)
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
45 @classmethod
46 def _validate_version(cls, version):
47 return _validate_version(version)
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]
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
61class Release(BaseModel):
62 """
63 A description of an extension release hosted on GitHub.
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
79 @field_validator("name")
80 @classmethod
81 def _validate_name(cls, name):
82 return _validate_version(name)
84 @field_validator("main_url")
85 @classmethod
86 def _check_main_url(cls, main_url: HttpUrl):
87 return _validate_primary_url(main_url)
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]
98class Extension(BaseModel):
99 """
100 A description of an extension.
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]
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...?
122class Catalog(BaseModel):
123 """
124 A catalog describing a collection of extensions.
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]
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
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
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