Skip to content

Commit

Permalink
Move parameter decorator related methods out of operation classes
Browse files Browse the repository at this point in the history
  • Loading branch information
RobbeSneyders committed Dec 23, 2022
1 parent 2438114 commit 7acbad0
Show file tree
Hide file tree
Showing 32 changed files with 748 additions and 695 deletions.
2 changes: 1 addition & 1 deletion connexion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
try:
from flask import request # NOQA

from .apis.flask_api import FlaskApi, context # NOQA
from .apis.flask_api import FlaskApi # NOQA
from .apps.flask_app import FlaskApp
except ImportError as e: # pragma: no cover
_flask_not_installed_error = not_installed_error(e)
Expand Down
78 changes: 23 additions & 55 deletions connexion/apis/abstract.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
"""
This module defines an AbstractAPI, which defines a standardized interface for a Connexion API.
"""

import abc
import logging
import pathlib
import sys
import typing as t
from enum import Enum

from ..datastructures import NoContent
from ..exceptions import ResolverError
from ..http_facts import METHODS
from ..jsonifier import Jsonifier
from ..lifecycle import ConnexionResponse
from ..operations import make_operation
from ..options import ConnexionOptions
from ..resolver import Resolver
from ..spec import Specification
from starlette.requests import Request

from connexion.datastructures import NoContent
from connexion.decorators.parameter import parameter_to_arg
from connexion.exceptions import ResolverError
from connexion.http_facts import METHODS
from connexion.jsonifier import Jsonifier
from connexion.lifecycle import ConnexionResponse
from connexion.operations import make_operation
from connexion.options import ConnexionOptions
from connexion.resolver import Resolver
from connexion.spec import Specification

MODULE_PATH = pathlib.Path(__file__).absolute().parent.parent
SWAGGER_UI_URL = "ui"
Expand Down Expand Up @@ -231,62 +233,44 @@ def add_operation(self, path, method):
self.resolver,
pythonic_params=self.pythonic_params,
uri_parser_class=self.options.uri_parser_class,
parameter_to_arg=parameter_to_arg,
)
self._add_operation_internal(method, path, operation)

@classmethod
@abc.abstractmethod
def get_request(self, *args, **kwargs):
def get_request(cls, uri_parser) -> Request:
"""
This method converts the user framework request to a ConnexionRequest.
"""

@classmethod
@abc.abstractmethod
def get_response(self, response, mimetype=None, request=None):
"""
This method converts a handler response to a framework response.
This method should just retrieve response from handler then call `cls._get_response`.
:param response: A response to cast (tuple, framework response, etc).
:param mimetype: The response mimetype.
:type mimetype: Union[None, str]
:param request: The request associated with this response (the user framework request).
"""

@classmethod
def _get_response(cls, response, mimetype=None, extra_context=None):
def _get_response(cls, response, mimetype=None):
"""
This method converts a handler response to a framework response.
The response can be a ConnexionResponse, an operation handler, a framework response or a tuple.
Other type than ConnexionResponse are handled by `cls._response_from_handler`
:param response: A response to cast (tuple, framework response, etc).
:param mimetype: The response mimetype.
:type mimetype: Union[None, str]
:param extra_context: dict of extra details, like url, to include in logs
:type extra_context: Union[None, dict]
"""
if extra_context is None:
extra_context = {}
logger.debug(
"Getting data and status code",
extra={"data": response, "data_type": type(response), **extra_context},
extra={"data": response, "data_type": type(response)},
)

if isinstance(response, ConnexionResponse):
framework_response = cls._connexion_to_framework_response(
response, mimetype, extra_context
response, mimetype
)
else:
framework_response = cls._response_from_handler(
response, mimetype, extra_context
)
framework_response = cls._response_from_handler(response, mimetype)

logger.debug(
"Got framework response",
extra={
"response": framework_response,
"response_type": type(framework_response),
**extra_context,
},
)
return framework_response
Expand All @@ -298,7 +282,6 @@ def _response_from_handler(
t.Any, str, t.Tuple[str], t.Tuple[str, int], t.Tuple[str, int, dict]
],
mimetype: str,
extra_context: t.Optional[dict] = None,
) -> t.Any:
"""
Create a framework response from the operation handler data.
Expand All @@ -311,7 +294,6 @@ def _response_from_handler(
:param response: A response from an operation handler.
:param mimetype: The response mimetype.
:param extra_context: dict of extra details, like url, to include in logs
"""
if cls._is_framework_response(response):
return response
Expand All @@ -320,25 +302,21 @@ def _response_from_handler(
len_response = len(response)
if len_response == 1:
(data,) = response
return cls._build_response(
mimetype=mimetype, data=data, extra_context=extra_context
)
return cls._build_response(mimetype=mimetype, data=data)
if len_response == 2:
if isinstance(response[1], (int, Enum)):
data, status_code = response
return cls._build_response(
mimetype=mimetype,
data=data,
status_code=status_code,
extra_context=extra_context,
)
else:
data, headers = response
return cls._build_response(
mimetype=mimetype,
data=data,
headers=headers,
extra_context=extra_context,
)
elif len_response == 3:
data, status_code, headers = response
Expand All @@ -347,7 +325,6 @@ def _response_from_handler(
data=data,
status_code=status_code,
headers=headers,
extra_context=extra_context,
)
else:
raise TypeError(
Expand All @@ -356,9 +333,7 @@ def _response_from_handler(
" (body, status), or (body, headers)."
)
else:
return cls._build_response(
mimetype=mimetype, data=response, extra_context=extra_context
)
return cls._build_response(mimetype=mimetype, data=response)

@classmethod
def get_connexion_response(cls, response, mimetype=None):
Expand All @@ -384,7 +359,7 @@ def _framework_to_connexion_response(cls, response, mimetype):

@classmethod
@abc.abstractmethod
def _connexion_to_framework_response(cls, response, mimetype, extra_context=None):
def _connexion_to_framework_response(cls, response, mimetype):
"""Cast ConnexionResponse to framework response class"""

@classmethod
Expand All @@ -396,7 +371,6 @@ def _build_response(
content_type=None,
status_code=None,
headers=None,
extra_context=None,
):
"""
Create a framework response from the provided arguments.
Expand All @@ -407,16 +381,12 @@ def _build_response(
:type status_code: int
:param headers: The response status code.
:type headers: Union[Iterable[Tuple[str, str]], Dict[str, str]]
:param extra_context: dict of extra details, like url, to include in logs
:type extra_context: Union[None, dict]
:return A framework response.
:rtype Response
"""

@classmethod
def _prepare_body_and_status_code(
cls, data, mimetype, status_code=None, extra_context=None
):
def _prepare_body_and_status_code(cls, data, mimetype, status_code=None):
if data is NoContent:
data = None

Expand All @@ -435,12 +405,10 @@ def _prepare_body_and_status_code(
else:
body = data

if extra_context is None:
extra_context = {}
logger.debug(
"Prepared body and status code (%d)",
status_code,
extra={"body": body, **extra_context},
extra={"body": body},
)

return body, status_code, mimetype
Expand Down
73 changes: 5 additions & 68 deletions connexion/apis/flask_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@
This module defines a Flask Connexion API which implements translations between Flask and
Connexion requests / responses.
"""

import logging
import warnings
from typing import Any

import flask
from werkzeug.local import LocalProxy

from connexion.apis import flask_utils
from connexion.apis.abstract import AbstractAPI
Expand Down Expand Up @@ -52,7 +48,7 @@ def _add_operation_internal(self, method, path, operation):
)

@classmethod
def get_response(cls, response, mimetype=None, request=None):
def get_response(cls, response, mimetype=None):
"""Gets ConnexionResponse instance for the operation handler
result. Status Code and Headers for response. If only body
data is returned by the endpoint function, then the status
Expand All @@ -64,9 +60,7 @@ def get_response(cls, response, mimetype=None, request=None):
:type response: flask.Response | (flask.Response,) | (flask.Response, int) | (flask.Response, dict) | (flask.Response, int, dict)
:rtype: ConnexionResponse
"""
return cls._get_response(
response, mimetype=mimetype, extra_context={"url": flask.request.url}
)
return cls._get_response(response, mimetype=mimetype)

@classmethod
def _is_framework_response(cls, response):
Expand All @@ -86,15 +80,14 @@ def _framework_to_connexion_response(cls, response, mimetype):
)

@classmethod
def _connexion_to_framework_response(cls, response, mimetype, extra_context=None):
def _connexion_to_framework_response(cls, response, mimetype):
"""Cast ConnexionResponse to framework response class"""
flask_response = cls._build_response(
mimetype=response.mimetype or mimetype,
content_type=response.content_type,
headers=response.headers,
status_code=response.status_code,
data=response.body,
extra_context=extra_context,
)

return flask_response
Expand All @@ -107,7 +100,6 @@ def _build_response(
headers=None,
status_code=None,
data=None,
extra_context=None,
):
if cls._is_framework_response(data):
return flask.current_app.make_response((data, status_code, headers))
Expand All @@ -116,7 +108,6 @@ def _build_response(
data=data,
mimetype=mimetype,
status_code=status_code,
extra_context=extra_context,
)

kwargs = {
Expand All @@ -133,72 +124,18 @@ def _build_response(
def _serialize_data(cls, data, mimetype):
if isinstance(mimetype, str) and is_json_mimetype(mimetype):
body = cls.jsonifier.dumps(data)
elif not (isinstance(data, bytes) or isinstance(data, str)):
warnings.warn(
"Implicit (flask) JSON serialization will change in the next major version. "
"This is triggered because a response body is being serialized as JSON "
"even though the mimetype is not a JSON type. "
"This will be replaced by something that is mimetype-specific and may "
"raise an error instead of silently converting everything to JSON. "
"Please make sure to specify media/mime types in your specs.",
FutureWarning, # a Deprecation targeted at application users.
)
body = cls.jsonifier.dumps(data)
else:
body = data

return body, mimetype

@classmethod
def get_request(cls, *args, **params):
# type: (*Any, **Any) -> ConnexionRequest
"""Gets ConnexionRequest instance for the operation handler
result. Status Code and Headers for response. If only body
data is returned by the endpoint function, then the status
code will be set to 200 and no headers will be added.
If the returned object is a flask.Response then it will just
pass the information needed to recreate it.
:rtype: ConnexionRequest
"""
flask_request = flask.request
scope = flask_request.environ["asgi.scope"]
context_dict = scope.get("extensions", {}).get("connexion_context", {})
setattr(flask.globals.request_ctx, "connexion_context", context_dict)
request = ConnexionRequest(
flask_request.url,
flask_request.method,
headers=flask_request.headers,
form=flask_request.form,
query=flask_request.args,
body=flask_request.get_data(),
json_getter=lambda: flask_request.get_json(silent=True),
files=flask_request.files,
path_params=params,
context=context_dict,
cookies=flask_request.cookies,
)
logger.debug(
"Getting data and status code",
extra={
"data": request.body,
"data_type": type(request.body),
"url": request.url,
},
)
return request
def get_request(cls, uri_parser) -> ConnexionRequest:
return ConnexionRequest(flask.request, uri_parser=uri_parser)

@classmethod
def _set_jsonifier(cls):
"""
Use Flask specific JSON loader
"""
cls.jsonifier = Jsonifier(flask.json, indent=2)


def _get_context():
return getattr(flask.globals.request_ctx, "connexion_context")


context = LocalProxy(_get_context)
1 change: 1 addition & 0 deletions connexion/apps/flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from types import FunctionType # NOQA

import a2wsgi
import asgiref.wsgi
import flask
import werkzeug.exceptions
from flask import signals
Expand Down
9 changes: 9 additions & 0 deletions connexion/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from asyncio import AbstractEventLoop
from contextvars import ContextVar

_context: ContextVar[AbstractEventLoop] = ContextVar("CONTEXT")


def __getattr__(name):
if name == "context":
return _context.get()
Loading

0 comments on commit 7acbad0

Please sign in to comment.