Coverage for python/lsst/daf/butler/registry/interfaces/_versioning.py: 44%
65 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-30 09:54 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-30 09:54 +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 software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
28from __future__ import annotations
30__all__ = [
31 "IncompatibleVersionError",
32 "VersionTuple",
33 "VersionedExtension",
34]
36from abc import ABC, abstractmethod
37from typing import NamedTuple
40class IncompatibleVersionError(RuntimeError):
41 """Exception raised when extension implemention is not compatible with
42 schema version defined in database.
43 """
45 pass
48class VersionTuple(NamedTuple):
49 """Class representing a version number.
51 Attributes
52 ----------
53 major : `int
54 Major version number.
55 minor : `int`
56 Minor version number.
57 patch : `int`
58 Patch level.
59 """
61 major: int
62 minor: int
63 patch: int
65 @classmethod
66 def fromString(cls, versionStr: str) -> VersionTuple:
67 """Extract version number from a string.
69 Parameters
70 ----------
71 versionStr : `str`
72 Version number in string form "X.Y.Z", all components must be
73 present.
75 Returns
76 -------
77 version : `VersionTuple`
78 Parsed version tuple.
80 Raises
81 ------
82 ValueError
83 Raised if string has an invalid format.
84 """
85 try:
86 version = tuple(int(v) for v in versionStr.split("."))
87 except ValueError as exc:
88 raise ValueError(f"Invalid version string '{versionStr}'") from exc
89 if len(version) != 3:
90 raise ValueError(f"Invalid version string '{versionStr}', must consist of three numbers")
91 return cls(*version)
93 def checkCompatibility(self, registry_schema_version: VersionTuple, update: bool) -> bool:
94 """Compare implementation schema version with schema version in
95 registry.
97 Parameters
98 ----------
99 registry_schema_version : `VersionTuple`
100 Schema version that exists in registry or defined in a
101 configuration for a registry to be created.
102 update : `bool`
103 If True then read-write access is expected.
105 Returns
106 -------
107 compatible : `bool`
108 True if schema versions are compatible.
110 Notes
111 -----
112 This method implements default rules for checking schema compatibility:
114 - if major numbers differ, schemas are not compatible;
115 - otherwise, if minor versions are different then newer version can
116 read schema made by older version, but cannot write into it;
117 older version can neither read nor write into newer schema;
118 - otherwise, different patch versions are totally compatible.
120 Extensions that implement different versioning model will need to
121 override their `VersionedExtension.checkCompatibility` method.
122 """
123 if self.major != registry_schema_version.major:
124 # different major versions are not compatible at all
125 return False
126 if self.minor != registry_schema_version.minor:
127 # different minor versions are backward compatible for read
128 # access only
129 return self.minor > registry_schema_version.minor and not update
130 # patch difference does not matter
131 return True
133 def __str__(self) -> str:
134 """Transform version tuple into a canonical string form."""
135 return f"{self.major}.{self.minor}.{self.patch}"
138class VersionedExtension(ABC):
139 """Interface for extension classes with versions.
141 Parameters
142 ----------
143 registry_schema_version : `VersionTuple` or `None`
144 Schema version of this extension as defined in registry. If `None`, it
145 means that registry schema was not initialized yet and the extension
146 should expect that schema version returned by `newSchemaVersion` method
147 will be used to initialize the registry database. If not `None`, it
148 is guaranteed that this version has passed compatibility check.
149 """
151 def __init__(self, *, registry_schema_version: VersionTuple | None = None):
152 self._registry_schema_version = registry_schema_version
154 @classmethod
155 def extensionName(cls) -> str:
156 """Return full name of the extension.
158 This name should match the name defined in registry configuration. It
159 is also stored in registry attributes. Default implementation returns
160 full class name.
162 Returns
163 -------
164 name : `str`
165 Full extension name.
166 """
167 return f"{cls.__module__}.{cls.__name__}"
169 @classmethod
170 @abstractmethod
171 def currentVersions(cls) -> list[VersionTuple]:
172 """Return schema version(s) supported by this extension class.
174 Returns
175 -------
176 version : `list` [`VersionTuple`]
177 Schema versions for this extension. Empty list is returned if an
178 extension does not require its version to be saved or checked.
179 """
180 raise NotImplementedError()
182 def newSchemaVersion(self) -> VersionTuple | None:
183 """Return schema version for newly created registry.
185 Returns
186 -------
187 version : `VersionTuple` or `None`
188 Schema version created by this extension. `None` is returned if an
189 extension does not require its version to be saved or checked.
191 Notes
192 -----
193 Extension classes that support multiple schema versions need to
194 override `_newDefaultSchemaVersion` method.
195 """
196 return self.clsNewSchemaVersion(self._registry_schema_version)
198 @classmethod
199 def clsNewSchemaVersion(cls, schema_version: VersionTuple | None) -> VersionTuple | None:
200 """Class method which returns schema version to use for newly created
201 registry database.
203 Parameters
204 ----------
205 schema_version : `VersionTuple` or `None`
206 Configured schema version or `None` if default schema version
207 should be created. If not `None` then it is guaranteed to be
208 compatible with `currentVersions`.
210 Returns
211 -------
212 version : `VersionTuple` or `None`
213 Schema version created by this extension. `None` is returned if an
214 extension does not require its version to be saved or checked.
216 Notes
217 -----
218 Default implementation of this method can work in simple cases. If
219 the extension only supports single schema version than that version is
220 returned. If the extension supports multiple schema versions and
221 ``schema_version`` is not `None` then ``schema_version`` is returned.
222 If the extension supports multiple schema versions, but
223 ``schema_version`` is `None` it calls ``_newDefaultSchemaVersion``
224 method which needs to be reimplemented in a subsclass.
225 """
226 my_versions = cls.currentVersions()
227 if not my_versions:
228 return None
229 elif len(my_versions) == 1:
230 return my_versions[0]
231 else:
232 if schema_version is not None:
233 assert schema_version in my_versions, "Schema version must be compatible."
234 return schema_version
235 else:
236 return cls._newDefaultSchemaVersion()
238 @classmethod
239 def _newDefaultSchemaVersion(cls) -> VersionTuple:
240 """Return default shema version for new registry for extensions that
241 support multiple schema versions.
243 Notes
244 -----
245 Default implementation simply raises an exception. Managers which
246 support multiple schema versions must re-implement this method.
247 """
248 raise NotImplementedError(
249 f"Extension {cls.extensionName()} supports multiple schema versions, "
250 "its newSchemaVersion() method needs to be re-implemented."
251 )
253 @classmethod
254 def checkCompatibility(cls, registry_schema_version: VersionTuple, update: bool) -> None:
255 """Check that schema version defined in registry is compatible with
256 current implementation.
258 Parameters
259 ----------
260 registry_schema_version : `VersionTuple`
261 Schema version that exists in registry or defined in a
262 configuration for a registry to be created.
263 update : `bool`
264 If True then read-write access is expected.
266 Raises
267 ------
268 IncompatibleVersionError
269 Raised if schema version is not supported by implementation.
271 Notes
272 -----
273 Default implementation uses `VersionTuple.checkCompatibility` on
274 the versions returned from `currentVersions` method. Subclasses that
275 support different compatibility model will overwrite this method.
276 """
277 # Extensions that do not define current versions are compatible with
278 # anything.
279 if my_versions := cls.currentVersions():
280 # If there are multiple version supported one of them should be
281 # compatible to succeed.
282 for version in my_versions:
283 if version.checkCompatibility(registry_schema_version, update):
284 return
285 raise IncompatibleVersionError(
286 f"Extension versions {my_versions} is not compatible with registry "
287 f"schema version {registry_schema_version} for extension {cls.extensionName()}"
288 )
290 @classmethod
291 def checkNewSchemaVersion(cls, schema_version: VersionTuple) -> None:
292 """Verify that requested schema version can be created by an extension.
294 Parameters
295 ----------
296 schema_version : `VersionTuple`
297 Schema version that this extension is asked to create.
299 Notes
300 -----
301 This method may be used only occasionally when a specific schema
302 version is given in a regisitry config file. This can be used with an
303 extension that supports multiple schem versions to make it create new
304 schema with a non-default version number. Default implementation
305 compares requested version with one of the version returned from
306 `currentVersions`.
307 """
308 if my_versions := cls.currentVersions():
309 # If there are multiple version supported one of them should be
310 # compatible to succeed.
311 for version in my_versions:
312 # Need to have an exact match
313 if version == schema_version:
314 return
315 raise IncompatibleVersionError(
316 f"Extension {cls.extensionName()} cannot create schema version {schema_version}, "
317 f"supported schema versions: {my_versions}"
318 )