Coverage for python/lsst/daf/butler/registry/datasets/byDimensions/_dataset_type_cache.py: 79%

64 statements  

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

28from __future__ import annotations 

29 

30__all__ = ("DatasetTypeCache",) 

31 

32from collections.abc import Iterable, Iterator 

33 

34from ...._dataset_type import DatasetType 

35from ....dimensions import DimensionGroup 

36from .tables import DynamicTables, TableCache 

37 

38 

39class DatasetTypeCache: 

40 """Cache for dataset types. 

41 

42 Notes 

43 ----- 

44 This cache is a pair of mappings with different kinds of keys: 

45 

46 - Dataset type name -> (`DatasetType`, database integer primary key) 

47 - `DimensionGroup` -> database table information 

48 

49 In some contexts (e.g. ``resolve_wildcard``) a full list of dataset types 

50 is needed. To signify that cache content can be used in such contexts, 

51 cache defines a special ``full`` flag that needs to be set by client. The 

52 ``dimensions_full`` flag similarly reports whether all per-dimension-group 

53 state is present in the cache. 

54 """ 

55 

56 def __init__(self) -> None: 

57 self.tables = TableCache() 

58 self._by_name_cache: dict[str, tuple[DatasetType, int]] = {} 

59 self._by_dimensions_cache: dict[DimensionGroup, DynamicTables] = {} 

60 self._full = False 

61 self._dimensions_full = False 

62 

63 def clone(self) -> DatasetTypeCache: 

64 """Make a copy of the caches that are safe to use in another thread. 

65 

66 Notes 

67 ----- 

68 After cloning, the ``tables`` cache will be shared between the new 

69 instance and the current instance. It is safe to read and update 

70 ``tables`` from multiple threads simultaneously -- the cached values 

71 are immutable table schemas, and they are looked up one at a time by 

72 name. 

73 

74 The other caches are copied, because their access patterns are more 

75 complex. 

76 

77 ``full`` and ``dimensions_full`` will initially return `False` in the 

78 new instance. This preserves the invariant that a Butler is able to 

79 see any changes to the database made before the Butler is instantiated. 

80 The downside is that the cloned cache will have to be re-fetched before 

81 it can be used for glob searches. 

82 """ 

83 clone = DatasetTypeCache() 

84 # Share DynamicTablesCache between instances. 

85 clone.tables = self.tables 

86 # The inner key/value objects are immutable in both of these caches, so 

87 # we can shallow-copy the dicts. 

88 clone._by_name_cache = self._by_name_cache.copy() 

89 clone._by_dimensions_cache = self._by_dimensions_cache.copy() 

90 return clone 

91 

92 @property 

93 def full(self) -> bool: 

94 """`True` if cache holds all known dataset types (`bool`).""" 

95 return self._full 

96 

97 @property 

98 def dimensions_full(self) -> bool: 

99 """`True` if cache holds all known dataset type dimensions (`bool`).""" 

100 return self._dimensions_full 

101 

102 def add(self, dataset_type: DatasetType, id: int) -> None: 

103 """Add one record to the cache. 

104 

105 Parameters 

106 ---------- 

107 dataset_type : `DatasetType` 

108 Dataset type, replaces any existing dataset type with the same 

109 name. 

110 id : `int` 

111 The dataset type primary key. 

112 Additional opaque object stored with this dataset type. 

113 """ 

114 self._by_name_cache[dataset_type.name] = (dataset_type, id) 

115 

116 def set( 

117 self, 

118 data: Iterable[tuple[DatasetType, int]], 

119 *, 

120 full: bool = False, 

121 dimensions_data: Iterable[tuple[DimensionGroup, DynamicTables]] | None = None, 

122 dimensions_full: bool = False, 

123 ) -> None: 

124 """Replace cache contents with the new set of dataset types. 

125 

126 Parameters 

127 ---------- 

128 data : `~collections.abc.Iterable` 

129 Sequence of tuples of `DatasetType` and an extra opaque object. 

130 full : `bool`, optional 

131 If `True` then ``data`` contains all known dataset types. 

132 dimensions_data : `~collections.abc.Iterable`, optional 

133 Sequence of tuples of `DimensionGroup` and an extra opaque object. 

134 dimensions_full : `bool`, optional 

135 If `True` then ``data`` contains all known dataset type dimensions. 

136 """ 

137 self.clear() 

138 for item in data: 

139 self._by_name_cache[item[0].name] = item 

140 self._full = full 

141 if dimensions_data is not None: 141 ↛ exitline 141 didn't return from function 'set' because the condition on line 141 was always true

142 self._by_dimensions_cache.update(dimensions_data) 

143 self._dimensions_full = dimensions_full 

144 

145 def clear(self) -> None: 

146 """Remove everything from the cache.""" 

147 self._by_name_cache = {} 

148 self._by_dimensions_cache = {} 

149 self._full = False 

150 self._dimensions_full = False 

151 

152 def discard(self, name: str) -> None: 

153 """Remove named dataset type from the cache. 

154 

155 Parameters 

156 ---------- 

157 name : `str` 

158 Name of the dataset type to remove. 

159 """ 

160 self._by_name_cache.pop(name, None) 

161 

162 def get(self, name: str) -> tuple[DatasetType | None, int | None]: 

163 """Return cached info given dataset type name. 

164 

165 Parameters 

166 ---------- 

167 name : `str` 

168 Dataset type name. 

169 

170 Returns 

171 ------- 

172 dataset_type : `DatasetType` or `None` 

173 Cached dataset type, `None` is returned if the name is not in the 

174 cache. 

175 extra : `Any` or `None` 

176 Cached opaque data, `None` is returned if the name is not in the 

177 cache. 

178 """ 

179 item = self._by_name_cache.get(name) 

180 if item is None: 

181 return (None, None) 

182 return item 

183 

184 def get_dataset_type(self, name: str) -> DatasetType | None: 

185 """Return dataset type given its name. 

186 

187 Parameters 

188 ---------- 

189 name : `str` 

190 Dataset type name. 

191 

192 Returns 

193 ------- 

194 dataset_type : `DatasetType` or `None` 

195 Cached dataset type, `None` is returned if the name is not in the 

196 cache. 

197 """ 

198 item = self._by_name_cache.get(name) 

199 if item is None: 

200 return None 

201 return item[0] 

202 

203 def items(self) -> Iterator[tuple[DatasetType, int]]: 

204 """Return iterator for the set of items in the cache, can only be 

205 used if `full` is true. 

206 

207 Returns 

208 ------- 

209 iter : `~collections.abc.Iterator` 

210 Iterator over tuples of `DatasetType` and opaque data. 

211 

212 Raises 

213 ------ 

214 RuntimeError 

215 Raised if ``self.full`` is `False`. 

216 """ 

217 if not self._full: 217 ↛ 218line 217 didn't jump to line 218 because the condition on line 217 was never true

218 raise RuntimeError("cannot call items() if cache is not full") 

219 return iter(self._by_name_cache.values()) 

220 

221 def add_by_dimensions(self, dimensions: DimensionGroup, tables: DynamicTables) -> None: 

222 """Add information about a set of dataset type dimensions to the cache. 

223 

224 Parameters 

225 ---------- 

226 dimensions : `DimensionGroup` 

227 Dimensions of one or more dataset types. 

228 tables : `DynamicTables` 

229 Additional opaque object stored with these dimensions. 

230 """ 

231 self._by_dimensions_cache[dimensions] = tables 

232 

233 def get_by_dimensions(self, dimensions: DimensionGroup) -> DynamicTables | None: 

234 """Get information about a set of dataset type dimensions. 

235 

236 Parameters 

237 ---------- 

238 dimensions : `DimensionGroup` 

239 Dimensions of one or more dataset types. 

240 

241 Returns 

242 ------- 

243 tables : `DynamicTables` or `None` 

244 Additional opaque object stored with these dimensions, or `None` if 

245 these dimensions are not present in the cache. 

246 """ 

247 return self._by_dimensions_cache.get(dimensions) 

248 

249 def by_dimensions_items(self) -> Iterator[tuple[DimensionGroup, DynamicTables]]: 

250 """Return iterator for all dimensions-keyed data in the cache. 

251 

252 This can only be called if `dimensions_full` is `True`. 

253 

254 Returns 

255 ------- 

256 iter : `~collections.abc.Iterator` 

257 Iterator over tuples of `DimensionGroup` and opaque data. 

258 

259 Raises 

260 ------ 

261 RuntimeError 

262 Raised if ``self.dimensions_full`` is `False`. 

263 """ 

264 if not self._dimensions_full: 

265 raise RuntimeError("cannot call by_dimensions_items() if cache does not have full dimensions.") 

266 return iter(self._by_dimensions_cache.items())