-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Sketch support for writing, reading sliced AwkwardArrays.
- Loading branch information
1 parent
8de8e8a
commit 830e312
Showing
18 changed files
with
459 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import awkward | ||
|
||
from ..catalog import in_memory | ||
from ..client import Context, from_context, record_history | ||
from ..server.app import build_app | ||
|
||
|
||
def test_awkward(tmpdir): | ||
catalog = in_memory(writable_storage=tmpdir) | ||
app = build_app(catalog) | ||
with Context.from_app(app) as context: | ||
client = from_context(context) | ||
|
||
# Write data into catalog. It will be stored as directory of buffers | ||
# named like 'node0-offsets' and 'node2-data'. | ||
array = awkward.Array( | ||
[ | ||
[{"x": 1.1, "y": [1]}, {"x": 2.2, "y": [1, 2]}], | ||
[], | ||
[{"x": 3.3, "y": [1, 2, 3]}], | ||
] | ||
) | ||
aac = client.write_awkward(array, key="test") | ||
|
||
# Read the data back out from the AwkwardArrrayClient, progressively sliced. | ||
assert awkward.almost_equal(aac.read(), array) | ||
assert awkward.almost_equal(aac[:], array) | ||
assert awkward.almost_equal(aac[0], array[0]) | ||
assert awkward.almost_equal(aac[0, "y"], array[0, "y"]) | ||
assert awkward.almost_equal(aac[0, "y", :1], array[0, "y", :1]) | ||
|
||
# When sliced, the serer sends less data. | ||
with record_history() as h: | ||
aac[:] | ||
assert len(h.responses) == 1 # sanity check | ||
full_response_size = len(h.responses[0].content) | ||
with record_history() as h: | ||
aac[0, "y"] | ||
assert len(h.responses) == 1 # sanity check | ||
sliced_response_size = len(h.responses[0].content) | ||
assert sliced_response_size < full_response_size |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
""" | ||
A directory containing awkward buffers, one file per form key. | ||
""" | ||
from urllib import parse | ||
|
||
from ..structures.core import StructureFamily | ||
|
||
|
||
class AwkwardBuffersAdapter: | ||
structure_family = StructureFamily.awkward | ||
|
||
def __init__( | ||
self, | ||
directory, | ||
structure, | ||
metadata=None, | ||
specs=None, | ||
access_policy=None, | ||
): | ||
self.directory = directory | ||
self._metadata = metadata or {} | ||
self._structure = structure | ||
self.specs = list(specs or []) | ||
self.access_policy = access_policy | ||
|
||
def metadata(self): | ||
return self._metadata | ||
|
||
@classmethod | ||
def init_storage(cls, directory, structure): | ||
from ..server.schemas import Asset | ||
|
||
directory.mkdir() | ||
data_uri = parse.urlunparse(("file", "localhost", str(directory), "", "", None)) | ||
return [Asset(data_uri=data_uri, is_directory=True)] | ||
|
||
def write(self, data): | ||
for form_key, value in data.items(): | ||
with open(self.directory / form_key, "wb") as file: | ||
file.write(value) | ||
|
||
def read(self, form_keys=None): | ||
selected_suffixed_form_keys = [] | ||
if form_keys is None: | ||
# Read all. | ||
selected_suffixed_form_keys.extend(self._structure.suffixed_form_keys) | ||
else: | ||
for form_key in form_keys: | ||
for suffixed_form_key in self._structure.suffixed_form_keys: | ||
if suffixed_form_key.startswith(form_key): | ||
selected_suffixed_form_keys.append(suffixed_form_key) | ||
buffers = {} | ||
for form_key in selected_suffixed_form_keys: | ||
with open(self.directory / form_key, "rb") as file: | ||
buffers[form_key] = file.read() | ||
return buffers | ||
|
||
def structure(self): | ||
return self._structure |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
tiled/catalog/migrations/versions/0b033e7fbe30_add_awkward_to_structurefamily_enum.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
"""Add 'awkward' to structurefamily enum. | ||
Revision ID: 0b033e7fbe30 | ||
Revises: 83889e049ddc | ||
Create Date: 2023-08-08 21:10:20.181470 | ||
""" | ||
import sqlalchemy as sa | ||
from alembic import op | ||
|
||
# revision identifiers, used by Alembic. | ||
revision = "0b033e7fbe30" | ||
down_revision = "83889e049ddc" | ||
branch_labels = None | ||
depends_on = None | ||
|
||
|
||
def upgrade(): | ||
connection = op.get_bind() | ||
|
||
if connection.engine.dialect.name == "postgresql": | ||
with op.get_context().autocommit_block(): | ||
op.execute( | ||
sa.text( | ||
"ALTER TYPE structurefamily ADD VALUE IF NOT EXISTS 'awkward' AFTER 'array'" | ||
) | ||
) | ||
|
||
|
||
def downgrade(): | ||
# This _could_ be implemented but we will wait for a need since we are | ||
# still in alpha releases. | ||
raise NotImplementedError |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import awkward | ||
|
||
from ..serialization.awkward import from_zipped_buffers, to_zipped_buffers | ||
from ..structures.awkward import project_form | ||
from .base import BaseClient | ||
from .utils import handle_error | ||
|
||
|
||
class AwkwardArrayClient(BaseClient): | ||
def __repr__(self): | ||
# TODO Include some summary of the structure. Probably | ||
# lift __repr__ code from awkward itself here. | ||
return f"<{type(self).__name__}>" | ||
|
||
def write(self, container): | ||
handle_error( | ||
self.context.http_client.put( | ||
self.item["links"]["full"], | ||
content=bytes(to_zipped_buffers(container, {})), | ||
headers={"Content-Type": "application/zip"}, | ||
) | ||
) | ||
|
||
def read(self, slice=...): | ||
structure = self.structure() | ||
form = awkward.forms.from_dict(structure.form) | ||
typetracer, report = awkward.typetracer.typetracer_with_report( | ||
form, | ||
forget_length=True, | ||
) | ||
proxy_array = awkward.Array(typetracer) | ||
# TODO Ask awkward to promote _touch_data to a public method. | ||
proxy_array[slice].layout._touch_data(recursive=True) | ||
form_keys_touched = set(report.data_touched) | ||
projected_form = project_form(form, form_keys_touched) | ||
# The order is not important, but sort so that the request is deterministic. | ||
params = {"form_key": sorted(list(form_keys_touched))} | ||
content = handle_error( | ||
self.context.http_client.get( | ||
self.item["links"]["full"], | ||
headers={"Accept": "application/zip"}, | ||
params=params, | ||
) | ||
).read() | ||
container = from_zipped_buffers(content) | ||
projected_array = awkward.from_buffers( | ||
projected_form, structure.length, container | ||
) | ||
return projected_array[slice] | ||
|
||
def __getitem__(self, slice): | ||
return self.read(slice=slice) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import io | ||
import zipfile | ||
|
||
from ..media_type_registration import deserialization_registry, serialization_registry | ||
|
||
|
||
@serialization_registry.register("awkward", "application/zip") | ||
def to_zipped_buffers(container, metadata): | ||
file = io.BytesIO() | ||
# Pack multiple buffers into a zipfile, uncompressed. This enables | ||
# multiple buffers in a single response, with random access. The | ||
# entire payload *may* be compressed using Tiled's normal compression | ||
# mechanisms. | ||
with zipfile.ZipFile(file, "w", compresslevel=zipfile.ZIP_STORED) as zip: | ||
for form_key, buffer in container.items(): | ||
zip.writestr(form_key, buffer) | ||
return file.getbuffer() | ||
|
||
|
||
@deserialization_registry.register("awkward", "application/zip") | ||
def from_zipped_buffers(buffer): | ||
file = io.BytesIO(buffer) | ||
with zipfile.ZipFile(file, "r") as zip: | ||
form_keys = zip.namelist() | ||
buffers = {} | ||
for form_key in form_keys: | ||
buffers[form_key] = zip.read(form_key) | ||
return buffers |
Oops, something went wrong.