Coverage for python/lsst/daf/butler/registry/interfaces/_versioning.py: 33%
56 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-01 02:05 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-01 02:05 -0700
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 Default implementation only forks for extensions that support single
184 schema version and it returns version obtained from `currentVersions`.
185 If `currentVersions` returns multiple version then default
186 implementation will raise an exception and the method has to be
187 reimplemented by a subclass.
188 """
189 my_versions = self.currentVersions()
190 if not my_versions:
191 return None
192 elif len(my_versions) == 1:
193 return my_versions[0]
194 raise NotImplementedError(
195 f"Extension {self.extensionName()} supports multiple schema versions, "
196 "its newSchemaVersion() method needs to be re-implemented."
197 )
199 @classmethod
200 def checkCompatibility(cls, registry_schema_version: VersionTuple, update: bool) -> None:
201 """Check that schema version defined in registry is compatible with
202 current implementation.
204 Parameters
205 ----------
206 registry_schema_version : `VersionTuple`
207 Schema version that exists in registry or defined in a
208 configuration for a registry to be created.
209 update : `bool`
210 If True then read-write access is expected.
212 Raises
213 ------
214 IncompatibleVersionError
215 Raised if schema version is not supported by implementation.
217 Notes
218 -----
219 Default implementation uses `VersionTuple.checkCompatibility` on
220 the versions returned from `currentVersions` method. Subclasses that
221 support different compatibility model will overwrite this method.
222 """
223 # Extensions that do not define current versions are compatible with
224 # anything.
225 if my_versions := cls.currentVersions():
226 # If there are multiple version supported one of them should be
227 # compatible to succeed.
228 for version in my_versions:
229 if version.checkCompatibility(registry_schema_version, update):
230 return
231 raise IncompatibleVersionError(
232 f"Extension versions {my_versions} is not compatible with registry "
233 f"schema version {registry_schema_version} for extension {cls.extensionName()}"
234 )
236 @classmethod
237 def checkNewSchemaVersion(cls, schema_version: VersionTuple) -> None:
238 """Verify that requested schema version can be created by an extension.
240 Parameters
241 ----------
242 schema_version : `VersionTuple`
243 Schema version that this extension is asked to create.
245 Notes
246 -----
247 This method may be used only occasionally when a specific schema
248 version is given in a regisitry config file. This can be used with an
249 extension that supports multiple schem versions to make it create new
250 schema with a non-default version number. Default implementation
251 compares requested version with one of the version returned from
252 `currentVersions`.
253 """
254 if my_versions := cls.currentVersions():
255 # If there are multiple version supported one of them should be
256 # compatible to succeed.
257 for version in my_versions:
258 # Need to have an exact match
259 if version == schema_version:
260 return
261 raise IncompatibleVersionError(
262 f"Extension {cls.extensionName()} cannot create schema version {schema_version}, "
263 f"supported schema versions: {my_versions}"
264 )