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

29 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-10-11 01:27 -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 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 ._collections import CollectionRecord 

50 from ._database import Database, StaticTablesContext 

51 from ._datasets import DatasetRecordStorageManager 

52 from ._dimensions import DimensionRecordStorageManager 

53 

54 

55class ObsCoreTableManager(VersionedExtension): 

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

57 

58 Parameters 

59 ---------- 

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

61 Version of registry schema. 

62 """ 

63 

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

65 super().__init__(registry_schema_version=registry_schema_version) 

66 

67 @abstractmethod 

68 def clone( 

69 self, 

70 *, 

71 db: Database, 

72 dimensions: DimensionRecordStorageManager, 

73 ) -> ObsCoreTableManager: 

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

75 instances of `Database` and other managers. 

76 

77 Parameters 

78 ---------- 

79 db : `Database` 

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

81 dimensions : `DimensionRecordStorageManager` 

82 New `DimensionRecordStorageManager` object to use when 

83 instantiating the manager. 

84 

85 Returns 

86 ------- 

87 instance : `ObsCoreTableManager` 

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

89 but bound to a new Database object. 

90 """ 

91 raise NotImplementedError() 

92 

93 @classmethod 

94 @abstractmethod 

95 def initialize( 

96 cls, 

97 db: Database, 

98 context: StaticTablesContext, 

99 *, 

100 universe: DimensionUniverse, 

101 config: Mapping, 

102 datasets: type[DatasetRecordStorageManager], 

103 dimensions: DimensionRecordStorageManager, 

104 registry_schema_version: VersionTuple | None = None, 

105 column_type_info: ColumnTypeInfo, 

106 ) -> ObsCoreTableManager: 

107 """Construct an instance of the manager. 

108 

109 Parameters 

110 ---------- 

111 db : `Database` 

112 Interface to the underlying database engine and namespace. 

113 context : `StaticTablesContext` 

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

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

116 implemented with this manager. 

117 universe : `DimensionUniverse` 

118 All dimensions known to the registry. 

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

120 Configuration of the obscore manager. 

121 datasets : `type` 

122 Type of dataset manager. 

123 dimensions : `DimensionRecordStorageManager` 

124 Manager for Registry dimensions. 

125 registry_schema_version : `VersionTuple` or `None` 

126 Schema version of this extension as defined in registry. 

127 column_type_info : `ColumnTypeInfo` 

128 Information about column types that can differ between data 

129 repositories and registry instances. 

130 

131 Returns 

132 ------- 

133 manager : `ObsCoreTableManager` 

134 An instance of a concrete `ObsCoreTableManager` subclass. 

135 """ 

136 raise NotImplementedError() 

137 

138 @abstractmethod 

139 def config_json(self) -> str: 

140 """Dump configuration in JSON format. 

141 

142 Returns 

143 ------- 

144 json : `str` 

145 Configuration serialized in JSON format. 

146 """ 

147 raise NotImplementedError() 

148 

149 @abstractmethod 

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

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

152 

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

154 collection. 

155 

156 Parameters 

157 ---------- 

158 refs : `iterable` [ `DatasetRef` ] 

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

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

161 the dataset is ignored. 

162 

163 Returns 

164 ------- 

165 count : `int` 

166 Actual number of records inserted into obscore table. 

167 

168 Notes 

169 ----- 

170 Dataset data types and collection names are checked against configured 

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

172 ignored and not added to the obscore table. 

173 

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

175 method should return immediately. 

176 

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

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

179 dataset table with "ON DELETE CASCADE" option. 

180 """ 

181 raise NotImplementedError() 

182 

183 @abstractmethod 

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

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

186 

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

188 a TAGGED collection. 

189 

190 Parameters 

191 ---------- 

192 refs : `iterable` [ `DatasetRef` ] 

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

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

195 the dataset is ignored. 

196 collection : `CollectionRecord` 

197 Collection record for a TAGGED collection. 

198 

199 Returns 

200 ------- 

201 count : `int` 

202 Actual number of records inserted into obscore table. 

203 

204 Notes 

205 ----- 

206 Dataset data types and collection names are checked against configured 

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

208 ignored and not added to the obscore table. 

209 

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

211 method should return immediately. 

212 """ 

213 raise NotImplementedError() 

214 

215 @abstractmethod 

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

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

218 

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

220 TAGGED collection. 

221 

222 Parameters 

223 ---------- 

224 refs : `iterable` [ `DatasetRef` ] 

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

226 collection : `CollectionRecord` 

227 Collection record for a TAGGED collection. 

228 

229 Returns 

230 ------- 

231 count : `int` 

232 Actual number of records removed from obscore table. 

233 

234 Notes 

235 ----- 

236 Dataset data types and collection names are checked against configured 

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

238 ignored and not added to the obscore table. 

239 

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

241 method should return immediately. 

242 """ 

243 raise NotImplementedError() 

244 

245 @abstractmethod 

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

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

248 

249 Parameters 

250 ---------- 

251 instrument : `str` 

252 Instrument name. 

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

254 `~lsst.sphgeom.Region` ]] 

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

256 detector ID, and corresponding region. 

257 

258 Returns 

259 ------- 

260 count : `int` 

261 Actual number of records updated. 

262 

263 Notes 

264 ----- 

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

266 are ingested before their corresponding visits are defined. Exposure 

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

268 from their matching visits automatically. 

269 """ 

270 raise NotImplementedError() 

271 

272 @abstractmethod 

273 @contextmanager 

274 def query( 

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

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

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

278 

279 Parameters 

280 ---------- 

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

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

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

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

285 **kwargs 

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

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

288 restrictions are ANDed together. 

289 

290 Returns 

291 ------- 

292 result_context : `sqlalchemy.engine.CursorResult` 

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

294 These results are invalidated when the context is exited. 

295 """ 

296 raise NotImplementedError()