From 7980dd425b4063445002aa8b19dc5bc60c9691b2 Mon Sep 17 00:00:00 2001 From: Jesus Lara Date: Fri, 30 Jun 2023 03:15:07 +0200 Subject: [PATCH] fix JSONContent conversion of binary columns --- navigator/libs/json.pyx | 48 ++++++++++++++++++++++++++++++++++++----- navigator/version.py | 2 +- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/navigator/libs/json.pyx b/navigator/libs/json.pyx index 92ab3af2..2e397674 100644 --- a/navigator/libs/json.pyx +++ b/navigator/libs/json.pyx @@ -1,11 +1,18 @@ +# cython: language_level=3, embedsignature=True, boundscheck=False, wraparound=True, initializedcheck=False +# Copyright (C) 2018-present Jesus Lara +# """ JSON Encoder, Decoder. """ +import uuid +import logging from asyncpg.pgproto import pgproto +from datetime import datetime from dataclasses import _MISSING_TYPE, MISSING +from psycopg2 import Binary # Import Binary from psycopg2 from typing import Any, Union +from pathlib import PosixPath, PurePath, Path from decimal import Decimal -from pathlib import PurePath, PosixPath, Path from navigator.exceptions.exceptions cimport ValidationError import orjson @@ -23,14 +30,21 @@ cdef class JSONContent: def default(self, object obj): if isinstance(obj, Decimal): return float(obj) + elif isinstance(obj, datetime): + return str(obj) elif hasattr(obj, "isoformat"): return obj.isoformat() elif isinstance(obj, pgproto.UUID): return str(obj) + elif isinstance(obj, uuid.UUID): + return obj elif isinstance(obj, (PosixPath, PurePath, Path)): return str(obj) elif hasattr(obj, "hex"): - return obj.hex + if isinstance(obj, bytes): + return obj.hex() + else: + return obj.hex elif hasattr(obj, 'lower'): # asyncPg Range: up = obj.upper if isinstance(up, int): @@ -42,13 +56,18 @@ cdef class JSONContent: return None elif obj == MISSING: return None - raise TypeError(f"{obj!r} is not JSON serializable") + elif isinstance(obj, Binary): # Handle bytea column from PostgreSQL + return str(obj) # Convert Binary object to string + logging.error(f'{obj!r} of Type {type(obj)} is not JSON serializable') + raise TypeError( + f'{obj!r} of Type {type(obj)} is not JSON serializable' + ) def encode(self, object obj, **kwargs) -> str: # decode back to str, as orjson returns bytes options = { "default": self.default, - "option": orjson.OPT_NAIVE_UTC | orjson.OPT_SERIALIZE_NUMPY| orjson.OPT_UTC_Z + "option": orjson.OPT_NAIVE_UTC | orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_PASSTHROUGH_DATETIME # | orjson.OPT_NAIVE_UTC } if kwargs: options = {**options, **kwargs} @@ -59,11 +78,15 @@ cdef class JSONContent: ).decode('utf-8') except orjson.JSONEncodeError as ex: raise ValidationError( - f"Invalid JSON data: {ex}" + f"Invalid JSON: {ex}" ) dumps = encode + @classmethod + def dump(cls, object obj, **kwargs): + return cls().encode(obj, **kwargs) + def decode(self, object obj): try: return orjson.loads( @@ -76,9 +99,24 @@ cdef class JSONContent: loads = decode + @classmethod + def load(cls, object obj, **kwargs): + return cls().decode(obj, **kwargs) + cpdef str json_encoder(object obj): return JSONContent().dumps(obj) cpdef object json_decoder(object obj): return JSONContent().loads(obj) + +cdef class BaseEncoder: + """ + Encoder replacement for json.dumps using orjson + """ + def __init__(self, *args, **kwargs): + # Filter/adapt JSON arguments to ORJSON ones + rjargs = () + rjkwargs = {} + encoder = JSONContent(*rjargs, **rjkwargs) + self.encode = encoder.__call__ diff --git a/navigator/version.py b/navigator/version.py index 39f5911d..da474a35 100644 --- a/navigator/version.py +++ b/navigator/version.py @@ -4,7 +4,7 @@ __description__ = ( "Navigator Web Framework based on aiohttp, " "with batteries included." ) -__version__ = "2.6.25" +__version__ = "2.6.26" __author__ = "Jesus Lara" __author_email__ = "jesuslarag@gmail.com" __license__ = "BSD"