Source code for quart_openapi.resource

"""resource.py

Provide the Resource Base class to inherit from for using the class based route definitions
"""

import logging
from typing import Any, Callable, Dict, Tuple

from quart import request
from quart.typing import ResponseReturnValue
from quart.views import MethodView

from .marshmallow import MARSHMALLOW, Schema
from .typing import ExpectedDescList, ValidatorTypes

LOGGER = logging.getLogger('quart.serving')

def get_expect_args(expect: ExpectedDescList, default_content_type: str = 'application/json'
                   ) -> Tuple[ValidatorTypes, str, Dict[str, Any]]:
    """Normalize the different tuple sizes for the expect decorator

    :param expect: Either a validator, a tuple of size 1 containing a validator,
                   a tuple of the validator and content type or a tuple of the
                   validator, content_type and a dict of other properties to add
    :return: Regardless of how the expect decorator was used, returns a tuple containing
             the validator, the content type and any extra kwargs
    """
    content_type = default_content_type
    kwargs = {}
    if isinstance(expect, tuple):
        if len(expect) == 2:
            expect, content_type = expect
        elif len(expect) == 3:
            expect, content_type, kwargs = expect
        else:
            expect = expect[0]
    return (expect, content_type, kwargs)

class Resource(MethodView):
    """Inherit from this to create RESTful routes with openapi docs

    A Resource subclass needs only to implement async functions corresponding to the HTTP
    verbs you want to handle. Utilizing the decorators from :class:`Pint` you can set
    the route, params, responses, and so on that will show up in the openapi documentation.

    An example is,

    .. code-block:: python

          app = Pint('sample')
          @app.route('/<id>')
          class SimpleRoute(Resource):
            async def get(self, id):
              return f"ID is {id}"

    That will enable a route '/<id>' which will return the string "ID is <id>" when called
    by a GET request. If using :meth:`Pint.expect` to define the expected request body,
    it will perform validation unless validate is set to false.
    """

[docs] async def dispatch_request(self, *args: Any, **kwargs: Any) -> ResponseReturnValue: """Can be overridden instead of creating verb functions This will be called with the request view_args, i.e. any url parameters """ handler = getattr(self, request.method.lower(), None) if handler is None and request.method == 'HEAD' or request.method == 'OPTIONS': handler = getattr(self, 'get', None) await self.validate_payload(handler) return await handler(*args, **kwargs)
[docs] async def validate_payload(self, func: Callable) -> bool: """This will perform validation Will check the api docs of the class as set by using the decorators in :class:`Pint` and if an expect was present without `validate` set to `False` or `None`, it will attempt to validate any request against the schema if json, or ensure the content_type matches at least. """ if getattr(func, '__apidoc__', False) is not False: doc = func.__apidoc__ validate = doc.get('validate', None) if validate: for expect in doc.get('expect', []): validator, content_type, _ = get_expect_args(expect) if content_type == 'application/json' and request.is_json: data = await request.get_json(force=True, cache=True) if MARSHMALLOW and isinstance(validator, Schema): return validator.load(data) return validator.validate(data) if content_type == request.mimetype: return LOGGER.error("Request didn't pass any of the available validations") raise ValueError("request didn't pass validation")