Skip to content

Django OpenAPI request/response factories #167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 94 additions & 24 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
************
openapi-core
************

Expand All @@ -15,13 +16,13 @@ openapi-core
:target: https://pypi.python.org/pypi/openapi-core

About
=====
#####

Openapi-core is a Python library that adds client-side and server-side support
for the `OpenAPI Specification v3.0.0 <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md>`__.

Installation
============
############

Recommended way (via pip):

Expand All @@ -37,7 +38,7 @@ Alternatively you can download the code and install from the repository:


Usage
=====
#####

Firstly create your specification:

Expand All @@ -47,6 +48,9 @@ Firstly create your specification:

spec = create_spec(spec_dict)

Request
*******

Now you can use it to validate requests

.. code-block:: python
Expand Down Expand Up @@ -83,14 +87,61 @@ or use shortcuts for simple validation
validated_params = validate_parameters(spec, request)
validated_body = validate_body(spec, request)

Request object should be instance of OpenAPIRequest class. You can use FlaskOpenAPIRequest a Flask/Werkzeug request factory:
Request object should be instance of OpenAPIRequest class (See `Integrations`_).

Response
********

You can also validate responses

.. code-block:: python

from openapi_core.shortcuts import ResponseValidator

validator = ResponseValidator(spec)
result = validator.validate(request, response)

# raise errors if response invalid
result.raise_for_errors()

# get list of errors
errors = result.errors

and unmarshal response data from validation result

.. code-block:: python

# get headers
validated_headers = result.headers

# get data
validated_data = result.data

or use shortcuts for simple validation

.. code-block:: python

from openapi_core import validate_data

validated_data = validate_data(spec, request, response)

Response object should be instance of OpenAPIResponse class (See `Integrations`_).


Integrations
############

Django
******

For Django 2.2 you can use DjangoOpenAPIRequest a Django request factory:

.. code-block:: python

from openapi_core.shortcuts import RequestValidator
from openapi_core.contrib.flask import FlaskOpenAPIRequest
from openapi_core.contrib.django import DjangoOpenAPIRequest

openapi_request = FlaskOpenAPIRequest(flask_request)
openapi_request = DjangoOpenAPIRequest(django_request)
validator = RequestValidator(spec)
result = validator.validate(openapi_request)

Expand All @@ -101,44 +152,58 @@ or simply specify request factory for shortcuts
from openapi_core import validate_parameters, validate_body

validated_params = validate_parameters(
spec, request, request_factory=FlaskOpenAPIRequest)
spec, request, request_factory=DjangoOpenAPIRequest)
validated_body = validate_body(
spec, request, request_factory=FlaskOpenAPIRequest)
spec, request, request_factory=DjangoOpenAPIRequest)

You can also validate responses
You can use DjangoOpenAPIResponse as a Django response factory:

.. code-block:: python

from openapi_core.shortcuts import ResponseValidator
from openapi_core.contrib.django import DjangoOpenAPIResponse

openapi_response = DjangoOpenAPIResponse(django_response)
validator = ResponseValidator(spec)
result = validator.validate(request, response)
result = validator.validate(openapi_request, openapi_response)

# raise errors if response invalid
result.raise_for_errors()
or simply specify response factory for shortcuts

# get list of errors
errors = result.errors
.. code-block:: python

and unmarshal response data from validation result
from openapi_core import validate_parameters, validate_body

validated_data = validate_data(
spec, request, response,
request_factory=DjangoOpenAPIRequest,
response_factory=DjangoOpenAPIResponse)

Flask
*****

You can use FlaskOpenAPIRequest a Flask/Werkzeug request factory:

.. code-block:: python

# get headers
validated_headers = result.headers
from openapi_core.shortcuts import RequestValidator
from openapi_core.contrib.flask import FlaskOpenAPIRequest

# get data
validated_data = result.data
openapi_request = FlaskOpenAPIRequest(flask_request)
validator = RequestValidator(spec)
result = validator.validate(openapi_request)

or use shortcuts for simple validation
or simply specify request factory for shortcuts

.. code-block:: python

from openapi_core import validate_data
from openapi_core import validate_parameters, validate_body

validated_data = validate_data(spec, request, response)
validated_params = validate_parameters(
spec, request, request_factory=FlaskOpenAPIRequest)
validated_body = validate_body(
spec, request, request_factory=FlaskOpenAPIRequest)

Response object should be instance of OpenAPIResponse class. You can use FlaskOpenAPIResponse a Flask/Werkzeug response factory:
You can use FlaskOpenAPIResponse as a Flask/Werkzeug response factory:

.. code-block:: python

Expand All @@ -160,7 +225,12 @@ or simply specify response factory for shortcuts
request_factory=FlaskOpenAPIRequest,
response_factory=FlaskOpenAPIResponse)

Pyramid
*******

See `pyramid_openapi3 <https://github.com/niteoweb/pyramid_openapi3>`_ project.

Related projects
================
################
* `openapi-spec-validator <https://github.com/p1c2u/openapi-spec-validator>`__
* `pyramid_openapi3 <https://github.com/niteoweb/pyramid_openapi3>`__
11 changes: 11 additions & 0 deletions openapi_core/contrib/django/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from openapi_core.contrib.django.requests import DjangoOpenAPIRequestFactory
from openapi_core.contrib.django.responses import DjangoOpenAPIResponseFactory

# backward compatibility
DjangoOpenAPIRequest = DjangoOpenAPIRequestFactory.create
DjangoOpenAPIResponse = DjangoOpenAPIResponseFactory.create

__all__ = [
'DjangoOpenAPIRequestFactory', 'DjangoOpenAPIResponseFactory',
'DjangoOpenAPIRequest', 'DjangoOpenAPIResponse',
]
51 changes: 51 additions & 0 deletions openapi_core/contrib/django/requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""OpenAPI core contrib django requests module"""
import re

from openapi_core.validation.request.datatypes import (
RequestParameters, OpenAPIRequest,
)

# https://docs.djangoproject.com/en/2.2/topics/http/urls/
#
# Currently unsupported are :
# - nested arguments, e.g.: ^comments/(?:page-(?P<page_number>\d+)/)?$
# - unnamed regex groups, e.g.: ^articles/([0-9]{4})/$
# - multiple named parameters between a single pair of slashes
# e.g.: <page_slug>-<page_id>/edit/
#
# The regex matches everything, except a "/" until "<". Than only the name
# is exported, after which it matches ">" and everything until a "/".
PATH_PARAMETER_PATTERN = r'(?:[^\/]*?)<(?:(?:.*?:))*?(\w+)>(?:[^\/]*)'


class DjangoOpenAPIRequestFactory(object):

path_regex = re.compile(PATH_PARAMETER_PATTERN)

@classmethod
def create(cls, request):
method = request.method.lower()

if request.resolver_match is None:
path_pattern = request.path
else:
route = cls.path_regex.sub(
r'{\1}', request.resolver_match.route)
path_pattern = '/' + route

path = request.resolver_match and request.resolver_match.kwargs or {}
parameters = RequestParameters(
path=path,
query=request.GET,
header=request.headers,
cookie=request.COOKIES,
)
return OpenAPIRequest(
host_url=request._current_scheme_host,
path=request.path,
method=method,
path_pattern=path_pattern,
parameters=parameters,
body=request.body,
mimetype=request.content_type,
)
14 changes: 14 additions & 0 deletions openapi_core/contrib/django/responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""OpenAPI core contrib django responses module"""
from openapi_core.validation.response.datatypes import OpenAPIResponse


class DjangoOpenAPIResponseFactory(object):

@classmethod
def create(cls, response):
mimetype = response["Content-Type"]
return OpenAPIResponse(
data=response.content,
status_code=response.status_code,
mimetype=mimetype,
)
17 changes: 17 additions & 0 deletions openapi_core/validation/request/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@ def __getitem__(self, location):

@attr.s
class OpenAPIRequest(object):
"""OpenAPI request dataclass.

Attributes:
path
Requested path as string.
path_pattern
The matched url pattern.
parameters
A RequestParameters object.
body
The request body, as string.
mimetype
Like content type, but without parameters (eg, without charset,
type etc.) and always lowercase.
For example if the content type is "text/HTML; charset=utf-8"
the mimetype would be "text/html".
"""

host_url = attr.ib()
path = attr.ib()
Expand Down
10 changes: 10 additions & 0 deletions openapi_core/validation/response/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@

@attr.s
class OpenAPIResponse(object):
"""OpenAPI request dataclass.

Attributes:
data
The response body, as string.
status_code
The status code as integer.
mimetype
Lowercase content type without charset.
"""

data = attr.ib()
status_code = attr.ib()
Expand Down
3 changes: 2 additions & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ mock==2.0.0
pytest==3.5.0
pytest-flake8
pytest-cov==2.5.1
flask
flask
django==2.2.6; python_version>="3.0"
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ exclude =
tests

[options.extras_require]
django = django>=2.2; python_version>="3.0"
flask = werkzeug

[tool:pytest]
Expand Down
Loading