Skip to content

Commit 08ac6d8

Browse files
committed
restore PUT_Value with query param, modified rsp to return indices
1 parent 990f7f4 commit 08ac6d8

6 files changed

Lines changed: 370 additions & 80 deletions

File tree

hsds/chunk_crawl.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from h5json.hdf5dtype import createDataType
2727
from h5json.array_util import jsonToArray, getNumpyValue
2828
from h5json.array_util import getNumElements, arrayToBytes, bytesToArray
29-
from h5json.shape_util import getShapeDims
29+
from h5json.shape_util import getShapeDims, getRank
3030
from h5json.dset_util import getChunkDims
3131
from h5json.time_util import getNow
3232

@@ -220,6 +220,7 @@ async def read_chunk_hyperslab(
220220
raise HTTPInternalServerError()
221221
type_json = dset_json["type"]
222222
dset_dt = createDataType(type_json)
223+
dset_rank = getRank(dset_json)
223224

224225
chunk_shape = None # expected return array shape
225226
chunk_sel = None # for hyperslab
@@ -255,6 +256,10 @@ async def read_chunk_hyperslab(
255256

256257
if query is None and query_update is None:
257258
query_dtype = None
259+
elif query_update is not None:
260+
# PUT_Chunk's query-update handling returns the global dataset
261+
# indices of matching elements, as (n, rank) coordinate tuples
262+
query_dtype = np.dtype("i8")
258263
else:
259264
# GET_Chunk's query handling (h5json.query_util.arrayQuery) returns
260265
# the matching values themselves, typed as select_dtype
@@ -381,9 +386,15 @@ async def read_chunk_hyperslab(
381386
log.debug(f"data: {len(array_data)} bytes")
382387
if query is not None or query_update is not None:
383388
# TBD: this needs to be fixed up for variable length dtypes
384-
nrows = len(array_data) // query_dtype.itemsize
389+
if query_update is not None:
390+
# indices are returned as (n, rank)
391+
nrows = len(array_data) // (query_dtype.itemsize * dset_rank)
392+
rsp_shape = (nrows, dset_rank)
393+
else:
394+
nrows = len(array_data) // query_dtype.itemsize
395+
rsp_shape = (nrows,)
385396
try:
386-
chunk_arr = bytesToArray(array_data, query_dtype, (nrows,))
397+
chunk_arr = bytesToArray(array_data, query_dtype, rsp_shape)
387398
except ValueError as ve:
388399
log.warn(f"bytesToArray ValueError: {ve}")
389400
raise HTTPBadRequest()

hsds/chunk_dn.py

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414
# handles regauests to read/write chunk data
1515
#
1616

17+
import json
1718
import numpy as np
1819
import traceback
1920
from aiohttp.web_exceptions import HTTPBadRequest, HTTPInternalServerError
20-
from aiohttp.web_exceptions import HTTPNotFound, HTTPServiceUnavailable, HTTPNotImplemented
21+
from aiohttp.web_exceptions import HTTPNotFound, HTTPServiceUnavailable
2122
from aiohttp.web import json_response, StreamResponse
2223

2324
from h5json.hdf5dtype import createDataType, getSubType
@@ -53,10 +54,20 @@ async def PUT_Chunk(request):
5354
bucket = None
5455
input_arr = None
5556
element_count = None
57+
limit = 0
5658

5759
if "query" in params:
5860
query = params["query"]
5961
log.info(f"PUT_Chunk query: {query}")
62+
if "Limit" in params:
63+
param_limit = params["Limit"]
64+
try:
65+
limit = int(param_limit)
66+
except ValueError:
67+
msg = f"invalid Limit param: {param_limit}"
68+
log.warn(msg)
69+
raise HTTPBadRequest(reason=msg)
70+
log.debug(f"PUT_Chunk limit: {limit}")
6071
chunk_id = request.match_info.get("id")
6172
if not chunk_id:
6273
msg = "Missing chunk id"
@@ -184,13 +195,74 @@ async def PUT_Chunk(request):
184195
raise HTTPNotFound()
185196

186197
if query:
187-
# TBD: query+update support was dropped when chunkUtil.chunkQuery was
188-
# removed in favor of h5json.query_util.arrayQuery. arrayQuery returns
189-
# match coordinates rather than rows, so this needs to be rewritten to
190-
# update chunk_arr at those coordinates and rebuild a response array.
191-
msg = "PUT_Chunk with query is not currently supported"
192-
log.error(msg)
193-
raise HTTPNotImplemented(reason=msg)
198+
try:
199+
indices = arrayQuery(query, chunk_arr, selection=selection, limit=limit)
200+
except (TypeError, ValueError) as e:
201+
msg = f"query: {query} is not valid, got exception: {e}"
202+
log.warn(msg)
203+
raise HTTPBadRequest(reason=msg)
204+
205+
log.debug(f"PUT_Chunk - query matched {len(indices)} elements")
206+
207+
try:
208+
update_value = await request.json()
209+
except json.JSONDecodeError:
210+
msg = "Unable to load JSON body for query update"
211+
log.warn(msg)
212+
raise HTTPBadRequest(reason=msg)
213+
214+
rank = len(chunk_arr.shape)
215+
fancy_index = tuple(indices[:, i] for i in range(rank))
216+
217+
if len(indices) > 0:
218+
# query_update is only allowed when the value is one element -
219+
# that element gets broadcast across all matching positions
220+
if select_dt.names:
221+
# compound type - value is a JSON object of field name to
222+
# value; only the given fields are updated, others are
223+
# left as-is
224+
if not isinstance(update_value, dict):
225+
msg = "expected a JSON object for compound type query update"
226+
log.warn(msg)
227+
raise HTTPBadRequest(reason=msg)
228+
for field_name, field_value in update_value.items():
229+
if field_name not in select_dt.names:
230+
msg = f"field: {field_name} not found in dataset type"
231+
log.warn(msg)
232+
raise HTTPBadRequest(reason=msg)
233+
chunk_arr[field_name][fancy_index] = field_value
234+
else:
235+
# simple type - value is the (scalar) element itself
236+
if isinstance(update_value, dict) and "value" in update_value:
237+
update_value = update_value["value"]
238+
chunk_arr[fancy_index] = update_value
239+
is_dirty = True
240+
241+
# return the global dataset indices of the matching elements -
242+
# the chunk's offset within the dataset (per dimension) is its
243+
# grid index times the chunk dims along that dimension
244+
chunk_index = getChunkIndex(chunk_id)
245+
offset = np.array([chunk_index[i] * dims[i] for i in range(rank)], dtype=indices.dtype)
246+
global_indices = indices + offset
247+
248+
read_resp = arrayToBytes(global_indices)
249+
try:
250+
resp = StreamResponse()
251+
resp.headers["Content-Type"] = "application/octet-stream"
252+
resp.content_length = len(read_resp)
253+
await resp.prepare(request)
254+
await resp.write(read_resp)
255+
except Exception as e:
256+
log.error(f"Exception during binary data write: {e}")
257+
raise HTTPInternalServerError()
258+
finally:
259+
await resp.write_eof()
260+
261+
if is_dirty or config.get("write_zero_chunks", default=False):
262+
save_chunk(app, chunk_id, dset_json, chunk_arr, bucket=bucket)
263+
264+
log.response(request, resp=resp)
265+
return resp
194266
else:
195267
# regular chunk update
196268
# check that the content_length is what we expect

hsds/chunk_sn.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ def _getPoints(body, rank=1):
277277
return points
278278

279279

280-
def _getQuery(params, dtype, rank=1, body=None):
280+
def _getQuery(params, dtype, body=None):
281281
""" get query parameter and validate if set """
282282

283283
kw = "query"
@@ -293,11 +293,6 @@ def _getQuery(params, dtype, rank=1, body=None):
293293
msg = "Query string can not be used with append parameter"
294294
log.warn(msg)
295295
raise HTTPBadRequest(reason=msg)
296-
# validate the query string
297-
if rank > 1:
298-
msg = "Query string is not supported for multidimensional datasets"
299-
log.warn(msg)
300-
raise HTTPBadRequest(reason=msg)
301296

302297
# following will throw HTTPBadRequest if query is malformed
303298
validateQuery(query, dtype)
@@ -383,12 +378,13 @@ async def _getRequestData(request, http_streaming=True):
383378

384379

385380
async def arrayResponse(arr, request, dset_json):
386-
""" return the array as binary or json response based on accept type """
381+
""" return the query-update match indices as a binary or json response
382+
based on accept type """
387383
response_type = getAcceptType(request)
388384

389385
if response_type == "binary":
390386
output_data = arr.tobytes()
391-
msg = f"PUT_Value query - returning {len(output_data)} bytes binary data"
387+
msg = f"PUT_Value query - returning {len(output_data)} bytes binary indices"
392388
log.debug(msg)
393389

394390
# write response
@@ -405,16 +401,16 @@ async def arrayResponse(arr, request, dset_json):
405401
except Exception as e:
406402
log.error(f"Exception during binary data write: {e}")
407403
else:
408-
log.debug("PUT Value query - returning JSON data")
404+
log.debug("PUT Value query - returning JSON indices")
409405
rsp_json = {}
410406
data = arr.tolist()
411-
log.debug(f"got rsp data {len(data)} points")
407+
log.debug(f"got rsp data {len(data)} indices")
412408
try:
413-
json_query_data = bytesArrayToList(data)
409+
indices_data = bytesArrayToList(data)
414410
except ValueError as err:
415411
msg = f"Cannot decode provided bytes to list: {err}"
416412
raise HTTPBadRequest(reason=msg)
417-
rsp_json["value"] = json_query_data
413+
rsp_json["indices"] = indices_data
418414
rsp_json["hrefs"] = get_hrefs(request, dset_json)
419415

420416
resp = await jsonResponse(request, rsp_json)
@@ -518,7 +514,7 @@ async def PUT_Value(request):
518514
# if there's no selection parameter, this will return entire dataspace
519515
selection = _getSelect(params, dset_json, body=body)
520516

521-
query = _getQuery(params, dset_dtype, rank=rank, body=body)
517+
query = _getQuery(params, dset_dtype, body=body)
522518

523519
element_count = _getElementCount(params, body=body)
524520

@@ -808,7 +804,7 @@ async def GET_Value(request):
808804
ignore_nan = False
809805
log.debug(f"ignore nan: {ignore_nan}")
810806

811-
query = _getQuery(params, dset_dtype, rank=rank)
807+
query = _getQuery(params, dset_dtype)
812808

813809
response_type = getAcceptType(request)
814810

hsds/dset_lib.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,12 @@ async def doReadSelection(
520520
select_dtype = dset_dtype
521521
if query is None:
522522
query_dtype = None
523+
elif query_update is not None:
524+
# PUT_Chunk's query-update handling returns the global dataset
525+
# indices of matching elements, as (n, rank) coordinate tuples
526+
log.debug(f"query: {query} limit: {limit} query_update: {query_update}")
527+
query_dtype = np.dtype("i8")
528+
query_rank = getRank(dset_json)
523529
else:
524530
# GET_Chunk's query handling (h5json.query_util.arrayQuery) returns
525531
# the matching values themselves, typed as select_dtype
@@ -600,7 +606,10 @@ async def doReadSelection(
600606
nrows = limit
601607
else:
602608
nrows = crawler._hits
603-
arr = np.empty((nrows,), dtype=query_dtype)
609+
if query_update is not None:
610+
arr = np.empty((nrows, query_rank), dtype=query_dtype)
611+
else:
612+
arr = np.empty((nrows,), dtype=query_dtype)
604613
start = 0
605614
for chunkid in chunk_ids:
606615
if chunkid not in chunk_map:

0 commit comments

Comments
 (0)