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

39 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-25 15:14 +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 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 

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

23`Registry`. 

24""" 

25 

26from __future__ import annotations 

27 

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

29 

30from abc import ABC, abstractmethod 

31from collections.abc import Iterable, Iterator, Mapping 

32from typing import TYPE_CHECKING, Any 

33 

34from ...core.ddl import TableSpec 

35from ._database import Database, StaticTablesContext 

36from ._versioning import VersionedExtension, VersionTuple 

37 

38if TYPE_CHECKING: 

39 from ...core.datastore import DatastoreTransaction 

40 

41 

42class OpaqueTableStorage(ABC): 

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

44 opaque table in a `Registry`. 

45 

46 Parameters 

47 ---------- 

48 name : `str` 

49 Name of the opaque table. 

50 """ 

51 

52 def __init__(self, name: str): 

53 self.name = name 

54 

55 @abstractmethod 

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

57 """Insert records into the table. 

58 

59 Parameters 

60 ---------- 

61 *data 

62 Each additional positional argument is a dictionary that represents 

63 a single row to be added. 

64 transaction : `DatastoreTransaction`, optional 

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

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

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

68 `None` if no external transaction is available. 

69 """ 

70 raise NotImplementedError() 

71 

72 @abstractmethod 

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

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

75 

76 Parameters 

77 ---------- 

78 *data 

79 Each additional positional argument is a dictionary that represents 

80 a single row to be added. 

81 transaction : `DatastoreTransaction`, optional 

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

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

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

85 `None` if no external transaction is available. 

86 """ 

87 raise NotImplementedError() 

88 

89 @abstractmethod 

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

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

92 but different. 

93 

94 Parameters 

95 ---------- 

96 *data 

97 Each additional positional argument is a dictionary that represents 

98 a single row to be added. 

99 transaction : `DatastoreTransaction`, optional 

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

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

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

103 `None` if no external transaction is available. 

104 """ 

105 raise NotImplementedError() 

106 

107 @abstractmethod 

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

109 """Retrieve records from an opaque table. 

110 

111 Parameters 

112 ---------- 

113 **where 

114 Additional keyword arguments are interpreted as equality 

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

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

117 must have. 

118 

119 Yields 

120 ------ 

121 row : `dict` 

122 A dictionary representing a single result row. 

123 """ 

124 raise NotImplementedError() 

125 

126 @abstractmethod 

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

128 """Remove records from an opaque table. 

129 

130 Parameters 

131 ---------- 

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

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

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

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

136 *rows 

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

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

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

140 """ 

141 raise NotImplementedError() 

142 

143 name: str 

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

145 """ 

146 

147 

148class OpaqueTableStorageManager(VersionedExtension): 

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

150 

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

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

153 for a different (logical) opaque table. 

154 

155 Notes 

156 ----- 

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

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

159 limited to this. 

160 

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

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

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

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

165 """ 

166 

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

168 super().__init__(registry_schema_version=registry_schema_version) 

169 

170 @classmethod 

171 @abstractmethod 

172 def initialize( 

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

174 ) -> OpaqueTableStorageManager: 

175 """Construct an instance of the manager. 

176 

177 Parameters 

178 ---------- 

179 db : `Database` 

180 Interface to the underlying database engine and namespace. 

181 context : `StaticTablesContext` 

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

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

184 implemented with this manager. 

185 registry_schema_version : `VersionTuple` or `None` 

186 Schema version of this extension as defined in registry. 

187 

188 Returns 

189 ------- 

190 manager : `OpaqueTableStorageManager` 

191 An instance of a concrete `OpaqueTableStorageManager` subclass. 

192 """ 

193 raise NotImplementedError() 

194 

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

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

197 `None` on failure. 

198 """ 

199 r = self.get(name) 

200 if r is None: 

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

202 return r 

203 

204 @abstractmethod 

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

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

207 an opaque logical table. 

208 

209 Parameters 

210 ---------- 

211 name : `str` 

212 Name of the logical table. 

213 

214 Returns 

215 ------- 

216 records : `OpaqueTableStorage` or `None` 

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

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

219 layer. 

220 

221 Notes 

222 ----- 

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

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

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

226 obtained from an existing data repository. 

227 """ 

228 raise NotImplementedError() 

229 

230 @abstractmethod 

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

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

233 table, creating new tables as necessary. 

234 

235 Parameters 

236 ---------- 

237 name : `str` 

238 Name of the logical table. 

239 spec : `TableSpec` 

240 Schema specification for the table to be created. 

241 

242 Returns 

243 ------- 

244 records : `OpaqueTableStorage` 

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

246 layer. 

247 

248 Notes 

249 ----- 

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

251 """ 

252 raise NotImplementedError()