Coverage for python/lsst/daf/butler/registry/interfaces/_versioning.py: 44%
65 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-10-02 08:00 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-10-02 08:00 +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 Parameters
52 ----------
53 major, minor, patch : `int`
54 Version number components
55 """
57 major: int
58 minor: int
59 patch: int
61 @classmethod
62 def fromString(cls, versionStr: str) -> VersionTuple:
63 """Extract version number from a string.
65 Parameters
66 ----------
67 versionStr : `str`
68 Version number in string form "X.Y.Z", all components must be
69 present.
71 Returns
72 -------
73 version : `VersionTuple`
74 Parsed version tuple.
76 Raises
77 ------
78 ValueError
79 Raised if string has an invalid format.
80 """
81 try:
82 version = tuple(int(v) for v in versionStr.split("."))
83 except ValueError as exc:
84 raise ValueError(f"Invalid version string '{versionStr}'") from exc
85 if len(version) != 3:
86 raise ValueError(f"Invalid version string '{versionStr}', must consist of three numbers")
87 return cls(*version)
89 def checkCompatibility(self, registry_schema_version: VersionTuple, update: bool) -> bool:
90 """Compare implementation schema version with schema version in
91 registry.
93 Parameters
94 ----------
95 registry_schema_version : `VersionTuple`
96 Schema version that exists in registry or defined in a
97 configuration for a registry to be created.
98 update : `bool`
99 If True then read-write access is expected.
101 Returns
102 -------
103 compatible : `bool`
104 True if schema versions are compatible.
106 Notes
107 -----
108 This method implements default rules for checking schema compatibility:
110 - if major numbers differ, schemas are not compatible;
111 - otherwise, if minor versions are different then newer version can
112 read schema made by older version, but cannot write into it;
113 older version can neither read nor write into newer schema;
114 - otherwise, different patch versions are totally compatible.
116 Extensions that implement different versioning model will need to
117 override their `VersionedExtension.checkCompatibility` method.
118 """
119 if self.major != registry_schema_version.major:
120 # different major versions are not compatible at all
121 return False
122 if self.minor != registry_schema_version.minor:
123 # different minor versions are backward compatible for read
124 # access only
125 return self.minor > registry_schema_version.minor and not update
126 # patch difference does not matter
127 return True
129 def __str__(self) -> str:
130 """Transform version tuple into a canonical string form."""
131 return f"{self.major}.{self.minor}.{self.patch}"
134class VersionedExtension(ABC):
135 """Interface for extension classes with versions.
137 Parameters
138 ----------
139 registry_schema_version : `VersionTuple` or `None`
140 Schema version of this extension as defined in registry. If `None`, it
141 means that registry schema was not initialized yet and the extension
142 should expect that schema version returned by `newSchemaVersion` method
143 will be used to initialize the registry database. If not `None`, it
144 is guaranteed that this version has passed compatibility check.
145 """
147 def __init__(self, *, registry_schema_version: VersionTuple | None = None):
148 self._registry_schema_version = registry_schema_version
150 @classmethod
151 def extensionName(cls) -> str:
152 """Return full name of the extension.
154 This name should match the name defined in registry configuration. It
155 is also stored in registry attributes. Default implementation returns
156 full class name.
158 Returns
159 -------
160 name : `str`
161 Full extension name.
162 """
163 return f"{cls.__module__}.{cls.__name__}"
165 @classmethod
166 @abstractmethod
167 def currentVersions(cls) -> list[VersionTuple]:
168 """Return schema version(s) supported by this extension class.
170 Returns
171 -------
172 version : `list` [`VersionTuple`]
173 Schema versions for this extension. Empty list is returned if an
174 extension does not require its version to be saved or checked.
175 """
176 raise NotImplementedError()
178 def newSchemaVersion(self) -> VersionTuple | None:
179 """Return schema version for newly created registry.
181 Returns
182 -------
183 version : `VersionTuple` or `None`
184 Schema version created by this extension. `None` is returned if an
185 extension does not require its version to be saved or checked.
187 Notes
188 -----
189 Extension classes that support multiple schema versions need to
190 override `_newDefaultSchemaVersion` method.
191 """
192 return self.clsNewSchemaVersion(self._registry_schema_version)
194 @classmethod
195 def clsNewSchemaVersion(cls, schema_version: VersionTuple | None) -> VersionTuple | None:
196 """Class method which returns schema version to use for newly created
197 registry database.
199 Parameters
200 ----------
201 schema_version : `VersionTuple` or `None`
202 Configured schema version or `None` if default schema version
203 should be created. If not `None` then it is guaranteed to be
204 compatible with `currentVersions`.
206 Returns
207 -------
208 version : `VersionTuple` or `None`
209 Schema version created by this extension. `None` is returned if an
210 extension does not require its version to be saved or checked.
212 Notes
213 -----
214 Default implementation of this method can work in simple cases. If
215 the extension only supports single schema version than that version is
216 returned. If the extension supports multiple schema versions and
217 ``schema_version`` is not `None` then ``schema_version`` is returned.
218 If the extension supports multiple schema versions, but
219 ``schema_version`` is `None` it calls ``_newDefaultSchemaVersion``
220 method which needs to be reimplemented in a subsclass.
221 """
222 my_versions = cls.currentVersions()
223 if not my_versions:
224 return None
225 elif len(my_versions) == 1:
226 return my_versions[0]
227 else:
228 if schema_version is not None:
229 assert schema_version in my_versions, "Schema version must be compatible."
230 return schema_version
231 else:
232 return cls._newDefaultSchemaVersion()
234 @classmethod
235 def _newDefaultSchemaVersion(cls) -> VersionTuple:
236 """Return default shema version for new registry for extensions that
237 support multiple schema versions.
239 Notes
240 -----
241 Default implementation simply raises an exception. Managers which
242 support multiple schema versions must re-implement this method.
243 """
244 raise NotImplementedError(
245 f"Extension {cls.extensionName()} supports multiple schema versions, "
246 "its newSchemaVersion() method needs to be re-implemented."
247 )
249 @classmethod
250 def checkCompatibility(cls, registry_schema_version: VersionTuple, update: bool) -> None:
251 """Check that schema version defined in registry is compatible with
252 current implementation.
254 Parameters
255 ----------
256 registry_schema_version : `VersionTuple`
257 Schema version that exists in registry or defined in a
258 configuration for a registry to be created.
259 update : `bool`
260 If True then read-write access is expected.
262 Raises
263 ------
264 IncompatibleVersionError
265 Raised if schema version is not supported by implementation.
267 Notes
268 -----
269 Default implementation uses `VersionTuple.checkCompatibility` on
270 the versions returned from `currentVersions` method. Subclasses that
271 support different compatibility model will overwrite this method.
272 """
273 # Extensions that do not define current versions are compatible with
274 # anything.
275 if my_versions := cls.currentVersions():
276 # If there are multiple version supported one of them should be
277 # compatible to succeed.
278 for version in my_versions:
279 if version.checkCompatibility(registry_schema_version, update):
280 return
281 raise IncompatibleVersionError(
282 f"Extension versions {my_versions} is not compatible with registry "
283 f"schema version {registry_schema_version} for extension {cls.extensionName()}"
284 )
286 @classmethod
287 def checkNewSchemaVersion(cls, schema_version: VersionTuple) -> None:
288 """Verify that requested schema version can be created by an extension.
290 Parameters
291 ----------
292 schema_version : `VersionTuple`
293 Schema version that this extension is asked to create.
295 Notes
296 -----
297 This method may be used only occasionally when a specific schema
298 version is given in a regisitry config file. This can be used with an
299 extension that supports multiple schem versions to make it create new
300 schema with a non-default version number. Default implementation
301 compares requested version with one of the version returned from
302 `currentVersions`.
303 """
304 if my_versions := cls.currentVersions():
305 # If there are multiple version supported one of them should be
306 # compatible to succeed.
307 for version in my_versions:
308 # Need to have an exact match
309 if version == schema_version:
310 return
311 raise IncompatibleVersionError(
312 f"Extension {cls.extensionName()} cannot create schema version {schema_version}, "
313 f"supported schema versions: {my_versions}"
314 )