mirror of
https://github.com/correl/openapi-core.git
synced 2024-12-28 19:19:23 +00:00
Django OpenAPI request/response factories
This commit is contained in:
parent
0d62c5f374
commit
2000b1215f
10 changed files with 305 additions and 105 deletions
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()
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
"""OpenAPI core wrappers module"""
|
||||
import re
|
||||
|
||||
from openapi_core.wrappers.base import BaseOpenAPIRequest, BaseOpenAPIResponse
|
||||
|
||||
# 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 DjangoOpenAPIRequest(BaseOpenAPIRequest):
|
||||
path_regex = re.compile(PATH_PARAMETER_PATTERN)
|
||||
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
@property
|
||||
def host_url(self):
|
||||
"""
|
||||
:return: The host with scheme as IRI.
|
||||
"""
|
||||
return self.request._current_scheme_host
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
"""
|
||||
:return: Requested path as unicode.
|
||||
"""
|
||||
return self.request.path
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
"""
|
||||
:return: The request method, in lowercase.
|
||||
"""
|
||||
return self.request.method.lower()
|
||||
|
||||
@property
|
||||
def path_pattern(self):
|
||||
"""
|
||||
:return: The matched url pattern.
|
||||
"""
|
||||
return self.path_regex.sub(r'{\1}', self.request.resolver_match.route)
|
||||
|
||||
@property
|
||||
def parameters(self):
|
||||
"""
|
||||
:return: A dictionary of all parameters.
|
||||
"""
|
||||
return {
|
||||
'path': self.request.resolver_match.kwargs,
|
||||
'query': self.request.GET,
|
||||
'header': self.request.headers,
|
||||
'cookie': self.request.COOKIES,
|
||||
}
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
"""
|
||||
:return: The request body, as string.
|
||||
"""
|
||||
return self.request.body
|
||||
|
||||
@property
|
||||
def mimetype(self):
|
||||
"""
|
||||
:return: 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".
|
||||
"""
|
||||
return self.request.content_type
|
||||
|
||||
|
||||
class DjangoOpenAPIResponse(BaseOpenAPIResponse):
|
||||
|
||||
def __init__(self, response):
|
||||
self.response = response
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
"""
|
||||
:return: The response body, as string.
|
||||
"""
|
||||
return self.response.content
|
||||
|
||||
@property
|
||||
def status_code(self):
|
||||
"""
|
||||
:return: The status code as integer.
|
||||
"""
|
||||
return self.response.status_code
|
||||
|
||||
@property
|
||||
def mimetype(self):
|
||||
"""
|
||||
:return: Lowercase content type without charset.
|
||||
"""
|
||||
return self.response["Content-Type"]
|
|
@ -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
|
||||
|
|
|
@ -43,6 +43,7 @@ exclude =
|
|||
tests
|
||||
|
||||
[options.extras_require]
|
||||
django = django
|
||||
flask = werkzeug
|
||||
|
||||
[tool:pytest]
|
||||
|
|
180
tests/integration/contrib/test_django.py
Normal file
180
tests/integration/contrib/test_django.py
Normal file
|
@ -0,0 +1,180 @@
|
|||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls import resolve
|
||||
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.fixture(autouse=True, scope='module')
|
||||
def django_settings():
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
class TestDjangoOpenAPIRequest(object):
|
||||
|
||||
@pytest.fixture
|
||||
def request_factory(self):
|
||||
return RequestFactory()
|
||||
|
||||
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):
|
||||
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):
|
||||
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:
|
||||
|
||||
def test_stream_response(self):
|
||||
response = HttpResponse()
|
||||
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 = HttpResponseRedirect('/redirected/')
|
||||
|
||||
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(object):
|
||||
|
||||
@pytest.fixture
|
||||
def request_factory(self):
|
||||
return RequestFactory()
|
||||
|
||||
@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):
|
||||
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 = HttpResponse(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):
|
||||
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