Coverage for python/lsst/daf/butler/registry/interfaces/_obscore.py: 94%

32 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-04 01:36 -0800

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 classes that manage obscore table(s) in a `Registry`.""" 

29 

30from __future__ import annotations 

31 

32__all__ = ["ObsCoreTableManager"] 

33 

34from abc import abstractmethod 

35from collections.abc import Iterable, Iterator, Mapping 

36from contextlib import contextmanager 

37from typing import TYPE_CHECKING, Any 

38 

39import sqlalchemy 

40 

41from ._versioning import VersionedExtension, VersionTuple 

42 

43if TYPE_CHECKING: 

44 from lsst.sphgeom import Region 

45 

46 from ..._column_type_info import ColumnTypeInfo 

47 from ..._dataset_ref import DatasetRef 

48 from ...dimensions import DimensionUniverse 

49 from ...queries import QueryFactoryFunction 

50 from ._collections import CollectionRecord 

51 from ._database import Database, StaticTablesContext 

52 from ._datasets import DatasetRecordStorageManager 

53 from ._dimensions import DimensionRecordStorageManager 

54 

55 

56class ObsCoreTableManager(VersionedExtension): 

57 """An interface for populating ObsCore tables(s). 

58 

59 Parameters 

60 ---------- 

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

62 Version of registry schema. 

63 """ 

64 

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

66 super().__init__(registry_schema_version=registry_schema_version) 

67 

68 @abstractmethod 

69 def clone( 

70 self, 

71 *, 

72 db: Database, 

73 dimensions: DimensionRecordStorageManager, 

74 ) -> ObsCoreTableManager: 

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

76 instances of `Database` and other managers. 

77 

78 Parameters 

79 ---------- 

80 db : `Database` 

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

82 dimensions : `DimensionRecordStorageManager` 

83 New `DimensionRecordStorageManager` object to use when 

84 instantiating the manager. 

85 

86 Returns 

87 ------- 

88 instance : `ObsCoreTableManager` 

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

90 but bound to a new Database object. 

91 """ 

92 raise NotImplementedError() 

93 

94 @classmethod 

95 @abstractmethod 

96 def initialize( 

97 cls, 

98 db: Database, 

99 context: StaticTablesContext, 

100 *, 

101 universe: DimensionUniverse, 

102 config: Mapping, 

103 datasets: type[DatasetRecordStorageManager], 

104 dimensions: DimensionRecordStorageManager, 

105 registry_schema_version: VersionTuple | None = None, 

106 column_type_info: ColumnTypeInfo, 

107 ) -> ObsCoreTableManager: 

108 """Construct an instance of the manager. 

109 

110 Parameters 

111 ---------- 

112 db : `Database` 

113 Interface to the underlying database engine and namespace. 

114 context : `StaticTablesContext` 

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

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

117 implemented with this manager. 

118 universe : `DimensionUniverse` 

119 All dimensions known to the registry. 

120 config : `dict` [ `str`, `Any` ] 

121 Configuration of the obscore manager. 

122 datasets : `type` 

123 Type of dataset manager. 

124 dimensions : `DimensionRecordStorageManager` 

125 Manager for Registry dimensions. 

126 registry_schema_version : `VersionTuple` or `None` 

127 Schema version of this extension as defined in registry. 

128 column_type_info : `ColumnTypeInfo` 

129 Information about column types that can differ between data 

130 repositories and registry instances. 

131 

132 Returns 

133 ------- 

134 manager : `ObsCoreTableManager` 

135 An instance of a concrete `ObsCoreTableManager` subclass. 

136 """ 

137 raise NotImplementedError() 

138 

139 @abstractmethod 

140 def set_query_function(self, query_func: QueryFactoryFunction) -> None: 

141 """Set up a function to be used for querying the database. This must 

142 be called before attempting to insert datasets. 

143 

144 Parameters 

145 ---------- 

146 query_func : `QueryFactoryFunction` 

147 Function returning a context manager that sets up a `Query` object 

148 for querying the registry. (That is, a function equivalent to 

149 ``Butler.query()``). 

150 """ 

151 pass 

152 

153 @abstractmethod 

154 def config_json(self) -> str: 

155 """Dump configuration in JSON format. 

156 

157 Returns 

158 ------- 

159 json : `str` 

160 Configuration serialized in JSON format. 

161 """ 

162 raise NotImplementedError() 

163 

164 @abstractmethod 

165 def add_datasets(self, refs: Iterable[DatasetRef]) -> int: 

166 """Possibly add datasets to the obscore table. 

167 

168 This method should be called when new datasets are added to a RUN 

169 collection. 

170 

171 Parameters 

172 ---------- 

173 refs : `iterable` [ `DatasetRef` ] 

174 Dataset refs to add. Dataset refs have to be completely expanded. 

175 If a record with the same dataset ID is already in obscore table, 

176 the dataset is ignored. 

177 

178 Returns 

179 ------- 

180 count : `int` 

181 Actual number of records inserted into obscore table. 

182 

183 Notes 

184 ----- 

185 Dataset data types and collection names are checked against configured 

186 list of collections and dataset types, non-matching datasets are 

187 ignored and not added to the obscore table. 

188 

189 When configuration parameter ``collection_type`` is not "RUN", this 

190 method should return immediately. 

191 

192 Note that there is no matching method to remove datasets from obscore 

193 table, we assume that removal happens via foreign key constraint to 

194 dataset table with "ON DELETE CASCADE" option. 

195 """ 

196 raise NotImplementedError() 

197 

198 @abstractmethod 

199 def associate(self, refs: Iterable[DatasetRef], collection: CollectionRecord) -> int: 

200 """Possibly add datasets to the obscore table. 

201 

202 This method should be called when existing datasets are associated with 

203 a TAGGED collection. 

204 

205 Parameters 

206 ---------- 

207 refs : `iterable` [ `DatasetRef` ] 

208 Dataset refs to add. Dataset refs have to be completely expanded. 

209 If a record with the same dataset ID is already in obscore table, 

210 the dataset is ignored. 

211 collection : `CollectionRecord` 

212 Collection record for a TAGGED collection. 

213 

214 Returns 

215 ------- 

216 count : `int` 

217 Actual number of records inserted into obscore table. 

218 

219 Notes 

220 ----- 

221 Dataset data types and collection names are checked against configured 

222 list of collections and dataset types, non-matching datasets are 

223 ignored and not added to the obscore table. 

224 

225 When configuration parameter ``collection_type`` is not "TAGGED", this 

226 method should return immediately. 

227 """ 

228 raise NotImplementedError() 

229 

230 @abstractmethod 

231 def disassociate(self, refs: Iterable[DatasetRef], collection: CollectionRecord) -> int: 

232 """Possibly remove datasets from the obscore table. 

233 

234 This method should be called when datasets are disassociated from a 

235 TAGGED collection. 

236 

237 Parameters 

238 ---------- 

239 refs : `iterable` [ `DatasetRef` ] 

240 Dataset refs to remove. Dataset refs have to be resolved. 

241 collection : `CollectionRecord` 

242 Collection record for a TAGGED collection. 

243 

244 Returns 

245 ------- 

246 count : `int` 

247 Actual number of records removed from obscore table. 

248 

249 Notes 

250 ----- 

251 Dataset data types and collection names are checked against configured 

252 list of collections and dataset types, non-matching datasets are 

253 ignored and not added to the obscore table. 

254 

255 When configuration parameter ``collection_type`` is not "TAGGED", this 

256 method should return immediately. 

257 """ 

258 raise NotImplementedError() 

259 

260 @abstractmethod 

261 def update_exposure_regions(self, instrument: str, region_data: Iterable[tuple[int, int, Region]]) -> int: 

262 """Update existing exposure records with spatial region data. 

263 

264 Parameters 

265 ---------- 

266 instrument : `str` 

267 Instrument name. 

268 region_data : `~collections.abc.Iterable` [`tuple` [`int`, `int`, \ 

269 `~lsst.sphgeom.Region` ]] 

270 Sequence of tuples, each tuple contains three values - exposure ID, 

271 detector ID, and corresponding region. 

272 

273 Returns 

274 ------- 

275 count : `int` 

276 Actual number of records updated. 

277 

278 Notes 

279 ----- 

280 This method is needed to update obscore records for raw exposures which 

281 are ingested before their corresponding visits are defined. Exposure 

282 records added when visit is already defined will get their regions 

283 from their matching visits automatically. 

284 """ 

285 raise NotImplementedError() 

286 

287 @abstractmethod 

288 @contextmanager 

289 def query( 

290 self, columns: Iterable[str | sqlalchemy.sql.expression.ColumnElement] | None = None, /, **kwargs: Any 

291 ) -> Iterator[sqlalchemy.engine.CursorResult]: 

292 """Run a SELECT query against obscore table and return result rows. 

293 

294 Parameters 

295 ---------- 

296 columns : `~collections.abc.Iterable` [`str`] 

297 Columns to return from query. It is a sequence which can include 

298 column names or any other column elements (e.g. 

299 `sqlalchemy.sql.functions.count` function). 

300 **kwargs 

301 Restriction on values of individual obscore columns. Key is the 

302 column name, value is the required value of the column. Multiple 

303 restrictions are ANDed together. 

304 

305 Returns 

306 ------- 

307 result_context : `sqlalchemy.engine.CursorResult` 

308 Context manager that returns the query result object when entered. 

309 These results are invalidated when the context is exited. 

310 """ 

311 raise NotImplementedError()