Coverage for python/lsst/daf/butler/registry/interfaces/_opaque.py: 95%

42 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-06 09:46 +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/>. 

27 

28"""Interfaces for the objects that manage opaque (logical) tables within a 

29`Registry`. 

30""" 

31 

32from __future__ import annotations 

33 

34__all__ = ["OpaqueTableStorage", "OpaqueTableStorageManager"] 

35 

36from abc import ABC, abstractmethod 

37from collections.abc import Iterable, Iterator, Mapping, Sequence 

38from typing import TYPE_CHECKING, Any 

39 

40from ...ddl import TableSpec 

41from ._database import Database, StaticTablesContext 

42from ._versioning import VersionedExtension, VersionTuple 

43 

44if TYPE_CHECKING: 

45 from ...datastore import DatastoreTransaction 

46 

47 

48class OpaqueTableStorage(ABC): 

49 """An interface that manages the records associated with a particular 

50 opaque table in a `Registry`. 

51 

52 Parameters 

53 ---------- 

54 name : `str` 

55 Name of the opaque table. 

56 """ 

57 

58 def __init__(self, name: str): 

59 self.name = name 

60 

61 @abstractmethod 

62 def insert(self, *data: dict, transaction: DatastoreTransaction | None = None) -> None: 

63 """Insert records into the table. 

64 

65 Parameters 

66 ---------- 

67 *data 

68 Each additional positional argument is a dictionary that represents 

69 a single row to be added. 

70 transaction : `DatastoreTransaction`, optional 

71 Transaction object that can be used to enable an explicit rollback 

72 of the insert to be registered. Can be ignored if rollback is 

73 handled via a different mechanism, such as by a database. Can be 

74 `None` if no external transaction is available. 

75 

76 Raises 

77 ------ 

78 ConflictingDefinitionError 

79 Raised if the record with a matching primary or unique key already 

80 exists. 

81 """ 

82 raise NotImplementedError() 

83 

84 @abstractmethod 

85 def ensure(self, *data: dict, transaction: DatastoreTransaction | None = None) -> None: 

86 """Insert records into the table, skipping rows that already exist. 

87 

88 Parameters 

89 ---------- 

90 *data 

91 Each additional positional argument is a dictionary that represents 

92 a single row to be added. 

93 transaction : `DatastoreTransaction`, optional 

94 Transaction object that can be used to enable an explicit rollback 

95 of the insert to be registered. Can be ignored if rollback is 

96 handled via a different mechanism, such as by a database. Can be 

97 `None` if no external transaction is available. 

98 """ 

99 raise NotImplementedError() 

100 

101 @abstractmethod 

102 def replace(self, *data: dict, transaction: DatastoreTransaction | None = None) -> None: 

103 """Insert records into the table, replacing if previously existing 

104 but different. 

105 

106 Parameters 

107 ---------- 

108 *data 

109 Each additional positional argument is a dictionary that represents 

110 a single row to be added. 

111 transaction : `DatastoreTransaction`, optional 

112 Transaction object that can be used to enable an explicit rollback 

113 of the insert to be registered. Can be ignored if rollback is 

114 handled via a different mechanism, such as by a database. Can be 

115 `None` if no external transaction is available. 

116 """ 

117 raise NotImplementedError() 

118 

119 @abstractmethod 

120 def fetch(self, **where: Any) -> Iterator[Mapping[Any, Any]]: 

121 """Retrieve records from an opaque table. 

122 

123 Parameters 

124 ---------- 

125 **where 

126 Additional keyword arguments are interpreted as equality 

127 constraints that restrict the returned rows (combined with AND); 

128 keyword arguments are column names and values are the values they 

129 must have. 

130 

131 Yields 

132 ------ 

133 row : `dict` 

134 A dictionary representing a single result row. 

135 """ 

136 raise NotImplementedError() 

137 

138 @abstractmethod 

139 def fetch_batches( 

140 self, 

141 **where: Any, 

142 ) -> Iterator[Sequence[Mapping]]: 

143 """Retrieve records from an opaque table in batches. 

144 

145 Parameters 

146 ---------- 

147 **where 

148 Same as ``OpaqueTableStorage.fetch``. 

149 

150 Yields 

151 ------ 

152 batch 

153 A batch of mappings representing a series of result rows. 

154 """ 

155 raise NotImplementedError() 

156 

157 @abstractmethod 

158 def delete(self, columns: Iterable[str], *rows: dict) -> None: 

159 """Remove records from an opaque table. 

160 

161 Parameters 

162 ---------- 

163 columns : `~collections.abc.Iterable` of `str` 

164 The names of columns that will be used to constrain the rows to 

165 be deleted; these will be combined via ``AND`` to form the 

166 ``WHERE`` clause of the delete query. 

167 *rows 

168 Positional arguments are the keys of rows to be deleted, as 

169 dictionaries mapping column name to value. The keys in all 

170 dictionaries must be exactly the names in ``columns``. 

171 """ 

172 raise NotImplementedError() 

173 

174 name: str 

175 """The name of the logical table this instance manages (`str`). 

176 """ 

177 

178 

179class OpaqueTableStorageManager(VersionedExtension): 

180 """An interface that manages the opaque tables in a `Registry`. 

181 

182 `OpaqueTableStorageManager` primarily serves as a container and factory for 

183 `OpaqueTableStorage` instances, which each provide access to the records 

184 for a different (logical) opaque table. 

185 

186 Parameters 

187 ---------- 

188 registry_schema_version : `VersionTuple` or `None`, optional 

189 Version of registry schema. 

190 

191 Notes 

192 ----- 

193 Opaque tables are primarily used by `Datastore` instances to manage their 

194 internal data in the same database that hold the `Registry`, but are not 

195 limited to this. 

196 

197 While an opaque table in a multi-layer `Registry` may in fact be the union 

198 of multiple tables in different layers, we expect this to be rare, as 

199 `Registry` layers will typically correspond to different leaf `Datastore` 

200 instances (each with their own opaque table) in a `ChainedDatastore`. 

201 """ 

202 

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

204 super().__init__(registry_schema_version=registry_schema_version) 

205 

206 @classmethod 

207 @abstractmethod 

208 def initialize( 

209 cls, db: Database, context: StaticTablesContext, registry_schema_version: VersionTuple | None = None 

210 ) -> OpaqueTableStorageManager: 

211 """Construct an instance of the manager. 

212 

213 Parameters 

214 ---------- 

215 db : `Database` 

216 Interface to the underlying database engine and namespace. 

217 context : `StaticTablesContext` 

218 Context object obtained from `Database.declareStaticTables`; used 

219 to declare any tables that should always be present in a layer 

220 implemented with this manager. 

221 registry_schema_version : `VersionTuple` or `None` 

222 Schema version of this extension as defined in registry. 

223 

224 Returns 

225 ------- 

226 manager : `OpaqueTableStorageManager` 

227 An instance of a concrete `OpaqueTableStorageManager` subclass. 

228 """ 

229 raise NotImplementedError() 

230 

231 def __getitem__(self, name: str) -> OpaqueTableStorage: 

232 """Interface to `get` that raises `LookupError` instead of returning 

233 `None` on failure. 

234 """ 

235 r = self.get(name) 

236 if r is None: 236 ↛ 237line 236 didn't jump to line 237 because the condition on line 236 was never true

237 raise LookupError(f"No logical table with name '{name}' found.") 

238 return r 

239 

240 @abstractmethod 

241 def get(self, name: str) -> OpaqueTableStorage | None: 

242 """Return an object that provides access to the records associated with 

243 an opaque logical table. 

244 

245 Parameters 

246 ---------- 

247 name : `str` 

248 Name of the logical table. 

249 

250 Returns 

251 ------- 

252 records : `OpaqueTableStorage` or `None` 

253 The object representing the records for the given table in this 

254 layer, or `None` if there are no records for that table in this 

255 layer. 

256 

257 Notes 

258 ----- 

259 Opaque tables must be registered with the layer (see `register`) by 

260 the same client before they can safely be retrieved with `get`. 

261 Unlike most other manager classes, the set of opaque tables cannot be 

262 obtained from an existing data repository. 

263 """ 

264 raise NotImplementedError() 

265 

266 @abstractmethod 

267 def register(self, name: str, spec: TableSpec) -> OpaqueTableStorage: 

268 """Ensure that this layer can hold records for the given opaque logical 

269 table, creating new tables as necessary. 

270 

271 Parameters 

272 ---------- 

273 name : `str` 

274 Name of the logical table. 

275 spec : `TableSpec` 

276 Schema specification for the table to be created. 

277 

278 Returns 

279 ------- 

280 records : `OpaqueTableStorage` 

281 The object representing the records for the given element in this 

282 layer. 

283 

284 Notes 

285 ----- 

286 This operation may not be invoked within a transaction context block. 

287 """ 

288 raise NotImplementedError() 

289 

290 @abstractmethod 

291 def clone(self, db: Database) -> OpaqueTableStorageManager: 

292 """Make an independent copy of this manager instance bound to a new 

293 `Database` instance. 

294 

295 Parameters 

296 ---------- 

297 db : `Database` 

298 New `Database` object to use when instantiating the manager. 

299 

300 Returns 

301 ------- 

302 instance : `OpaqueTableStorageManager` 

303 New manager instance with the same configuration as this instance, 

304 but bound to a new Database object. 

305 """ 

306 raise NotImplementedError()