From 709d0f9c51b2c4fdf97fc026ffc05635378f1449 Mon Sep 17 00:00:00 2001 From: Sharoon Thomas Date: Tue, 7 May 2024 16:58:51 +0200 Subject: [PATCH] Update to v3 serialization * v3 is lighter and now fully supported on the server side * deserialization is backwards compatible with v2 [sc-74126] --- fulfil_client/serialization.py | 167 ++++++++++++++++++--------------- setup.py | 1 + 2 files changed, 90 insertions(+), 78 deletions(-) diff --git a/fulfil_client/serialization.py b/fulfil_client/serialization.py index 5c97151..1e412c5 100644 --- a/fulfil_client/serialization.py +++ b/fulfil_client/serialization.py @@ -3,6 +3,8 @@ from decimal import Decimal from collections import namedtuple from functools import partial +import isodate + try: import simplejson as json except ImportError: @@ -10,7 +12,7 @@ import base64 -CONTENT_TYPE = 'application/vnd.fulfil.v2+json' +CONTENT_TYPE = "application/vnd.fulfil.v3+json" class JSONDecoder(object): @@ -23,69 +25,90 @@ def register(cls, klass, decoder): cls.decoders[klass] = decoder def __call__(self, dct): - if dct.get('__class__') in self.decoders: - return self.decoders[dct['__class__']](dct) + if dct.get("__class__") in self.decoders: + return self.decoders[dct["__class__"]](dct) return dct -JSONDecoder.register( - 'datetime', - lambda dct: datetime.datetime( - dct['year'], dct['month'], dct['day'], - dct['hour'], dct['minute'], dct['second'], dct['microsecond'] +def register_decoder(klass): + def decorator(decoder): + assert klass not in JSONDecoder.decoders + JSONDecoder.decoders[klass] = decoder + + return decorator + + +@register_decoder("datetime") +def datetime_decoder(v): + if v.get("iso_string"): + return isodate.parse_datetime(v["iso_string"]) + return datetime.datetime( + v["year"], + v["month"], + v["day"], + v["hour"], + v["minute"], + v["second"], + v["microsecond"], ) -) -JSONDecoder.register( - 'date', - lambda dct: datetime.date(dct['year'], dct['month'], dct['day']) -) -JSONDecoder.register( - 'time', - lambda dct: datetime.time( - dct['hour'], dct['minute'], dct['second'], dct['microsecond'] + + +@register_decoder("date") +def date_decoder(v): + if v.get("iso_string"): + return isodate.parse_date(v["iso_string"]) + return datetime.date( + v["year"], + v["month"], + v["day"], ) -) -JSONDecoder.register( - 'timedelta', - lambda dct: datetime.timedelta(seconds=dct['seconds']) -) -def _bytes_decoder(dct): +@register_decoder("time") +def time_decoder(v): + if v.get("iso_string"): + return isodate.parse_time(v["iso_string"]) + return datetime.time(v["hour"], v["minute"], v["second"], v["microsecond"]) + + +@register_decoder("timedelta") +def timedelta_decoder(v): + if v.get("iso_string"): + return isodate.parse_duration(v["iso_string"]) + return datetime.timedelta(seconds=v["seconds"]) + + +@register_decoder("bytes") +def _bytes_decoder(v): cast = bytearray if bytes == str else bytes - return cast(base64.b64decode(dct['base64'].encode('utf-8'))) + return cast(base64.decodebytes(v["base64"].encode("utf-8"))) -JSONDecoder.register('bytes', _bytes_decoder) -JSONDecoder.register( - 'Decimal', lambda dct: Decimal(dct['decimal']) -) +@register_decoder("Decimal") +def decimal_decoder(v): + return Decimal(v["decimal"]) -dummy_record = namedtuple('Record', ['model_name', 'id', 'rec_name']) -JSONDecoder.register( - 'Model', lambda dct: dummy_record( - dct['model_name'], dct['id'], dct.get('rec_name') - ) -) +dummy_record = namedtuple("Record", ["model_name", "id", "rec_name"]) + +@register_decoder("Model") +def model_decoder(dct): + return dummy_record(dct["model_name"], dct["id"], dct.get("rec_name")) + +@register_decoder("AsyncResult") def parse_async_result(dct): from .client import AsyncResult - return AsyncResult(dct['task_id'], dct['token'], None) - -JSONDecoder.register( - 'AsyncResult', parse_async_result -) + return AsyncResult(dct["task_id"], dct["token"], None) class JSONEncoder(json.JSONEncoder): - serializers = {} def __init__(self, *args, **kwargs): - super(JSONEncoder, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # Force to use our custom decimal with simplejson self.use_decimal = False @@ -95,63 +118,51 @@ def register(cls, klass, encoder): cls.serializers[klass] = encoder def default(self, obj): - marshaller = self.serializers.get( - type(obj), - super(JSONEncoder, self).default - ) + marshaller = self.serializers.get(type(obj), super().default) return marshaller(obj) JSONEncoder.register( datetime.datetime, lambda o: { - '__class__': 'datetime', - 'year': o.year, - 'month': o.month, - 'day': o.day, - 'hour': o.hour, - 'minute': o.minute, - 'second': o.second, - 'microsecond': o.microsecond, - 'iso_string': o.isoformat(), - }) + "__class__": "datetime", + "iso_string": o.isoformat(), + }, +) JSONEncoder.register( datetime.date, lambda o: { - '__class__': 'date', - 'year': o.year, - 'month': o.month, - 'day': o.day, - 'iso_string': o.isoformat(), - }) + "__class__": "date", + "iso_string": o.isoformat(), + }, +) JSONEncoder.register( datetime.time, lambda o: { - '__class__': 'time', - 'hour': o.hour, - 'minute': o.minute, - 'second': o.second, - 'microsecond': o.microsecond, - }) + "__class__": "time", + "iso_string": o.isoformat(), + }, +) JSONEncoder.register( datetime.timedelta, lambda o: { - '__class__': 'timedelta', - 'seconds': o.total_seconds(), - }) -_bytes_encoder = lambda o: { - '__class__': 'bytes', - 'base64': base64.encodestring(o).decode('utf-8'), - } + "__class__": "timedelta", + "iso_string": isodate.duration_isoformat(o), + }, +) +_bytes_encoder = lambda o: { # noqa + "__class__": "bytes", + "base64": base64.b64encode(o).decode("utf-8"), +} JSONEncoder.register(bytes, _bytes_encoder) JSONEncoder.register(bytearray, _bytes_encoder) JSONEncoder.register( Decimal, lambda o: { - '__class__': 'Decimal', - 'decimal': str(o), - }) - + "__class__": "Decimal", + "decimal": str(o), + }, +) dumps = partial(json.dumps, cls=JSONEncoder) diff --git a/setup.py b/setup.py index b22b499..38e2f23 100755 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ 'babel', 'six', 'more-itertools', + 'isodate', ]