Coverage for python/lsst/daf/butler/registry/interfaces/_versioning.py: 33%

65 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-09 02:11 -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 Extension classes that support multiple schema versions need to 

184 override `_newDefaultSchemaVersion` method. 

185 """ 

186 return self.clsNewSchemaVersion(self._registry_schema_version) 

187 

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. 

192 

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`. 

199 

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. 

205 

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

227 

228 @classmethod 

229 def _newDefaultSchemaVersion(cls) -> VersionTuple: 

230 """Return default shema version for new registry for extensions that 

231 support multiple schema versions. 

232 

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 ) 

242 

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. 

247 

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. 

255 

256 Raises 

257 ------ 

258 IncompatibleVersionError 

259 Raised if schema version is not supported by implementation. 

260 

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 ) 

279 

280 @classmethod 

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

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

283 

284 Parameters 

285 ---------- 

286 schema_version : `VersionTuple` 

287 Schema version that this extension is asked to create. 

288 

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 )