Coverage for python/lsst/daf/butler/registry/datasets/byDimensions/_dataset_type_cache.py: 79%
64 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 01:32 -0800
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 01:32 -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/>.
28from __future__ import annotations
30__all__ = ("DatasetTypeCache",)
32from collections.abc import Iterable, Iterator
34from ...._dataset_type import DatasetType
35from ....dimensions import DimensionGroup
36from .tables import DynamicTables, TableCache
39class DatasetTypeCache:
40 """Cache for dataset types.
42 Notes
43 -----
44 This cache is a pair of mappings with different kinds of keys:
46 - Dataset type name -> (`DatasetType`, database integer primary key)
47 - `DimensionGroup` -> database table information
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 """
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
63 def clone(self) -> DatasetTypeCache:
64 """Make a copy of the caches that are safe to use in another thread.
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.
74 The other caches are copied, because their access patterns are more
75 complex.
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
92 @property
93 def full(self) -> bool:
94 """`True` if cache holds all known dataset types (`bool`)."""
95 return self._full
97 @property
98 def dimensions_full(self) -> bool:
99 """`True` if cache holds all known dataset type dimensions (`bool`)."""
100 return self._dimensions_full
102 def add(self, dataset_type: DatasetType, id: int) -> None:
103 """Add one record to the cache.
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)
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.
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
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
152 def discard(self, name: str) -> None:
153 """Remove named dataset type from the cache.
155 Parameters
156 ----------
157 name : `str`
158 Name of the dataset type to remove.
159 """
160 self._by_name_cache.pop(name, None)
162 def get(self, name: str) -> tuple[DatasetType | None, int | None]:
163 """Return cached info given dataset type name.
165 Parameters
166 ----------
167 name : `str`
168 Dataset type name.
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
184 def get_dataset_type(self, name: str) -> DatasetType | None:
185 """Return dataset type given its name.
187 Parameters
188 ----------
189 name : `str`
190 Dataset type name.
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]
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.
207 Returns
208 -------
209 iter : `~collections.abc.Iterator`
210 Iterator over tuples of `DatasetType` and opaque data.
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())
221 def add_by_dimensions(self, dimensions: DimensionGroup, tables: DynamicTables) -> None:
222 """Add information about a set of dataset type dimensions to the cache.
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
233 def get_by_dimensions(self, dimensions: DimensionGroup) -> DynamicTables | None:
234 """Get information about a set of dataset type dimensions.
236 Parameters
237 ----------
238 dimensions : `DimensionGroup`
239 Dimensions of one or more dataset types.
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)
249 def by_dimensions_items(self) -> Iterator[tuple[DimensionGroup, DynamicTables]]:
250 """Return iterator for all dimensions-keyed data in the cache.
252 This can only be called if `dimensions_full` is `True`.
254 Returns
255 -------
256 iter : `~collections.abc.Iterator`
257 Iterator over tuples of `DimensionGroup` and opaque data.
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())