mirror of
https://github.com/correl/openapi-core.git
synced 2024-12-28 03:00:11 +00:00
initial version
This commit is contained in:
parent
e3d831c005
commit
553b7228b1
27 changed files with 1138 additions and 2 deletions
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal file
|
@ -0,0 +1,2 @@
|
|||
include requirements.txt
|
||||
include requirements_dev.txt
|
|
@ -1,2 +0,0 @@
|
|||
# openapi-core
|
||||
OpenAPI core
|
24
README.rst
Normal file
24
README.rst
Normal file
|
@ -0,0 +1,24 @@
|
|||
openapi-core
|
||||
************
|
||||
|
||||
Openapi-core is a Python library that adds client-side and server-side support
|
||||
for the `OpenAPI Specification v3.0.0 <github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md>`__.
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Recommended way (via pip):
|
||||
|
||||
::
|
||||
|
||||
$ pip install openapi-core
|
||||
|
||||
Alternatively you can download the code and install from the repository:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install -e git+https://github.com/p1c2u/openapi-core.git#egg=openapi_core
|
||||
|
||||
Related projects
|
||||
================
|
||||
* `openapi-spec-validator <https://github.com/p1c2u/openapi-spec-validator>`__
|
10
openapi_core/__init__.py
Normal file
10
openapi_core/__init__.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
"""OpenAPI core module"""
|
||||
from openapi_core.shortcuts import create_spec
|
||||
|
||||
__author__ = 'Artur Maciąg'
|
||||
__email__ = 'maciag.artur@gmail.com'
|
||||
__version__ = '0.0.1'
|
||||
__url__ = 'https://github.com/p1c2u/openapi-core'
|
||||
__license__ = 'BSD 3-Clause License'
|
||||
|
||||
__all__ = ['create_spec', ]
|
17
openapi_core/exceptions.py
Normal file
17
openapi_core/exceptions.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
"""OpenAPI core exceptions module"""
|
||||
|
||||
|
||||
class OpenAPIError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OpenAPIMappingError(OpenAPIError):
|
||||
pass
|
||||
|
||||
|
||||
class MissingParameterError(OpenAPIMappingError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidContentTypeError(OpenAPIMappingError):
|
||||
pass
|
37
openapi_core/media_types.py
Normal file
37
openapi_core/media_types.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
"""OpenAPI core mediaTypes module"""
|
||||
from six import iteritems
|
||||
|
||||
from openapi_core.schemas import SchemaFactory
|
||||
|
||||
|
||||
class MediaType(object):
|
||||
"""Represents an OpenAPI MediaType."""
|
||||
|
||||
def __init__(self, content_type, schema=None):
|
||||
self.content_type = content_type
|
||||
self.schema = schema
|
||||
|
||||
def unmarshal(self, value):
|
||||
if not self.schema:
|
||||
return value
|
||||
|
||||
return self.schema.unmarshal(value)
|
||||
|
||||
|
||||
class MediaTypeGenerator(object):
|
||||
|
||||
def __init__(self, dereferencer):
|
||||
self.dereferencer = dereferencer
|
||||
|
||||
def generate(self, content):
|
||||
for content_type, media_type in iteritems(content):
|
||||
schema_spec = media_type.get('schema')
|
||||
|
||||
schema = None
|
||||
if schema_spec:
|
||||
schema = self._create_schema(schema_spec)
|
||||
|
||||
yield content_type, MediaType(content_type, schema)
|
||||
|
||||
def _create_schema(self, schema_spec):
|
||||
return SchemaFactory(self.dereferencer).create(schema_spec)
|
64
openapi_core/operations.py
Normal file
64
openapi_core/operations.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""OpenAPI core operations module"""
|
||||
import logging
|
||||
|
||||
from six import iteritems
|
||||
|
||||
from openapi_core.parameters import ParametersGenerator
|
||||
from openapi_core.request_bodies import RequestBodyFactory
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Operation(object):
|
||||
"""Represents an OpenAPI Operation."""
|
||||
|
||||
def __init__(
|
||||
self, http_method, path_name, parameters, request_body=None,
|
||||
deprecated=False, operation_id=None):
|
||||
self.http_method = http_method
|
||||
self.path_name = path_name
|
||||
self.parameters = dict(parameters)
|
||||
self.request_body = request_body
|
||||
self.deprecated = deprecated
|
||||
self.operation_id = operation_id
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self.parameters[name]
|
||||
|
||||
|
||||
class OperationsGenerator(object):
|
||||
"""Represents an OpenAPI Operation in a service."""
|
||||
|
||||
def __init__(self, dereferencer):
|
||||
self.dereferencer = dereferencer
|
||||
|
||||
def generate(self, path_name, path):
|
||||
path_deref = self.dereferencer.dereference(path)
|
||||
for http_method, operation in iteritems(path_deref):
|
||||
if http_method.startswith('x-') or http_method == 'parameters':
|
||||
continue
|
||||
|
||||
operation_deref = self.dereferencer.dereference(operation)
|
||||
deprecated = operation_deref.get('deprecated', False)
|
||||
parameters_list = operation_deref.get('parameters', [])
|
||||
parameters = self._generate_parameters(parameters_list)
|
||||
|
||||
request_body = None
|
||||
if 'requestBody' in operation_deref:
|
||||
request_body_spec = operation_deref.get('requestBody')
|
||||
request_body = self._create_request_body(request_body_spec)
|
||||
|
||||
yield (
|
||||
http_method,
|
||||
Operation(
|
||||
http_method, path_name, list(parameters),
|
||||
request_body=request_body, deprecated=deprecated,
|
||||
),
|
||||
)
|
||||
|
||||
def _generate_parameters(self, parameters):
|
||||
return ParametersGenerator(self.dereferencer).generate(parameters)
|
||||
|
||||
def _create_request_body(self, request_body_spec):
|
||||
return RequestBodyFactory(self.dereferencer).create(request_body_spec)
|
59
openapi_core/parameters.py
Normal file
59
openapi_core/parameters.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
"""OpenAPI core parameters module"""
|
||||
import logging
|
||||
|
||||
from openapi_core.schemas import SchemaFactory
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Parameter(object):
|
||||
"""Represents an OpenAPI operation Parameter."""
|
||||
|
||||
def __init__(
|
||||
self, name, location, schema=None, default=None,
|
||||
required=False, deprecated=False, allow_empty_value=False,
|
||||
items=None, collection_format=None):
|
||||
self.name = name
|
||||
self.location = location
|
||||
self.schema = schema
|
||||
self.default = default
|
||||
self.required = required
|
||||
self.deprecated = deprecated
|
||||
self.allow_empty_value = allow_empty_value
|
||||
self.items = items
|
||||
self.collection_format = collection_format
|
||||
|
||||
def unmarshal(self, value):
|
||||
if not self.schema:
|
||||
return value
|
||||
|
||||
return self.schema.unmarshal(value)
|
||||
|
||||
|
||||
class ParametersGenerator(object):
|
||||
|
||||
def __init__(self, dereferencer):
|
||||
self.dereferencer = dereferencer
|
||||
|
||||
def generate(self, paramters):
|
||||
for parameter in paramters:
|
||||
parameter_deref = self.dereferencer.dereference(parameter)
|
||||
|
||||
default = parameter_deref.get('default')
|
||||
required = parameter_deref.get('required', False)
|
||||
|
||||
schema_spec = parameter_deref.get('schema', None)
|
||||
schema = None
|
||||
if schema_spec:
|
||||
schema = self._create_schema(schema_spec)
|
||||
|
||||
yield (
|
||||
parameter_deref['name'],
|
||||
Parameter(
|
||||
parameter_deref['name'], parameter_deref['in'],
|
||||
schema=schema, default=default, required=required,
|
||||
),
|
||||
)
|
||||
|
||||
def _create_schema(self, schema_spec):
|
||||
return SchemaFactory(self.dereferencer).create(schema_spec)
|
30
openapi_core/paths.py
Normal file
30
openapi_core/paths.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
"""OpenAPI core paths module"""
|
||||
from six import iteritems
|
||||
|
||||
from openapi_core.operations import OperationsGenerator
|
||||
|
||||
|
||||
class Path(object):
|
||||
"""Represents an OpenAPI Path."""
|
||||
|
||||
def __init__(self, name, operations):
|
||||
self.name = name
|
||||
self.operations = dict(operations)
|
||||
|
||||
def __getitem__(self, http_method):
|
||||
return self.operations[http_method]
|
||||
|
||||
|
||||
class PathsGenerator(object):
|
||||
|
||||
def __init__(self, dereferencer):
|
||||
self.dereferencer = dereferencer
|
||||
|
||||
def generate(self, paths):
|
||||
paths_deref = self.dereferencer.dereference(paths)
|
||||
for path_name, path in iteritems(paths_deref):
|
||||
operations = self._generate_operations(path_name, path)
|
||||
yield path_name, Path(path_name, list(operations))
|
||||
|
||||
def _generate_operations(self, path_name, path):
|
||||
return OperationsGenerator(self.dereferencer).generate(path_name, path)
|
30
openapi_core/request_bodies.py
Normal file
30
openapi_core/request_bodies.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
"""OpenAPI core requestBodies module"""
|
||||
from openapi_core.media_types import MediaTypeGenerator
|
||||
|
||||
|
||||
class RequestBody(object):
|
||||
"""Represents an OpenAPI RequestBody."""
|
||||
|
||||
def __init__(self, content, required=False):
|
||||
self.content = dict(content)
|
||||
self.required = required
|
||||
|
||||
def __getitem__(self, content_type):
|
||||
return self.content[content_type]
|
||||
|
||||
|
||||
class RequestBodyFactory(object):
|
||||
|
||||
def __init__(self, dereferencer):
|
||||
self.dereferencer = dereferencer
|
||||
|
||||
def create(self, request_body_spec):
|
||||
request_body_deref = self.dereferencer.dereference(
|
||||
request_body_spec)
|
||||
content = request_body_deref['content']
|
||||
media_types = self._generate_media_types(content)
|
||||
required = request_body_deref.get('required', False)
|
||||
return RequestBody(media_types, required=required)
|
||||
|
||||
def _generate_media_types(self, content):
|
||||
return MediaTypeGenerator(self.dereferencer).generate(content)
|
113
openapi_core/schemas.py
Normal file
113
openapi_core/schemas.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
"""OpenAPI core schemas module"""
|
||||
import logging
|
||||
from distutils.util import strtobool
|
||||
from collections import defaultdict
|
||||
|
||||
from json import loads
|
||||
from six import iteritems
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_CAST_CALLABLE_GETTER = {
|
||||
'integer': int,
|
||||
'number': float,
|
||||
'boolean': lambda x: bool(strtobool(x)),
|
||||
'object': loads,
|
||||
}
|
||||
|
||||
|
||||
class Schema(object):
|
||||
"""Represents an OpenAPI Schema."""
|
||||
|
||||
def __init__(
|
||||
self, schema_type, properties=None, items=None, spec_format=None,
|
||||
required=False):
|
||||
self.type = schema_type
|
||||
self.properties = properties and dict(properties) or {}
|
||||
self.items = items
|
||||
self.format = spec_format
|
||||
self.required = required
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self.properties[name]
|
||||
|
||||
def get_cast_mapping(self):
|
||||
mapping = DEFAULT_CAST_CALLABLE_GETTER.copy()
|
||||
if self.items:
|
||||
mapping.update({
|
||||
'array': lambda x: list(map(self.items.unmarshal, x)),
|
||||
})
|
||||
|
||||
return defaultdict(lambda: lambda x: x, mapping)
|
||||
|
||||
def cast(self, value):
|
||||
"""Cast value to schema type"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
cast_mapping = self.get_cast_mapping()
|
||||
|
||||
if self.type in cast_mapping and value == '':
|
||||
return None
|
||||
|
||||
cast_callable = cast_mapping[self.type]
|
||||
try:
|
||||
return cast_callable(value)
|
||||
except ValueError:
|
||||
log.warning(
|
||||
"Failed to cast value of %s to %s", value, self.type,
|
||||
)
|
||||
return value
|
||||
|
||||
def unmarshal(self, value):
|
||||
"""Unmarshal parameter from the value."""
|
||||
casted = self.cast(value)
|
||||
|
||||
if casted is None and not self.required:
|
||||
return None
|
||||
|
||||
return casted
|
||||
|
||||
|
||||
class PropertiesGenerator(object):
|
||||
|
||||
def __init__(self, dereferencer):
|
||||
self.dereferencer = dereferencer
|
||||
|
||||
def generate(self, properties):
|
||||
for property_name, schema_spec in iteritems(properties):
|
||||
schema = self._create_schema(schema_spec)
|
||||
yield property_name, schema
|
||||
|
||||
def _create_schema(self, schema_spec):
|
||||
return SchemaFactory(self.dereferencer).create(schema_spec)
|
||||
|
||||
|
||||
class SchemaFactory(object):
|
||||
|
||||
def __init__(self, dereferencer):
|
||||
self.dereferencer = dereferencer
|
||||
|
||||
def create(self, schema_spec):
|
||||
schema_deref = self.dereferencer.dereference(schema_spec)
|
||||
schema_type = schema_deref['type']
|
||||
required = schema_deref.get('required', False)
|
||||
properties_spec = schema_deref.get('properties', None)
|
||||
items_spec = schema_deref.get('items', None)
|
||||
|
||||
properties = None
|
||||
if properties_spec:
|
||||
properties = self._generate_properties(properties_spec)
|
||||
|
||||
items = None
|
||||
if items_spec:
|
||||
items = self._create_items(items_spec)
|
||||
|
||||
return Schema(
|
||||
schema_type, properties=properties, items=items, required=required)
|
||||
|
||||
def _generate_properties(self, properties_spec):
|
||||
return PropertiesGenerator(self.dereferencer).generate(properties_spec)
|
||||
|
||||
def _create_items(self, items_spec):
|
||||
return SchemaFactory(self.dereferencer).create(items_spec)
|
14
openapi_core/shortcuts.py
Normal file
14
openapi_core/shortcuts.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
"""OpenAPI core shortcuts module"""
|
||||
from jsonschema.validators import RefResolver
|
||||
from openapi_spec_validator.validators import Dereferencer
|
||||
from openapi_spec_validator import default_handlers
|
||||
|
||||
from openapi_core.specs import SpecFactory
|
||||
|
||||
|
||||
def create_spec(spec_dict, spec_url=''):
|
||||
spec_resolver = RefResolver(
|
||||
spec_url, spec_dict, handlers=default_handlers)
|
||||
dereferencer = Dereferencer(spec_resolver)
|
||||
spec_factory = SpecFactory(dereferencer)
|
||||
return spec_factory.create(spec_dict, spec_url=spec_url)
|
60
openapi_core/specs.py
Normal file
60
openapi_core/specs.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""OpenAPI core specs module"""
|
||||
import logging
|
||||
from functools import partialmethod
|
||||
|
||||
from openapi_spec_validator import openapi_v3_spec_validator
|
||||
|
||||
from openapi_core.paths import PathsGenerator
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Spec(object):
|
||||
"""Represents an OpenAPI Specification for a service."""
|
||||
|
||||
def __init__(self, servers=None, paths=None):
|
||||
self.servers = servers or []
|
||||
self.paths = paths and dict(paths) or {}
|
||||
|
||||
def __getitem__(self, path_name):
|
||||
return self.paths[path_name]
|
||||
|
||||
def get_server_url(self, index=0):
|
||||
return self.servers[index]['url']
|
||||
|
||||
def get_operation(self, path_pattern, http_method):
|
||||
return self.paths[path_pattern].operations[http_method]
|
||||
|
||||
# operations shortcuts
|
||||
|
||||
get = partialmethod(get_operation, http_method='get')
|
||||
put = partialmethod(get_operation, http_method='put')
|
||||
post = partialmethod(get_operation, http_method='post')
|
||||
delete = partialmethod(get_operation, http_method='delete')
|
||||
options = partialmethod(get_operation, http_method='options')
|
||||
head = partialmethod(get_operation, http_method='head')
|
||||
patch = partialmethod(get_operation, http_method='patch')
|
||||
|
||||
|
||||
class SpecFactory(object):
|
||||
|
||||
def __init__(self, dereferencer, config=None):
|
||||
self.dereferencer = dereferencer
|
||||
self.config = config or {}
|
||||
|
||||
def create(self, spec_dict, spec_url=''):
|
||||
if self.config.get('validate_spec', True):
|
||||
openapi_v3_spec_validator.validate(spec_dict, spec_url=spec_url)
|
||||
|
||||
spec_dict_deref = self.dereferencer.dereference(spec_dict)
|
||||
|
||||
servers = spec_dict_deref.get('servers', [])
|
||||
|
||||
paths = spec_dict_deref.get('paths', [])
|
||||
paths = self._generate_paths(paths)
|
||||
return Spec(servers=servers, paths=list(paths))
|
||||
|
||||
def _generate_paths(self, paths):
|
||||
return PathsGenerator(self.dereferencer).generate(paths)
|
101
openapi_core/wrappers.py
Normal file
101
openapi_core/wrappers.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
"""OpenAPI core wrappers module"""
|
||||
from six import iteritems
|
||||
|
||||
from openapi_core.exceptions import (
|
||||
OpenAPIMappingError, MissingParameterError, InvalidContentTypeError,
|
||||
)
|
||||
|
||||
SPEC_LOCATION_TO_REQUEST_LOCATION_MAPPING = {
|
||||
'path': 'view_args',
|
||||
'query': 'args',
|
||||
'headers': 'headers',
|
||||
'cookies': 'cookies',
|
||||
}
|
||||
|
||||
|
||||
class RequestParameters(dict):
|
||||
|
||||
valid_locations = ['path', 'query', 'headers', 'cookies']
|
||||
|
||||
def __getitem__(self, location):
|
||||
self.validate_location(location)
|
||||
|
||||
return self.setdefault(location, {})
|
||||
|
||||
def __setitem__(self, location, value):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def validate_location(cls, location):
|
||||
if location not in cls.valid_locations:
|
||||
raise OpenAPIMappingError(
|
||||
"Unknown parameter location: {0}".format(str(location)))
|
||||
|
||||
|
||||
class RequestParametersFactory(object):
|
||||
|
||||
def __init__(self, attr_mapping=SPEC_LOCATION_TO_REQUEST_LOCATION_MAPPING):
|
||||
self.attr_mapping = attr_mapping
|
||||
|
||||
def create(self, request, spec):
|
||||
operation = spec.get_operation(request.path_pattern, request.method)
|
||||
|
||||
params = RequestParameters()
|
||||
for param_name, param in iteritems(operation.parameters):
|
||||
try:
|
||||
value = self._unmarshal_param(request, param)
|
||||
except MissingParameterError:
|
||||
if param.required:
|
||||
raise
|
||||
continue
|
||||
|
||||
params[param.location][param_name] = value
|
||||
return params
|
||||
|
||||
def _unmarshal_param(self, request, param):
|
||||
request_location = self.attr_mapping[param.location]
|
||||
request_attr = getattr(request, request_location)
|
||||
|
||||
try:
|
||||
raw_value = request_attr[param.name]
|
||||
except KeyError:
|
||||
raise MissingParameterError(
|
||||
"Missing required `{0}` parameter".format(param.name))
|
||||
|
||||
return param.unmarshal(raw_value)
|
||||
|
||||
|
||||
class RequestBodyFactory(object):
|
||||
|
||||
def create(self, request, spec):
|
||||
operation = spec.get_operation(request.path_pattern, request.method)
|
||||
|
||||
try:
|
||||
media_type = operation.request_body[request.content_type]
|
||||
except KeyError:
|
||||
raise InvalidContentTypeError(
|
||||
"Invalid Content-Type `{0}`".format(request.content_type))
|
||||
|
||||
return media_type.unmarshal(request.data)
|
||||
|
||||
|
||||
class BaseOpenAPIRequest(object):
|
||||
|
||||
path = NotImplemented
|
||||
path_pattern = NotImplemented
|
||||
method = NotImplemented
|
||||
|
||||
args = NotImplemented
|
||||
view_args = NotImplemented
|
||||
headers = NotImplemented
|
||||
cookies = NotImplemented
|
||||
|
||||
data = NotImplemented
|
||||
|
||||
content_type = NotImplemented
|
||||
|
||||
def get_parameters(self, spec):
|
||||
return RequestParametersFactory().create(self, spec)
|
||||
|
||||
def get_body(self, spec):
|
||||
return RequestBodyFactory().create(self, spec)
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
openapi-spec-validator
|
||||
six
|
5
requirements_dev.txt
Normal file
5
requirements_dev.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
mock
|
||||
pytest
|
||||
pytest-pep8
|
||||
pytest-flakes
|
||||
pytest-cov
|
81
setup.py
Normal file
81
setup.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
"""OpenAPI core setup module"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
|
||||
def read_requirements(filename):
|
||||
"""Open a requirements file and return list of its lines."""
|
||||
contents = read_file(filename).strip('\n')
|
||||
return contents.split('\n') if contents else []
|
||||
|
||||
|
||||
def read_file(filename):
|
||||
"""Open and a file, read it and return its contents."""
|
||||
path = os.path.join(os.path.dirname(__file__), filename)
|
||||
with open(path) as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def get_metadata(init_file):
|
||||
"""Read metadata from a given file and return a dictionary of them"""
|
||||
return dict(re.findall("__([a-z]+)__ = '([^']+)'", init_file))
|
||||
|
||||
|
||||
class PyTest(TestCommand):
|
||||
|
||||
"""Command to run unit tests after in-place build."""
|
||||
|
||||
def finalize_options(self):
|
||||
TestCommand.finalize_options(self)
|
||||
self.test_args = [
|
||||
'-sv',
|
||||
'--pep8',
|
||||
'--flakes',
|
||||
'--junitxml', 'reports/junit.xml',
|
||||
'--cov', 'openapi_core',
|
||||
'--cov-report', 'term-missing',
|
||||
'--cov-report', 'xml:reports/coverage.xml',
|
||||
]
|
||||
self.test_suite = True
|
||||
|
||||
def run_tests(self):
|
||||
# Importing here, `cause outside the eggs aren't loaded.
|
||||
import pytest
|
||||
errno = pytest.main(self.test_args)
|
||||
sys.exit(errno)
|
||||
|
||||
|
||||
init_path = os.path.join('openapi_core', '__init__.py')
|
||||
init_py = read_file(init_path)
|
||||
metadata = get_metadata(init_py)
|
||||
|
||||
|
||||
setup(
|
||||
name='openapi-core',
|
||||
version=metadata['version'],
|
||||
author=metadata['author'],
|
||||
author_email=metadata['email'],
|
||||
url=metadata['url'],
|
||||
long_description=read_file("README.rst"),
|
||||
packages=find_packages(include=('openapi_core*',)),
|
||||
include_package_data=True,
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
'Intended Audience :: Developers',
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Operating System :: OS Independent",
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Topic :: Software Development :: Libraries',
|
||||
],
|
||||
install_requires=read_requirements('requirements.txt'),
|
||||
tests_require=read_requirements('requirements_dev.txt'),
|
||||
cmdclass={'test': PyTest},
|
||||
zip_safe=False,
|
||||
)
|
30
tests/integration/conftest.py
Normal file
30
tests/integration/conftest.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from os import path
|
||||
|
||||
import pytest
|
||||
from six.moves.urllib import request
|
||||
from yaml import safe_load
|
||||
|
||||
|
||||
def spec_from_file(spec_file):
|
||||
directory = path.abspath(path.dirname(__file__))
|
||||
path_full = path.join(directory, spec_file)
|
||||
with open(path_full) as fh:
|
||||
return safe_load(fh)
|
||||
|
||||
|
||||
def spec_from_url(spec_url):
|
||||
content = request.urlopen(spec_url)
|
||||
return safe_load(content)
|
||||
|
||||
|
||||
class Factory(dict):
|
||||
__getattr__ = dict.__getitem__
|
||||
__setattr__ = dict.__setitem__
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def factory():
|
||||
return Factory(
|
||||
spec_from_file=spec_from_file,
|
||||
spec_from_url=spec_from_url,
|
||||
)
|
1
tests/integration/data/v3.0/empty.yaml
Normal file
1
tests/integration/data/v3.0/empty.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
openapi: "3.0.0"
|
134
tests/integration/data/v3.0/petstore.yaml
Normal file
134
tests/integration/data/v3.0/petstore.yaml
Normal file
|
@ -0,0 +1,134 @@
|
|||
openapi: "3.0.0"
|
||||
info:
|
||||
version: 1.0.0
|
||||
title: Swagger Petstore
|
||||
license:
|
||||
name: MIT
|
||||
servers:
|
||||
- url: http://petstore.swagger.io/v1
|
||||
paths:
|
||||
/pets:
|
||||
get:
|
||||
summary: List all pets
|
||||
operationId: listPets
|
||||
tags:
|
||||
- pets
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
description: How many items to return at one time (max 100)
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
- name: ids
|
||||
in: query
|
||||
description: Filter pets with Ids
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
format: int32
|
||||
responses:
|
||||
'200':
|
||||
description: An paged array of pets
|
||||
headers:
|
||||
x-next:
|
||||
description: A link to the next page of responses
|
||||
schema:
|
||||
type: string
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pets"
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
post:
|
||||
summary: Create a pet
|
||||
operationId: createPets
|
||||
tags:
|
||||
- pets
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PetCreate'
|
||||
responses:
|
||||
'201':
|
||||
description: Null response
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/pets/{petId}:
|
||||
get:
|
||||
summary: Info for a specific pet
|
||||
operationId: showPetById
|
||||
tags:
|
||||
- pets
|
||||
parameters:
|
||||
- name: petId
|
||||
in: path
|
||||
required: true
|
||||
description: The id of the pet to retrieve
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'200':
|
||||
description: Expected response to a valid request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pets"
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
components:
|
||||
schemas:
|
||||
Pet:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
PetCreate:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
Pets:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Pet"
|
||||
Error:
|
||||
type: object
|
||||
required:
|
||||
- code
|
||||
- message
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
format: int32
|
||||
message:
|
||||
type: string
|
19
tests/integration/test_empty.py
Normal file
19
tests/integration/test_empty.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
import pytest
|
||||
from jsonschema.exceptions import ValidationError
|
||||
|
||||
from openapi_core.shortcuts import create_spec
|
||||
|
||||
|
||||
class TestEmpty(object):
|
||||
|
||||
@pytest.fixture
|
||||
def spec_dict(self, factory):
|
||||
return factory.spec_from_file("data/v3.0/empty.yaml")
|
||||
|
||||
@pytest.fixture
|
||||
def spec(self, spec_dict):
|
||||
return create_spec(spec_dict)
|
||||
|
||||
def test_raises_on_invalid(self, spec_dict):
|
||||
with pytest.raises(ValidationError):
|
||||
create_spec(spec_dict)
|
201
tests/integration/test_petstore.py
Normal file
201
tests/integration/test_petstore.py
Normal file
|
@ -0,0 +1,201 @@
|
|||
import json
|
||||
import pytest
|
||||
from six import iteritems
|
||||
|
||||
from openapi_core.exceptions import (
|
||||
MissingParameterError, InvalidContentTypeError,
|
||||
)
|
||||
from openapi_core.media_types import MediaType
|
||||
from openapi_core.operations import Operation
|
||||
from openapi_core.paths import Path
|
||||
from openapi_core.request_bodies import RequestBody
|
||||
from openapi_core.schemas import Schema
|
||||
from openapi_core.shortcuts import create_spec
|
||||
from openapi_core.wrappers import BaseOpenAPIRequest
|
||||
|
||||
|
||||
class RequestMock(BaseOpenAPIRequest):
|
||||
|
||||
def __init__(
|
||||
self, method, path, path_pattern=None, args=None, view_args=None,
|
||||
headers=None, cookies=None, data=None,
|
||||
content_type='application/json'):
|
||||
self.path = path
|
||||
self.path_pattern = path_pattern or path
|
||||
self.method = method
|
||||
|
||||
self.args = args or {}
|
||||
self.view_args = view_args or {}
|
||||
self.headers = headers or {}
|
||||
self.cookies = cookies or {}
|
||||
self.data = data or ''
|
||||
|
||||
self.content_type = content_type
|
||||
|
||||
|
||||
class TestPetstore(object):
|
||||
|
||||
@pytest.fixture
|
||||
def spec_dict(self, factory):
|
||||
return factory.spec_from_file("data/v3.0/petstore.yaml")
|
||||
|
||||
@pytest.fixture
|
||||
def spec(self, spec_dict):
|
||||
return create_spec(spec_dict)
|
||||
|
||||
def test_spec(self, spec, spec_dict):
|
||||
assert spec.servers == spec_dict['servers']
|
||||
assert spec.get_server_url() == spec_dict['servers'][0]['url']
|
||||
|
||||
for path_name, path in iteritems(spec.paths):
|
||||
assert type(path) == Path
|
||||
assert path.name == path_name
|
||||
|
||||
for http_method, operation in iteritems(path.operations):
|
||||
assert type(operation) == Operation
|
||||
assert operation.path_name == path_name
|
||||
assert operation.http_method == http_method
|
||||
|
||||
operation_spec = spec_dict['paths'][path_name][http_method]
|
||||
request_body_spec = operation_spec.get('requestBody')
|
||||
|
||||
assert bool(request_body_spec) == bool(operation.request_body)
|
||||
|
||||
if not request_body_spec:
|
||||
continue
|
||||
|
||||
assert type(operation.request_body) == RequestBody
|
||||
assert bool(operation.request_body.required) ==\
|
||||
request_body_spec.get('required', False)
|
||||
|
||||
for content_type, media_type in iteritems(
|
||||
operation.request_body.content):
|
||||
assert type(media_type) == MediaType
|
||||
assert media_type.content_type == content_type
|
||||
|
||||
content_spec = request_body_spec['content'][content_type]
|
||||
schema_spec = content_spec.get('schema')
|
||||
assert bool(schema_spec) == bool(media_type.schema)
|
||||
|
||||
if not schema_spec:
|
||||
continue
|
||||
|
||||
# @todo: test with defererence
|
||||
if '$ref' in schema_spec:
|
||||
continue
|
||||
|
||||
assert type(media_type.schema) == Schema
|
||||
assert media_type.schema.type == schema_spec['type']
|
||||
assert media_type.schema.required == schema_spec.get(
|
||||
'required', False)
|
||||
|
||||
def test_get_pets(self, spec):
|
||||
query_params = {
|
||||
'limit': '20',
|
||||
'ids': ['12', '13'],
|
||||
}
|
||||
|
||||
request = RequestMock('get', '/pets', args=query_params)
|
||||
|
||||
parameters = request.get_parameters(spec)
|
||||
|
||||
assert parameters == {
|
||||
'query': {
|
||||
'limit': 20,
|
||||
'ids': [12, 13],
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_pets_raises_missing_required_param(self, spec):
|
||||
request = RequestMock('get', '/pets')
|
||||
|
||||
with pytest.raises(MissingParameterError):
|
||||
request.get_parameters(spec)
|
||||
|
||||
def test_get_pets_failed_to_cast(self, spec):
|
||||
query_params = {
|
||||
'limit': 'non_integer_value',
|
||||
}
|
||||
|
||||
request = RequestMock('get', '/pets', args=query_params)
|
||||
|
||||
parameters = request.get_parameters(spec)
|
||||
|
||||
assert parameters == {
|
||||
'query': {
|
||||
'limit': 'non_integer_value',
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_pets_empty_value(self, spec):
|
||||
query_params = {
|
||||
'limit': '',
|
||||
}
|
||||
|
||||
request = RequestMock('get', '/pets', args=query_params)
|
||||
|
||||
parameters = request.get_parameters(spec)
|
||||
|
||||
assert parameters == {
|
||||
'query': {
|
||||
'limit': None,
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_pets_none_value(self, spec):
|
||||
query_params = {
|
||||
'limit': None,
|
||||
}
|
||||
|
||||
request = RequestMock('get', '/pets', args=query_params)
|
||||
|
||||
parameters = request.get_parameters(spec)
|
||||
|
||||
assert parameters == {
|
||||
'query': {
|
||||
'limit': None,
|
||||
}
|
||||
}
|
||||
|
||||
def test_post_pets(self, spec):
|
||||
data_json = {
|
||||
'name': 'Cat',
|
||||
'tag': 'cats',
|
||||
}
|
||||
data = json.dumps(data_json)
|
||||
|
||||
request = RequestMock('post', '/pets', data=data)
|
||||
|
||||
body = request.get_body(spec)
|
||||
|
||||
assert body == data_json
|
||||
|
||||
def test_post_pets_raises_invalid_content_type(self, spec):
|
||||
data_json = {
|
||||
'name': 'Cat',
|
||||
'tag': 'cats',
|
||||
}
|
||||
data = json.dumps(data_json)
|
||||
|
||||
request = RequestMock(
|
||||
'post', '/pets', data=data, content_type='text/html')
|
||||
|
||||
with pytest.raises(InvalidContentTypeError):
|
||||
request.get_body(spec)
|
||||
|
||||
def test_get_pet(self, spec):
|
||||
view_args = {
|
||||
'petId': '1',
|
||||
}
|
||||
request = RequestMock(
|
||||
'get', '/pets/1', path_pattern='/pets/{petId}',
|
||||
view_args=view_args,
|
||||
)
|
||||
|
||||
parameters = request.get_parameters(spec)
|
||||
|
||||
assert parameters == {
|
||||
'path': {
|
||||
'petId': 1,
|
||||
}
|
||||
}
|
20
tests/unit/test_operations.py
Normal file
20
tests/unit/test_operations.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
import mock
|
||||
import pytest
|
||||
|
||||
from openapi_core.operations import Operation
|
||||
|
||||
|
||||
class TestSchemas(object):
|
||||
|
||||
@pytest.fixture
|
||||
def oepration(self):
|
||||
parameters = {
|
||||
'parameter_1': mock.sentinel.parameter_1,
|
||||
'parameter_2': mock.sentinel.parameter_2,
|
||||
}
|
||||
return Operation('get', '/path', parameters=parameters)
|
||||
|
||||
@property
|
||||
def test_iteritems(self, oepration):
|
||||
for name in oepration.parameters.keys():
|
||||
assert oepration[name] == oepration.parameters[name]
|
21
tests/unit/test_paths.py
Normal file
21
tests/unit/test_paths.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
import mock
|
||||
import pytest
|
||||
|
||||
from openapi_core.paths import Path
|
||||
|
||||
|
||||
class TestPaths(object):
|
||||
|
||||
@pytest.fixture
|
||||
def path(self):
|
||||
operations = {
|
||||
'get': mock.sentinel.get,
|
||||
'post': mock.sentinel.post,
|
||||
}
|
||||
return Path('/path', operations)
|
||||
|
||||
@property
|
||||
def test_iteritems(self, path):
|
||||
for http_method in path.operations.keys():
|
||||
assert path[http_method] ==\
|
||||
path.operations[http_method]
|
21
tests/unit/test_request_bodies.py
Normal file
21
tests/unit/test_request_bodies.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
import mock
|
||||
import pytest
|
||||
|
||||
from openapi_core.request_bodies import RequestBody
|
||||
|
||||
|
||||
class TestRequestBodies(object):
|
||||
|
||||
@pytest.fixture
|
||||
def request_body(self):
|
||||
content = {
|
||||
'application/json': mock.sentinel.application_json,
|
||||
'text/csv': mock.sentinel.text_csv,
|
||||
}
|
||||
return RequestBody(content)
|
||||
|
||||
@property
|
||||
def test_iteritems(self, request_body):
|
||||
for content_type in request_body.content.keys():
|
||||
assert request_body[content_type] ==\
|
||||
request_body.content[content_type]
|
20
tests/unit/test_schemas.py
Normal file
20
tests/unit/test_schemas.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
import mock
|
||||
import pytest
|
||||
|
||||
from openapi_core.schemas import Schema
|
||||
|
||||
|
||||
class TestSchemas(object):
|
||||
|
||||
@pytest.fixture
|
||||
def schema(self):
|
||||
properties = {
|
||||
'application/json': mock.sentinel.application_json,
|
||||
'text/csv': mock.sentinel.text_csv,
|
||||
}
|
||||
return Schema('object', properties=properties)
|
||||
|
||||
@property
|
||||
def test_iteritems(self, schema):
|
||||
for name in schema.properties.keys():
|
||||
assert schema[name] == schema.properties[name]
|
22
tests/unit/test_specs.py
Normal file
22
tests/unit/test_specs.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
import mock
|
||||
import pytest
|
||||
|
||||
from openapi_core.specs import Spec
|
||||
|
||||
|
||||
class TestSpecs(object):
|
||||
|
||||
@pytest.fixture
|
||||
def spec(self):
|
||||
servers = []
|
||||
paths = {
|
||||
'get': mock.sentinel.get,
|
||||
'post': mock.sentinel.post,
|
||||
}
|
||||
return Spec(servers, paths)
|
||||
|
||||
@property
|
||||
def test_iteritems(self, spec):
|
||||
for path_name in spec.paths.keys():
|
||||
assert spec[path_name] ==\
|
||||
spec.paths[path_name]
|
Loading…
Reference in a new issue