Skip to content

Requests support #32

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

Closed
wants to merge 10 commits into from
Closed
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
180 changes: 179 additions & 1 deletion openapi_core/wrappers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
"""OpenAPI core wrappers module"""
import re
from string import Formatter
import warnings

from urllib.parse import urlparse
from six.moves.urllib.parse import urljoin
from werkzeug.datastructures import ImmutableMultiDict

from openapi_core.exceptions import InvalidServer, InvalidOperation


class BaseOpenAPIRequest(object):

Expand Down Expand Up @@ -107,6 +112,83 @@ def mimetype(self):
return self.request.mimetype


class RequestsOpenAPIRequest(BaseOpenAPIRequest):

def __init__(self, response, path_pattern, regex_pattern, server_pattern):
Copy link
Collaborator

@p1c2u p1c2u Apr 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should accept request object (not response).
http://docs.python-requests.org/en/master/api/#lower-level-classes
User don't need to have response to be able to validate request.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the requests library explicitly uses a request object in its normal flow. Instead the response starts out as a function. You don't get an actual response object until after the http result is returned.
response = requests.post('http://httpbin.org/post', data = {'key':'value'}) request = response.request

Copy link
Author

@AMcManigal AMcManigal Apr 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I misread what you were getting at. I pass in the response because of the way that the requests library distributes its data. For example, in order to get cookies it needs the response object even though cookies are a part of the request model in your interface.

I also use it to extract the correct mimetype in case the request doesn't have an 'Accept' header.

"""

:param response: The Requests Responce Object
:type response: requests.models.Response
:param path_pattern: The path pattern determined by the factory
:type path_pattern: str
:param regex_pattern: Used to extract path params.
:type regex_pattern: str
"""
self.response = response
self.url = urlparse(self.response.url)
self._path_pattern = path_pattern
self._regex_pattern = regex_pattern
self._server_pattern = server_pattern
self._parameters = {
'path': self._extract_path_params(),
'query': self._extract_query_params(),
'headers': self.response.request.headers,
'cookies': self.response.cookies,
}

@property
def full_url_pattern(self):
return self.host_url + self.path_pattern

@property
def host_url(self):
return re.match(self._server_pattern, self.response.url).group(0)

@property
def path(self):
return re.sub(self._server_pattern, '', urljoin(self.host_url,
self.url.path))

@property
def method(self):
return self.response.request.method.lower()

@property
def path_pattern(self):
return self._path_pattern

@property
def parameters(self):
return self._parameters

def _extract_body(self):
if hasattr(self.response.request, 'text'):
return self.response.request.text
return ''

def _extract_query_params(self):
if hasattr(self.response.request, 'qs'):
return self.response.request.qs
return {}

def _extract_path_params(self):
# Get the values of the path parameters
groups = re.match(self._regex_pattern, self.path).groups()
# Get the names of path parameters
names = [fname[1] for fname in Formatter()
.parse(self.path_pattern) if fname]
return {name: group for name, group in zip(names, groups)}

@property
def body(self):
return self._extract_body()

@property
def mimetype(self):
return self.response.request.headers.get('Accept') or\
self.response.headers.get('Content-Type')


class BaseOpenAPIResponse(object):

body = NotImplemented
Expand Down Expand Up @@ -135,8 +217,104 @@ def data(self):

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

@property
def mimetype(self):
return self.response.mimetype


class RequestsOpenAPIResponse(BaseOpenAPIResponse):

def __init__(self, response):
self.response = response

@property
def data(self):
return self._extract_data()

@property
def status_code(self):
return self.response.status_code

@property
def mimetype(self):
return self.response.headers.get('Content-Type')

def _extract_data(self):
if hasattr(self.response, 'text'):
return self.response.text
return ''


class RequestsFactory(object):
path_regex = re.compile('{(.*?)}')

def __init__(self, spec):
"""
Creates the request factory. A spec is required.

:param spec: The openapi spec to use for decoding the request
:type spec: openapi_core.specs.Spec
"""
self.paths_regex = self._create_paths_regex(spec)
self.server_regex = self._create_server_regex(spec)

def _create_server_regex(self, spec):
server_regex = []
for server in spec.servers:
var_map = {}
for var in server.variables:
if server.variables[var].enum:
var_map[var] = "(" +\
"|".join(server.variables[var].enum) + ")"
else:
var_map[var] = "(.*)"
server_regex.append(server.url.format_map(var_map))
return server_regex

def _create_paths_regex(self, spec):
paths_regex = {}
for path in spec.paths:
pattern = self.path_regex.sub('(.*)', path)
paths_regex[pattern] = path
return paths_regex

def _match_operation(self, path_pattern):
for expr in self.paths_regex:
if re.fullmatch(expr, path_pattern):
return expr
return None

def _match_server(self, server_pattern):
for expr in self.server_regex:
if re.match(expr, server_pattern):
return expr
return None

def create_request(self, response):
"""
Creates an OpenApi compatible request out of the raw requests request

:param request: requests.models.Request
:return: RequestsOpenApiRequest
"""
server_pattern = self._match_server(response.url)
if not server_pattern:
raise InvalidServer("Url server not in spec.")
path = re.sub(server_pattern, '', response.url)
pattern = self._match_operation(path)
if not pattern:
raise InvalidOperation("Operation not in spec.")
response = RequestsOpenAPIRequest(
response, self.paths_regex[pattern], pattern, server_pattern)
return response

def create_response(self, response):
"""

:param request:
:type request: requests.models.PreparedRequest
:return: RequestsOpenAPIResponse
"""
return RequestsOpenAPIResponse(response)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
openapi-spec-validator
six
typing
yarl
2 changes: 2 additions & 0 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ pytest
pytest-pep8
pytest-flakes
pytest-cov
requests
requests-mock
flask
1 change: 0 additions & 1 deletion tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from os import path

import pytest
from six.moves.urllib import request
from yaml import safe_load
Expand Down
8 changes: 8 additions & 0 deletions tests/integration/data/v3.0/petstore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ paths:
schema:
$ref: '#/components/schemas/PetCreate'
responses:
'200':
description: Pet Created Response
content:
application/json:
schema:
$ref: "#/components/schemas/PetData"
'201':
description: Null response
default:
Expand Down Expand Up @@ -232,3 +238,5 @@ components:
application/json:
schema:
$ref: "#/components/schemas/ExtendedError"


21 changes: 21 additions & 0 deletions tests/integration/data/v3.0/server_path_variations.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
openapi: "3.0.0"
info:
title: Minimal valid OpenAPI specification with explicit 'servers' array
version: "0.1"
servers:
- url: https://{customerId}.saas-app.com:{port}/v2
variables:
customerId:
default: demo
description: Customer ID assigned by the service provider
port:
enum:
- '443'
- '8443'
default: '443'
paths:
/status:
get:
responses:
default:
description: Return the API status.
Loading