Coverage for python / lsst / daf / butler / tests / _examplePythonTypes.py: 34%
114 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 08:55 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 08:55 +0000
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/>.
28"""
29Python classes that can be used to test datastores without requiring
30large external dependencies on python classes such as afw or serialization
31formats such as FITS or HDF5.
32"""
34from __future__ import annotations
36__all__ = (
37 "ListDelegate",
38 "MetricsDelegate",
39 "MetricsExample",
40 "MetricsExampleDataclass",
41 "MetricsExampleModel",
42 "registerMetricsExample",
43)
46import copy
47import dataclasses
48import types
49import uuid
50from collections.abc import Mapping
51from typing import TYPE_CHECKING, Any
53from pydantic import BaseModel
55from lsst.daf.butler import DatasetProvenance, StorageClass, StorageClassDelegate
57if TYPE_CHECKING:
58 from lsst.daf.butler import Butler, Datastore, FormatterFactory
61def registerMetricsExample(butler: Butler) -> None:
62 """Modify a repository to support reading and writing
63 `MetricsExample` objects.
65 This method allows `MetricsExample` to be used with test repositories
66 in any package without needing to provide a custom configuration there.
68 Parameters
69 ----------
70 butler : `lsst.daf.butler.Butler`
71 The repository that needs to support `MetricsExample`.
73 Notes
74 -----
75 This method enables the following storage classes:
77 ``StructuredData``
78 A `MetricsExample` whose ``summary``, ``output``, and ``data`` members
79 can be retrieved as dataset components.
80 ``StructuredDataNoComponents``
81 A monolithic write of a `MetricsExample`.
83 These definitions must match the equivalent definitions in the test YAML
84 files.
85 """
86 yamlDict = _addFullStorageClass(
87 butler,
88 "StructuredDataDictYaml",
89 "lsst.daf.butler.formatters.yaml.YamlFormatter",
90 pytype=dict,
91 )
93 yamlList = _addFullStorageClass(
94 butler,
95 "StructuredDataListYaml",
96 "lsst.daf.butler.formatters.yaml.YamlFormatter",
97 pytype=list,
98 parameters={"slice"},
99 delegate="lsst.daf.butler.tests.ListDelegate",
100 )
102 _addFullStorageClass(
103 butler,
104 "StructuredDataNoComponents",
105 "lsst.daf.butler.formatters.pickle.PickleFormatter",
106 pytype=MetricsExample,
107 parameters={"slice"},
108 delegate="lsst.daf.butler.tests.MetricsDelegate",
109 converters={"dict": "lsst.daf.butler.tests.MetricsExample.makeFromDict"},
110 )
112 _addFullStorageClass(
113 butler,
114 "StructuredData",
115 "lsst.daf.butler.formatters.yaml.YamlFormatter",
116 pytype=MetricsExample,
117 components={
118 "summary": yamlDict,
119 "output": yamlDict,
120 "data": yamlList,
121 },
122 delegate="lsst.daf.butler.tests.MetricsDelegate",
123 )
126def _addFullStorageClass(butler: Butler, name: str, formatter: str, **kwargs: Any) -> StorageClass:
127 """Create a storage class-formatter pair in a repository if it does not
128 already exist.
130 Parameters
131 ----------
132 butler : `lsst.daf.butler.Butler`
133 The repository that needs to contain the class.
134 name : `str`
135 The name to use for the class.
136 formatter : `str`
137 The formatter to use with the storage class. Ignored if ``butler``
138 does not use formatters.
139 **kwargs
140 Arguments, other than ``name``, to the `~lsst.daf.butler.StorageClass`
141 constructor.
143 Returns
144 -------
145 class : `lsst.daf.butler.StorageClass`
146 The newly created storage class, or the class of the same name
147 previously found in the repository.
148 """
149 storageRegistry = butler._datastore.storageClassFactory
150 storage = StorageClass(name, **kwargs)
151 try:
152 storageRegistry.registerStorageClass(storage)
153 except ValueError:
154 storage = storageRegistry.getStorageClass(name)
156 for registry in _getAllFormatterRegistries(butler._datastore):
157 registry.registerFormatter(storage, formatter)
159 return storage
162def _getAllFormatterRegistries(datastore: Datastore) -> list[FormatterFactory]:
163 """Return all formatter registries used by a datastore.
165 Parameters
166 ----------
167 datastore : `lsst.daf.butler.Datastore`
168 A datastore containing zero or more formatter registries.
170 Returns
171 -------
172 registries : `list` [`lsst.daf.butler.FormatterFactory`]
173 A possibly empty list of all formatter registries used
174 by ``datastore``.
175 """
176 try:
177 datastores = datastore.datastores # type: ignore[attr-defined]
178 except AttributeError:
179 datastores = [datastore]
181 registries = []
182 for datastore in datastores:
183 try:
184 # Not all datastores have a formatterFactory
185 formatterRegistry = datastore.formatterFactory # type: ignore[attr-defined]
186 except AttributeError:
187 pass # no formatter needed
188 else:
189 registries.append(formatterRegistry)
190 return registries
193class MetricsExample:
194 """Smorgasboard of information that might be the result of some
195 processing.
197 Parameters
198 ----------
199 summary : `dict`
200 Simple dictionary mapping key performance metrics to a scalar
201 result.
202 output : `dict`
203 Structured nested data.
204 data : `list`, optional
205 Arbitrary array data.
206 """
208 def __init__(
209 self,
210 summary: dict[str, Any] | None = None,
211 output: dict[str, Any] | None = None,
212 data: list[Any] | None = None,
213 ) -> None:
214 self.summary = summary
215 self.output = output
216 self.data = data
218 def __eq__(self, other: Any) -> bool:
219 try:
220 return self.summary == other.summary and self.output == other.output and self.data == other.data
221 except AttributeError:
222 pass
223 return NotImplemented
225 def __str__(self) -> str:
226 return str(self.exportAsDict())
228 def __repr__(self) -> str:
229 return f"MetricsExample({self.exportAsDict()})"
231 def exportAsDict(self) -> dict[str, list | dict | None]:
232 """Convert object contents to a single python dict."""
233 exportDict: dict[str, list | dict | None] = {"summary": self.summary, "output": self.output}
234 if self.data is not None:
235 exportDict["data"] = list(self.data)
236 else:
237 exportDict["data"] = None
238 return exportDict
240 def _asdict(self) -> dict[str, list | dict | None]:
241 """Convert object contents to a single Python dict.
243 This interface is used for JSON serialization.
245 Returns
246 -------
247 exportDict : `dict`
248 Object contents in the form of a dict with keys corresponding
249 to object attributes.
250 """
251 return self.exportAsDict()
253 @classmethod
254 def makeFromDict(cls, exportDict: dict[str, list | dict | None]) -> MetricsExample:
255 """Create a new object from a dict that is compatible with that
256 created by `exportAsDict`.
258 Parameters
259 ----------
260 exportDict : `dict`
261 `dict` with keys "summary", "output", and (optionally) "data".
263 Returns
264 -------
265 newobject : `MetricsExample`
266 New `MetricsExample` object.
267 """
268 data = exportDict["data"] if "data" in exportDict else None
269 assert isinstance(data, list | types.NoneType)
270 assert isinstance(exportDict["summary"], dict | types.NoneType)
271 assert isinstance(exportDict["output"], dict | types.NoneType)
272 return cls(exportDict["summary"], exportDict["output"], data)
274 @classmethod
275 def from_model(cls, model: MetricsExampleModel) -> MetricsExample:
276 """Create metrics from Pydantic model.
278 Parameters
279 ----------
280 model : `MetricsExampleModel`
281 Source model.
283 Returns
284 -------
285 newobject : `MetricsExample`
286 New `MetricsExample` object.
287 """
288 return cls(model.summary, model.output, model.data)
291class MetricsExampleModel(BaseModel):
292 """A variant of `MetricsExample` based on model."""
294 summary: dict[str, Any] | None = None
295 output: dict[str, Any] | None = None
296 data: list[Any] | None = None
297 provenance: DatasetProvenance | None = None
298 dataset_id: uuid.UUID | None = None
300 @classmethod
301 def from_metrics(cls, metrics: MetricsExample) -> MetricsExampleModel:
302 """Create a model based on an example.
304 Parameters
305 ----------
306 metrics : `MetricsExample`
307 Metrics from which to construct the model.
309 Returns
310 -------
311 model : `MetricsExampleModel`
312 New model.
313 """
314 d = metrics.exportAsDict()
315 # Assume pydantic v2 but fallback to v1
316 try:
317 return cls.model_validate(d) # type: ignore
318 except AttributeError:
319 return cls.parse_obj(d)
322@dataclasses.dataclass
323class MetricsExampleDataclass:
324 """A variant of `MetricsExample` based on a dataclass."""
326 summary: dict[str, Any] | None
327 output: dict[str, Any] | None
328 data: list[Any] | None
331class ListDelegate(StorageClassDelegate):
332 """Parameter handler for list parameters."""
334 def handleParameters(self, inMemoryDataset: Any, parameters: Mapping[str, Any] | None = None) -> Any:
335 """Modify the in-memory dataset using the supplied parameters,
336 returning a possibly new object.
338 Parameters
339 ----------
340 inMemoryDataset : `object`
341 Object to modify based on the parameters.
342 parameters : `dict`
343 Parameters to apply. Values are specific to the parameter.
344 Supported parameters are defined in the associated
345 `StorageClass`. If no relevant parameters are specified the
346 inMemoryDataset will be return unchanged.
348 Returns
349 -------
350 inMemoryDataset : `object`
351 Updated form of supplied in-memory dataset, after parameters
352 have been used.
353 """
354 inMemoryDataset = copy.deepcopy(inMemoryDataset)
355 use = self.storageClass.filterParameters(parameters, subset={"slice"})
356 if use:
357 inMemoryDataset = inMemoryDataset[use["slice"]]
358 return inMemoryDataset
361class MetricsDelegate(StorageClassDelegate):
362 """Parameter handler for parameters using Metrics."""
364 def handleParameters(self, inMemoryDataset: Any, parameters: Mapping[str, Any] | None = None) -> Any:
365 """Modify the in-memory dataset using the supplied parameters,
366 returning a possibly new object.
368 Parameters
369 ----------
370 inMemoryDataset : `object`
371 Object to modify based on the parameters.
372 parameters : `dict`
373 Parameters to apply. Values are specific to the parameter.
374 Supported parameters are defined in the associated
375 `StorageClass`. If no relevant parameters are specified the
376 inMemoryDataset will be return unchanged.
378 Returns
379 -------
380 inMemoryDataset : `object`
381 Updated form of supplied in-memory dataset, after parameters
382 have been used.
383 """
384 inMemoryDataset = copy.deepcopy(inMemoryDataset)
385 use = self.storageClass.filterParameters(parameters, subset={"slice"})
386 if use:
387 inMemoryDataset.data = inMemoryDataset.data[use["slice"]]
388 return inMemoryDataset
390 def getComponent(self, composite: Any, componentName: str) -> Any:
391 if componentName == "counter":
392 return len(composite.data)
393 return super().getComponent(composite, componentName)
395 @classmethod
396 def selectResponsibleComponent(cls, readComponent: str, fromComponents: set[str | None]) -> str:
397 forwarderMap = {
398 "counter": "data",
399 }
400 forwarder = forwarderMap.get(readComponent)
401 if forwarder is not None and forwarder in fromComponents:
402 return forwarder
403 raise ValueError(f"Can not calculate read component {readComponent} from {fromComponents}")