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

35 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-12 10:56 -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 

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 fetch(self, **where: Any) -> Iterator[Mapping[Any, Any]]: 

74 """Retrieve records from an opaque table. 

75 

76 Parameters 

77 ---------- 

78 **where 

79 Additional keyword arguments are interpreted as equality 

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

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

82 must have. 

83 

84 Yields 

85 ------ 

86 row : `dict` 

87 A dictionary representing a single result row. 

88 """ 

89 raise NotImplementedError() 

90 

91 @abstractmethod 

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

93 """Remove records from an opaque table. 

94 

95 Parameters 

96 ---------- 

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

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

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

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

101 *rows 

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

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

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

105 """ 

106 raise NotImplementedError() 

107 

108 name: str 

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

110 """ 

111 

112 

113class OpaqueTableStorageManager(VersionedExtension): 

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

115 

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

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

118 for a different (logical) opaque table. 

119 

120 Notes 

121 ----- 

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

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

124 limited to this. 

125 

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

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

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

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

130 """ 

131 

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

133 super().__init__(registry_schema_version=registry_schema_version) 

134 

135 @classmethod 

136 @abstractmethod 

137 def initialize( 

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

139 ) -> OpaqueTableStorageManager: 

140 """Construct an instance of the manager. 

141 

142 Parameters 

143 ---------- 

144 db : `Database` 

145 Interface to the underlying database engine and namespace. 

146 context : `StaticTablesContext` 

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

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

149 implemented with this manager. 

150 registry_schema_version : `VersionTuple` or `None` 

151 Schema version of this extension as defined in registry. 

152 

153 Returns 

154 ------- 

155 manager : `OpaqueTableStorageManager` 

156 An instance of a concrete `OpaqueTableStorageManager` subclass. 

157 """ 

158 raise NotImplementedError() 

159 

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

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

162 `None` on failure. 

163 """ 

164 r = self.get(name) 

165 if r is None: 

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

167 return r 

168 

169 @abstractmethod 

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

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

172 an opaque logical table. 

173 

174 Parameters 

175 ---------- 

176 name : `str` 

177 Name of the logical table. 

178 

179 Returns 

180 ------- 

181 records : `OpaqueTableStorage` or `None` 

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

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

184 layer. 

185 

186 Notes 

187 ----- 

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

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

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

191 obtained from an existing data repository. 

192 """ 

193 raise NotImplementedError() 

194 

195 @abstractmethod 

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

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

198 table, creating new tables as necessary. 

199 

200 Parameters 

201 ---------- 

202 name : `str` 

203 Name of the logical table. 

204 spec : `TableSpec` 

205 Schema specification for the table to be created. 

206 

207 Returns 

208 ------- 

209 records : `OpaqueTableStorage` 

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

211 layer. 

212 

213 Notes 

214 ----- 

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

216 """ 

217 raise NotImplementedError()