mirror of
https://github.com/correl/openapi-core.git
synced 2025-01-01 11:03:19 +00:00
Merge pull request #167 from p1c2u/feature/django-support
Django OpenAPI request/response factories
This commit is contained in:
commit
fd99117278
10 changed files with 416 additions and 26 deletions
120
README.rst
120
README.rst
|
@ -1,3 +1,4 @@
|
|||
************
|
||||
openapi-core
|
||||
************
|
||||
|
||||
|
@ -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):
|
||||
|
||||
|
@ -37,7 +38,7 @@ Alternatively you can download the code and install from the repository:
|
|||
|
||||
|
||||
Usage
|
||||
=====
|
||||
#####
|
||||
|
||||
Firstly create your specification:
|
||||
|
||||
|
@ -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
|
||||
|
@ -83,27 +87,10 @@ 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`_).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from openapi_core.shortcuts import RequestValidator
|
||||
from openapi_core.contrib.flask import FlaskOpenAPIRequest
|
||||
|
||||
openapi_request = FlaskOpenAPIRequest(flask_request)
|
||||
validator = RequestValidator(spec)
|
||||
result = validator.validate(openapi_request)
|
||||
|
||||
or simply specify request factory for shortcuts
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from openapi_core import validate_parameters, validate_body
|
||||
|
||||
validated_params = validate_parameters(
|
||||
spec, request, request_factory=FlaskOpenAPIRequest)
|
||||
validated_body = validate_body(
|
||||
spec, request, request_factory=FlaskOpenAPIRequest)
|
||||
Response
|
||||
********
|
||||
|
||||
You can also validate responses
|
||||
|
||||
|
@ -138,7 +125,85 @@ or use shortcuts for simple validation
|
|||
|
||||
validated_data = validate_data(spec, request, response)
|
||||
|
||||
Response object should be instance of OpenAPIResponse class. You can use FlaskOpenAPIResponse a Flask/Werkzeug response factory:
|
||||
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.django import DjangoOpenAPIRequest
|
||||
|
||||
openapi_request = DjangoOpenAPIRequest(django_request)
|
||||
validator = RequestValidator(spec)
|
||||
result = validator.validate(openapi_request)
|
||||
|
||||
or simply specify request factory for shortcuts
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from openapi_core import validate_parameters, validate_body
|
||||
|
||||
validated_params = validate_parameters(
|
||||
spec, request, request_factory=DjangoOpenAPIRequest)
|
||||
validated_body = validate_body(
|
||||
spec, request, request_factory=DjangoOpenAPIRequest)
|
||||
|
||||
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(openapi_request, openapi_response)
|
||||
|
||||
or simply specify response factory for shortcuts
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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
|
||||
|
||||
from openapi_core.shortcuts import RequestValidator
|
||||
from openapi_core.contrib.flask import FlaskOpenAPIRequest
|
||||
|
||||
openapi_request = FlaskOpenAPIRequest(flask_request)
|
||||
validator = RequestValidator(spec)
|
||||
result = validator.validate(openapi_request)
|
||||
|
||||
or simply specify request factory for shortcuts
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from openapi_core import validate_parameters, validate_body
|
||||
|
||||
validated_params = validate_parameters(
|
||||
spec, request, request_factory=FlaskOpenAPIRequest)
|
||||
validated_body = validate_body(
|
||||
spec, request, request_factory=FlaskOpenAPIRequest)
|
||||
|
||||
You can use FlaskOpenAPIResponse as a Flask/Werkzeug response factory:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -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
openapi_core/contrib/django/__init__.py
Normal file
11
openapi_core/contrib/django/__init__.py
Normal file
|
@ -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
openapi_core/contrib/django/requests.py
Normal file
51
openapi_core/contrib/django/requests.py
Normal file
|
@ -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
openapi_core/contrib/django/responses.py
Normal file
14
openapi_core/contrib/django/responses.py
Normal file
|
@ -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,
|
||||
)
|
|
@ -21,6 +21,23 @@ class RequestParameters(object):
|
|||
|
||||
@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()
|
||||
|
|
|
@ -6,6 +6,16 @@ from openapi_core.validation.datatypes import BaseValidationResult
|
|||
|
||||
@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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -43,6 +43,7 @@ exclude =
|
|||
tests
|
||||
|
||||
[options.extras_require]
|
||||
django = django>=2.2; python_version>="3.0"
|
||||
flask = werkzeug
|
||||
|
||||
[tool:pytest]
|
||||
|
|
196
tests/integration/contrib/test_django.py
Normal file
196
tests/integration/contrib/test_django.py
Normal file
|
@ -0,0 +1,196 @@
|
|||
import sys
|
||||
|
||||
import pytest
|
||||
from six import b
|
||||
|
||||
from openapi_core.contrib.django import (
|
||||
DjangoOpenAPIRequest, DjangoOpenAPIResponse,
|
||||
)
|
||||
from openapi_core.shortcuts import create_spec
|
||||
from openapi_core.validation.request.datatypes import RequestParameters
|
||||
from openapi_core.validation.request.validators import RequestValidator
|
||||
from openapi_core.validation.response.validators import ResponseValidator
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 0), reason="requires python3")
|
||||
class BaseTestDjango(object):
|
||||
|
||||
@pytest.fixture(autouse=True, scope='module')
|
||||
def django_settings(self):
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
if settings.configured:
|
||||
return
|
||||
|
||||
settings.configure(
|
||||
ALLOWED_HOSTS=[
|
||||
'testserver',
|
||||
],
|
||||
INSTALLED_APPS=[
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.sessions',
|
||||
],
|
||||
MIDDLEWARE=[
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
]
|
||||
)
|
||||
django.setup()
|
||||
settings.ROOT_URLCONF = (
|
||||
path('admin/', admin.site.urls),
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def request_factory(self):
|
||||
from django.test.client import RequestFactory
|
||||
return RequestFactory()
|
||||
|
||||
@pytest.fixture
|
||||
def response_factory(self):
|
||||
from django.http import HttpResponse
|
||||
|
||||
def create(content=b(''), status_code=None):
|
||||
return HttpResponse(content, status=status_code)
|
||||
|
||||
return create
|
||||
|
||||
|
||||
class TestDjangoOpenAPIRequest(BaseTestDjango):
|
||||
|
||||
def test_no_resolver(self, request_factory):
|
||||
request = request_factory.get('/admin/')
|
||||
|
||||
openapi_request = DjangoOpenAPIRequest(request)
|
||||
|
||||
path = {}
|
||||
query = {}
|
||||
headers = {
|
||||
'Cookie': '',
|
||||
}
|
||||
cookies = {}
|
||||
assert openapi_request.parameters == RequestParameters(
|
||||
path=path,
|
||||
query=query,
|
||||
header=headers,
|
||||
cookie=cookies,
|
||||
)
|
||||
assert openapi_request.host_url == request._current_scheme_host
|
||||
assert openapi_request.path == request.path
|
||||
assert openapi_request.method == request.method.lower()
|
||||
assert openapi_request.path_pattern == request.path
|
||||
assert openapi_request.body == request.body
|
||||
assert openapi_request.mimetype == request.content_type
|
||||
|
||||
def test_simple(self, request_factory):
|
||||
from django.urls import resolve
|
||||
request = request_factory.get('/admin/')
|
||||
request.resolver_match = resolve('/admin/')
|
||||
|
||||
openapi_request = DjangoOpenAPIRequest(request)
|
||||
|
||||
path = {}
|
||||
query = {}
|
||||
headers = {
|
||||
'Cookie': '',
|
||||
}
|
||||
cookies = {}
|
||||
assert openapi_request.parameters == RequestParameters(
|
||||
path=path,
|
||||
query=query,
|
||||
header=headers,
|
||||
cookie=cookies,
|
||||
)
|
||||
assert openapi_request.host_url == request._current_scheme_host
|
||||
assert openapi_request.path == request.path
|
||||
assert openapi_request.method == request.method.lower()
|
||||
assert openapi_request.path_pattern == request.path
|
||||
assert openapi_request.body == request.body
|
||||
assert openapi_request.mimetype == request.content_type
|
||||
|
||||
def test_url_rule(self, request_factory):
|
||||
from django.urls import resolve
|
||||
request = request_factory.get('/admin/auth/group/1/')
|
||||
request.resolver_match = resolve('/admin/auth/group/1/')
|
||||
|
||||
openapi_request = DjangoOpenAPIRequest(request)
|
||||
|
||||
path = {
|
||||
'object_id': '1',
|
||||
}
|
||||
query = {}
|
||||
headers = {
|
||||
'Cookie': '',
|
||||
}
|
||||
cookies = {}
|
||||
assert openapi_request.parameters == RequestParameters(
|
||||
path=path,
|
||||
query=query,
|
||||
header=headers,
|
||||
cookie=cookies,
|
||||
)
|
||||
assert openapi_request.host_url == request._current_scheme_host
|
||||
assert openapi_request.path == request.path
|
||||
assert openapi_request.method == request.method.lower()
|
||||
assert openapi_request.path_pattern == \
|
||||
"/admin/auth/group/{object_id}/"
|
||||
assert openapi_request.body == request.body
|
||||
assert openapi_request.mimetype == request.content_type
|
||||
|
||||
|
||||
class TestDjangoOpenAPIResponse(BaseTestDjango):
|
||||
|
||||
def test_stream_response(self, response_factory):
|
||||
response = response_factory()
|
||||
response.writelines(['foo\n', 'bar\n', 'baz\n'])
|
||||
|
||||
openapi_response = DjangoOpenAPIResponse(response)
|
||||
|
||||
assert openapi_response.data == b('foo\nbar\nbaz\n')
|
||||
assert openapi_response.status_code == response.status_code
|
||||
assert openapi_response.mimetype == response["Content-Type"]
|
||||
|
||||
def test_redirect_response(self, response_factory):
|
||||
response = response_factory('/redirected/', status_code=302)
|
||||
|
||||
openapi_response = DjangoOpenAPIResponse(response)
|
||||
|
||||
assert openapi_response.data == response.content
|
||||
assert openapi_response.status_code == response.status_code
|
||||
assert openapi_response.mimetype == response["Content-Type"]
|
||||
|
||||
|
||||
class TestDjangoOpenAPIValidation(BaseTestDjango):
|
||||
|
||||
@pytest.fixture
|
||||
def django_spec(self, factory):
|
||||
specfile = 'data/v3.0/django_factory.yaml'
|
||||
return create_spec(factory.spec_from_file(specfile))
|
||||
|
||||
def test_response_validator_path_pattern(
|
||||
self, django_spec, request_factory, response_factory):
|
||||
from django.urls import resolve
|
||||
validator = ResponseValidator(django_spec)
|
||||
request = request_factory.get('/admin/auth/group/1/')
|
||||
request.resolver_match = resolve('/admin/auth/group/1/')
|
||||
openapi_request = DjangoOpenAPIRequest(request)
|
||||
response = response_factory(b('Some item'))
|
||||
openapi_response = DjangoOpenAPIResponse(response)
|
||||
result = validator.validate(openapi_request, openapi_response)
|
||||
assert not result.errors
|
||||
|
||||
def test_request_validator_path_pattern(
|
||||
self, django_spec, request_factory):
|
||||
from django.urls import resolve
|
||||
validator = RequestValidator(django_spec)
|
||||
request = request_factory.get('/admin/auth/group/1/')
|
||||
request.resolver_match = resolve('/admin/auth/group/1/')
|
||||
openapi_request = DjangoOpenAPIRequest(request)
|
||||
result = validator.validate(openapi_request)
|
||||
assert not result.errors
|
19
tests/integration/data/v3.0/django_factory.yaml
Normal file
19
tests/integration/data/v3.0/django_factory.yaml
Normal file
|
@ -0,0 +1,19 @@
|
|||
openapi: "3.0.0"
|
||||
info:
|
||||
title: Basic OpenAPI specification used with test_flask.TestFlaskOpenAPIIValidation
|
||||
version: "0.1"
|
||||
servers:
|
||||
- url: 'http://testserver'
|
||||
paths:
|
||||
'/admin/auth/group/{object_id}/':
|
||||
parameters:
|
||||
- name: object_id
|
||||
in: path
|
||||
required: true
|
||||
description: the ID of the resource to retrieve
|
||||
schema:
|
||||
type: integer
|
||||
get:
|
||||
responses:
|
||||
default:
|
||||
description: Return the resource.
|
Loading…
Reference in a new issue