Skip to content

Separate werkzeug contrib #426

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 1 commit into from
Sep 28, 2022
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
42 changes: 32 additions & 10 deletions docs/integrations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ In Flask, all unmarshalled request data are provided as Flask request object's `
Low level
~~~~~~~~~

You can use `FlaskOpenAPIRequest` as a Flask/Werkzeug request factory:
You can use `FlaskOpenAPIRequest` as a Flask request factory:

.. code-block:: python

Expand All @@ -202,15 +202,7 @@ You can use `FlaskOpenAPIRequest` as a Flask/Werkzeug request factory:
openapi_request = FlaskOpenAPIRequest(flask_request)
result = openapi_request_validator.validate(spec, openapi_request)

You can use `FlaskOpenAPIResponse` as a Flask/Werkzeug response factory:

.. code-block:: python

from openapi_core.validation.response import openapi_response_validator
from openapi_core.contrib.flask import FlaskOpenAPIResponse

openapi_response = FlaskOpenAPIResponse(flask_response)
result = openapi_response_validator.validate(spec, openapi_request, openapi_response)
For response factory see `Werkzeug`_ integration.


Pyramid
Expand Down Expand Up @@ -247,7 +239,37 @@ You can use `RequestsOpenAPIResponse` as a Requests response factory:
openapi_response = RequestsOpenAPIResponse(requests_response)
result = openapi_respose_validator.validate(spec, openapi_request, openapi_response)


Tornado
-------

See `tornado-openapi3 <https://github.com/correl/tornado-openapi3>`_ project.


Werkzeug
--------

This section describes integration with `Werkzeug <https://werkzeug.palletsprojects.com>`__ a WSGI web application library.

Low level
~~~~~~~~~

You can use `WerkzeugOpenAPIRequest` as a Werkzeug request factory:

.. code-block:: python

from openapi_core.validation.request import openapi_request_validator
from openapi_core.contrib.werkzeug import WerkzeugOpenAPIRequest

openapi_request = WerkzeugOpenAPIRequest(werkzeug_request)
result = openapi_request_validator.validate(spec, openapi_request)

You can use `WerkzeugOpenAPIResponse` as a Werkzeug response factory:

.. code-block:: python

from openapi_core.validation.response import openapi_response_validator
from openapi_core.contrib.werkzeug import WerkzeugOpenAPIResponse

openapi_response = WerkzeugOpenAPIResponse(werkzeug_response)
result = openapi_response_validator.validate(spec, openapi_request, openapi_response)
36 changes: 4 additions & 32 deletions openapi_core/contrib/flask/requests.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
"""OpenAPI core contrib flask requests module"""
import re
from typing import Optional

from flask.wrappers import Request
from werkzeug.datastructures import Headers
from werkzeug.datastructures import ImmutableMultiDict

from openapi_core.contrib.werkzeug.requests import WerkzeugOpenAPIRequest
from openapi_core.validation.request.datatypes import RequestParameters

# http://flask.pocoo.org/docs/1.0/quickstart/#variable-rules
PATH_PARAMETER_PATTERN = r"<(?:(?:string|int|float|path|uuid):)?(\w+)>"


class FlaskOpenAPIRequest:

path_regex = re.compile(PATH_PARAMETER_PATTERN)

class FlaskOpenAPIRequest(WerkzeugOpenAPIRequest):
def __init__(self, request: Request):
self.request = request
self.request: Request = request

self.parameters = RequestParameters(
path=self.request.view_args or {},
Expand All @@ -26,29 +18,9 @@ def __init__(self, request: Request):
cookie=self.request.cookies,
)

@property
def host_url(self) -> str:
return self.request.host_url

@property
def path(self) -> str:
return self.request.path

@property
def path_pattern(self) -> str:
if self.request.url_rule is None:
return self.request.path
else:
return self.path_regex.sub(r"{\1}", self.request.url_rule.rule)

@property
def method(self) -> str:
return self.request.method.lower()

@property
def body(self) -> Optional[str]:
return self.request.get_data(as_text=True)

@property
def mimetype(self) -> str:
return self.request.mimetype
return self.path_regex.sub(r"{\1}", self.request.url_rule.rule)
27 changes: 4 additions & 23 deletions openapi_core/contrib/flask/responses.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,5 @@
"""OpenAPI core contrib flask responses module"""
from flask.wrappers import Response
from werkzeug.datastructures import Headers
from openapi_core.contrib.werkzeug.responses import (
WerkzeugOpenAPIResponse as FlaskOpenAPIResponse,
)


class FlaskOpenAPIResponse:
def __init__(self, response: Response):
self.response = response

@property
def data(self) -> str:
return self.response.get_data(as_text=True)

@property
def status_code(self) -> int:
return self.response._status_code

@property
def mimetype(self) -> str:
return str(self.response.mimetype)

@property
def headers(self) -> Headers:
return Headers(self.response.headers)
__all__ = ["FlaskOpenAPIResponse"]
7 changes: 7 additions & 0 deletions openapi_core/contrib/werkzeug/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from openapi_core.contrib.werkzeug.requests import WerkzeugOpenAPIRequest
from openapi_core.contrib.werkzeug.responses import WerkzeugOpenAPIResponse

__all__ = [
"WerkzeugOpenAPIRequest",
"WerkzeugOpenAPIResponse",
]
46 changes: 46 additions & 0 deletions openapi_core/contrib/werkzeug/requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""OpenAPI core contrib werkzeug requests module"""
import re
from typing import Optional

from werkzeug.datastructures import Headers
from werkzeug.datastructures import ImmutableMultiDict
from werkzeug.wrappers import Request

from openapi_core.validation.request.datatypes import RequestParameters

# http://flask.pocoo.org/docs/1.0/quickstart/#variable-rules
PATH_PARAMETER_PATTERN = r"<(?:(?:string|int|float|path|uuid):)?(\w+)>"


class WerkzeugOpenAPIRequest:

path_regex = re.compile(PATH_PARAMETER_PATTERN)

def __init__(self, request: Request):
self.request = request

self.parameters = RequestParameters(
query=ImmutableMultiDict(self.request.args),
header=Headers(self.request.headers),
cookie=self.request.cookies,
)

@property
def host_url(self) -> str:
return self.request.host_url

@property
def path(self) -> str:
return self.request.path

@property
def method(self) -> str:
return self.request.method.lower()

@property
def body(self) -> Optional[str]:
return self.request.get_data(as_text=True)

@property
def mimetype(self) -> str:
return self.request.mimetype
24 changes: 24 additions & 0 deletions openapi_core/contrib/werkzeug/responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""OpenAPI core contrib werkzeug responses module"""
from werkzeug.datastructures import Headers
from werkzeug.wrappers import Response


class WerkzeugOpenAPIResponse:
def __init__(self, response: Response):
self.response = response

@property
def data(self) -> str:
return self.response.get_data(as_text=True)

@property
def status_code(self) -> int:
return self.response._status_code

@property
def mimetype(self) -> str:
return str(self.response.mimetype)

@property
def headers(self) -> Headers:
return Headers(self.response.headers)
79 changes: 79 additions & 0 deletions tests/integration/contrib/werkzeug/test_werkzeug_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from json import dumps

import pytest
import requests
import responses
from werkzeug.test import Client
from werkzeug.wrappers import Request
from werkzeug.wrappers import Response

from openapi_core.contrib.werkzeug import WerkzeugOpenAPIRequest
from openapi_core.contrib.werkzeug import WerkzeugOpenAPIResponse
from openapi_core.validation.request import openapi_request_validator
from openapi_core.validation.response import openapi_response_validator


class TestWerkzeugOpenAPIValidation:
@pytest.fixture
def spec(self, factory):
specfile = "contrib/requests/data/v3.0/requests_factory.yaml"
return factory.spec_from_file(specfile)

@pytest.fixture
def app(self):
def test_app(environ, start_response):
req = Request(environ, populate_request=False)
if req.args.get("q") == "string":
response = Response(
dumps({"data": "data"}),
headers={"X-Rate-Limit": "12"},
mimetype="application/json",
status=200,
)
else:
response = Response("Not Found", status=404)
return response(environ, start_response)

return test_app

@pytest.fixture
def client(self, app):
return Client(app)

def test_request_validator_path_pattern(self, client, spec):
query_string = {
"q": "string",
}
headers = {"content-type": "application/json"}
data = {"param1": 1}
response = client.post(
"/browse/12/",
base_url="http://localhost",
query_string=query_string,
json=data,
headers=headers,
)
openapi_request = WerkzeugOpenAPIRequest(response.request)
result = openapi_request_validator.validate(spec, openapi_request)
assert not result.errors

@responses.activate
def test_response_validator_path_pattern(self, client, spec):
query_string = {
"q": "string",
}
headers = {"content-type": "application/json"}
data = {"param1": 1}
response = client.post(
"/browse/12/",
base_url="http://localhost",
query_string=query_string,
json=data,
headers=headers,
)
openapi_request = WerkzeugOpenAPIRequest(response.request)
openapi_response = WerkzeugOpenAPIResponse(response)
result = openapi_response_validator.validate(
spec, openapi_request, openapi_response
)
assert not result.errors