diff --git a/docs/source/reference/python-client.md b/docs/source/reference/python-client.md index 3eb516ef7..fc023947d 100644 --- a/docs/source/reference/python-client.md +++ b/docs/source/reference/python-client.md @@ -165,6 +165,7 @@ Tiled currently includes two clients for each structure family: tiled.client.array.DaskArrayClient.export tiled.client.array.DaskArrayClient.write tiled.client.array.DaskArrayClient.write_block + tiled.client.array.DaskArrayClient.patch ``` ```{eval-rst} diff --git a/docs/source/tutorials/writing.md b/docs/source/tutorials/writing.md index 479ba8196..cdb49d3d3 100644 --- a/docs/source/tutorials/writing.md +++ b/docs/source/tutorials/writing.md @@ -60,6 +60,19 @@ Write array and tabular data. >>> client.write_array(numpy.array([4, 5, 6]), metadata={"color": "blue", "barcode": 11}) +# Create an array and grow it by one +>>> new_array = client.write_array([1, 2, 3]) +>>> new_array + + +# Extend the array. This array has only one dimension, here we extend by one +# along that dimension. +>>> new_array.patch([4], offset=(3,), extend=True) +>>> new_array + +>>> new_array.read() +array([1, 2, 3, 4]) + # Write a table (DataFrame). >>> import pandas >>> client.write_dataframe(pandas.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]}), metadata={"color": "green", "barcode": 12}) diff --git a/tiled/_tests/test_protocols.py b/tiled/_tests/test_protocols.py index e54921cd5..22bb27b82 100644 --- a/tiled/_tests/test_protocols.py +++ b/tiled/_tests/test_protocols.py @@ -18,13 +18,13 @@ SparseAdapter, TableAdapter, ) -from tiled.adapters.type_alliases import JSON, Filters, NDSlice, Scopes from tiled.server.schemas import Principal, PrincipalType from tiled.structures.array import ArrayStructure, BuiltinDtype from tiled.structures.awkward import AwkwardStructure from tiled.structures.core import Spec, StructureFamily from tiled.structures.sparse import COOStructure from tiled.structures.table import TableStructure +from tiled.type_aliases import JSON, Filters, NDSlice, Scopes class CustomArrayAdapter: diff --git a/tiled/_tests/test_writing.py b/tiled/_tests/test_writing.py index 571aaad59..f31c9bde4 100644 --- a/tiled/_tests/test_writing.py +++ b/tiled/_tests/test_writing.py @@ -121,6 +121,55 @@ def test_write_array_chunked(tree): assert result.specs == specs +def test_extend_array(tree): + "Extend an array with additional data, expanding its shape." + with Context.from_app( + build_app(tree, validation_registry=validation_registry) + ) as context: + client = from_context(context) + + a = numpy.ones((3, 2, 2)) + new_data = numpy.ones((1, 2, 2)) * 2 + full_array = numpy.concatenate((a, new_data), axis=0) + + # Upload a (3, 2, 2) array. + ac = client.write_array(a) + assert ac.shape == a.shape + + # Patching data into a region beyond the current extent of the array + # raises a ValueError (catching a 409 from the server). + with pytest.raises(ValueError): + ac.patch(new_data, offset=(3,)) + # With extend=True, the array is expanded. + ac.patch(new_data, offset=(3,), extend=True) + # The local cache of the structure is updated. + assert ac.shape == full_array.shape + actual = ac.read() + # The array has the expected shape and data. + assert actual.shape == full_array.shape + numpy.testing.assert_equal(actual, full_array) + + # Overwrite data (do not extend). + revised_data = numpy.ones((1, 2, 2)) * 3 + revised_array = full_array.copy() + revised_array[3, :, :] = 3 + ac.patch(revised_data, offset=(3,)) + numpy.testing.assert_equal(ac.read(), revised_array) + + # Extend out of order. + ones = numpy.ones((1, 2, 2)) + ac.patch(ones * 7, offset=(7,), extend=True) + ac.patch(ones * 5, offset=(5,), extend=True) + ac.patch(ones * 6, offset=(6,), extend=True) + numpy.testing.assert_equal(ac[5:6], ones * 5) + numpy.testing.assert_equal(ac[6:7], ones * 6) + numpy.testing.assert_equal(ac[7:8], ones * 7) + + # Offset given as an int is acceptable. + ac.patch(ones * 8, offset=8, extend=True) + numpy.testing.assert_equal(ac[8:9], ones * 8) + + def test_write_dataframe_full(tree): with Context.from_app( build_app(tree, validation_registry=validation_registry) diff --git a/tiled/adapters/array.py b/tiled/adapters/array.py index c849bc799..3c1e1fa46 100644 --- a/tiled/adapters/array.py +++ b/tiled/adapters/array.py @@ -5,8 +5,8 @@ from ..structures.array import ArrayStructure from ..structures.core import Spec, StructureFamily +from ..type_aliases import JSON, NDSlice from .protocols import AccessPolicy -from .type_alliases import JSON, NDSlice class ArrayAdapter: diff --git a/tiled/adapters/arrow.py b/tiled/adapters/arrow.py index 84e5afffb..9c2da456c 100644 --- a/tiled/adapters/arrow.py +++ b/tiled/adapters/arrow.py @@ -9,10 +9,10 @@ from ..structures.core import Spec, StructureFamily from ..structures.data_source import Asset, DataSource, Management from ..structures.table import TableStructure +from ..type_aliases import JSON from ..utils import ensure_uri, path_from_uri from .array import ArrayAdapter from .protocols import AccessPolicy -from .type_alliases import JSON class ArrowAdapter: diff --git a/tiled/adapters/awkward.py b/tiled/adapters/awkward.py index c5e76d1dc..9da5432dd 100644 --- a/tiled/adapters/awkward.py +++ b/tiled/adapters/awkward.py @@ -6,9 +6,9 @@ from ..structures.awkward import AwkwardStructure from ..structures.core import Spec, StructureFamily +from ..type_aliases import JSON from .awkward_directory_container import DirectoryContainer from .protocols import AccessPolicy -from .type_alliases import JSON class AwkwardAdapter: diff --git a/tiled/adapters/awkward_buffers.py b/tiled/adapters/awkward_buffers.py index 8a595e6d8..ab26dcccd 100644 --- a/tiled/adapters/awkward_buffers.py +++ b/tiled/adapters/awkward_buffers.py @@ -9,11 +9,11 @@ from ..server.schemas import Asset from ..structures.awkward import AwkwardStructure from ..structures.core import Spec, StructureFamily +from ..type_aliases import JSON from ..utils import path_from_uri from .awkward import AwkwardAdapter from .awkward_directory_container import DirectoryContainer from .protocols import AccessPolicy -from .type_alliases import JSON class AwkwardBuffersAdapter(AwkwardAdapter): diff --git a/tiled/adapters/csv.py b/tiled/adapters/csv.py index 08eed0554..8e9fd6680 100644 --- a/tiled/adapters/csv.py +++ b/tiled/adapters/csv.py @@ -7,12 +7,12 @@ from ..structures.core import Spec, StructureFamily from ..structures.data_source import Asset, DataSource, Management from ..structures.table import TableStructure +from ..type_aliases import JSON from ..utils import ensure_uri, path_from_uri from .array import ArrayAdapter from .dataframe import DataFrameAdapter from .protocols import AccessPolicy from .table import TableAdapter -from .type_alliases import JSON def read_csv( diff --git a/tiled/adapters/hdf5.py b/tiled/adapters/hdf5.py index 6606d8b13..b2f228a0b 100644 --- a/tiled/adapters/hdf5.py +++ b/tiled/adapters/hdf5.py @@ -14,11 +14,11 @@ from ..structures.array import ArrayStructure from ..structures.core import Spec, StructureFamily from ..structures.table import TableStructure +from ..type_aliases import JSON from ..utils import node_repr, path_from_uri from .array import ArrayAdapter from .protocols import AccessPolicy from .resource_cache import with_resource_cache -from .type_alliases import JSON SWMR_DEFAULT = bool(int(os.getenv("TILED_HDF5_SWMR_DEFAULT", "0"))) INLINED_DEPTH = int(os.getenv("TILED_HDF5_INLINED_CONTENTS_MAX_DEPTH", "7")) diff --git a/tiled/adapters/jpeg.py b/tiled/adapters/jpeg.py index ae6a6ffa8..fe8c27da5 100644 --- a/tiled/adapters/jpeg.py +++ b/tiled/adapters/jpeg.py @@ -7,11 +7,11 @@ from ..structures.array import ArrayStructure, BuiltinDtype from ..structures.core import Spec, StructureFamily +from ..type_aliases import JSON, NDSlice from ..utils import path_from_uri from .protocols import AccessPolicy from .resource_cache import with_resource_cache from .sequence import FileSequenceAdapter -from .type_alliases import JSON, NDSlice class JPEGAdapter: diff --git a/tiled/adapters/mapping.py b/tiled/adapters/mapping.py index ab9a27d66..52b5b55f9 100644 --- a/tiled/adapters/mapping.py +++ b/tiled/adapters/mapping.py @@ -38,9 +38,9 @@ from ..server.schemas import SortingItem from ..structures.core import Spec, StructureFamily from ..structures.table import TableStructure +from ..type_aliases import JSON from ..utils import UNCHANGED, Sentinel from .protocols import AccessPolicy, AnyAdapter -from .type_alliases import JSON from .utils import IndexersMixin if sys.version_info < (3, 9): diff --git a/tiled/adapters/parquet.py b/tiled/adapters/parquet.py index cc7138c00..0bc38296f 100644 --- a/tiled/adapters/parquet.py +++ b/tiled/adapters/parquet.py @@ -7,10 +7,10 @@ from ..server.schemas import Asset from ..structures.core import Spec, StructureFamily from ..structures.table import TableStructure +from ..type_aliases import JSON from ..utils import path_from_uri from .dataframe import DataFrameAdapter from .protocols import AccessPolicy -from .type_alliases import JSON class ParquetDatasetAdapter: diff --git a/tiled/adapters/protocols.py b/tiled/adapters/protocols.py index 01d5cdacc..4a92522f0 100644 --- a/tiled/adapters/protocols.py +++ b/tiled/adapters/protocols.py @@ -14,8 +14,8 @@ from ..structures.core import Spec, StructureFamily from ..structures.sparse import SparseStructure from ..structures.table import TableStructure +from ..type_aliases import JSON, Filters, NDSlice, Scopes from .awkward_directory_container import DirectoryContainer -from .type_alliases import JSON, Filters, NDSlice, Scopes class BaseAdapter(Protocol): diff --git a/tiled/adapters/sequence.py b/tiled/adapters/sequence.py index 56718e042..12878daf6 100644 --- a/tiled/adapters/sequence.py +++ b/tiled/adapters/sequence.py @@ -11,9 +11,9 @@ from ..structures.array import ArrayStructure, BuiltinDtype from ..structures.core import Spec +from ..type_aliases import JSON, NDSlice from ..utils import path_from_uri from .protocols import AccessPolicy -from .type_alliases import JSON, NDSlice def force_reshape(arr: np.array, desired_shape: Tuple[int, ...]) -> np.array: diff --git a/tiled/adapters/sparse.py b/tiled/adapters/sparse.py index 60b122d5c..b30fc820d 100644 --- a/tiled/adapters/sparse.py +++ b/tiled/adapters/sparse.py @@ -8,9 +8,9 @@ from ..structures.core import Spec, StructureFamily from ..structures.sparse import COOStructure +from ..type_aliases import JSON, NDSlice from .array import slice_and_shape_from_block_and_chunks from .protocols import AccessPolicy -from .type_alliases import JSON, NDSlice class COOAdapter: diff --git a/tiled/adapters/sparse_blocks_parquet.py b/tiled/adapters/sparse_blocks_parquet.py index 1a5ed7dbb..0a53ffae7 100644 --- a/tiled/adapters/sparse_blocks_parquet.py +++ b/tiled/adapters/sparse_blocks_parquet.py @@ -12,9 +12,9 @@ from ..server.schemas import Asset from ..structures.core import Spec, StructureFamily from ..structures.sparse import COOStructure +from ..type_aliases import JSON, NDSlice from ..utils import path_from_uri from .protocols import AccessPolicy -from .type_alliases import JSON, NDSlice def load_block(uri: str) -> Tuple[List[int], Tuple[NDArray[Any], Any]]: @@ -113,18 +113,13 @@ def write_block( self, data: Union[dask.dataframe.DataFrame, pandas.DataFrame], block: Tuple[int, ...], + slice: NDSlice = ..., ) -> None: - """ - - Parameters - ---------- - data : - block : - - Returns - ------- - - """ + if slice != ...: + raise NotImplementedError( + "Writing into a slice of a sparse block is not yet supported." + ) + "Write into a block of the array." uri = self.blocks[block] data.to_parquet(path_from_uri(uri)) diff --git a/tiled/adapters/table.py b/tiled/adapters/table.py index 4b7d3ff0c..375645e5e 100644 --- a/tiled/adapters/table.py +++ b/tiled/adapters/table.py @@ -6,9 +6,9 @@ from ..structures.core import Spec, StructureFamily from ..structures.table import TableStructure +from ..type_aliases import JSON from .array import ArrayAdapter from .protocols import AccessPolicy -from .type_alliases import JSON class TableAdapter: diff --git a/tiled/adapters/tiff.py b/tiled/adapters/tiff.py index 75ef69ad1..6571975fa 100644 --- a/tiled/adapters/tiff.py +++ b/tiled/adapters/tiff.py @@ -6,11 +6,11 @@ from ..structures.array import ArrayStructure, BuiltinDtype from ..structures.core import Spec, StructureFamily +from ..type_aliases import JSON, NDSlice from ..utils import path_from_uri from .protocols import AccessPolicy from .resource_cache import with_resource_cache from .sequence import FileSequenceAdapter -from .type_alliases import JSON, NDSlice class TiffAdapter: diff --git a/tiled/adapters/zarr.py b/tiled/adapters/zarr.py index 7a914965c..88eb7d143 100644 --- a/tiled/adapters/zarr.py +++ b/tiled/adapters/zarr.py @@ -14,10 +14,10 @@ from ..server.schemas import Asset from ..structures.array import ArrayStructure from ..structures.core import Spec, StructureFamily -from ..utils import node_repr, path_from_uri +from ..type_aliases import JSON, NDSlice +from ..utils import Conflicts, node_repr, path_from_uri from .array import ArrayAdapter, slice_and_shape_from_block_and_chunks from .protocols import AccessPolicy -from .type_alliases import JSON, NDSlice INLINED_DEPTH = int(os.getenv("TILED_HDF5_INLINED_CONTENTS_MAX_DEPTH", "7")) @@ -75,7 +75,6 @@ def init_storage(cls, data_uri: str, structure: ArrayStructure) -> List[Asset]: directory = path_from_uri(data_uri) directory.mkdir(parents=True, exist_ok=True) storage = zarr.storage.DirectoryStore(str(directory)) - zarr.storage.init_array( storage, shape=shape, @@ -162,7 +161,6 @@ async def write_block( self, data: NDArray[Any], block: Tuple[int, ...], - slice: Optional[NDSlice] = ..., ) -> None: """ @@ -176,13 +174,70 @@ async def write_block( ------- """ - if slice is not ...: - raise NotImplementedError block_slice, shape = slice_and_shape_from_block_and_chunks( block, self.structure().chunks ) self._array[block_slice] = data + async def patch( + self, + data: NDArray[Any], + offset: Tuple[int, ...], + extend: bool = False, + ) -> Tuple[Tuple[int, ...], Tuple[Tuple[int, ...], ...]]: + """ + Write data into a slice of the array, maybe extending it. + + If the specified slice does not fit into the array, and extend=True, the + array will be resized (expanded, never shrunk) to fit it. + + Parameters + ---------- + data : array-like + offset : tuple[int] + Where to place the new data + extend : bool + If slice does not fit wholly within the shape of the existing array, + reshape (expand) it to fit if this is True. + + Raises + ------ + ValueError : + If slice does not fit wholly with the shape of the existing array + and expand is False + """ + current_shape = self._array.shape + normalized_offset = [0] * len(current_shape) + normalized_offset[: len(offset)] = list(offset) + new_shape = [] + slice_ = [] + for data_dim, offset_dim, current_dim in zip( + data.shape, normalized_offset, current_shape + ): + new_shape.append(max(current_dim, data_dim + offset_dim)) + slice_.append(slice(offset_dim, offset_dim + data_dim)) + new_shape_tuple = tuple(new_shape) + if new_shape_tuple != current_shape: + if extend: + # Resize the Zarr array to accommodate new data + self._array.resize(new_shape_tuple) + else: + raise Conflicts( + f"Slice {slice} does not fit into array shape {current_shape}. " + "Use ?extend=true to extend array dimension to fit." + ) + self._array[tuple(slice_)] = data + new_chunks = [] + # Zarr has regularly-sized chunks, so no user input is required to + # simply extend the existing pattern. + for chunk_size, size in zip(self._array.chunks, new_shape_tuple): + dim = [chunk_size] * (size // chunk_size) + if size % chunk_size: + dim.append(size % chunk_size) + new_chunks.append(tuple(dim)) + new_chunks_tuple = tuple(new_chunks) + return new_shape_tuple, new_chunks_tuple + if sys.version_info < (3, 9): from typing_extensions import Mapping diff --git a/tiled/catalog/adapter.py b/tiled/catalog/adapter.py index 2844bcdef..13bd2353c 100644 --- a/tiled/catalog/adapter.py +++ b/tiled/catalog/adapter.py @@ -1,4 +1,5 @@ import collections +import copy import importlib import itertools as it import logging @@ -1040,6 +1041,37 @@ async def write_block(self, *args, **kwargs): (await self.get_adapter()).write_block, *args, **kwargs ) + async def patch(self, *args, **kwargs): + # assumes a single DataSource (currently only supporting zarr) + async with self.context.session() as db: + new_shape_and_chunks = await ensure_awaitable( + (await self.get_adapter()).patch, *args, **kwargs + ) + node = await db.get(orm.Node, self.node.id) + if len(node.data_sources) != 1: + raise NotImplementedError("Only handles one data source") + data_source = node.data_sources[0] + structure_row = await db.get(orm.Structure, data_source.structure_id) + # Get the current structure row, update the shape, and write it back + structure_dict = copy.deepcopy(structure_row.structure) + structure_dict["shape"], structure_dict["chunks"] = new_shape_and_chunks + new_structure_id = compute_structure_id(structure_dict) + statement = ( + self.insert(orm.Structure) + .values( + id=new_structure_id, + structure=structure_dict, + ) + .on_conflict_do_nothing(index_elements=["id"]) + ) + await db.execute(statement) + new_structure = await db.get(orm.Structure, new_structure_id) + data_source.structure = new_structure + data_source.structure_id = new_structure_id + db.add(data_source) + await db.commit() + return structure_dict + class CatalogAwkwardAdapter(CatalogNodeAdapter): async def read(self, *args, **kwargs): @@ -1330,6 +1362,7 @@ def in_memory( access_policy=access_policy, writable_storage=writable_storage, readable_storage=readable_storage, + init_if_not_exists=True, echo=echo, adapters_by_mimetype=adapters_by_mimetype, ) diff --git a/tiled/client/array.py b/tiled/client/array.py index edfdffa5d..ba97263e3 100644 --- a/tiled/client/array.py +++ b/tiled/client/array.py @@ -1,9 +1,13 @@ import itertools +from typing import Union import dask import dask.array +import httpx import numpy +from numpy.typing import NDArray +from ..structures.core import STRUCTURE_TYPES from .base import BaseClient from .utils import export_util, handle_error, params_from_slice @@ -167,15 +171,94 @@ def write(self, array): ) ) - def write_block(self, array, block): + def write_block(self, array, block, slice=...): handle_error( self.context.http_client.put( self.item["links"]["block"].format(*block), content=array.tobytes(), headers={"Content-Type": "application/octet-stream"}, + params=params_from_slice(slice), ) ) + def patch(self, array: NDArray, offset: Union[int, tuple[int, ...]], extend=False): + """ + Write data into a slice of an array, maybe extending the shape. + + Parameters + ---------- + array : array-like + The data to write + offset : tuple[int, ...] + Where to place this data in the array + extend : bool + Extend the array shape to fit the new slice, if necessary + + Examples + -------- + + Create a (3, 2, 2) array of ones. + + >>> ac = c.write_array(numpy.ones((3, 2, 2)), key='y') + >>> ac + + + Read it. + + >>> ac.read() + array([[[1., 1.], + [1., 1.]], + + [[1., 1.], + [1., 1.]], + + [[1., 1.], + [1., 1.]]]) + + Extend the array by concatenating a (1, 2, 2) array of zeros. + + >>> ac.patch(numpy.zeros((1, 2, 2)), offset=(3,), extend=True) + + Read it. + + >>> array([[[1., 1.], + [1., 1.]], + + [[1., 1.], + [1., 1.]], + + [[1., 1.], + [1., 1.]], + + [[0., 0.], + [0., 0.]]]) + """ + array_ = numpy.ascontiguousarray(array) + if isinstance(offset, int): + offset = (offset,) + params = { + "offset": ",".join(map(str, offset)), + "shape": ",".join(map(str, array_.shape)), + "extend": bool(extend), + } + response = self.context.http_client.patch( + self.item["links"]["full"], + content=array_.tobytes(), + headers={"Content-Type": "application/octet-stream"}, + params=params, + ) + if response.status_code == httpx.codes.CONFLICT: + raise ValueError( + f"Slice {slice} does not fit within current array shape. " + "Pass keyword argument extend=True to extend the array " + "dimensions to fit." + ) + 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) + def __getitem__(self, slice): return self.read(slice) diff --git a/tiled/client/base.py b/tiled/client/base.py index 1f7b8b6b5..b9a489342 100644 --- a/tiled/client/base.py +++ b/tiled/client/base.py @@ -184,6 +184,11 @@ def refresh(self): ) ).json() self._item = content["data"] + if self.structure_family != StructureFamily.container: + structure_type = STRUCTURE_TYPES[self.structure_family] + self._structure = structure_type.from_json( + self._item["attributes"]["structure"] + ) return self @property diff --git a/tiled/server/dependencies.py b/tiled/server/dependencies.py index f13c676fc..0eefa0c56 100644 --- a/tiled/server/dependencies.py +++ b/tiled/server/dependencies.py @@ -1,5 +1,5 @@ from functools import lru_cache -from typing import Optional +from typing import Optional, Tuple, Union import pydantic_settings from fastapi import Depends, HTTPException, Query, Request, Security @@ -165,6 +165,20 @@ def expected_shape( return tuple(map(int, expected_shape.split(","))) +def shape_param( + shape: str = Query(..., min_length=1, pattern="^[0-9]+(,[0-9]+)*$|^scalar$"), +): + "Specify and parse a shape parameter." + return tuple(map(int, shape.split(","))) + + +def offset_param( + offset: str = Query(..., min_length=1, pattern="^[0-9]+(,[0-9]+)*$"), +): + "Specify and parse an offset parameter." + return tuple(map(int, offset.split(","))) + + def np_style_slicer(indices: tuple): return indices[0] if len(indices) == 1 else slice_func(*indices) @@ -175,7 +189,7 @@ def parse_slice_str(dim: str): def slice_( slice: Optional[str] = Query(None, pattern=SLICE_REGEX), -): +) -> Tuple[Union[slice, int], ...]: "Specify and parse a block index parameter." return tuple(parse_slice_str(dim) for dim in (slice or "").split(",") if dim) diff --git a/tiled/server/pydantic_array.py b/tiled/server/pydantic_array.py index 2c37a51dc..3acad6176 100644 --- a/tiled/server/pydantic_array.py +++ b/tiled/server/pydantic_array.py @@ -172,7 +172,6 @@ class ArrayStructure(BaseModel): shape: Tuple[int, ...] # tuple of ints like (3, 3) dims: Optional[Tuple[str, ...]] = None # None or tuple of names like ("x", "y") resizable: Union[bool, Tuple[bool, ...]] = False - model_config = ConfigDict(extra="forbid") @classmethod diff --git a/tiled/server/router.py b/tiled/server/router.py index 70d9a154f..15e251025 100644 --- a/tiled/server/router.py +++ b/tiled/server/router.py @@ -55,6 +55,8 @@ get_query_registry, get_serialization_registry, get_validation_registry, + offset_param, + shape_param, slice_, ) from .file_response_with_range import FileResponseWithRange @@ -1290,6 +1292,33 @@ async def put_array_block( return json_or_msgpack(request, None) +@router.patch("/array/full/{path:path}") +async def patch_array_full( + request: Request, + offset=Depends(offset_param), + shape=Depends(shape_param), + extend: bool = False, + entry=SecureEntry( + scopes=["write:data"], + structure_families={StructureFamily.array}, + ), + deserialization_registry=Depends(get_deserialization_registry), +): + if not hasattr(entry, "patch"): + raise HTTPException( + status_code=HTTP_405_METHOD_NOT_ALLOWED, + detail="This node cannot accept array data.", + ) + + dtype = entry.structure().data_type.to_numpy_dtype() + body = await request.body() + media_type = request.headers["content-type"] + deserializer = deserialization_registry.dispatch("array", media_type) + data = await ensure_awaitable(deserializer, body, dtype, shape) + structure = await ensure_awaitable(entry.patch, data, offset, extend) + return json_or_msgpack(request, structure) + + @router.put("/table/full/{path:path}") @router.put("/node/full/{path:path}", deprecated=True) async def put_node_full( diff --git a/tiled/structures/array.py b/tiled/structures/array.py index 23b625d51..53207b84e 100644 --- a/tiled/structures/array.py +++ b/tiled/structures/array.py @@ -270,5 +270,8 @@ def from_array(cls, array, shape=None, chunks=None, dims=None) -> "ArrayStructur else: data_type = BuiltinDtype.from_numpy_dtype(array.dtype) return ArrayStructure( - data_type=data_type, shape=shape, chunks=normalized_chunks, dims=dims + data_type=data_type, + shape=shape, + chunks=normalized_chunks, + dims=dims, ) diff --git a/tiled/adapters/type_alliases.py b/tiled/type_aliases.py similarity index 100% rename from tiled/adapters/type_alliases.py rename to tiled/type_aliases.py