diff --git a/navigator/actions/rest.py b/navigator/actions/rest.py index 1dea42f..c23f403 100644 --- a/navigator/actions/rest.py +++ b/navigator/actions/rest.py @@ -17,8 +17,8 @@ from bs4 import BeautifulSoup as bs from lxml import html, etree from proxylists.proxies import FreeProxy +from datamodel.parsers.json import JSONContent from asyncdb.utils.functions import cPrint -from ..libs.json import JSONContent from ..exceptions import ConfigError from .abstract import AbstractAction @@ -342,7 +342,7 @@ async def process_request(self, future, url: str): result = filename # getting the result, based on the Accept logic elif self.file_buffer is True: - data = response.content + data = response.content buffer = BytesIO(data) buffer.seek(0) result = buffer diff --git a/navigator/brokers/rabbitmq/connection.py b/navigator/brokers/rabbitmq/connection.py index d19ba69..79db270 100644 --- a/navigator/brokers/rabbitmq/connection.py +++ b/navigator/brokers/rabbitmq/connection.py @@ -8,7 +8,7 @@ import aiormq from aiormq.abc import AbstractConnection, AbstractChannel from datamodel import BaseModel -from navigator.libs.json import json_encoder, json_decoder +from datamodel.parsers.json import json_encoder, json_decoder from navigator.exceptions import ValidationError from ...conf import rabbitmq_dsn from ..wrapper import BaseWrapper diff --git a/navigator/brokers/redis/connection.py b/navigator/brokers/redis/connection.py index 37b0f5b..1063200 100644 --- a/navigator/brokers/redis/connection.py +++ b/navigator/brokers/redis/connection.py @@ -9,7 +9,7 @@ from redis import asyncio as aioredis from datamodel import Model, BaseModel from navconfig.logging import logging -from navigator.libs.json import json_encoder, json_decoder +from datamodel.parsers.json import json_encoder, json_decoder from navigator.exceptions import ValidationError from ..connection import BaseConnection from ..wrapper import BaseWrapper diff --git a/navigator/brokers/sqs/connection.py b/navigator/brokers/sqs/connection.py index 45d6e94..83749b9 100644 --- a/navigator/brokers/sqs/connection.py +++ b/navigator/brokers/sqs/connection.py @@ -10,7 +10,7 @@ from datamodel import Model, BaseModel from navconfig import config from navconfig.logging import logging -from navigator.libs.json import json_encoder, json_decoder +from datamodel.parsers.json import json_encoder, json_decoder from navigator.exceptions import ValidationError from ..connection import BaseConnection from ..wrapper import BaseWrapper diff --git a/navigator/libs/json.py b/navigator/libs/json.py new file mode 100644 index 0000000..46d6376 --- /dev/null +++ b/navigator/libs/json.py @@ -0,0 +1,6 @@ +from datamodel.parsers.json import ( + json_encoder, + json_decoder, + BaseEncoder, + JSONContent +) diff --git a/navigator/libs/json.pyx b/navigator/libs/json.pyx deleted file mode 100644 index 3ef33f3..0000000 --- a/navigator/libs/json.pyx +++ /dev/null @@ -1,130 +0,0 @@ -# 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 enum import Enum, EnumType -from ..exceptions.exceptions cimport ValidationError -import orjson - - -cdef class JSONContent: - """ - Basic Encoder using orjson - """ - # def __init__(self, **kwargs): - # # eventually take into consideration when serializing - # self.options = kwargs - def __call__(self, object obj, **kwargs): - return self.encode(obj, **kwargs) - - 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"): - if isinstance(obj, bytes): - return obj.hex() - else: - return obj.hex - elif hasattr(obj, 'lower'): # asyncPg Range: - up = obj.upper - if isinstance(up, int): - up = up - 1 # discrete representation - return [obj.lower, up] - elif hasattr(obj, 'tolist'): # numpy array - return obj.tolist() - elif isinstance(obj, Enum): # Handle Enum serialization - if obj is None: - return None - return obj.value if hasattr(obj, 'value') else obj.name - elif isinstance(obj, type) and issubclass(obj, Enum): - return [{'value': e.value, 'name': e.name} for e in obj] - # return [e.name for e in obj] # Serialize the names of the Enum class members - elif isinstance(obj, _MISSING_TYPE): - return None - elif obj == MISSING: - return None - 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_PASSTHROUGH_DATETIME # | orjson.OPT_NAIVE_UTC - } - if kwargs: - options = {**options, **kwargs} - try: - return orjson.dumps( - obj, - **options - ).decode('utf-8') - except orjson.JSONEncodeError as ex: - raise ValidationError( - 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( - obj - ) - except orjson.JSONDecodeError as ex: - raise ValidationError( - f"Invalid JSON data: {ex}" - ) - - 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/template/decorators.py b/navigator/template/decorators.py index 6f5f149..c23b5b8 100644 --- a/navigator/template/decorators.py +++ b/navigator/template/decorators.py @@ -5,8 +5,8 @@ import inspect from aiohttp import web from aiohttp.abc import AbstractView +from datamodel.parsers.json import json_encoder, json_decoder from .parser import TemplateParser -from ..libs.json import json_decoder F = TypeVar("F", bound=Callable[..., Any]) diff --git a/navigator/version.py b/navigator/version.py index c917133..8c9c207 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.12.11" +__version__ = "2.12.12" __copyright__ = "Copyright (c) 2020-2024 Jesus Lara" __author__ = "Jesus Lara" __author_email__ = "jesuslarag@gmail.com" diff --git a/navigator/views/__init__.py b/navigator/views/__init__.py index d6212cf..218e79a 100644 --- a/navigator/views/__init__.py +++ b/navigator/views/__init__.py @@ -3,7 +3,7 @@ NAV aiohttp Class-Based Views. """ -from ..libs.json import json_encoder, json_decoder +from datamodel.parsers.json import json_encoder, json_decoder from .base import BaseHandler, BaseView from .data import DataView from .model import ModelView, load_models diff --git a/navigator/views/base.py b/navigator/views/base.py index aa0adb9..a28c5c1 100644 --- a/navigator/views/base.py +++ b/navigator/views/base.py @@ -7,6 +7,7 @@ from pathlib import Path from dataclasses import dataclass from urllib import parse +import aiohttp from aiohttp import web from aiohttp.web_exceptions import ( HTTPMethodNotAllowed, @@ -16,19 +17,26 @@ import orjson from orjson import JSONDecodeError import aiohttp_cors +from datamodel.parsers.json import ( + JSONContent, + json_encoder, + json_decoder +) from datamodel.exceptions import ValidationError from navconfig.logging import logging, loglevel from navigator_session import get_session from ..exceptions import NavException, InvalidArgument -from ..libs.json import JSONContent, json_encoder, json_decoder from ..responses import JSONResponse from ..applications.base import BaseApplication from ..types import WebApp from ..conf import CORS_MAX_AGE - +# Monkey-patching DEFAULT_JSON_ENCODER = json_encoder DEFAULT_JSON_DECODER = json_decoder +# monkey-patch the JSON encoder +aiohttp.typedefs.DEFAULT_JSON_ENCODER = json_encoder +aiohttp.typedefs.DEFAULT_JSON_DECODER = json_decoder class BaseHandler(ABC): @@ -72,10 +80,7 @@ async def get_userid(self, session, idx: str = 'user_id') -> int: status=403 ) try: - if 'session' in session: - return session['session'][idx] - else: - return session[idx] + return session['session'][idx] if 'session' in session else session[idx] except KeyError: self.error(reason="Unauthorized", status=403) diff --git a/navigator/views/model.py b/navigator/views/model.py index 3d06a79..045ca28 100644 --- a/navigator/views/model.py +++ b/navigator/views/model.py @@ -1,5 +1,5 @@ from collections.abc import Iterable -from typing import Optional, Union, Any, Awaitable, Callable +from typing import Optional, Union, Any, Awaitable, Callable, Hashable import importlib import asyncio from aiohttp import web @@ -73,7 +73,7 @@ class ModelView(AbstractModel): get_model: BaseModel = None model_name: str = None # Override the current model with other. path: str = None - pk: Optional[Iterable] = None + pk: Union[Hashable, str] = None _required: list = [] _primaries: list = [] _hidden: list = [] @@ -204,7 +204,7 @@ async def _model_response( try: # Run the coroutine in a new thread asyncio.run_coroutine_threadsafe( - self._get_callback(response, result), + self._get_callback(response, result), # pylint: disable=E1102 loop ) except Exception as ex: @@ -512,7 +512,7 @@ async def set_column_value(value): for name, column in self.model.get_columns().items(): ### if a function with name _get_{column name} exists ### then that function is called for getting the field value - fn = getattr(self, f'_set_{name}', None) + fn = getattr(self, f'_post_{name}', None) or getattr(self, f'_set_{name}', None) if not fn: fn = getattr(self, f'_get_{name}', None) if fn: @@ -559,7 +559,7 @@ async def _patch_response(self, result, status: int = 202) -> web.Response: try: # Run the coroutine in a new thread asyncio.run_coroutine_threadsafe( - self._patch_callback(response, result), + self._patch_callback(response, result), # pylint: disable=E1102 loop ) except Exception as ex: @@ -626,6 +626,7 @@ async def _calculate_column( return async def _set_update(self, model, data): + """ Set the value for a field in the model """ for key, val in data.items(): if key in model.get_fields(): col = model.column(key) @@ -654,9 +655,7 @@ async def set_column_value(value): for name, column in self.model.get_columns().items(): ### if a function with name _get_{column name} exists ### then that function is called for getting the field value - fn = getattr(self, f'_set_{name}', None) - if not fn: - fn = getattr(self, f'_patch_{name}', None) + fn = getattr(self, f'_patch_{name}', None) or getattr(self, f'_set_{name}', None) if fn: try: val = value.get(name, None) @@ -864,7 +863,7 @@ async def _post_response(self, result: BaseModel, status: int = 200, fields: lis try: # Run the coroutine in a new thread asyncio.run_coroutine_threadsafe( - self._post_callback(response, result), + self._post_callback(response, result), # pylint: disable=E1102 loop ) except Exception as ex: @@ -885,7 +884,7 @@ async def _put_response(self, result: BaseModel, status: int = 200, fields: list try: # Run the coroutine in a new thread asyncio.run_coroutine_threadsafe( - self._put_callback(response, result), + self._put_callback(response, result), # pylint: disable=E1102 loop ) except Exception as ex: diff --git a/setup.py b/setup.py index 9ee2777..f5add9e 100644 --- a/setup.py +++ b/setup.py @@ -25,12 +25,6 @@ extra_compile_args=COMPILE_ARGS, language="c" ), - Extension( - name='navigator.libs.json', - sources=['navigator/libs/json.pyx'], - extra_compile_args=COMPILE_ARGS, - language="c++" - ), Extension( name='navigator.exceptions.exceptions', sources=['navigator/exceptions/exceptions.pyx'],