Source code for tiled.client.ragged

from __future__ import annotations

from pathlib import Path
from typing import Any, Union
from urllib.parse import parse_qs, urlparse

import httpx
import numpy as np
import orjson
import ragged

from ..ndslice import NDSlice
from ..serialization.ragged import from_zipped_buffers, to_zipped_buffers
from ..structures.core import STRUCTURE_TYPES
from ..structures.ragged import RaggedCompatibleType, make_ragged_array
from ..utils import Conflicts
from .base import BaseClient
from .utils import export_util, handle_error, params_from_slice, retry_context


[docs] class RaggedClient(BaseClient):
[docs] def write(self, array: RaggedCompatibleType, persist=True): """Write a ragged array in full. Parameters ---------- array: RaggedCompatibleType The array to write. Can be a ragged.array or compatible awkward.Array, or any list-of-lists structure with consistent dimensions. persist: bool Whether to persist the changes on server storage. If False, the update is still streamed to subscribed listeners, but the server may choose not to save the changes. """ array = make_ragged_array(array) url_path = self.item["links"]["full"] params: dict[str, Any] = {**parse_qs(urlparse(url_path).query)} if persist is False: # Extend the query only for non-default behavior. params["persist"] = persist for attempt in retry_context(): with attempt: handle_error( self.context.http_client.put( url_path, content=to_zipped_buffers( mimetype="application/zip", array=array, metadata={} ), headers={"Content-Type": "application/zip"}, params=params, ) )
def write_block( self, array: RaggedCompatibleType, block: Union[int, tuple[int, ...]], persist=True, ): """Write a block of ragged array data. Parameters ---------- array: RaggedCompatibleType The array to write. Can be a ragged.array or compatible awkward.Array, or any list-of-lists structure with consistent dimensions. block: int or tuple[int, ...] The block to write to. Must be a non-negative integer less than the number of partitions or a tuple of such integers, one for each partitioned dimension. persist: bool Whether to persist the changes on server storage. If False, the update is still streamed to subscribed listeners, but the server may choose not to save the changes. """ block_tuple = (block,) if isinstance(block, int) else tuple(block) url_path = self.item["links"]["block"] params: dict[str, Any] = { **parse_qs(urlparse(url_path).query), "block": ",".join(map(str, block_tuple)), } if persist is False: params["persist"] = persist for attempt in retry_context(): with attempt: handle_error( self.context.http_client.put( url_path, content=to_zipped_buffers( mimetype="application/zip", array=array, metadata={} ), headers={"Content-Type": "application/zip"}, params=params, ) ) def patch( self, array: RaggedCompatibleType, offset: Union[int, tuple[int, ...]], extend=False, persist=True, ): """Write data into a slice of an array, possibly extending the shape. Parameters ---------- array : RaggedCompatibleType The data to write offset : int | tuple[int, ...] Where to place this data in the array extend : bool Extend the array shape to fit the new slice, if necessary persist : bool | None Persist the changes on server storage if True. [default behavior] If False, the update is still streamed to subscribed listeners. """ if not extend or not persist: raise NotImplementedError( "Only extend=True and persist=True are currently supported" ) offset = (offset,) if isinstance(offset, int) else offset if offset[0] != self.shape[0]: raise NotImplementedError( "Only appending to the end of the leftmost dimension is currently supported" ) array = make_ragged_array(array) if array.dtype != self.dtype: raise ValueError( f"Data given to patch has dtype {array.dtype} which does not " f"match the dtype of this array {self.dtype}." ) url_path = self.item["links"]["full"] params = { **parse_qs(urlparse(url_path).query), "shape": str(array.shape[0]), "offset": ",".join(map(str, offset)), "extend": bool(extend), } if persist is False: # Extend the query only for non-default behavior. params["persist"] = persist for attempt in retry_context(): with attempt: response = self.context.http_client.patch( url_path, content=to_zipped_buffers( mimetype="application/zip", array=array, metadata={} ), headers={"Content-Type": "application/zip"}, params=params, ) if response.status_code in [ httpx.codes.BAD_REQUEST, httpx.codes.CONFLICT, ]: detail = ( response.json() .get("detail", "Array parameters conflict.") .replace( "Use ?", # URL query param "Pass keyword argument ", # Python function argument ) ) if response.status_code == httpx.codes.CONFLICT: raise Conflicts(detail) raise ValueError(detail) handle_error(response) # Update cached structure. new_structure = response.json() structure_type = STRUCTURE_TYPES[self.structure_family] self._structure = structure_type.from_json(new_structure)
[docs] def read(self, slice: Any | None = None) -> ragged.array: """Read the entire array, or optionally a slice of it. Parameters ---------- slice: Any, optional A numpy-style slice. """ url_path = self.item["links"]["full"] url_params: dict[str, Any] = {**parse_qs(urlparse(url_path).query)} if slice is not None: url_params.update(**params_from_slice(slice)) if is_scalar := (self.ndim == 0) or NDSlice(slice).is_scalar(self.shape): accept_header = "application/json" else: accept_header = "application/zip" for attempt in retry_context(): with attempt: content = handle_error( self.context.http_client.get( url_path, headers={"Accept": accept_header}, params=url_params, ) ).read() if is_scalar: return ragged.array(orjson.loads(content)) return from_zipped_buffers(buffer=content, structure=self.structure())
def __getitem__(self, _slice: Any) -> ragged.array: """ Access the array with slicing logic. Parameters ---------- slice: Any A numpy-style slice. """ return self.read(slice=_slice)
[docs] def export( self, filepath: str | Path, *, slice: Any | None = None, format: str | None = None, ): """Download data in some format and write to a file. Parameters ---------- filepath: str or Path Filepath or writeable buffer. format : str, optional If format is None and `filepath` is a filepath, the format is inferred from the name, like 'ragged.json' implies format="application/json". The format may be given as a file extension ("json") or a media type ("application/json"). slice: Any, optional A tuple of slice objects. """ return export_util( filepath, format, self.context.http_client.get, self.item["links"]["full"], params=params_from_slice(slice), )
@property def dims(self) -> tuple[str, ...] | None: """The dimension names of the array, if set.""" return self.structure().dims @property def shape(self) -> tuple[int | None, ...]: """The shape of the array, where unknown variable dimensions are given as ``None``.""" return self.structure().shape @property def size(self) -> int: """The total number of elements in the array.""" return self.structure().size @property def dtype(self) -> np.dtype: """The data type of the array.""" return self.structure().data_type.to_numpy_dtype() @property def chunks(self) -> tuple[tuple[int, ...] | None, ...]: """The dask-like chunks of the array, where the first dimension is always partitioned into known integer chunks, and any variable dimensions are `None`. """ return self.structure().chunks @property def chunked(self) -> bool: """Whether the array is chunked along any dimension.""" return any(c is not None and len(c) > 1 for c in self.chunks) @property def ndim(self) -> int: """The dimensionality of the array.""" return len(self.shape) def __repr__(self): """Return a brief representation of the ragged data accessible to the ``RaggedClient``.""" attrs: dict[str, Any] = { "shape": self.shape, "size": self.size, "chunks": self.chunks, "dtype": self.dtype, } if self.dims: attrs["dims"] = self.dims return ( f"<{type(self).__name__}" + "".join(f" {k}={v}" for k, v in attrs.items()) + ">" )