Skip to content

Commit d2aed25

Browse files
authored
Merge pull request #73 from labthings/cbor
CBOR representations
2 parents 9bd3016 + 6926cdb commit d2aed25

File tree

15 files changed

+244
-122
lines changed

15 files changed

+244
-122
lines changed

examples/components/pdf_component.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def __init__(self):
2121
"mode": "spectrum",
2222
"light_on": True,
2323
"user": {"name": "Squidward", "id": 1},
24+
"bytes": b"\x80\x04\x95\x1a\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x06object\x94\x93\x94)\x81\x94.",
2425
}
2526

2627
def noisy_pdf(self, x, mu=0.0, sigma=25.0):

labthings/core/utilities.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55
import os
66
import copy
7+
import typing
78
from functools import reduce
89

910
PY3 = sys.version_info > (3,)
@@ -98,11 +99,11 @@ def rapply(data, func, *args, apply_to_iterables=True, **kwargs):
9899
)
99100
for key, val in data.items()
100101
}
101-
# If the object is iterable but NOT a dictionary or a string
102+
# If the object is a list, tuple, or range
102103
elif apply_to_iterables and (
103-
isinstance(data, collections.abc.Iterable)
104-
and not isinstance(data, collections.abc.Mapping)
105-
and not isinstance(data, str)
104+
isinstance(data, typing.List)
105+
or isinstance(data, typing.Tuple)
106+
or isinstance(data, range)
106107
):
107108
return [
108109
rapply(x, func, *args, apply_to_iterables=apply_to_iterables, **kwargs)

labthings/server/fields.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
# Marshmallow fields
2+
from marshmallow import ValidationError
3+
4+
from base64 import b64decode
5+
26
from marshmallow.fields import (
37
Field,
48
Raw,
@@ -33,6 +37,7 @@
3337
)
3438

3539
__all__ = [
40+
"Bytes",
3641
"Field",
3742
"Raw",
3843
"Nested",
@@ -64,3 +69,20 @@
6469
"Constant",
6570
"Pluck",
6671
]
72+
73+
74+
class Bytes(Field):
75+
def _validate(self, value):
76+
if not isinstance(value, bytes):
77+
raise ValidationError("Invalid input type.")
78+
79+
if value is None or value == b"":
80+
raise ValidationError("Invalid value")
81+
82+
def _deserialize(self, value, attr, data, **kwargs):
83+
if isinstance(value, bytes):
84+
return value
85+
if isinstance(value, str):
86+
return b64decode(value)
87+
else:
88+
raise self.make_error("invalid", input=value)

labthings/server/labthing.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from flask import url_for
22
from apispec import APISpec
3-
from apispec.ext.marshmallow import MarshmallowPlugin
3+
4+
# from apispec.ext.marshmallow import MarshmallowPlugin
45

56
from .names import (
67
EXTENSION_NAME,
@@ -14,6 +15,7 @@
1415
from .logging import LabThingLogger
1516
from .representations import LabThingsJSONEncoder
1617
from .spec.apispec import rule_to_apispec_path
18+
from .spec.apispec_plugins import MarshmallowPlugin
1719
from .spec.utilities import get_spec
1820
from .spec.td import ThingDescription
1921
from .decorators import tag
@@ -82,13 +84,15 @@ def __init__(
8284
self.log_handler = LabThingLogger()
8385
logging.getLogger().addHandler(self.log_handler)
8486

87+
# API Spec
8588
self.spec = APISpec(
8689
title=self.title,
8790
version=self.version,
8891
openapi_version="3.0.2",
8992
plugins=[MarshmallowPlugin()],
9093
)
9194

95+
# Thing description
9296
self.thing_description = ThingDescription(self.spec)
9397

9498
if app is not None:

labthings/server/representations.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
# Flask JSON encoder so we get UUID, datetime etc support
44
from flask.json import JSONEncoder
5-
from json import dumps
5+
from base64 import b64encode
6+
import json
7+
import cbor2
68

79

810
from ..core.utilities import PY3
@@ -16,17 +18,14 @@ class LabThingsJSONEncoder(JSONEncoder):
1618
def default(self, o):
1719
if isinstance(o, set):
1820
return list(o)
21+
if isinstance(o, bytes):
22+
return b64encode(o).decode()
1923
return JSONEncoder.default(self, o)
2024

2125

2226
def encode_json(data, encoder=LabThingsJSONEncoder, **settings):
2327
"""Makes JSON encoded data using the LabThings JSON encoder"""
24-
25-
# always end the json dumps with a new line
26-
# see https://github.com/mitsuhiko/flask/pull/1262
27-
dumped = dumps(data, cls=encoder, **settings) + "\n"
28-
29-
return dumped
28+
return json.dumps(data, cls=encoder, **settings) + "\n"
3029

3130

3231
def output_json(data, code, headers=None):
@@ -35,9 +34,6 @@ def output_json(data, code, headers=None):
3534
settings = current_app.config.get("LABTHINGS_JSON", {})
3635
encoder = current_app.json_encoder
3736

38-
# If we're in debug mode, and the indent is not set, we set it to a
39-
# reasonable value here. Note that this won't override any existing value
40-
# that was set. We also set the "sort_keys" value.
4137
if current_app.debug:
4238
settings.setdefault("indent", 4)
4339
settings.setdefault("sort_keys", not PY3)
@@ -46,7 +42,29 @@ def output_json(data, code, headers=None):
4642

4743
resp = make_response(dumped, code)
4844
resp.headers.extend(headers or {})
45+
resp.mimetype = "application/json"
46+
return resp
47+
48+
49+
def encode_cbor(data, **settings):
50+
"""Makes CBOR encoded data using the default CBOR encoder"""
51+
return cbor2.dumps(data, **settings)
52+
53+
54+
def output_cbor(data, code, headers=None):
55+
"""Makes a Flask response with a CBOR encoded body, using app CBOR settings"""
56+
57+
settings = current_app.config.get("LABTHINGS_CBOR", {})
58+
59+
dumped = encode_cbor(data, **settings)
60+
61+
resp = make_response(dumped, code)
62+
resp.headers.extend(headers or {})
63+
resp.mimetype = "application/cbor"
4964
return resp
5065

5166

52-
DEFAULT_REPRESENTATIONS = [("application/json", output_json)]
67+
DEFAULT_REPRESENTATIONS = [
68+
("application/json", output_json),
69+
("application/cbor", output_cbor),
70+
]

labthings/server/schema.py

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,13 @@
11
# -*- coding: utf-8 -*-
2-
from flask import jsonify, url_for
2+
from flask import url_for
33
from werkzeug.routing import BuildError
4-
import marshmallow
4+
from marshmallow import Schema, pre_dump
55

66
from .names import TASK_ENDPOINT, EXTENSION_LIST_ENDPOINT
77
from .utilities import view_class_from_endpoint, description_from_view
88
from . import fields
99

10-
sentinel = object()
11-
12-
13-
class Schema(marshmallow.Schema):
14-
"""Base serializer with which to define custom serializers.
15-
See `marshmallow.Schema` for more details about the `Schema` API.
16-
"""
17-
18-
def jsonify(self, obj, *args, many=sentinel, **kwargs):
19-
"""Return a JSON response containing the serialized data.
20-
:param obj: Object to serialize.
21-
:param bool many: Whether `obj` should be serialized as an instance
22-
or as a collection. If unset, defaults to the value of the
23-
`many` attribute on this Schema.
24-
:param kwargs: Additional keyword arguments passed to `flask.jsonify`.
25-
"""
26-
if many is sentinel:
27-
many = self.many
28-
data = self.dump(obj, many=many)
29-
return jsonify(data, *args, **kwargs)
10+
__all__ = ["Schema", "FieldSchema", "TaskSchema", "ExtensionSchema"]
3011

3112

3213
class FieldSchema:
@@ -64,17 +45,6 @@ def serialize(self, value):
6445
def dump(self, value):
6546
return self.serialize(value)
6647

67-
def jsonify(self, value):
68-
"""Serialize a value to JSON
69-
70-
Args:
71-
value: Data to serialize
72-
73-
Returns:
74-
Serialized JSON data
75-
"""
76-
return jsonify(self.serialize(value))
77-
7848

7949
class TaskSchema(Schema):
8050
_ID = fields.String(data_key="id")
@@ -89,7 +59,7 @@ class TaskSchema(Schema):
8959

9060
links = fields.Dict()
9161

92-
@marshmallow.pre_dump
62+
@pre_dump
9363
def generate_links(self, data, **kwargs):
9464
try:
9565
url = url_for(TASK_ENDPOINT, task_id=data.id, _external=True)
@@ -114,7 +84,7 @@ class ExtensionSchema(Schema):
11484

11585
links = fields.Dict()
11686

117-
@marshmallow.pre_dump
87+
@pre_dump
11888
def generate_links(self, data, **kwargs):
11989
d = {}
12090
for view_id, view_data in data.views.items():
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from apispec.ext.marshmallow import (
2+
MarshmallowPlugin as _MarshmallowPlugin,
3+
OpenAPIConverter,
4+
)
5+
from labthings.server.fields import Bytes as BytesField
6+
7+
8+
class ExtendedOpenAPIConverter(OpenAPIConverter):
9+
field_mapping = OpenAPIConverter.field_mapping
10+
field_mapping.update({BytesField: ("string", None)})
11+
12+
def init_attribute_functions(self, *args, **kwargs):
13+
OpenAPIConverter.init_attribute_functions(self, *args, **kwargs)
14+
self.attribute_functions.append(self.bytes2json)
15+
16+
def bytes2json(self, field, **kwargs):
17+
ret = {}
18+
if isinstance(field, BytesField):
19+
ret.update({"contentEncoding": "base64"})
20+
return ret
21+
22+
23+
class MarshmallowPlugin(_MarshmallowPlugin):
24+
Converter = ExtendedOpenAPIConverter

labthings/server/types/registry.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
Dict,
2424
List,
2525
list,
26+
bytes,
2627
]
2728

2829

@@ -59,6 +60,7 @@ def __init__(self) -> None:
5960
UUID: fields.UUID,
6061
dict: fields.Dict,
6162
Dict: fields.Dict,
63+
bytes: fields.Bytes,
6264
_empty: fields.Field,
6365
}.items()
6466
}

poetry.lock

Lines changed: 15 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ flask-cors = "^3.0.8"
2121
gevent = ">=1.4,<21.0"
2222
gevent-websocket = "^0.10.1"
2323
zeroconf = ">=0.24.5,<0.27.0"
24+
cbor2 = "^5.1.0"
2425

2526
[tool.poetry.dev-dependencies]
2627
pytest = "^5.2"

0 commit comments

Comments
 (0)