Coverage for python/lsst/daf/butler/registry/interfaces/_versioning.py: 33%
65 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-14 09:11 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-14 09:11 +0000
1# This file is part of daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
22from __future__ import annotations
24__all__ = [
25 "IncompatibleVersionError",
26 "VersionTuple",
27 "VersionedExtension",
28]
30from abc import ABC, abstractmethod
31from typing import NamedTuple
34class IncompatibleVersionError(RuntimeError):
35 """Exception raised when extension implemention is not compatible with
36 schema version defined in database.
37 """
39 pass
42class VersionTuple(NamedTuple):
43 """Class representing a version number.
45 Parameters
46 ----------
47 major, minor, patch : `int`
48 Version number components
49 """
51 major: int
52 minor: int
53 patch: int
55 @classmethod
56 def fromString(cls, versionStr: str) -> VersionTuple:
57 """Extract version number from a string.
59 Parameters
60 ----------
61 versionStr : `str`
62 Version number in string form "X.Y.Z", all components must be
63 present.
65 Returns
66 -------
67 version : `VersionTuple`
68 Parsed version tuple.
70 Raises
71 ------
72 ValueError
73 Raised if string has an invalid format.
74 """
75 try:
76 version = tuple(int(v) for v in versionStr.split("."))
77 except ValueError as exc:
78 raise ValueError(f"Invalid version string '{versionStr}'") from exc
79 if len(version) != 3:
80 raise ValueError(f"Invalid version string '{versionStr}', must consist of three numbers")
81 return cls(*version)
83 def checkCompatibility(self, registry_schema_version: VersionTuple, update: bool) -> bool:
84 """Compare implementation schema version with schema version in
85 registry.
87 Parameters
88 ----------
89 registry_schema_version : `VersionTuple`
90 Schema version that exists in registry or defined in a
91 configuration for a registry to be created.
92 update : `bool`
93 If True then read-write access is expected.
95 Returns
96 -------
97 compatible : `bool`
98 True if schema versions are compatible.
100 Notes
101 -----
102 This method implements default rules for checking schema compatibility:
104 - if major numbers differ, schemas are not compatible;
105 - otherwise, if minor versions are different then newer version can
106 read schema made by older version, but cannot write into it;
107 older version can neither read nor write into newer schema;
108 - otherwise, different patch versions are totally compatible.
110 Extensions that implement different versioning model will need to
111 override their `VersionedExtension.checkCompatibility` method.
112 """
113 if self.major != registry_schema_version.major:
114 # different major versions are not compatible at all
115 return False
116 if self.minor != registry_schema_version.minor:
117 # different minor versions are backward compatible for read
118 # access only
119 return self.minor > registry_schema_version.minor and not update
120 # patch difference does not matter
121 return True
123 def __str__(self) -> str:
124 """Transform version tuple into a canonical string form."""
125 return f"{self.major}.{self.minor}.{self.patch}"
128class VersionedExtension(ABC):
129 """Interface for extension classes with versions.
131 Parameters
132 ----------
133 registry_schema_version : `VersionTuple` or `None`
134 Schema version of this extension as defined in registry. If `None`, it
135 means that registry schema was not initialized yet and the extension
136 should expect that schema version returned by `newSchemaVersion` method
137 will be used to initialize the registry database. If not `None`, it
138 is guaranteed that this version has passed compatibility check.
139 """
141 def __init__(self, *, registry_schema_version: VersionTuple | None = None):
142 self._registry_schema_version = registry_schema_version
144 @classmethod
145 def extensionName(cls) -> str:
146 """Return full name of the extension.
148 This name should match the name defined in registry configuration. It
149 is also stored in registry attributes. Default implementation returns
150 full class name.
152 Returns
153 -------
154 name : `str`
155 Full extension name.
156 """
157 return f"{cls.__module__}.{cls.__name__}"
159 @classmethod
160 @abstractmethod
161 def currentVersions(cls) -> list[VersionTuple]:
162 """Return schema version(s) supported by this extension class.
164 Returns
165 -------
166 version : `list` [`VersionTuple`]
167 Schema versions for this extension. Empty list is returned if an
168 extension does not require its version to be saved or checked.
169 """
170 raise NotImplementedError()
172 def newSchemaVersion(self) -> VersionTuple | None:
173 """Return schema version for newly created registry.
175 Returns
176 -------
177 version : `VersionTuple` or `None`
178 Schema version created by this extension. `None` is returned if an
179 extension does not require its version to be saved or checked.
181 Notes
182 -----
183 Extension classes that support multiple schema versions need to
184 override `_newDefaultSchemaVersion` method.
185 """
186 return self.clsNewSchemaVersion(self._registry_schema_version)
188 @classmethod
189 def clsNewSchemaVersion(cls, schema_version: VersionTuple | None) -> VersionTuple | None:
190 """Class method which returns schema version to use for newly created
191 registry database.
193 Parameters
194 ----------
195 schema_version : `VersionTuple` or `None`
196 Configured schema version or `None` if default schema version
197 should be created. If not `None` then it is guaranteed to be
198 compatible with `currentVersions`.
200 Returns
201 -------
202 version : `VersionTuple` or `None`
203 Schema version created by this extension. `None` is returned if an
204 extension does not require its version to be saved or checked.
206 Notes
207 -----
208 Default implementation of this method can work in simple cases. If
209 the extension only supports single schema version than that version is
210 returned. If the extension supports multiple schema versions and
211 ``schema_version`` is not `None` then ``schema_version`` is returned.
212 If the extension supports multiple schema versions, but
213 ``schema_version`` is `None` it calls ``_newDefaultSchemaVersion``
214 method which needs to be reimplemented in a subsclass.
215 """
216 my_versions = cls.currentVersions()
217 if not my_versions:
218 return None
219 elif len(my_versions) == 1:
220 return my_versions[0]
221 else:
222 if schema_version is not None:
223 assert schema_version in my_versions, "Schema version must be compatible."
224 return schema_version
225 else:
226 return cls._newDefaultSchemaVersion()
228 @classmethod
229 def _newDefaultSchemaVersion(cls) -> VersionTuple:
230 """Return default shema version for new registry for extensions that
231 support multiple schema versions.
233 Notes
234 -----
235 Default implementation simply raises an exception. Managers which
236 support multiple schema versions must re-implement this method.
237 """
238 raise NotImplementedError(
239 f"Extension {cls.extensionName()} supports multiple schema versions, "
240 "its newSchemaVersion() method needs to be re-implemented."
241 )
243 @classmethod
244 def checkCompatibility(cls, registry_schema_version: VersionTuple, update: bool) -> None:
245 """Check that schema version defined in registry is compatible with
246 current implementation.
248 Parameters
249 ----------
250 registry_schema_version : `VersionTuple`
251 Schema version that exists in registry or defined in a
252 configuration for a registry to be created.
253 update : `bool`
254 If True then read-write access is expected.
256 Raises
257 ------
258 IncompatibleVersionError
259 Raised if schema version is not supported by implementation.
261 Notes
262 -----
263 Default implementation uses `VersionTuple.checkCompatibility` on
264 the versions returned from `currentVersions` method. Subclasses that
265 support different compatibility model will overwrite this method.
266 """
267 # Extensions that do not define current versions are compatible with
268 # anything.
269 if my_versions := cls.currentVersions():
270 # If there are multiple version supported one of them should be
271 # compatible to succeed.
272 for version in my_versions:
273 if version.checkCompatibility(registry_schema_version, update):
274 return
275 raise IncompatibleVersionError(
276 f"Extension versions {my_versions} is not compatible with registry "
277 f"schema version {registry_schema_version} for extension {cls.extensionName()}"
278 )
280 @classmethod
281 def checkNewSchemaVersion(cls, schema_version: VersionTuple) -> None:
282 """Verify that requested schema version can be created by an extension.
284 Parameters
285 ----------
286 schema_version : `VersionTuple`
287 Schema version that this extension is asked to create.
289 Notes
290 -----
291 This method may be used only occasionally when a specific schema
292 version is given in a regisitry config file. This can be used with an
293 extension that supports multiple schem versions to make it create new
294 schema with a non-default version number. Default implementation
295 compares requested version with one of the version returned from
296 `currentVersions`.
297 """
298 if my_versions := cls.currentVersions():
299 # If there are multiple version supported one of them should be
300 # compatible to succeed.
301 for version in my_versions:
302 # Need to have an exact match
303 if version == schema_version:
304 return
305 raise IncompatibleVersionError(
306 f"Extension {cls.extensionName()} cannot create schema version {schema_version}, "
307 f"supported schema versions: {my_versions}"
308 )