Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions dascore/core/attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
VALID_DATA_TYPES,
max_lens,
)
from dascore.utils.attrs import _raise_if_coord_attr_updates
from dascore.utils.misc import (
to_str,
)
Expand Down Expand Up @@ -148,7 +147,6 @@ def from_dict(

def update(self, **kwargs) -> Self:
"""Update an attribute in the model, return new model."""
_raise_if_coord_attr_updates(kwargs)
out = self.model_dump(exclude_unset=True)
out.update(kwargs)
return self.from_dict(out)
Expand Down
51 changes: 1 addition & 50 deletions dascore/core/coordmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,13 @@
from collections import defaultdict
from collections.abc import Mapping, Sequence
from itertools import zip_longest
from typing import Annotated, Any, TypeVar
from typing import Annotated, TypeVar

import numpy as np
from pydantic import field_validator, model_validator
from rich.text import Text
from typing_extensions import Self

import dascore as dc
from dascore.constants import dascore_styles, select_values_description
from dascore.core.coords import BaseCoord, CoordSummary, get_coord
from dascore.exceptions import (
Expand All @@ -61,7 +60,6 @@
ParameterError,
PatchBroadcastError,
)
from dascore.utils.attrs import separate_coord_info
from dascore.utils.docs import compose_docstring
from dascore.utils.mapping import FrozenDict
from dascore.utils.misc import (
Expand Down Expand Up @@ -306,35 +304,6 @@ def _divide_kwargs(kwargs):
# we need this here to maintain backwards compatibility
update_coords = update

def update_from_attrs(
self, attrs: Mapping | dc.PatchAttrs
) -> tuple[Self, dc.PatchAttrs]:
"""
Update coordinates from attrs.

This will also return a PatchAttrs which conforms to coords.

Parameters
----------
attrs
The attribute source, either PatchAttrs instance or mapping.
"""
coord_info, attr_info = separate_coord_info(attrs, dims=self.dims)
out = dict(self.coord_map)
for name in set(coord_info) & set(out):
maybe_updates = coord_info[name]
coord = self.coord_map[name]
# convert values to dict to determine which should be updated.
model_contents = coord.to_summary().model_dump(exclude_defaults=True)
# see what has changed.
diff = {
i: v for i, v in maybe_updates.items() if v != model_contents.get(i)
}
out[name] = coord.update(**diff)
coords = self.new(coord_map=out)
attrs = dc.PatchAttrs.from_dict(attr_info)
return coords, attrs

def sort(
self, *coords, array: MaybeArray = None, reverse: bool = False
) -> tuple[Self, MaybeArray]:
Expand Down Expand Up @@ -1147,7 +1116,6 @@ def _flip_coord(coord, axis):
def get_coord_manager(
coords: CoordManagerInput | CoordManager | None = None,
dims: tuple[str, ...] | None = None,
attrs: dc.PatchAttrs | dict[str, Any] | None = None,
shape=None,
) -> CoordManager:
"""
Expand All @@ -1162,11 +1130,6 @@ def get_coord_manager(
[`CoordManager`](`dascore.core.CoordManager`).
dims
Tuple specify dimension names
attrs
Attributes which can be used to create coordinates.
Cannot be used with coords argument.
If you want to update [`CoordManager`](`dascore.core.CoordManager`)
use [`update_from_attrs`](`dascore.core.CoordManager.update_from_attrs`).
shape
The data array shape which will be managed by coord manager. This
allows non-coordinate dimensions to be initiated.
Expand All @@ -1193,12 +1156,6 @@ def get_coord_manager(
>>> coords['quality'] = (("distance", "time"), quality)
>>> cm = get_coord_manager(coords=coords, dims=dims)
"""
if coords is not None and attrs is not None:
msg = (
"Cannot use both attrs and coords in get_coord_manager. "
"Perhaps you want CoordManager.update_from_attrs?"
)
raise ParameterError(msg)
# return coords if we already have a coord manager.
if isinstance(coords, CoordManager):
# maybe try to rename dims.
Expand All @@ -1220,12 +1177,6 @@ def get_coord_manager(
for name in missing_dims:
coord_map[name] = get_coord(shape=shape[dims.index(name)])
dim_map[name] = (name,)
if attrs:
coord_updates, _ = separate_coord_info(attrs, dims)
updateable_coords = set(coord_updates) - set(coord_map)
for name in updateable_coords:
coord_map[name] = get_coord(**coord_updates[name])
dim_map[name] = (name,)
out = CoordManager(coord_map=coord_map, dim_map=dim_map, dims=dims)
return out

Expand Down
9 changes: 0 additions & 9 deletions dascore/core/coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,15 +897,6 @@ def index(self, indexer, axis: int | None = None) -> Self:
array = self.data[indexer]
return get_coord(data=array, units=self.units)

def get_attrs_dict(self, name):
"""Get attrs dict."""
out = {f"{name}_min": self.min(), f"{name}_max": self.max()}
if self.step:
out[f"{name}_step"] = self.step
if self.units:
out[f"{name}_units"] = self.units
return out

def to_summary(self, dims=()) -> CoordSummary:
"""Get the summary info about the coord."""
return CoordSummary(
Expand Down
103 changes: 103 additions & 0 deletions dascore/io/dasdae/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""
Backward compatibility for legacy DASDAE metadata.

Older DASDAE files mixed coordinate metadata into the patch attr namespace:
coord summaries were stored as a (sometimes pickled) ``coords`` attr, and
flat keys such as ``time_min`` or ``d_time`` lived alongside true patch
attrs. Files written after attrs and coords were fully separated carry the
``__attrs_coords_separate__`` root marker and store only true attrs.

This module is private to ``dascore.io.dasdae``; nothing else in DASCore may
import it. Attrs and coords are independent everywhere else, so all knowledge
of the old mixed shapes is quarantined here.
"""

from __future__ import annotations

import contextlib
import pickle
from collections.abc import Iterable

from dascore.config import get_config
from dascore.core.coords import CoordSummary
from dascore.exceptions import InvalidFiberFileError

# Every flat coord-summary key an old file may contain ({name}_{field}).
_LEGACY_COORD_FIELDS = tuple(CoordSummary.model_fields)
# The subset legacy writers actually flattened into attrs; dims/fingerprint
# never appeared as flat keys, so translate re-emits only these while the
# strip above removes the full (superset) field family.
_LEGACY_FLAT_FIELDS = ("min", "max", "step", "units", "dtype", "len")


def strip_legacy_coord_fields(attrs: dict, coord_names: Iterable[str]) -> dict:
"""
Remove legacy flat coordinate metadata from an attr mapping.

Only exact ``{name}_{field}`` compositions for the provided coord names
are removed (plus the deprecated ``d_{name}`` spelling and the structural
``coords``/``dims`` keys). Nothing is inferred from key shape, so a true
attr like ``pulse_len`` survives unless the file really stores a ``pulse``
coordinate.
"""
out = dict(attrs)
out.pop("coords", None)
out.pop("dims", None)
for name in coord_names:
for field in _LEGACY_COORD_FIELDS:
out.pop(f"{name}_{field}", None)
out.pop(f"d_{name}", None)
return out


def translate_legacy_attrs(attrs):
"""Normalize legacy DASDAE attr payloads to flat coord metadata."""
out = dict(attrs)
coords = out.pop("coords", {})
if isinstance(coords, str):
# Older DASDAE files stored the coord-summary payload as a pickled
# string attr. Unpickling runs arbitrary code, so the opt-in gate
# must come before any decode attempt — a malicious payload executes
# during pickle.loads itself, not when the result is used.
if not get_config().allow_dasdae_format_unpickle:
msg = (
"This DASDAE file contains legacy pickled coordinate metadata. "
"Unpickling DASDAE format metadata is disabled by default for "
"security. If you trust this file, enable legacy compatibility "
"with dc.set_config(allow_dasdae_format_unpickle=True)."
)
raise InvalidFiberFileError(msg)
with contextlib.suppress(
AttributeError,
EOFError,
KeyError,
pickle.PickleError,
TypeError,
UnicodeError,
ValueError,
):
coords = pickle.loads(coords.encode("latin1"))
if hasattr(coords, "to_summary_dict"):
coords = coords.to_summary_dict()
if not hasattr(coords, "items"):
coords = {}
for name, summary in coords.items():
if hasattr(summary, "to_summary"):
summary = summary.to_summary()
if hasattr(summary, "model_dump"):
summary = summary.model_dump()
if not isinstance(summary, dict):
continue
for field in _LEGACY_FLAT_FIELDS:
key = f"{name}_{field}"
value = summary.get(field)
if key not in out and value not in (None, ""):
out[key] = value
dims = out.get("dims", "")
dims = tuple(dims.split(",")) if isinstance(dims, str) else tuple(dims or ())
for name in dims:
old_name = f"d_{name}"
new_name = f"{name}_step"
if new_name not in out and old_name in out:
out[new_name] = out.pop(old_name)
return out
10 changes: 7 additions & 3 deletions dascore/io/dasdae/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
from dascore.utils.patch import get_patch_names

from .utils import (
_get_attrs,
_get_contents_from_patch_groups_generic,
_get_patch_attrs,
_is_legacy_file,
_is_legacy_group,
_kwargs_empty,
_matches_attr_filters,
_read_patch,
Expand Down Expand Up @@ -118,14 +120,16 @@ def read(self, resource: H5Reader, source_patch_id=(), **kwargs) -> SpoolType:
waveform_group = resource["waveforms"]
except (KeyError, IndexError):
return dc.spool([])
file_legacy = _is_legacy_file(resource)
for patch_group in waveform_group.values():
patch_name = str(patch_group.name).rsplit("/", maxsplit=1)[-1]
if source_patch_ids and patch_name not in source_patch_ids:
continue
attrs = _get_attrs(patch_group)
legacy = _is_legacy_group(patch_group, file_legacy)
attrs = _get_patch_attrs(patch_group, legacy)
if not _matches_attr_filters(attrs, kwargs):
continue
patch = _read_patch(patch_group, **kwargs)
patch = _read_patch(patch_group, legacy=legacy, **kwargs)
if not patch.data.size and not _kwargs_empty(kwargs):
continue
patches.append(patch)
Expand Down
Loading
Loading