-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added json serializer that encodes tables and images (#29)
Closes #20 ### Summary of Changes - added SafeDSEncoder that is used for custom types (tables, images) - don't crash when unknown types are attempted to be sent, instead send a "\<Not Displayable\>" placeholder value - added encoding tests + not displayable to existing message exchange --------- Co-authored-by: megalinter-bot <[email protected]>
- Loading branch information
1 parent
7ed7c5c
commit 054cca4
Showing
4 changed files
with
148 additions
and
10 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,51 @@ | ||
"""Module containing JSON encoding utilities for Safe-DS types.""" | ||
|
||
import base64 | ||
import json | ||
import math | ||
from typing import Any | ||
|
||
from safeds.data.image.containers import Image | ||
from safeds.data.image.typing import ImageFormat | ||
from safeds.data.tabular.containers import Table | ||
|
||
|
||
class SafeDsEncoder(json.JSONEncoder): | ||
"""JSON Encoder for custom Safe-DS types.""" | ||
|
||
def default(self, o: Any) -> Any: | ||
""" | ||
Convert specific Safe-DS types to a JSON-serializable representation. | ||
If values are custom Safe-DS types (such as Table or Image) they are converted to a serializable representation. | ||
If a value is not handled here, the default encoding implementation is called. | ||
In case of Tables, note that NaN values are converted to JSON null values. | ||
Parameters | ||
---------- | ||
o: Any | ||
An object that needs to be encoded to JSON. | ||
Returns | ||
------- | ||
Any | ||
The passed object represented in a way that is serializable to JSON. | ||
""" | ||
if isinstance(o, Table): | ||
dict_with_nan_infinity = o.to_dict() | ||
# Convert NaN / Infinity to None, as the JSON encoder generates invalid JSON otherwise | ||
return { | ||
key: [ | ||
value if not isinstance(value, float) or math.isfinite(value) else None | ||
for value in dict_with_nan_infinity[key] | ||
] | ||
for key in dict_with_nan_infinity | ||
} | ||
if isinstance(o, Image): | ||
# Send images together with their format | ||
match o.format: | ||
case ImageFormat.JPEG: | ||
return {"format": o.format.value, "bytes": str(base64.encodebytes(o._repr_jpeg_()), "utf-8")} | ||
case ImageFormat.PNG: | ||
return {"format": o.format.value, "bytes": str(base64.encodebytes(o._repr_png_()), "utf-8")} | ||
return json.JSONEncoder.default(self, o) |
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,65 @@ | ||
import base64 | ||
import json | ||
import math | ||
from io import BytesIO | ||
from typing import Any | ||
|
||
import pytest | ||
from safeds.data.image.containers import Image | ||
from safeds.data.image.typing import ImageFormat | ||
from safeds.data.tabular.containers import Table | ||
from safeds_runner.server.json_encoder import SafeDsEncoder | ||
|
||
|
||
@pytest.mark.parametrize( | ||
argnames="data,expected_string", | ||
argvalues=[ | ||
( | ||
Table.from_dict({"a": [1, 2], "b": [3.2, 4.0], "c": [math.nan, 5.6], "d": [5, -6]}), | ||
'{"a": [1, 2], "b": [3.2, 4.0], "c": [null, 5.6], "d": [5, -6]}', | ||
), | ||
( | ||
Image( | ||
BytesIO( | ||
base64.b64decode( | ||
"iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5" | ||
"+AAAAD0lEQVQIW2NkQAOMpAsAAADuAAVDMQ2mAAAAAElFTkSuQmCC", | ||
), | ||
), | ||
ImageFormat.PNG, | ||
), | ||
( | ||
'{"format": "png", "bytes": ' | ||
'"iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAADElEQVR4nGNgoBwAAABEAAHX40j9\\nAAAAAElFTkSuQmCC\\n"}' | ||
), | ||
), | ||
( | ||
Image( | ||
BytesIO( | ||
base64.b64decode( | ||
"/9j/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wgALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAAA//aAAgBAQAAAAE//9k=", | ||
), | ||
), | ||
ImageFormat.JPEG, | ||
), | ||
( | ||
'{"format": "jpeg", "bytes":' | ||
' "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a' | ||
"\\nHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAABAAEBAREA/8QAHwAAAQUBAQEB\\nAQEAAAAAAAAAAAECAwQFBgcICQoL" | ||
"/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1Fh" | ||
"\\nByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZ" | ||
"\\nWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXG\\nx8jJytLT1NXW19jZ2uHi4" | ||
'+Tl5ufo6erx8vP09fb3+Pn6/9oACAEBAAA/AEr/2Q==\\n"}' | ||
), | ||
), | ||
], | ||
ids=["encode_table", "encode_image_png", "encode_image_jpeg"], | ||
) | ||
def test_encoding_custom_types(data: Any, expected_string: str) -> None: | ||
assert json.dumps(data, cls=SafeDsEncoder) == expected_string | ||
|
||
|
||
@pytest.mark.parametrize(argnames="data", argvalues=[(object())], ids=["encode_object"]) | ||
def test_encoding_unsupported_types(data: Any) -> None: | ||
with pytest.raises(TypeError): | ||
json.dumps(data, cls=SafeDsEncoder) |
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