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-04 02:06 -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/>. 

21 

22from __future__ import annotations 

23 

24__all__ = [ 

25 "IncompatibleVersionError", 

26 "VersionTuple", 

27 "VersionedExtension", 

28] 

29 

30from abc import ABC, abstractmethod 

31from typing import NamedTuple 

32 

33 

34class IncompatibleVersionError(RuntimeError): 

35 """Exception raised when extension implemention is not compatible with 

36 schema version defined in database. 

37 """ 

38 

39 pass 

40 

41 

42class VersionTuple(NamedTuple): 

43 """Class representing a version number. 

44 

45 Parameters 

46 ---------- 

47 major, minor, patch : `int` 

48 Version number components 

49 """ 

50 

51 major: int 

52 minor: int 

53 patch: int 

54 

55 @classmethod 

56 def fromString(cls, versionStr: str) -> VersionTuple: 

57 """Extract version number from a string. 

58 

59 Parameters 

60 ---------- 

61 versionStr : `str` 

62 Version number in string form "X.Y.Z", all components must be 

63 present. 

64 

65 Returns 

66 ------- 

67 version : `VersionTuple` 

68 Parsed version tuple. 

69 

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) 

82 

83 def checkCompatibility(self, registry_schema_version: VersionTuple, update: bool) -> bool: 

84 """Compare implementation schema version with schema version in 

85 registry. 

86 

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. 

94 

95 Returns 

96 ------- 

97 compatible : `bool` 

98 True if schema versions are compatible. 

99 

100 Notes 

101 ----- 

102 This method implements default rules for checking schema compatibility: 

103 

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. 

109 

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 

122 

123 def __str__(self) -> str: 

124 """Transform version tuple into a canonical string form.""" 

125 return f"{self.major}.{self.minor}.{self.patch}" 

126 

127 

128class VersionedExtension(ABC): 

129 """Interface for extension classes with versions. 

130 

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 """ 

140 

141 def __init__(self, *, registry_schema_version: VersionTuple | None = None): 

142 self._registry_schema_version = registry_schema_version 

143 

144 @classmethod 

145 def extensionName(cls) -> str: 

146 """Return full name of the extension. 

147 

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. 

151 

152 Returns 

153 ------- 

154 name : `str` 

155 Full extension name. 

156 """ 

157 return f"{cls.__module__}.{cls.__name__}" 

158 

159 @classmethod 

160 @abstractmethod 

161 def currentVersions(cls) -> list[VersionTuple]: 

162 """Return schema version(s) supported by this extension class. 

163 

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() 

171 

172 def newSchemaVersion(self) -> VersionTuple | None: 

173 """Return schema version for newly created registry. 

174 

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. 

180 

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 ) 

198 

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. 

203 

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. 

211 

212 Raises 

213 ------ 

214 IncompatibleVersionError 

215 Raised if schema version is not supported by implementation. 

216 

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 ) 

235 

236 @classmethod 

237 def checkNewSchemaVersion(cls, schema_version: VersionTuple) -> None: 

238 """Verify that requested schema version can be created by an extension. 

239 

240 Parameters 

241 ---------- 

242 schema_version : `VersionTuple` 

243 Schema version that this extension is asked to create. 

244 

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 )