Skip to content

Commit

Permalink
Merge pull request #44 from silx-kit/binary
Browse files Browse the repository at this point in the history
Added bin format to encoders
  • Loading branch information
loichuder authored Oct 25, 2021
2 parents cf3b833 + cdff304 commit 0e1ee2c
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 25 deletions.
5 changes: 3 additions & 2 deletions docs/low_level.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ These methods are directly plugged to the endpoints from the example implementat

## `encoders` module

The [encoders](https://silx-kit.github.io/h5grove/reference.html#encoders-module) module contain functions that encode data and provide the appropriate headers to build request responses. The module provides a JSON encoder using `orjson` and a binary encoder for NumPy arrays.
The [encoders](https://silx-kit.github.io/h5grove/reference.html#encoders-module) module contain functions that encode data and provide the appropriate headers to build request responses. The module provides a JSON encoder using `orjson` and binary encoders for NumPy arrays.

### General

Expand All @@ -61,8 +61,9 @@ The [encoders](https://silx-kit.github.io/h5grove/reference.html#encoders-module
.. autofunction:: h5grove.encoders.orjson_encode
```

### npy
### binary

```{eval-rst}
.. autofunction:: h5grove.encoders.bin_encode
.. autofunction:: h5grove.encoders.npy_stream
```
29 changes: 24 additions & 5 deletions h5grove/encoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
from .utils import sanitize_array


def bin_encode(array: Sequence[Number]) -> bytes:
"""Sanitize an array and convert it to bytes.
:param array: Data to convert
"""
sanitized_array = sanitize_array(array)

return sanitized_array.tobytes()


def orjson_default(o) -> Union[list, str, None]:
"""Converts Python objects to JSON-serializable objects.
Expand Down Expand Up @@ -66,7 +76,7 @@ def npy_stream(array: Sequence[Number]) -> Generator[bytes, None, None]:


class Response(NamedTuple):
content: Generator[bytes, None, None]
content: Union[Generator[bytes, None, None], bytes]
""" Encoded `content` as a generator of bytes """
headers: Dict[str, str]
""" Associated headers """
Expand All @@ -86,16 +96,25 @@ def encode(content, encoding: Optional[str] = "json") -> Response:
"""
if encoding in ("json", None):
return Response(
(chunk for chunk in (orjson_encode(content),)), # generator
orjson_encode(content),
headers={"Content-Type": "application/json"},
)
elif encoding == "npy":

if encoding == "npy":
return Response(
npy_stream(content),
headers={
"Content-Type": "application/octet-stream",
"Content-Disposition": 'attachment; filename="data.npy"',
},
)
else:
raise ValueError(f"Unsupported encoding {encoding}")

if encoding == "bin":
return Response(
bin_encode(content),
headers={
"Content-Type": "application/octet-stream",
},
)

raise ValueError(f"Unsupported encoding {encoding}")
24 changes: 12 additions & 12 deletions h5grove/flaskutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
]


def make_encoded_response(content, format: Optional[str]) -> Response:
def make_encoded_response(content, format_arg: Optional[str]) -> Response:
"""Prepare flask Response according to format"""
encoded_content, headers = encode(content, format)
encoded_content, headers = encode(content, format_arg)
response = Response(encoded_content)
response.headers.update(headers)
return response
Expand All @@ -35,8 +35,8 @@ def get_content(h5file: h5py.File, path: Optional[str], resolve_links: bool = Tr
abort(404, str(e))


def get_filename(request: Request) -> str:
file_path = request.args.get("file")
def get_filename(a_request: Request) -> str:
file_path = a_request.args.get("file")
if file_path is None:
raise KeyError("File argument is required")

Expand All @@ -51,53 +51,53 @@ def attr_route():
"""`/attr/` endpoint handler"""
filename = get_filename(request)
path = request.args.get("path")
format = request.args.get("format")
format_arg = request.args.get("format")

with h5py.File(filename, mode="r") as h5file:
content = get_content(h5file, path)
assert isinstance(content, ResolvedEntityContent)
return make_encoded_response(content.attributes(), format)
return make_encoded_response(content.attributes(), format_arg)


def data_route():
"""`/data/` endpoint handler"""
filename = get_filename(request)
path = request.args.get("path")
selection = request.args.get("selection")
format = request.args.get("format")
format_arg = request.args.get("format")

with h5py.File(filename, mode="r") as h5file:
content = get_content(h5file, path)
assert isinstance(content, DatasetContent)
return make_encoded_response(content.data(selection), format)
return make_encoded_response(content.data(selection), format_arg)


def meta_route():
"""`/meta/` endpoint handler"""
filename = get_filename(request)
path = request.args.get("path")
format = request.args.get("format")
format_arg = request.args.get("format")
resolve_links_arg = request.args.get("resolve_links")
resolve_links = (
resolve_links_arg.lower() != "false" if resolve_links_arg is not None else True
)

with h5py.File(filename, mode="r") as h5file:
content = get_content(h5file, path, resolve_links)
return make_encoded_response(content.metadata(), format)
return make_encoded_response(content.metadata(), format_arg)


def stats_route():
"""`/stats/` endpoint handler"""
filename = get_filename(request)
path = request.args.get("path")
selection = request.args.get("selection")
format = request.args.get("format")
format_arg = request.args.get("format")

with h5py.File(filename, mode="r") as h5file:
content = get_content(h5file, path)
assert isinstance(content, DatasetContent)
return make_encoded_response(content.data_stats(selection), format)
return make_encoded_response(content.data_stats(selection), format_arg)


URL_RULES = {
Expand Down
14 changes: 9 additions & 5 deletions h5grove/tornadoutils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Helpers for usage with `Tornado <https://www.tornadoweb.org>`_"""
from h5grove.utils import NotFoundError
import os
from typing import Any, Optional
from typing import Any, Generator, Optional
import h5py
from tornado.web import RequestHandler, MissingArgumentError, HTTPError
from .content import DatasetContent, ResolvedEntityContent, create_content
Expand Down Expand Up @@ -31,7 +31,7 @@ def get(self):
raise MissingArgumentError("file")

path = self.get_query_argument("path", None)
format = self.get_query_argument("format", None)
format_arg = self.get_query_argument("format", None)

full_file_path = os.path.join(self.base_dir, file_path)
if not os.path.isfile(full_file_path):
Expand All @@ -43,12 +43,16 @@ def get(self):
except NotFoundError as e:
raise HTTPError(status_code=404, reason=str(e))

encoded_content_chunks, headers = encode(content, format)
chunks, headers = encode(content, format_arg)

for key, value in headers.items():
self.set_header(key, value)
for chunk in encoded_content_chunks:
self.write(chunk)

if isinstance(chunks, Generator):
for chunk in chunks:
self.write(chunk)
else:
self.write(chunks)
self.finish()

def get_content(self, h5file, path):
Expand Down
2 changes: 1 addition & 1 deletion test/test_benchmark_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pytest

# Benchmark conditions
BENCHMARK_FORMAT = "json", "npy"
BENCHMARK_FORMAT = "json", "npy", "bin"
BENCHMARKS = dict(
(f"/{size}square_{dtype}", (size, dtype))
for dtype in ("float32",)
Expand Down

0 comments on commit 0e1ee2c

Please sign in to comment.