Coverage for python/lsst/images/serialization/_input_archive.py: 100%
53 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-23 08:41 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-23 08:41 +0000
1# This file is part of lsst-images.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14__all__ = ("ArchiveInfo", "InputArchive")
16from abc import ABC, abstractmethod
17from collections.abc import Callable
18from contextlib import AbstractContextManager
19from types import EllipsisType
20from typing import TYPE_CHECKING, Any, TypeVar
22import astropy.io.fits
23import astropy.table
24import astropy.units
25import numpy as np
26import pydantic
28from lsst.resources import ResourcePath, ResourcePathExpression
30from ._asdf_utils import ArrayReferenceModel, InlineArrayModel
31from ._common import SCHEMA_URL_HOST, ArchiveTree, OpaqueArchiveMetadata, no_header_updates
32from ._tables import TableModel
34if TYPE_CHECKING:
35 from .._transforms import FrameSet
38# This pre-python-3.12 declaration is needed by Sphinx (probably the
39# autodoc-typehints plugin.
40P = TypeVar("P", bound=pydantic.BaseModel)
43class ArchiveInfo(pydantic.BaseModel, frozen=True):
44 """Basic identifying information about an on-disk archive.
46 Read from a file's headers/metadata without deserializing pixel data.
47 """
49 schema_url: str
50 """Canonical schema URL of the top-level tree."""
52 schema_name: str
53 """Schema name parsed from ``schema_url``."""
55 schema_version: str
56 """Schema version parsed from ``schema_url``."""
58 format_version: int | None
59 """Container layout version (FITS ``FMTVER`` / NDF ``FORMAT_VERSION``);
60 `None` for formats with no separate container version (JSON)."""
62 @classmethod
63 def from_schema_url(cls, schema_url: str, *, format_version: int | None) -> ArchiveInfo:
64 """Build an `ArchiveInfo` by parsing a schema URL of the form
65 ``https://images.lsst.io/schemas/{name}-{version}``.
67 The URL is parsed with `~lsst.resources.ResourcePath` and its
68 hostname must be ``images.lsst.io``, so a ``DATAMODL`` header written
69 by an unrelated tool cannot steer reads toward an arbitrary schema.
70 """
71 parsed = ResourcePath(schema_url)
72 if parsed.netloc != SCHEMA_URL_HOST:
73 raise ValueError(
74 f"Schema URL {schema_url!r} is not hosted at {SCHEMA_URL_HOST!r}; "
75 "this file was not written by lsst.images."
76 )
77 tail = parsed.basename()
78 # Split on the last hyphen: schema names may contain hyphens; the
79 # version (after the final hyphen) is assumed not to.
80 name, _, version = tail.rpartition("-")
81 if not name or not version:
82 raise ValueError(f"Cannot parse schema name/version from URL {schema_url!r}.")
83 return cls(
84 schema_url=schema_url,
85 schema_name=name,
86 schema_version=version,
87 format_version=format_version,
88 )
91class InputArchive[P: pydantic.BaseModel](ABC):
92 """Abstract interface for reading from a file format.
94 Notes
95 -----
96 An input archive instance is assumed to be paired with a Pydantic model
97 that represents a JSON tree, with the archive used to deserialize data that
98 is not native JSON from data that is (which may just be a reference to
99 binary data stored elsewhere in the file). The archive doesn't actually
100 hold that model instance because we'd prefer to avoid making the input
101 archive generic over the model type. It is expected that most concrete
102 archive implementations will provide a method to load the paired model from
103 a file, but this is not part of the base class interface.
104 """
106 @classmethod
107 def get_basic_info(cls, path: ResourcePathExpression) -> ArchiveInfo:
108 """Return basic identifying information for the archive at ``path``
109 without deserializing pixel data.
111 Each concrete backend reads only the headers/metadata it needs.
112 """
113 raise NotImplementedError(f"{cls.__name__} does not implement get_basic_info.")
115 @classmethod
116 def open_tree(
117 cls,
118 path: ResourcePathExpression,
119 *,
120 partial: bool = True,
121 **backend_kwargs: Any,
122 ) -> AbstractContextManager[tuple[InputArchive[P], ArchiveTree, ArchiveInfo]]:
123 """Open ``path``, load and validate its top-level tree, and yield
124 ``(archive, tree, info)`` as a context manager.
126 Parameters
127 ----------
128 path
129 File to be opened. Can be local or remote.
130 partial
131 Whether the file should be opened for incremental reads or not.
132 Can be ignored by a backend where not relevant.
133 **backend_kwargs
134 Any keyword parameters that should be forwarded to the backend
135 open.
137 Raises
138 ------
139 ArchiveReadError
140 If the file's schema is not registered.
142 Notes
143 -----
144 Each concrete backend implements this.
145 """
146 raise NotImplementedError(f"{cls.__name__} does not implement open_tree.")
148 @abstractmethod
149 def deserialize_pointer[U: ArchiveTree, V](
150 self, pointer: P, model_type: type[U], deserializer: Callable[[U, InputArchive[P]], V]
151 ) -> V:
152 """Deserialize an object that was saved by
153 `~lsst.serialization.OutputArchive.serialize_pointer`.
155 Parameters
156 ----------
157 pointer
158 JSON Pointer model to dereference.
159 model_type
160 Pydantic model type that the pointer should dereference to.
161 deserializer
162 Callable that takes an instance of ``model_type`` and an input
163 archive, and returns the deserialized object.
165 Returns
166 -------
167 V
168 The deserialized object.
170 Notes
171 -----
172 Implementations are required to remember previously-deserialized
173 objects and return them when the same pointer is passed in multiple
174 times.
176 There is no ``deserialize_direct`` (to pair with
177 `~lsst.serialization.OutputArchive.serialize_direct`) because the
178 caller can just call a deserializer function directly on a sub-model
179 of its Pydantic tree.
180 """
181 raise NotImplementedError()
183 @abstractmethod
184 def get_frame_set(self, ref: P) -> FrameSet:
185 """Return an already-deserialized frame set from the archive.
187 Parameters
188 ----------
189 ref
190 Implementation-specific reference to the frame set.
192 Returns
193 -------
194 FrameSet
195 Loaded frame set.
196 """
197 raise NotImplementedError()
199 @abstractmethod
200 def get_array(
201 self,
202 model: ArrayReferenceModel | InlineArrayModel,
203 *,
204 slices: tuple[slice, ...] | EllipsisType = ...,
205 strip_header: Callable[[astropy.io.fits.Header], None] = no_header_updates,
206 ) -> np.ndarray:
207 """Load an array from the archive.
209 Parameters
210 ----------
211 model
212 A Pydantic model that references or holds the array.
213 slices
214 Slices that specify a subset of the original array to read.
215 strip_header
216 A callable that strips out any FITS header cards added by the
217 ``update_header`` argument in the corresponding call to
218 `~lsst.images.serialization.OutputArchive.add_array`.
219 """
220 raise NotImplementedError()
222 @abstractmethod
223 def get_table(
224 self,
225 model: TableModel,
226 strip_header: Callable[[astropy.io.fits.Header], None] = no_header_updates,
227 ) -> astropy.table.Table:
228 """Load a table from the archive.
230 Parameters
231 ----------
232 model
233 A Pydantic model that references or holds the table.
234 strip_header
235 A callable that strips out any FITS header cards added by the
236 ``update_header`` argument in the corresponding call to
237 `~lsst.serialization.OutputArchive.add_table`.
239 Returns
240 -------
241 astropy.table.Table
242 The loaded table.
243 """
244 raise NotImplementedError()
246 @abstractmethod
247 def get_structured_array(
248 self,
249 model: TableModel,
250 strip_header: Callable[[astropy.io.fits.Header], None] = no_header_updates,
251 ) -> np.ndarray:
252 """Load a table from the archive as a structured array.
254 Parameters
255 ----------
256 model
257 A Pydantic model that references or holds the table.
258 strip_header
259 A callable that strips out any FITS header cards added by the
260 ``update_header`` argument in the corresponding call to
261 `~lsst.serialization.OutputArchive.add_structured_array`.
263 Returns
264 -------
265 numpy.ndarray
266 The loaded table as a structured array.
267 """
268 raise NotImplementedError()
270 def get_opaque_metadata(self) -> OpaqueArchiveMetadata | None:
271 """Return opaque metadata loaded from the file that should be saved if
272 another version of the object is saved to the same file format.
274 Returns
275 -------
276 OpaqueArchiveMetadata
277 Opaque metadata specific to this archive type that should be
278 round-tripped if it is saved in the same format.
279 """
280 return None