"""Utilities for handlers.""" import json from typing import TypeVar, Type, Generic, Any, cast import marshmallow_dataclass from flask import request, make_response, current_app from flask.json import jsonify from marshmallow import fields, Schema, ValidationError from werkzeug.exceptions import BadRequest from api import ma from utils import exceptions from utils.exceptions import JsonValidationError, UnsupportedMediaType T = TypeVar("T") class TypedSchema(Generic[T]): """Typed marshmallow schema wrapper""" def dump(self, model: T, many=None) -> str: pass def jsonify(self, model: T, many=None) -> str: pass def load(self, json: Any) -> T: pass def to_schema(model_class: Type[T], many=None) -> TypedSchema[T]: schema = marshmallow_dataclass.class_schema(model_class, base_schema=ma.Schema) schemaInstance = schema(many=many) return cast(TypedSchema[T], schemaInstance) RequestModel = TypeVar("RequestModel") def __is_list_model(model_class: Type[RequestModel]) -> bool: return hasattr(model_class, "__origin__") and model_class.__origin__ is list def __get_generic_class(model_class: Type[RequestModel]): return model_class.__args__[0] def get_request_model(model_class: Type[RequestModel]) -> RequestModel: is_list = __is_list_model(model_class) type = model_class if is_list: type = __get_generic_class(model_class) schema = cast(Schema, to_schema(type, many=is_list)) return cast(RequestModel, get_request_json(schema)) def get_request_query_model(model_class: Type[RequestModel]) -> RequestModel: schema = cast(Schema, to_schema(model_class)) return cast(RequestModel, get_query_parameters(schema)) def get_request_json(schema=None): """ Parse and validate JSON from request. Args: schema (marshmallow.Schema): Optional Marshmallow schema for validating and cleaning the JSON. Returns: JSON parsed to native Python data structure. Raises: BadRequest: If request body cannot be parsed. JsonValidationError: If JSON does not pass schema validation. UnsupportedMediaType: If request media type is neither application/json nor application/*+json. """ if not request.is_json: raise UnsupportedMediaType('Unsupported media type "{}". Expected "application/json".'.format(request.mimetype)) if not schema: return request.get_json() try: data = schema.load(request.get_json()) except ValidationError as errors: raise JsonValidationError(extra=errors) if data is None: # schema.load(None) does not return errors. # That is why this case is checked here. raise JsonValidationError() return data def get_query_parameters(schema): """ Parse and validate query parameters from request. Args: schema (marshmallow.Schema): Optional Marshmallow schema for validating and cleaning the parameters. Returns: Query parsed to native Python data structure. Raises: BadRequest: If request body cannot be parsed. JsonValidationError: If JSON does not pass schema validation. UnsupportedMediaType: If request media type is neither application/json nor application/*+json. """ data = {} for field_name, field in schema.fields.items(): load_from = field_name if load_from not in request.args: continue if isinstance(field, fields.List): data[field_name] = request.args.getlist(load_from) else: data[field_name] = request.args[load_from] try: data = schema.load(data) except ValidationError as error: raise exceptions.ValidationError("Error parsing query parameters. Error: {}.".format(error)) if data is None: # schema.load(None) does not return errors. # That is why this case is checked here. raise exceptions.ValidationError("Error parsing query parameters") return data def parse_request_json(schema=None, many: bool = False, request_data: dict or object = None): """Parse and optionally validate request JSON. Args: schema (type): Marshmallow schema to validate request. many (bool): Load collection of objects. request_data (dict or object): Request data. Returns: dict: Parsed and possibly (if schema is set) validated request. Raises: RequestError: Request JSON is invalid. ValidationError: Request data did not pass the schema validation. """ if not request_data: try: request_data = request.get_json() except BadRequest: raise exceptions.RequestError("Invalid JSON.") if isinstance(request_data, (bytes, bytearray)): request_data = request_data.decode("utf-8") if isinstance(request_data, str): request_data = json.loads(request_data) if not schema: return request_data result = schema(many=many).load(request_data) if result.errors: raise exceptions.ValidationError(result.errors) return result.data def create_response( schema=None, many: bool = False, response_data: dict or object = None, schema_context_data: dict = None, status: int = 200, ): """Create response with data as JSON. Args: schema (type): Marshmallow schema to dump results. many (bool): Dump collection of objects. response_data (dict or object): Response data. schema_context_data (dict): Data for schema context. status (int): Response status code. Returns: tuple: response tuple. """ if schema: response_schema = schema(many=many, context=schema_context_data) dump_data = response_schema.dump(response_data) return jsonify(dump_data.data), status return jsonify(response_data), status def jsonify_no_content(): response = make_response("", 204) response.mimetype = current_app.config["JSONIFY_MIMETYPE"] return response