Skip to content
6 changes: 4 additions & 2 deletions dascore/core/attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

from collections.abc import Mapping
from typing import Annotated, Any
from typing import Annotated, Any, cast

from pydantic import ConfigDict, Field, PlainValidator, model_validator
from typing_extensions import Self
Expand Down Expand Up @@ -141,7 +141,9 @@ def from_dict(
if isinstance(out, Mapping):
out = dict(out)
out.pop("dims", None)
return cls(**out)
# Anything else may still be unpackable -- a pandas Series, say --
# and the constructor has always been what rejects the rest.
return cls(**cast("Mapping[str, Any]", out))

def update(self, **kwargs) -> Self:
"""Update an attribute in the model, return new model."""
Expand Down
9 changes: 6 additions & 3 deletions dascore/core/coordmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ def snap(
assert out.shape == self.shape
return out, array

def new(self, dims=None, coord_map=None, dim_map=None) -> Self:
def new(self, dims=None, coord_map=None, dim_map=None, **kwargs) -> Self:
"""
Return a new coordmanager with specified attributes replaced.

Expand All @@ -442,12 +442,13 @@ def new(self, dims=None, coord_map=None, dim_map=None) -> Self:
dims=dims if dims is not None else self.dims,
coord_map=coord_map if coord_map is not None else self.coord_map,
dim_map=dim_map if dim_map is not None else self.dim_map,
**kwargs,
)
return out

def drop_coords(
self,
*coords: str | Sequence[str],
*coords: str,
array: MaybeArray = None,
) -> tuple[Self, MaybeArray]:
"""
Expand Down Expand Up @@ -1133,7 +1134,7 @@ def _flip_coord(coord, axis):

def get_coord_manager(
coords: CoordManagerInput | CoordManager | None = None,
dims: tuple[str, ...] | None = None,
dims: Sequence[str] | None = None,
shape=None,
) -> CoordManager:
"""
Expand Down Expand Up @@ -1175,6 +1176,8 @@ def get_coord_manager(
>>> cm = get_coord_manager(coords=coords, dims=dims)
"""
# return coords if we already have a coord manager.
# A list of dims would never compare equal to a CoordManager's tuple.
dims = None if dims is None else tuple(dims)
if isinstance(coords, CoordManager):
# maybe try to rename dims.
if dims is not None and dims != coords.dims:
Expand Down
52 changes: 29 additions & 23 deletions dascore/core/coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,13 +341,13 @@ def check_time_units(cls, data: Any) -> Any:

@field_validator("shape", mode="before")
@classmethod
def _validate_nullish_to_nan(cls, value):
def _validate_shape_to_tuple(cls, value):
"""Ensure shape is a tuple."""
# This also allows shape to be an int.
return tuple(iterate(value))

@abc.abstractmethod
def convert_units(self, unit) -> Self:
def convert_units(self, units) -> Self:
"""Convert from one unit to another. Set units if None are set."""

def _get_value_index(self, coord_array, values_to_find):
Expand Down Expand Up @@ -433,16 +433,16 @@ def _select_by_samples(self, arg):

@abc.abstractmethod
def select(
self, arg, relative=False, samples=False
) -> tuple[Self, slice | ArrayLike]:
self, args, relative=False, samples=False
) -> tuple[BaseCoord, slice | ArrayLike]:
"""
Returns an entity that can be used in a list for numpy indexing
and selected coord.
"""

def order(
self, array, relative=False, samples=False
) -> tuple[Self, slice | ArrayLike]:
) -> tuple[BaseCoord, slice | ArrayLike]:
"""
Order coordinate according to array values or samples.

Expand Down Expand Up @@ -700,7 +700,7 @@ def coord_range(self, extend: bool = True):
def sort(self, reverse=False) -> tuple[BaseCoord, slice | ArrayLike]:
"""Sort the contents of the coord. Return new coord and slice for sorting."""

def snap(self) -> CoordRange:
def snap(self) -> BaseCoord:
"""
Snap the coordinates to evenly sampled grid points.

Expand Down Expand Up @@ -766,7 +766,7 @@ def get_discontinuities(self, kind="all", tolerance=None) -> pd.DataFrame:
return pd.DataFrame(columns=columns)

@abc.abstractmethod
def update_limits(self, min=None, max=None, step=None, **kwargs) -> Self:
def update_limits(self, min=None, max=None, step=None, **kwargs) -> BaseCoord:
"""
Update the limits or sampling of the coordinates.

Expand Down Expand Up @@ -796,7 +796,7 @@ def update_data(
data: ArrayLike | np.ndarray | None = None,
values: ArrayLike | np.ndarray | None = None,
**kwargs,
) -> Self:
) -> BaseCoord:
"""
Update the data of the coordinate.

Expand Down Expand Up @@ -903,7 +903,7 @@ def _get_relative_values(self, value):
out = self.min() + value if pos else self.max() + value
return out

def empty(self, axes=None) -> Self:
def empty(self, axes=None) -> BaseCoord:
"""
Empty out the coordinate.

Expand All @@ -922,7 +922,7 @@ def empty(self, axes=None) -> Self:
data = np.empty(tuple(new_shape), dtype=self.dtype)
return get_coord(data=data)

def index(self, indexer, axis: int | None = None) -> Self:
def index(self, indexer, axis: int | None = None) -> BaseCoord:
"""
Index the coordinate and return new coordinate.

Expand Down Expand Up @@ -1240,9 +1240,15 @@ def update(self, **kwargs):
"""No values to change so update can just call new."""
return self.new(**kwargs)

# Other operations that normally modify data do not in this case.
update_limits = update
set_units = update
# update_limits is spelled out rather than aliased to update so it keeps
# the signature its base declares. It must forward only what the caller
# supplied: a None reaching _validate_nullish_to_nan would overwrite the
# stored start, stop or step with nan.
def update_limits(self, min=None, max=None, step=None, **kwargs) -> BaseCoord:
"""No values to limit, so only what was passed is applied."""
limits = {"min": min, "max": max, "step": step}
passed = {i: v for i, v in limits.items() if v is not None}
return self.update(**passed, **kwargs)

def convert_units(self, units) -> Self:
"""Convert scalar metadata units, or set units if none exist."""
Expand Down Expand Up @@ -1307,7 +1313,7 @@ def select(
@compose_docstring(doc=get_docstring(BaseCoord.order))
def order(
self, array, relative=False, samples=False
) -> tuple[Self, slice | ArrayLike]:
) -> tuple[BaseCoord, slice | ArrayLike]:
"""
{doc}.
"""
Expand Down Expand Up @@ -1616,7 +1622,7 @@ def _get_index(self, value, forward=True):
return fraction.astype(np.int64)

@compose_docstring(doc=get_docstring(BaseCoord.update_limits))
def update_limits(self, min=None, max=None, step=None, **kwargs) -> Self:
def update_limits(self, min=None, max=None, step=None, **kwargs) -> BaseCoord:
"""{doc}."""
if all(x is not None for x in [min, max, step]):
msg = "At most two parameters can be specified in update_limits."
Expand Down Expand Up @@ -1730,7 +1736,7 @@ def convert_units(self, units) -> Self:

def select(
self, args, relative=False, samples=False
) -> tuple[Self, slice | ArrayLike]:
) -> tuple[BaseCoord, slice | ArrayLike]:
"""Apply select, return selected coords and index for selecting data."""
if is_array(args):
return self._select_by_array(args, relative=relative, samples=samples)
Expand Down Expand Up @@ -1800,7 +1806,7 @@ def snap(self):
return out.change_length(len(self))

@compose_docstring(doc=get_docstring(BaseCoord.update_limits))
def update_limits(self, min=None, max=None, step=None, **kwargs) -> Self:
def update_limits(self, min=None, max=None, step=None, **kwargs) -> BaseCoord:
"""{doc}."""
if sum(x is not None for x in [min, max, step]) > 1:
msg = "At most one parameter can be specified in update_limits."
Expand Down Expand Up @@ -1863,7 +1869,7 @@ class CoordMonotonicArray(CoordArray):

def select(
self, args, relative=False, samples=False
) -> tuple[Self, slice | ArrayLike]:
) -> tuple[BaseCoord, slice | ArrayLike]:
"""Apply select, return selected coords and index for selecting data."""
if is_array(args):
return self._select_by_array(args, relative=relative, samples=samples)
Expand Down Expand Up @@ -2332,7 +2338,7 @@ def sort(self, reverse=False) -> tuple[BaseCoord, slice | ArrayLike]:
return self.new(segments=segments), slice(None, None, -1)

@compose_docstring(doc=get_docstring(BaseCoord.update_limits))
def update_limits(self, min=None, max=None, step=None, **kwargs) -> Self:
def update_limits(self, min=None, max=None, step=None, **kwargs) -> BaseCoord:
"""{doc}."""
if step is not None:
msg = (
Expand Down Expand Up @@ -2708,9 +2714,9 @@ def _validate_values(cls, values):
values["step"] = None
return values

def convert_units(self, unit) -> Self:
def convert_units(self, units) -> Self:
"""String coordinates cannot be converted between units."""
if unit not in (None, ""):
if units not in (None, ""):
_raise_string_coord_error("unit conversion")
return self

Expand All @@ -2730,7 +2736,7 @@ def _get_compatible_value(self, value, relative=False):

def select(
self, args, relative=False, samples=False
) -> tuple[Self, slice | ArrayLike]:
) -> tuple[BaseCoord, slice | ArrayLike]:
"""Select by exact values, wildcard patterns, regexes, samples, or masks."""
if relative:
_raise_string_coord_error("relative selection")
Expand Down Expand Up @@ -2779,7 +2785,7 @@ def reverse_sorted(self) -> bool:
return False
return bool(np.all(values[:-1] >= values[1:]))

def update_limits(self, min=None, max=None, step=None, **kwargs) -> Self:
def update_limits(self, min=None, max=None, step=None, **kwargs) -> BaseCoord:
"""Reject numeric limit updates on string coords."""
# Deliberately match BaseCoord/CoordRange parameter names for API parity.
unsupported_kwargs = set(kwargs) - {"data"}
Expand Down
5 changes: 2 additions & 3 deletions dascore/core/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from collections.abc import Mapping, Sequence
from functools import cached_property
from typing import Final
from typing import Any, Final
from uuid import uuid4

import numpy as np
Expand All @@ -17,7 +17,6 @@
from dascore.compat import DataArray, array
from dascore.core.attrs import PatchAttrs
from dascore.core.coordmanager import CoordManager, get_coord_manager
from dascore.core.coords import BaseCoord
from dascore.core.summary import PatchSummary
from dascore.utils.array import (
PatchUFunc,
Expand Down Expand Up @@ -77,7 +76,7 @@ class Patch(NamespaceOwner):
def __init__(
self,
data: ArrayLike | DataArray | None = None,
coords: Mapping[str, ArrayLike | BaseCoord] | CoordManager | None = None,
coords: Mapping[str, Any] | CoordManager | None = None,
dims: Sequence[str] | None = None,
attrs: Mapping | PatchAttrs | None = None,
):
Expand Down
4 changes: 2 additions & 2 deletions dascore/core/summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def _flatten_coord_summary(
exclude: set[str] | None = None,
) -> dict[str, Any]:
"""Flatten a single coord summary into scan/index-style fields."""
exclude = set() if exclude is None else exclude
exclude = set[str]() if exclude is None else exclude
summary_dict = _coord_summary_to_dict(summary)
out = {}
if dim_tuple and coord_name not in exclude:
Expand Down Expand Up @@ -261,7 +261,7 @@ def dump_structured(self) -> dict[str, Any]:

def flat_dump(self, dim_tuple: bool = False, exclude=None) -> dict[str, Any]:
"""Return a flat dict suitable for indexing/dataframes."""
exclude = set(() if exclude is None else exclude)
exclude = set[str](() if exclude is None else exclude)
# Build flattened attrs first, then overlay coord summaries so coord-
# derived fields win over any attr using the same simplified key.
out = self.attrs.flat_dump(exclude=exclude)
Expand Down
2 changes: 1 addition & 1 deletion dascore/io/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ def _yield_extensions(self, extension, input_type=None):

def _get_format(
self,
path: str | Path | IOResourceManager,
path: path_types | IOResourceManager,
file_format: str | None = None,
file_version: str | None = None,
fiber_io_hint: dict[str, FiberIO] | None = None,
Expand Down
8 changes: 6 additions & 2 deletions dascore/io/index/indexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@
)
from dascore.io.index.schema import SPOOL_HIDDEN_COLUMNS
from dascore.utils.misc import _iter_filesystem
from dascore.utils.paths import directory_writable, requires_local_directory
from dascore.utils.paths import (
coerce_to_local_path,
directory_writable,
requires_local_directory,
)


def _path_digest(path) -> str:
Expand Down Expand Up @@ -132,7 +136,7 @@ def __init__(
):
path = UPath(path).absolute() if isinstance(path, UPath) else Path(path)
requires_local_directory(path, label="DBDirectoryIndexer")
self.path = Path(path).absolute()
self.path = Path(coerce_to_local_path(path)).absolute()
self.index_path = Path(self._find_index_path(index_path))
try:
self._backend = get_backend(self.index_path)
Expand Down
2 changes: 1 addition & 1 deletion dascore/io/index/planned.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ def __init__(
# informational only: the directory/file the plan derived from
self.origin_path = origin_path

def live_entries(self) -> Mapping[str, dc.Patch]:
def live_entries(self) -> dict[str, dc.Patch]:
"""Expose the loader's live registry (for absorption/transfer)."""
return self.loader.live_entries()

Expand Down
20 changes: 12 additions & 8 deletions dascore/io/sintela/protobuf_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from __future__ import annotations

import struct
from collections.abc import Iterator
from collections.abc import Iterable, Iterator
from functools import cache
from typing import Any

Expand Down Expand Up @@ -464,7 +464,7 @@ def _common_header_time(common_header) -> np.datetime64 | None:


def _parse_records(
records: list[EnvelopeRecord], *, scan_mode: bool = False
records: Iterable[EnvelopeRecord], *, scan_mode: bool = False
) -> tuple[list[Any], ParsedMeta]:
"""Decode protobuf payloads and return messages plus selected META."""
messages = _get_proto_messages(include_sample_fields=not scan_mode)
Expand Down Expand Up @@ -530,8 +530,14 @@ def _get_distance_coord(start_channel: int, spacing: float, count: int, step: in
)


def _get_times(times: list[np.datetime64]):
"""Build a time coordinate from packet timestamps."""
def _get_times(times: list[np.datetime64 | None]):
"""
Build a time coordinate from packet timestamps.

Callers reject a packet with no header time before getting here; the
None in the signature is what the list comprehension produces, not a
supported input.
"""
return get_coord(data=np.asarray(times, dtype="datetime64[ns]"))


Expand All @@ -557,7 +563,7 @@ def _base_attrs(
Each packet family supplies its own ``data_type``/``data_units`` via
``extra``; the fields below are shared across all families.
"""
attrs = dict(
attrs = SintelaProtobufAttrs(
data_category="DAS",
packet_type=packet_type,
recorder_namespace=meta.recorder_namespace,
Expand All @@ -572,9 +578,7 @@ def _base_attrs(
start_channel=int(getattr(common_header, "start_channel", 0)),
channel_step=None,
)
if extra:
attrs.update(extra)
return SintelaProtobufAttrs(**attrs)
return attrs.new(**extra) if extra else attrs


def _get_band_attr_data_type(band_def: tuple[tuple[Any, ...], ...]) -> tuple[str, str]:
Expand Down
3 changes: 1 addition & 2 deletions dascore/io/terra15/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,7 @@ def _get_time_coord(data_node, snap_dims=True):
"""Get the time coordinate."""
t_min, t_max, _time_len, d_time = _get_scanned_time_info(data_node)
if snap_dims:
kwargs = dict(start=t_min, stop=t_max + d_time, step=d_time, units="s")
time_coord = get_coord(**kwargs)
time_coord = get_coord(start=t_min, stop=t_max + d_time, step=d_time, units="s")
else:
time_coord = _get_raw_time_coord(data_node)
return time_coord
Expand Down
Loading
Loading