Coverage for python/lsst/images/ndf/_common.py: 100%
34 statements
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 08:57 +0000
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 08:57 +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__ = (
15 "HdsNameShrinker",
16 "NdfPointerModel",
17 "archive_path_to_hdf5_path",
18 "archive_path_to_hdf5_path_components",
19)
21import pydantic
23from ._hds import DAT__SZNAM
26class NdfPointerModel(pydantic.BaseModel):
27 """Reference to an NDF-archive sub-tree by HDF5 path.
29 Used by `NdfOutputArchive`/`NdfInputArchive` to point to
30 sub-trees that have been hoisted out of the main JSON tree into separate
31 HDS components.
32 """
34 path: str
35 """HDF5 absolute path (e.g. ``/MORE/LSST/PSF``)."""
38class HdsNameShrinker:
39 """Shrink HDS component names to fit the HDS object-name length limit.
41 Names are uppercased; names at or under the limit pass through unchanged.
42 Each distinct over-long name is assigned a readable prefix followed by an
43 underscore-separated hexadecimal counter that increments per assignment,
44 so distinct names written through the same shrinker never collide.
45 Assignments are remembered, so repeated requests for the same name return
46 the same result.
48 A shrinker is scoped to a single output file: shrunk names are not stable
49 across files, so readers must use the paths recorded in the file's JSON
50 tree rather than recomputing them.
52 Parameters
53 ----------
54 max_length
55 Maximum component length, by default the HDS limit (``DAT__SZNAM``).
56 """
58 def __init__(self, max_length: int = DAT__SZNAM) -> None:
59 self._max_length = max_length
60 self._assigned: dict[tuple[str, int], str] = {}
61 self._counter = 0
63 def shrink(self, name: str, reserve: int = 0) -> str:
64 """Shrink a component name to fit in ``max_length - reserve``.
66 Parameters
67 ----------
68 name
69 The component name to shrink.
70 reserve
71 Number of characters to leave available for a suffix the caller
72 will append (e.g. a version suffix).
74 Returns
75 -------
76 `str`
77 The uppercased name, unchanged if it fits, otherwise truncated
78 and suffixed with ``_`` and an uppercase hexadecimal counter
79 (at least three digits) so the result is exactly
80 ``max_length - reserve`` characters.
81 """
82 max_length = self._max_length - reserve
83 name = name.upper()
84 if len(name) <= max_length:
85 return name
86 key = (name, max_length)
87 if (shrunk := self._assigned.get(key)) is None:
88 self._counter += 1
89 token = f"_{self._counter:03X}"
90 shrunk = f"{name[: max_length - len(token)]}{token}"
91 self._assigned[key] = shrunk
92 return shrunk
94 def shrink_versioned(self, base: str, version: int) -> str:
95 """Shrink a component while preserving a visible version suffix.
97 When ``version`` is greater than one a ``_{version}`` suffix is
98 reserved at the tail and the ``base`` is shrunk into the remaining
99 characters, so the version number stays readable in Starlink tools.
100 Version one (the first occurrence) is shrunk exactly like an
101 unversioned component.
102 """
103 suffix = f"_{version}" if version > 1 else ""
104 return self.shrink(base, reserve=len(suffix)) + suffix
107def archive_path_to_hdf5_path(archive_path: str, shrinker: HdsNameShrinker) -> str:
108 """Translate a serialization archive path to an NDF HDF5 path.
110 The empty path maps to the main JSON tree at ``/MORE/LSST/JSON``.
111 Any non-empty path is uppercased and kept hierarchical under
112 ``/MORE/LSST/``. This mirrors the serialization path while keeping HDS
113 component names within the HDS object-name limit (``DAT__SZNAM``).
114 """
115 if not archive_path:
116 return "/MORE/LSST/JSON"
117 components = archive_path_to_hdf5_path_components(archive_path, shrinker)
118 return "/MORE/LSST/" + "/".join(components)
121def archive_path_to_hdf5_path_components(archive_path: str, shrinker: HdsNameShrinker) -> list[str]:
122 """Return HDS-compatible path components for an archive path.
124 Each component is uppercased; components longer than the HDS object-name
125 limit (``DAT__SZNAM``) are shrunk by ``shrinker``.
126 """
127 return [shrinker.shrink(component) for component in archive_path.strip("/").split("/") if component]