Merge pull request #167 from p1c2u/feature/django-support

Django OpenAPI request/response factories
This commit is contained in:
A 2019-10-20 09:10:58 +01:00 committed by GitHub
commit fd99117278
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 416 additions and 26 deletions

View file

@ -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>`__

View 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',
]

View 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,
)

View 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,
)

View file

@ -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()

View file

@ -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()

View file

@ -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"

View file

@ -43,6 +43,7 @@ exclude =
tests
[options.extras_require]
django = django>=2.2; python_version>="3.0"
flask = werkzeug
[tool:pytest]

View 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

View 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.