diff --git a/openapi_core/exceptions.py b/openapi_core/exceptions.py deleted file mode 100644 index 5ed9356e..00000000 --- a/openapi_core/exceptions.py +++ /dev/null @@ -1,77 +0,0 @@ -"""OpenAPI core exceptions module""" - - -class OpenAPIError(Exception): - pass - - -class OpenAPIMappingError(OpenAPIError): - pass - - -class OpenAPIServerError(OpenAPIMappingError): - pass - - -class OpenAPIOperationError(OpenAPIMappingError): - pass - - -class InvalidValueType(OpenAPIMappingError): - pass - - -class OpenAPIParameterError(OpenAPIMappingError): - pass - - -class OpenAPIBodyError(OpenAPIMappingError): - pass - - -class InvalidServer(OpenAPIServerError): - pass - - -class InvalidOperation(OpenAPIOperationError): - pass - - -class EmptyValue(OpenAPIParameterError): - pass - - -class MissingParameter(OpenAPIParameterError): - pass - - -class InvalidParameterValue(OpenAPIParameterError): - pass - - -class MissingBody(OpenAPIBodyError): - pass - - -class InvalidMediaTypeValue(OpenAPIBodyError): - pass - - -class UndefinedSchemaProperty(OpenAPIBodyError): - pass - - -class MissingProperty(OpenAPIBodyError): - pass - - -class InvalidContentType(OpenAPIBodyError): - pass - - -class InvalidResponse(OpenAPIMappingError): - pass - - -class InvalidValue(OpenAPIMappingError): - pass diff --git a/openapi_core/extensions/__init__.py b/openapi_core/extensions/__init__.py new file mode 100644 index 00000000..5f89115d --- /dev/null +++ b/openapi_core/extensions/__init__.py @@ -0,0 +1 @@ +"""OpenAPI extensions package""" diff --git a/openapi_core/extensions/models/__init__.py b/openapi_core/extensions/models/__init__.py new file mode 100644 index 00000000..68a0561d --- /dev/null +++ b/openapi_core/extensions/models/__init__.py @@ -0,0 +1 @@ +"""OpenAPI X-Model extension package""" diff --git a/openapi_core/extensions/models/factories.py b/openapi_core/extensions/models/factories.py new file mode 100644 index 00000000..0dd348ec --- /dev/null +++ b/openapi_core/extensions/models/factories.py @@ -0,0 +1,12 @@ +"""OpenAPI X-Model extension factories module""" +from openapi_core.extensions.models.models import BaseModel + + +class ModelFactory(object): + + def create(self, properties, name=None): + model = BaseModel + if name is not None: + model = type(name, (BaseModel, ), {}) + + return model(**properties) diff --git a/openapi_core/models.py b/openapi_core/extensions/models/models.py similarity index 58% rename from openapi_core/models.py rename to openapi_core/extensions/models/models.py index 59177097..5fc8f012 100644 --- a/openapi_core/models.py +++ b/openapi_core/extensions/models/models.py @@ -1,8 +1,8 @@ -"""OpenAPI core models module""" +"""OpenAPI X-Model extension models module""" class BaseModel(dict): - """Base class for OpenAPI models.""" + """Base class for OpenAPI X-Model.""" def __getattr__(self, attr_name): """Only search through properties if attribute not found normally. @@ -15,13 +15,3 @@ def __getattr__(self, attr_name): 'type object {0!r} has no attribute {1!r}' .format(type(self).__name__, attr_name) ) - - -class ModelFactory(object): - - def create(self, properties, name=None): - model = BaseModel - if name is not None: - model = type(name, (BaseModel, ), {}) - - return model(**properties) diff --git a/openapi_core/parameters.py b/openapi_core/parameters.py deleted file mode 100644 index cafde800..00000000 --- a/openapi_core/parameters.py +++ /dev/null @@ -1,149 +0,0 @@ -"""OpenAPI core parameters module""" -import logging -import warnings - -from functools import lru_cache -from six import iteritems - -from openapi_core.enums import ParameterLocation, ParameterStyle, SchemaType -from openapi_core.exceptions import ( - EmptyValue, InvalidValueType, InvalidParameterValue, -) - -log = logging.getLogger(__name__) - -PARAMETER_STYLE_DESERIALIZERS = { - ParameterStyle.FORM: lambda x: x.split(','), - ParameterStyle.SIMPLE: lambda x: x.split(','), - ParameterStyle.SPACE_DELIMITED: lambda x: x.split(' '), - ParameterStyle.PIPE_DELIMITED: lambda x: x.split('|'), -} - - -class Parameter(object): - """Represents an OpenAPI operation Parameter.""" - - def __init__( - self, name, location, schema=None, required=False, - deprecated=False, allow_empty_value=False, - items=None, style=None, explode=None): - self.name = name - self.location = ParameterLocation(location) - self.schema = schema - self.required = ( - True if self.location == ParameterLocation.PATH else required - ) - self.deprecated = deprecated - self.allow_empty_value = ( - allow_empty_value if self.location == ParameterLocation.QUERY - else False - ) - self.items = items - self.style = ParameterStyle(style or self.default_style) - self.explode = self.default_explode if explode is None else explode - - @property - def aslist(self): - return ( - self.schema and - self.schema.type in [SchemaType.ARRAY, SchemaType.OBJECT] - ) - - @property - def default_style(self): - simple_locations = [ParameterLocation.PATH, ParameterLocation.HEADER] - return ( - 'simple' if self.location in simple_locations else "form" - ) - - @property - def default_explode(self): - return self.style == ParameterStyle.FORM - - def get_dererializer(self): - return PARAMETER_STYLE_DESERIALIZERS[self.style] - - def deserialize(self, value): - if not self.aslist or self.explode: - return value - - deserializer = self.get_dererializer() - return deserializer(value) - - def unmarshal(self, value): - if self.deprecated: - warnings.warn( - "{0} parameter is deprecated".format(self.name), - DeprecationWarning, - ) - - if (self.location == ParameterLocation.QUERY and value == "" and - not self.allow_empty_value): - raise EmptyValue( - "Value of {0} parameter cannot be empty".format(self.name)) - - if not self.schema: - return value - - deserialized = self.deserialize(value) - - try: - return self.schema.unmarshal(deserialized) - except InvalidValueType as exc: - raise InvalidParameterValue(str(exc)) - - -class ParameterFactory(object): - - def __init__(self, dereferencer, schemas_registry): - self.dereferencer = dereferencer - self.schemas_registry = schemas_registry - - def create(self, parameter_spec, parameter_name=None): - parameter_deref = self.dereferencer.dereference(parameter_spec) - - parameter_name = parameter_name or parameter_deref['name'] - parameter_in = parameter_deref.get('in', 'header') - - allow_empty_value = parameter_deref.get('allowEmptyValue') - required = parameter_deref.get('required', False) - - style = parameter_deref.get('style') - explode = parameter_deref.get('explode') - - schema_spec = parameter_deref.get('schema', None) - schema = None - if schema_spec: - schema, _ = self.schemas_registry.get_or_create(schema_spec) - - return Parameter( - parameter_name, parameter_in, - schema=schema, required=required, - allow_empty_value=allow_empty_value, - style=style, explode=explode, - ) - - -class ParametersGenerator(object): - - def __init__(self, dereferencer, schemas_registry): - self.dereferencer = dereferencer - self.schemas_registry = schemas_registry - - def generate(self, parameters): - for parameter_name, parameter_spec in iteritems(parameters): - parameter = self.parameter_factory.create( - parameter_spec, parameter_name=parameter_name) - - yield (parameter_name, parameter) - - def generate_from_list(self, parameters_list): - for parameter_spec in parameters_list: - parameter = self.parameter_factory.create(parameter_spec) - - yield (parameter.name, parameter) - - @property - @lru_cache() - def parameter_factory(self): - return ParameterFactory(self.dereferencer, self.schemas_registry) diff --git a/openapi_core/schema/__init__.py b/openapi_core/schema/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/schema/components/__init__.py b/openapi_core/schema/components/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/components.py b/openapi_core/schema/components/factories.py similarity index 73% rename from openapi_core/components.py rename to openapi_core/schema/components/factories.py index 39a955a8..8ee7ceb8 100644 --- a/openapi_core/components.py +++ b/openapi_core/schema/components/factories.py @@ -1,18 +1,7 @@ from functools import lru_cache -from openapi_core.schemas import SchemasGenerator - - -class Components(object): - """Represents an OpenAPI Components in a service.""" - - def __init__( - self, schemas=None, responses=None, parameters=None, - request_bodies=None): - self.schemas = schemas and dict(schemas) or {} - self.responses = responses and dict(responses) or {} - self.parameters = parameters and dict(parameters) or {} - self.request_bodies = request_bodies and dict(request_bodies) or {} +from openapi_core.schema.components.models import Components +from openapi_core.schema.schemas.generators import SchemasGenerator class ComponentsFactory(object): diff --git a/openapi_core/schema/components/models.py b/openapi_core/schema/components/models.py new file mode 100644 index 00000000..78c150df --- /dev/null +++ b/openapi_core/schema/components/models.py @@ -0,0 +1,10 @@ +class Components(object): + """Represents an OpenAPI Components in a service.""" + + def __init__( + self, schemas=None, responses=None, parameters=None, + request_bodies=None): + self.schemas = schemas and dict(schemas) or {} + self.responses = responses and dict(responses) or {} + self.parameters = parameters and dict(parameters) or {} + self.request_bodies = request_bodies and dict(request_bodies) or {} diff --git a/openapi_core/schema/exceptions.py b/openapi_core/schema/exceptions.py new file mode 100644 index 00000000..029e48b6 --- /dev/null +++ b/openapi_core/schema/exceptions.py @@ -0,0 +1,9 @@ +"""OpenAPI core schema exceptions module""" + + +class OpenAPIError(Exception): + pass + + +class OpenAPIMappingError(OpenAPIError): + pass diff --git a/openapi_core/schema/infos/__init__.py b/openapi_core/schema/infos/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/infos.py b/openapi_core/schema/infos/factories.py similarity index 72% rename from openapi_core/infos.py rename to openapi_core/schema/infos/factories.py index e92eacd3..339724c1 100644 --- a/openapi_core/infos.py +++ b/openapi_core/schema/infos/factories.py @@ -1,8 +1,5 @@ -class Info(object): - - def __init__(self, title, version): - self.title = title - self.version = version +"""OpenAPI core infos factories module""" +from openapi_core.schema.infos.models import Info class InfoFactory(object): diff --git a/openapi_core/schema/infos/models.py b/openapi_core/schema/infos/models.py new file mode 100644 index 00000000..c124b506 --- /dev/null +++ b/openapi_core/schema/infos/models.py @@ -0,0 +1,8 @@ +"""OpenAPI core infos models module""" + + +class Info(object): + + def __init__(self, title, version): + self.title = title + self.version = version diff --git a/openapi_core/schema/media_types/__init__.py b/openapi_core/schema/media_types/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/schema/media_types/exceptions.py b/openapi_core/schema/media_types/exceptions.py new file mode 100644 index 00000000..293f4383 --- /dev/null +++ b/openapi_core/schema/media_types/exceptions.py @@ -0,0 +1,13 @@ +from openapi_core.schema.exceptions import OpenAPIMappingError + + +class OpenAPIMediaTypeError(OpenAPIMappingError): + pass + + +class InvalidMediaTypeValue(OpenAPIMediaTypeError): + pass + + +class InvalidContentType(OpenAPIMediaTypeError): + pass diff --git a/openapi_core/schema/media_types/generators.py b/openapi_core/schema/media_types/generators.py new file mode 100644 index 00000000..1db858dc --- /dev/null +++ b/openapi_core/schema/media_types/generators.py @@ -0,0 +1,21 @@ +"""OpenAPI core media types generators module""" +from six import iteritems + +from openapi_core.schema.media_types.models import MediaType + + +class MediaTypeGenerator(object): + + def __init__(self, dereferencer, schemas_registry): + self.dereferencer = dereferencer + self.schemas_registry = schemas_registry + + def generate(self, content): + for mimetype, media_type in iteritems(content): + schema_spec = media_type.get('schema') + + schema = None + if schema_spec: + schema, _ = self.schemas_registry.get_or_create(schema_spec) + + yield mimetype, MediaType(mimetype, schema) diff --git a/openapi_core/media_types.py b/openapi_core/schema/media_types/models.py similarity index 60% rename from openapi_core/media_types.py rename to openapi_core/schema/media_types/models.py index 5c528346..194d0050 100644 --- a/openapi_core/media_types.py +++ b/openapi_core/schema/media_types/models.py @@ -1,10 +1,10 @@ -"""OpenAPI core mediaTypes module""" +"""OpenAPI core media types models module""" from collections import defaultdict from json import loads -from six import iteritems -from openapi_core.exceptions import InvalidValueType, InvalidMediaTypeValue +from openapi_core.schema.media_types.exceptions import InvalidMediaTypeValue +from openapi_core.schema.schemas.exceptions import InvalidSchemaValue MEDIA_TYPE_DESERIALIZERS = { @@ -42,22 +42,5 @@ def unmarshal(self, value): try: return self.schema.unmarshal(deserialized) - except InvalidValueType as exc: + except InvalidSchemaValue as exc: raise InvalidMediaTypeValue(str(exc)) - - -class MediaTypeGenerator(object): - - def __init__(self, dereferencer, schemas_registry): - self.dereferencer = dereferencer - self.schemas_registry = schemas_registry - - def generate(self, content): - for mimetype, media_type in iteritems(content): - schema_spec = media_type.get('schema') - - schema = None - if schema_spec: - schema, _ = self.schemas_registry.get_or_create(schema_spec) - - yield mimetype, MediaType(mimetype, schema) diff --git a/openapi_core/schema/operations/__init__.py b/openapi_core/schema/operations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/schema/operations/exceptions.py b/openapi_core/schema/operations/exceptions.py new file mode 100644 index 00000000..b49d1896 --- /dev/null +++ b/openapi_core/schema/operations/exceptions.py @@ -0,0 +1,9 @@ +from openapi_core.schema.exceptions import OpenAPIMappingError + + +class OpenAPIOperationError(OpenAPIMappingError): + pass + + +class InvalidOperation(OpenAPIOperationError): + pass diff --git a/openapi_core/operations.py b/openapi_core/schema/operations/generators.py similarity index 58% rename from openapi_core/operations.py rename to openapi_core/schema/operations/generators.py index bf9d520b..025f08e9 100644 --- a/openapi_core/operations.py +++ b/openapi_core/schema/operations/generators.py @@ -1,50 +1,14 @@ # -*- coding: utf-8 -*- -"""OpenAPI core operations module""" -import logging +"""OpenAPI core operations models module""" from functools import lru_cache from six import iteritems from openapi_spec_validator.validators import PathItemValidator -from openapi_core.exceptions import InvalidResponse -from openapi_core.parameters import ParametersGenerator -from openapi_core.request_bodies import RequestBodyFactory -from openapi_core.responses import ResponsesGenerator - -log = logging.getLogger(__name__) - - -class Operation(object): - """Represents an OpenAPI Operation.""" - - def __init__( - self, http_method, path_name, responses, parameters, - request_body=None, deprecated=False, operation_id=None): - self.http_method = http_method - self.path_name = path_name - self.responses = dict(responses) - 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] - - def get_response(self, http_status='default'): - try: - return self.responses[http_status] - except KeyError: - # try range - http_status_range = '{0}XX'.format(http_status[0]) - if http_status_range in self.responses: - return self.responses[http_status_range] - - if 'default' not in self.responses: - raise InvalidResponse( - "Unknown response http status {0}".format(http_status)) - - return self.responses['default'] +from openapi_core.schema.operations.models import Operation +from openapi_core.schema.parameters.generators import ParametersGenerator +from openapi_core.schema.request_bodies.factories import RequestBodyFactory +from openapi_core.schema.responses.generators import ResponsesGenerator class OperationsGenerator(object): diff --git a/openapi_core/schema/operations/models.py b/openapi_core/schema/operations/models.py new file mode 100644 index 00000000..27edc433 --- /dev/null +++ b/openapi_core/schema/operations/models.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +"""OpenAPI core operations models module""" +from openapi_core.schema.responses.exceptions import InvalidResponse + + +class Operation(object): + """Represents an OpenAPI Operation.""" + + def __init__( + self, http_method, path_name, responses, parameters, + request_body=None, deprecated=False, operation_id=None): + self.http_method = http_method + self.path_name = path_name + self.responses = dict(responses) + 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] + + def get_response(self, http_status='default'): + # @todo: move to Responses object + try: + return self.responses[http_status] + except KeyError: + # try range + http_status_range = '{0}XX'.format(http_status[0]) + if http_status_range in self.responses: + return self.responses[http_status_range] + + if 'default' not in self.responses: + raise InvalidResponse( + "Unknown response http status {0}".format(http_status)) + + return self.responses['default'] diff --git a/openapi_core/schema/parameters/__init__.py b/openapi_core/schema/parameters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/enums.py b/openapi_core/schema/parameters/enums.py similarity index 53% rename from openapi_core/enums.py rename to openapi_core/schema/parameters/enums.py index 05ff58f6..51fa238f 100644 --- a/openapi_core/enums.py +++ b/openapi_core/schema/parameters/enums.py @@ -1,3 +1,4 @@ +"""OpenAPI core parameters enums module""" from enum import Enum @@ -22,27 +23,3 @@ class ParameterStyle(Enum): SPACE_DELIMITED = 'spaceDelimited' PIPE_DELIMITED = 'pipeDelimited' DEEP_OBJECT = 'deepObject' - - -class SchemaType(Enum): - - INTEGER = 'integer' - NUMBER = 'number' - STRING = 'string' - BOOLEAN = 'boolean' - ARRAY = 'array' - OBJECT = 'object' - - -class SchemaFormat(Enum): - - NONE = None - INT32 = 'int32' - INT64 = 'int64' - FLOAT = 'float' - DOUBLE = 'double' - BYTE = 'byte' - BINARY = 'binary' - DATE = 'date' - DATETIME = 'date-time' - PASSWORD = 'password' diff --git a/openapi_core/schema/parameters/exceptions.py b/openapi_core/schema/parameters/exceptions.py new file mode 100644 index 00000000..7fa76e09 --- /dev/null +++ b/openapi_core/schema/parameters/exceptions.py @@ -0,0 +1,21 @@ +from openapi_core.schema.exceptions import OpenAPIMappingError + + +class OpenAPIParameterError(OpenAPIMappingError): + pass + + +class MissingParameter(OpenAPIParameterError): + pass + + +class MissingRequiredParameter(OpenAPIParameterError): + pass + + +class EmptyParameterValue(OpenAPIParameterError): + pass + + +class InvalidParameterValue(OpenAPIParameterError): + pass diff --git a/openapi_core/schema/parameters/factories.py b/openapi_core/schema/parameters/factories.py new file mode 100644 index 00000000..2e13cebc --- /dev/null +++ b/openapi_core/schema/parameters/factories.py @@ -0,0 +1,33 @@ +"""OpenAPI core parameters factories module""" +from openapi_core.schema.parameters.models import Parameter + + +class ParameterFactory(object): + + def __init__(self, dereferencer, schemas_registry): + self.dereferencer = dereferencer + self.schemas_registry = schemas_registry + + def create(self, parameter_spec, parameter_name=None): + parameter_deref = self.dereferencer.dereference(parameter_spec) + + parameter_name = parameter_name or parameter_deref['name'] + parameter_in = parameter_deref.get('in', 'header') + + allow_empty_value = parameter_deref.get('allowEmptyValue') + required = parameter_deref.get('required', False) + + style = parameter_deref.get('style') + explode = parameter_deref.get('explode') + + schema_spec = parameter_deref.get('schema', None) + schema = None + if schema_spec: + schema, _ = self.schemas_registry.get_or_create(schema_spec) + + return Parameter( + parameter_name, parameter_in, + schema=schema, required=required, + allow_empty_value=allow_empty_value, + style=style, explode=explode, + ) diff --git a/openapi_core/schema/parameters/generators.py b/openapi_core/schema/parameters/generators.py new file mode 100644 index 00000000..bc7f0da3 --- /dev/null +++ b/openapi_core/schema/parameters/generators.py @@ -0,0 +1,31 @@ +"""OpenAPI core parameters generators module""" +from functools import lru_cache + +from six import iteritems + +from openapi_core.schema.parameters.factories import ParameterFactory + + +class ParametersGenerator(object): + + def __init__(self, dereferencer, schemas_registry): + self.dereferencer = dereferencer + self.schemas_registry = schemas_registry + + def generate(self, parameters): + for parameter_name, parameter_spec in iteritems(parameters): + parameter = self.parameter_factory.create( + parameter_spec, parameter_name=parameter_name) + + yield (parameter_name, parameter) + + def generate_from_list(self, parameters_list): + for parameter_spec in parameters_list: + parameter = self.parameter_factory.create(parameter_spec) + + yield (parameter.name, parameter) + + @property + @lru_cache() + def parameter_factory(self): + return ParameterFactory(self.dereferencer, self.schemas_registry) diff --git a/openapi_core/schema/parameters/models.py b/openapi_core/schema/parameters/models.py new file mode 100644 index 00000000..62a3a39a --- /dev/null +++ b/openapi_core/schema/parameters/models.py @@ -0,0 +1,116 @@ +"""OpenAPI core parameters models module""" +import logging +import warnings + +from openapi_core.schema.parameters.enums import ( + ParameterLocation, ParameterStyle, +) +from openapi_core.schema.parameters.exceptions import ( + MissingRequiredParameter, MissingParameter, InvalidParameterValue, + EmptyParameterValue, +) +from openapi_core.schema.schemas.enums import SchemaType +from openapi_core.schema.schemas.exceptions import InvalidSchemaValue + +log = logging.getLogger(__name__) + + +class Parameter(object): + """Represents an OpenAPI operation Parameter.""" + + PARAMETER_STYLE_DESERIALIZERS = { + ParameterStyle.FORM: lambda x: x.split(','), + ParameterStyle.SIMPLE: lambda x: x.split(','), + ParameterStyle.SPACE_DELIMITED: lambda x: x.split(' '), + ParameterStyle.PIPE_DELIMITED: lambda x: x.split('|'), + } + + def __init__( + self, name, location, schema=None, required=False, + deprecated=False, allow_empty_value=False, + items=None, style=None, explode=None): + self.name = name + self.location = ParameterLocation(location) + self.schema = schema + self.required = ( + True if self.location == ParameterLocation.PATH else required + ) + self.deprecated = deprecated + self.allow_empty_value = ( + allow_empty_value if self.location == ParameterLocation.QUERY + else False + ) + self.items = items + self.style = ParameterStyle(style or self.default_style) + self.explode = self.default_explode if explode is None else explode + + @property + def aslist(self): + return ( + self.schema and + self.schema.type in [SchemaType.ARRAY, SchemaType.OBJECT] + ) + + @property + def default_style(self): + simple_locations = [ParameterLocation.PATH, ParameterLocation.HEADER] + return ( + 'simple' if self.location in simple_locations else "form" + ) + + @property + def default_explode(self): + return self.style == ParameterStyle.FORM + + def get_dererializer(self): + return self.PARAMETER_STYLE_DESERIALIZERS[self.style] + + def deserialize(self, value): + if not self.aslist or self.explode: + return value + + deserializer = self.get_dererializer() + return deserializer(value) + + def get_value(self, request): + location = request.parameters[self.location.value] + + try: + raw = location[self.name] + except KeyError: + if self.required: + raise MissingRequiredParameter( + "Missing required `{0}` parameter".format(self.name)) + + if not self.schema or self.schema.default is None: + raise MissingParameter( + "Missing `{0}` parameter".format(self.name)) + + raw = self.schema.default + + if self.aslist and self.explode: + return location.getlist(self.name) + + return raw + + def unmarshal(self, value): + if self.deprecated: + warnings.warn( + "{0} parameter is deprecated".format(self.name), + DeprecationWarning, + ) + + if (self.location == ParameterLocation.QUERY and value == "" and + not self.allow_empty_value): + raise EmptyParameterValue( + "Value of {0} parameter cannot be empty".format(self.name)) + + if not self.schema: + return value + + deserialized = self.deserialize(value) + + try: + return self.schema.unmarshal(deserialized) + except InvalidSchemaValue as exc: + raise InvalidParameterValue(str(exc)) diff --git a/openapi_core/schema/paths/__init__.py b/openapi_core/schema/paths/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/paths.py b/openapi_core/schema/paths/generators.py similarity index 66% rename from openapi_core/paths.py rename to openapi_core/schema/paths/generators.py index 97ad8582..71c83a24 100644 --- a/openapi_core/paths.py +++ b/openapi_core/schema/paths/generators.py @@ -1,20 +1,10 @@ -"""OpenAPI core paths module""" +"""OpenAPI core paths generators module""" from functools import lru_cache 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] +from openapi_core.schema.operations.generators import OperationsGenerator +from openapi_core.schema.paths.models import Path class PathsGenerator(object): diff --git a/openapi_core/schema/paths/models.py b/openapi_core/schema/paths/models.py new file mode 100644 index 00000000..c4880a61 --- /dev/null +++ b/openapi_core/schema/paths/models.py @@ -0,0 +1,12 @@ +"""OpenAPI core paths models module""" + + +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] diff --git a/openapi_core/schema/properties/__init__.py b/openapi_core/schema/properties/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/schema/properties/generators.py b/openapi_core/schema/properties/generators.py new file mode 100644 index 00000000..4e3d7027 --- /dev/null +++ b/openapi_core/schema/properties/generators.py @@ -0,0 +1,17 @@ +"""OpenAPI core properties generators module""" +from six import iteritems + + +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): + from openapi_core.schema.schemas.factories import SchemaFactory + return SchemaFactory(self.dereferencer).create(schema_spec) diff --git a/openapi_core/schema/request_bodies/__init__.py b/openapi_core/schema/request_bodies/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/schema/request_bodies/exceptions.py b/openapi_core/schema/request_bodies/exceptions.py new file mode 100644 index 00000000..0e07998a --- /dev/null +++ b/openapi_core/schema/request_bodies/exceptions.py @@ -0,0 +1,9 @@ +from openapi_core.schema.exceptions import OpenAPIMappingError + + +class OpenAPIRequestBodyError(OpenAPIMappingError): + pass + + +class MissingRequestBody(OpenAPIRequestBodyError): + pass diff --git a/openapi_core/request_bodies.py b/openapi_core/schema/request_bodies/factories.py similarity index 56% rename from openapi_core/request_bodies.py rename to openapi_core/schema/request_bodies/factories.py index 96366887..63d387eb 100644 --- a/openapi_core/request_bodies.py +++ b/openapi_core/schema/request_bodies/factories.py @@ -1,23 +1,8 @@ -"""OpenAPI core requestBodies module""" +"""OpenAPI core request bodies factories module""" from functools import lru_cache -from openapi_core.exceptions import InvalidContentType -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, mimetype): - try: - return self.content[mimetype] - except KeyError: - raise InvalidContentType( - "Invalid mime type `{0}`".format(mimetype)) +from openapi_core.schema.media_types.generators import MediaTypeGenerator +from openapi_core.schema.request_bodies.models import RequestBody class RequestBodyFactory(object): diff --git a/openapi_core/schema/request_bodies/models.py b/openapi_core/schema/request_bodies/models.py new file mode 100644 index 00000000..9aff295f --- /dev/null +++ b/openapi_core/schema/request_bodies/models.py @@ -0,0 +1,25 @@ +"""OpenAPI core request bodies models module""" + +from openapi_core.schema.media_types.exceptions import InvalidContentType +from openapi_core.schema.request_bodies.exceptions import MissingRequestBody + + +class RequestBody(object): + """Represents an OpenAPI RequestBody.""" + + def __init__(self, content, required=False): + self.content = dict(content) + self.required = required + + def __getitem__(self, mimetype): + try: + return self.content[mimetype] + except KeyError: + raise InvalidContentType( + "Invalid mime type `{0}`".format(mimetype)) + + def get_value(self, request): + if not request.body and self.required: + raise MissingRequestBody("Missing required request body") + + return request.body diff --git a/openapi_core/schema/responses/__init__.py b/openapi_core/schema/responses/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/schema/responses/exceptions.py b/openapi_core/schema/responses/exceptions.py new file mode 100644 index 00000000..01ba3739 --- /dev/null +++ b/openapi_core/schema/responses/exceptions.py @@ -0,0 +1,13 @@ +from openapi_core.schema.exceptions import OpenAPIMappingError + + +class OpenAPIResponseError(OpenAPIMappingError): + pass + + +class InvalidResponse(OpenAPIResponseError): + pass + + +class MissingResponseContent(OpenAPIResponseError): + pass diff --git a/openapi_core/responses.py b/openapi_core/schema/responses/generators.py similarity index 61% rename from openapi_core/responses.py rename to openapi_core/schema/responses/generators.py index 5fbaff20..b6a410f7 100644 --- a/openapi_core/responses.py +++ b/openapi_core/schema/responses/generators.py @@ -1,30 +1,11 @@ -"""OpenAPI core responses module""" +"""OpenAPI core responses generators module""" from functools import lru_cache from six import iteritems -from openapi_core.exceptions import InvalidContentType -from openapi_core.media_types import MediaTypeGenerator -from openapi_core.parameters import ParametersGenerator - - -class Response(object): - - def __init__( - self, http_status, description, headers=None, content=None, - links=None): - self.http_status = http_status - self.description = description - self.headers = headers and dict(headers) or {} - self.content = content and dict(content) or {} - self.links = links and dict(links) or {} - - def __getitem__(self, mimetype): - try: - return self.content[mimetype] - except KeyError: - raise InvalidContentType( - "Invalid mime type `{0}`".format(mimetype)) +from openapi_core.schema.media_types.generators import MediaTypeGenerator +from openapi_core.schema.parameters.generators import ParametersGenerator +from openapi_core.schema.responses.models import Response class ResponsesGenerator(object): diff --git a/openapi_core/schema/responses/models.py b/openapi_core/schema/responses/models.py new file mode 100644 index 00000000..daf5b7e6 --- /dev/null +++ b/openapi_core/schema/responses/models.py @@ -0,0 +1,28 @@ +"""OpenAPI core responses models module""" +from openapi_core.schema.media_types.exceptions import InvalidContentType +from openapi_core.schema.responses.exceptions import MissingResponseContent + + +class Response(object): + + def __init__( + self, http_status, description, headers=None, content=None, + links=None): + self.http_status = http_status + self.description = description + self.headers = headers and dict(headers) or {} + self.content = content and dict(content) or {} + self.links = links and dict(links) or {} + + def __getitem__(self, mimetype): + try: + return self.content[mimetype] + except KeyError: + raise InvalidContentType( + "Invalid mime type `{0}`".format(mimetype)) + + def get_value(self, response): + if not response.data: + raise MissingResponseContent("Missing response content") + + return response.data diff --git a/openapi_core/schema/schemas/__init__.py b/openapi_core/schema/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/schema/schemas/enums.py b/openapi_core/schema/schemas/enums.py new file mode 100644 index 00000000..3ba8b881 --- /dev/null +++ b/openapi_core/schema/schemas/enums.py @@ -0,0 +1,26 @@ +"""OpenAPI core schemas enums module""" +from enum import Enum + + +class SchemaType(Enum): + + INTEGER = 'integer' + NUMBER = 'number' + STRING = 'string' + BOOLEAN = 'boolean' + ARRAY = 'array' + OBJECT = 'object' + + +class SchemaFormat(Enum): + + NONE = None + INT32 = 'int32' + INT64 = 'int64' + FLOAT = 'float' + DOUBLE = 'double' + BYTE = 'byte' + BINARY = 'binary' + DATE = 'date' + DATETIME = 'date-time' + PASSWORD = 'password' diff --git a/openapi_core/schema/schemas/exceptions.py b/openapi_core/schema/schemas/exceptions.py new file mode 100644 index 00000000..0f777d22 --- /dev/null +++ b/openapi_core/schema/schemas/exceptions.py @@ -0,0 +1,17 @@ +from openapi_core.schema.exceptions import OpenAPIMappingError + + +class OpenAPISchemaError(OpenAPIMappingError): + pass + + +class InvalidSchemaValue(OpenAPISchemaError): + pass + + +class UndefinedSchemaProperty(OpenAPISchemaError): + pass + + +class MissingSchemaProperty(OpenAPISchemaError): + pass diff --git a/openapi_core/schema/schemas/factories.py b/openapi_core/schema/schemas/factories.py new file mode 100644 index 00000000..efc4d171 --- /dev/null +++ b/openapi_core/schema/schemas/factories.py @@ -0,0 +1,56 @@ +"""OpenAPI core schemas factories module""" +import logging +from functools import lru_cache + +from openapi_core.schema.properties.generators import PropertiesGenerator +from openapi_core.schema.schemas.models import Schema + +log = logging.getLogger(__name__) + + +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.get('type', 'object') + schema_format = schema_deref.get('format') + model = schema_deref.get('x-model', None) + required = schema_deref.get('required', False) + default = schema_deref.get('default', None) + properties_spec = schema_deref.get('properties', None) + items_spec = schema_deref.get('items', None) + nullable = schema_deref.get('nullable', False) + enum = schema_deref.get('enum', None) + deprecated = schema_deref.get('deprecated', False) + all_of_spec = schema_deref.get('allOf', None) + + properties = None + if properties_spec: + properties = self.properties_generator.generate(properties_spec) + + all_of = [] + if all_of_spec: + all_of = map(self.create, all_of_spec) + + items = None + if items_spec: + items = self._create_items(items_spec) + + return Schema( + schema_type=schema_type, model=model, properties=properties, + items=items, schema_format=schema_format, required=required, + default=default, nullable=nullable, enum=enum, + deprecated=deprecated, all_of=all_of, + ) + + @property + @lru_cache() + def properties_generator(self): + return PropertiesGenerator(self.dereferencer) + + def _create_items(self, items_spec): + return self.create(items_spec) diff --git a/openapi_core/schema/schemas/generators.py b/openapi_core/schema/schemas/generators.py new file mode 100644 index 00000000..59fd548b --- /dev/null +++ b/openapi_core/schema/schemas/generators.py @@ -0,0 +1,20 @@ +"""OpenAPI core schemas generators module""" +import logging + +from six import iteritems + +log = logging.getLogger(__name__) + + +class SchemasGenerator(object): + + def __init__(self, dereferencer, schemas_registry): + self.dereferencer = dereferencer + self.schemas_registry = schemas_registry + + def generate(self, schemas_spec): + schemas_deref = self.dereferencer.dereference(schemas_spec) + + for schema_name, schema_spec in iteritems(schemas_deref): + schema, _ = self.schemas_registry.get_or_create(schema_spec) + yield schema_name, schema diff --git a/openapi_core/schemas.py b/openapi_core/schema/schemas/models.py similarity index 50% rename from openapi_core/schemas.py rename to openapi_core/schema/schemas/models.py index a96c7057..cee2545d 100644 --- a/openapi_core/schemas.py +++ b/openapi_core/schema/schemas/models.py @@ -1,39 +1,29 @@ -"""OpenAPI core schemas module""" +"""OpenAPI core schemas models module""" import logging from collections import defaultdict import warnings -from distutils.util import strtobool -from functools import lru_cache - from six import iteritems -from openapi_core.enums import SchemaType, SchemaFormat -from openapi_core.exceptions import ( - InvalidValueType, UndefinedSchemaProperty, MissingProperty, InvalidValue, +from openapi_core.extensions.models.factories import ModelFactory +from openapi_core.schema.schemas.enums import SchemaType, SchemaFormat +from openapi_core.schema.schemas.exceptions import ( + InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty, ) -from openapi_core.models import ModelFactory +from openapi_core.schema.schemas.util import forcebool log = logging.getLogger(__name__) -def forcebool(val): - if isinstance(val, str): - val = strtobool(val) - - return bool(val) - - -DEFAULT_CAST_CALLABLE_GETTER = { - SchemaType.INTEGER: int, - SchemaType.NUMBER: float, - SchemaType.BOOLEAN: forcebool, -} - - class Schema(object): """Represents an OpenAPI Schema.""" + DEFAULT_CAST_CALLABLE_GETTER = { + SchemaType.INTEGER: int, + SchemaType.NUMBER: float, + SchemaType.BOOLEAN: forcebool, + } + def __init__( self, schema_type=None, model=None, properties=None, items=None, schema_format=None, required=None, default=None, nullable=False, @@ -72,7 +62,7 @@ def get_all_required_properties(self): return required def get_cast_mapping(self): - mapping = DEFAULT_CAST_CALLABLE_GETTER.copy() + mapping = self.DEFAULT_CAST_CALLABLE_GETTER.copy() mapping.update({ SchemaType.ARRAY: self._unmarshal_collection, SchemaType.OBJECT: self._unmarshal_object, @@ -84,7 +74,7 @@ def cast(self, value): """Cast value to schema type""" if value is None: if not self.nullable: - raise InvalidValueType("Null value for non-nullable schema") + raise InvalidSchemaValue("Null value for non-nullable schema") return self.default if self.type is None: @@ -99,7 +89,7 @@ def cast(self, value): try: return cast_callable(value) except ValueError: - raise InvalidValueType( + raise InvalidSchemaValue( "Failed to cast value of {0} to {1}".format(value, self.type) ) @@ -114,7 +104,7 @@ def unmarshal(self, value): return None if self.enum and casted not in self.enum: - raise InvalidValue( + raise InvalidSchemaValue( "Value of {0} not in enum choices: {1}".format( value, self.enum) ) @@ -126,7 +116,8 @@ def _unmarshal_collection(self, value): def _unmarshal_object(self, value): if not isinstance(value, (dict, )): - raise InvalidValueType("Value of {0} not an object".format(value)) + raise InvalidSchemaValue( + "Value of {0} not an object".format(value)) all_properties = self.get_all_properties() all_required_properties = self.get_all_required_properties() @@ -145,102 +136,10 @@ def _unmarshal_object(self, value): prop_value = value[prop_name] except KeyError: if prop_name in all_required_properties: - raise MissingProperty( + raise MissingSchemaProperty( "Missing schema property {0}".format(prop_name)) if not prop.nullable and not prop.default: continue prop_value = prop.default properties[prop_name] = prop.unmarshal(prop_value) return ModelFactory().create(properties, name=self.model) - - -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.get('type', 'object') - schema_format = schema_deref.get('format') - model = schema_deref.get('x-model', None) - required = schema_deref.get('required', False) - default = schema_deref.get('default', None) - properties_spec = schema_deref.get('properties', None) - items_spec = schema_deref.get('items', None) - nullable = schema_deref.get('nullable', False) - enum = schema_deref.get('enum', None) - deprecated = schema_deref.get('deprecated', False) - all_of_spec = schema_deref.get('allOf', None) - - properties = None - if properties_spec: - properties = self.properties_generator.generate(properties_spec) - - all_of = [] - if all_of_spec: - all_of = map(self.create, all_of_spec) - - items = None - if items_spec: - items = self._create_items(items_spec) - - return Schema( - schema_type=schema_type, model=model, properties=properties, - items=items, schema_format=schema_format, required=required, - default=default, nullable=nullable, enum=enum, - deprecated=deprecated, all_of=all_of, - ) - - @property - @lru_cache() - def properties_generator(self): - return PropertiesGenerator(self.dereferencer) - - def _create_items(self, items_spec): - return self.create(items_spec) - - -class SchemaRegistry(SchemaFactory): - - def __init__(self, dereferencer): - super(SchemaRegistry, self).__init__(dereferencer) - self._schemas = {} - - def get_or_create(self, schema_spec): - schema_deref = self.dereferencer.dereference(schema_spec) - model = schema_deref.get('x-model', None) - - if model and model in self._schemas: - return self._schemas[model], False - - return self.create(schema_deref), True - - -class SchemasGenerator(object): - - def __init__(self, dereferencer, schemas_registry): - self.dereferencer = dereferencer - self.schemas_registry = schemas_registry - - def generate(self, schemas_spec): - schemas_deref = self.dereferencer.dereference(schemas_spec) - - for schema_name, schema_spec in iteritems(schemas_deref): - schema, _ = self.schemas_registry.get_or_create(schema_spec) - yield schema_name, schema diff --git a/openapi_core/schema/schemas/registries.py b/openapi_core/schema/schemas/registries.py new file mode 100644 index 00000000..ad62cdb5 --- /dev/null +++ b/openapi_core/schema/schemas/registries.py @@ -0,0 +1,22 @@ +"""OpenAPI core schemas registries module""" +import logging + +from openapi_core.schema.schemas.factories import SchemaFactory + +log = logging.getLogger(__name__) + + +class SchemaRegistry(SchemaFactory): + + def __init__(self, dereferencer): + super(SchemaRegistry, self).__init__(dereferencer) + self._schemas = {} + + def get_or_create(self, schema_spec): + schema_deref = self.dereferencer.dereference(schema_spec) + model = schema_deref.get('x-model', None) + + if model and model in self._schemas: + return self._schemas[model], False + + return self.create(schema_deref), True diff --git a/openapi_core/schema/schemas/util.py b/openapi_core/schema/schemas/util.py new file mode 100644 index 00000000..cf2c9173 --- /dev/null +++ b/openapi_core/schema/schemas/util.py @@ -0,0 +1,9 @@ +"""OpenAPI core schemas util module""" +from distutils.util import strtobool + + +def forcebool(val): + if isinstance(val, str): + val = strtobool(val) + + return bool(val) diff --git a/openapi_core/schema/servers/__init__.py b/openapi_core/schema/servers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/schema/servers/exceptions.py b/openapi_core/schema/servers/exceptions.py new file mode 100644 index 00000000..fbce7782 --- /dev/null +++ b/openapi_core/schema/servers/exceptions.py @@ -0,0 +1,9 @@ +from openapi_core.schema.exceptions import OpenAPIMappingError + + +class OpenAPIServerError(OpenAPIMappingError): + pass + + +class InvalidServer(OpenAPIServerError): + pass diff --git a/openapi_core/servers.py b/openapi_core/schema/servers/generators.py similarity index 63% rename from openapi_core/servers.py rename to openapi_core/schema/servers/generators.py index 91b3508a..86b079a1 100644 --- a/openapi_core/servers.py +++ b/openapi_core/schema/servers/generators.py @@ -1,37 +1,9 @@ +"""OpenAPI core servers generators module""" from functools import lru_cache from six import iteritems - -class Server(object): - - def __init__(self, url, variables=None): - self.url = url - self.variables = variables and dict(variables) or {} - - @property - def default_url(self): - return self.get_url() - - @property - def default_variables(self): - defaults = {} - for name, variable in iteritems(self.variables): - defaults[name] = variable.default - return defaults - - def get_url(self, **variables): - if not variables: - variables = self.default_variables - return self.url.format(**variables) - - -class ServerVariable(object): - - def __init__(self, name, default, enum=None): - self.name = name - self.default = default - self.enum = enum and list(enum) or [] +from openapi_core.schema.servers.models import Server, ServerVariable class ServersGenerator(object): diff --git a/openapi_core/schema/servers/models.py b/openapi_core/schema/servers/models.py new file mode 100644 index 00000000..f5754c4d --- /dev/null +++ b/openapi_core/schema/servers/models.py @@ -0,0 +1,33 @@ +"""OpenAPI core servers models module""" +from six import iteritems + + +class Server(object): + + def __init__(self, url, variables=None): + self.url = url + self.variables = variables and dict(variables) or {} + + @property + def default_url(self): + return self.get_url() + + @property + def default_variables(self): + defaults = {} + for name, variable in iteritems(self.variables): + defaults[name] = variable.default + return defaults + + def get_url(self, **variables): + if not variables: + variables = self.default_variables + return self.url.format(**variables) + + +class ServerVariable(object): + + def __init__(self, name, default, enum=None): + self.name = name + self.default = default + self.enum = enum and list(enum) or [] diff --git a/openapi_core/schema/specs/__init__.py b/openapi_core/schema/specs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/schema/specs/factories.py b/openapi_core/schema/specs/factories.py new file mode 100644 index 00000000..06868f62 --- /dev/null +++ b/openapi_core/schema/specs/factories.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +"""OpenAPI core specs factories module""" +from functools import lru_cache + +from openapi_spec_validator import openapi_v3_spec_validator + +from openapi_core.schema.components.factories import ComponentsFactory +from openapi_core.schema.infos.factories import InfoFactory +from openapi_core.schema.paths.generators import PathsGenerator +from openapi_core.schema.schemas.registries import SchemaRegistry +from openapi_core.schema.servers.generators import ServersGenerator +from openapi_core.schema.specs.models import Spec + + +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) + + info_spec = spec_dict_deref.get('info', {}) + servers_spec = spec_dict_deref.get('servers', []) + paths = spec_dict_deref.get('paths', {}) + components_spec = spec_dict_deref.get('components', {}) + + info = self.info_factory.create(info_spec) + servers = self.servers_generator.generate(servers_spec) + paths = self.paths_generator.generate(paths) + components = self.components_factory.create(components_spec) + spec = Spec( + info, list(paths), servers=list(servers), components=components) + return spec + + @property + @lru_cache() + def schemas_registry(self): + return SchemaRegistry(self.dereferencer) + + @property + @lru_cache() + def info_factory(self): + return InfoFactory(self.dereferencer) + + @property + @lru_cache() + def servers_generator(self): + return ServersGenerator(self.dereferencer) + + @property + @lru_cache() + def paths_generator(self): + return PathsGenerator(self.dereferencer, self.schemas_registry) + + @property + @lru_cache() + def components_factory(self): + return ComponentsFactory(self.dereferencer, self.schemas_registry) diff --git a/openapi_core/schema/specs/models.py b/openapi_core/schema/specs/models.py new file mode 100644 index 00000000..79a27e88 --- /dev/null +++ b/openapi_core/schema/specs/models.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +"""OpenAPI core specs models module""" +import logging +from functools import partialmethod + +from openapi_core.schema.operations.exceptions import InvalidOperation +from openapi_core.schema.servers.exceptions import InvalidServer + + +log = logging.getLogger(__name__) + + +class Spec(object): + """Represents an OpenAPI Specification for a service.""" + + def __init__(self, info, paths, servers=None, components=None): + self.info = info + self.paths = paths and dict(paths) + self.servers = servers or [] + self.components = components + + def __getitem__(self, path_name): + return self.paths[path_name] + + @property + def default_url(self): + return self.servers[0].default_url + + def get_server(self, full_url_pattern): + for spec_server in self.servers: + if spec_server.default_url in full_url_pattern: + return spec_server + + raise InvalidServer( + "Invalid request server {0}".format(full_url_pattern)) + + def get_server_url(self, index=0): + return self.servers[index].default_url + + def get_operation(self, path_pattern, http_method): + try: + return self.paths[path_pattern].operations[http_method] + except KeyError: + raise InvalidOperation( + "Unknown operation path {0} with method {1}".format( + path_pattern, http_method)) + + def get_schema(self, name): + return self.components.schemas[name] + + # 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') diff --git a/openapi_core/shortcuts.py b/openapi_core/shortcuts.py index 5bdd4c2a..bcf4d31d 100644 --- a/openapi_core/shortcuts.py +++ b/openapi_core/shortcuts.py @@ -3,9 +3,15 @@ from openapi_spec_validator.validators import Dereferencer from openapi_spec_validator import default_handlers -from openapi_core.exceptions import OpenAPIParameterError, OpenAPIBodyError -from openapi_core.specs import SpecFactory -from openapi_core.validators import RequestValidator, ResponseValidator +from openapi_core.schema.media_types.exceptions import OpenAPIMediaTypeError +from openapi_core.schema.parameters.exceptions import OpenAPIParameterError +from openapi_core.schema.request_bodies.exceptions import ( + OpenAPIRequestBodyError, +) +from openapi_core.schema.schemas.exceptions import OpenAPISchemaError +from openapi_core.schema.specs.factories import SpecFactory +from openapi_core.validation.request.validators import RequestValidator +from openapi_core.validation.response.validators import ResponseValidator def create_spec(spec_dict, spec_url=''): @@ -25,7 +31,10 @@ def validate_parameters(spec, request, wrapper_class=None): try: result.raise_for_errors() - except OpenAPIBodyError: + except ( + OpenAPIRequestBodyError, OpenAPIMediaTypeError, + OpenAPISchemaError, + ): return result.parameters else: return result.parameters diff --git a/openapi_core/specs.py b/openapi_core/specs.py deleted file mode 100644 index 906ff2e2..00000000 --- a/openapi_core/specs.py +++ /dev/null @@ -1,116 +0,0 @@ -# -*- coding: utf-8 -*- -"""OpenAPI core specs module""" -import logging -from functools import partialmethod, lru_cache - -from openapi_spec_validator import openapi_v3_spec_validator - -from openapi_core.components import ComponentsFactory -from openapi_core.exceptions import InvalidOperation, InvalidServer -from openapi_core.infos import InfoFactory -from openapi_core.paths import PathsGenerator -from openapi_core.schemas import SchemaRegistry -from openapi_core.servers import ServersGenerator - - -log = logging.getLogger(__name__) - - -class Spec(object): - """Represents an OpenAPI Specification for a service.""" - - def __init__(self, info, paths, servers=None, components=None): - self.info = info - self.paths = paths and dict(paths) - self.servers = servers or [] - self.components = components - - def __getitem__(self, path_name): - return self.paths[path_name] - - @property - def default_url(self): - return self.servers[0].default_url - - def get_server(self, full_url_pattern): - for spec_server in self.servers: - if spec_server.default_url in full_url_pattern: - return spec_server - - raise InvalidServer( - "Invalid request server {0}".format(full_url_pattern)) - - def get_server_url(self, index=0): - return self.servers[index].default_url - - def get_operation(self, path_pattern, http_method): - try: - return self.paths[path_pattern].operations[http_method] - except KeyError: - raise InvalidOperation( - "Unknown operation path {0} with method {1}".format( - path_pattern, http_method)) - - def get_schema(self, name): - return self.components.schemas[name] - - # 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) - - info_spec = spec_dict_deref.get('info', {}) - servers_spec = spec_dict_deref.get('servers', []) - paths = spec_dict_deref.get('paths', {}) - components_spec = spec_dict_deref.get('components', {}) - - info = self.info_factory.create(info_spec) - servers = self.servers_generator.generate(servers_spec) - paths = self.paths_generator.generate(paths) - components = self.components_factory.create(components_spec) - spec = Spec( - info, list(paths), servers=list(servers), components=components) - return spec - - @property - @lru_cache() - def schemas_registry(self): - return SchemaRegistry(self.dereferencer) - - @property - @lru_cache() - def info_factory(self): - return InfoFactory(self.dereferencer) - - @property - @lru_cache() - def servers_generator(self): - return ServersGenerator(self.dereferencer) - - @property - @lru_cache() - def paths_generator(self): - return PathsGenerator(self.dereferencer, self.schemas_registry) - - @property - @lru_cache() - def components_factory(self): - return ComponentsFactory(self.dereferencer, self.schemas_registry) diff --git a/openapi_core/validation/__init__.py b/openapi_core/validation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/validation/models.py b/openapi_core/validation/models.py new file mode 100644 index 00000000..e6366bb2 --- /dev/null +++ b/openapi_core/validation/models.py @@ -0,0 +1,11 @@ +"""OpenAPI core validation models module""" + + +class BaseValidationResult(object): + + def __init__(self, errors): + self.errors = errors + + def raise_for_errors(self): + for error in self.errors: + raise error diff --git a/openapi_core/validation/request/__init__.py b/openapi_core/validation/request/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/validation/request/models.py b/openapi_core/validation/request/models.py new file mode 100644 index 00000000..e7d57cf9 --- /dev/null +++ b/openapi_core/validation/request/models.py @@ -0,0 +1,31 @@ +"""OpenAPI core validation request models module""" +from openapi_core.schema.exceptions import OpenAPIMappingError + +from openapi_core.validation.models import BaseValidationResult + + +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 RequestValidationResult(BaseValidationResult): + + def __init__(self, errors, body=None, parameters=None): + super(RequestValidationResult, self).__init__(errors) + self.body = body + self.parameters = parameters or RequestParameters() diff --git a/openapi_core/validation/request/validators.py b/openapi_core/validation/request/validators.py new file mode 100644 index 00000000..87b638a2 --- /dev/null +++ b/openapi_core/validation/request/validators.py @@ -0,0 +1,85 @@ +"""OpenAPI core validation request validators module""" +from six import iteritems + +from openapi_core.schema.exceptions import OpenAPIMappingError +from openapi_core.schema.parameters.exceptions import MissingParameter +from openapi_core.validation.request.models import ( + RequestParameters, RequestValidationResult, +) +from openapi_core.validation.util import get_operation_pattern + + +class RequestValidator(object): + + def __init__(self, spec): + self.spec = spec + + def validate(self, request): + try: + server = self.spec.get_server(request.full_url_pattern) + # don't process if server errors + except OpenAPIMappingError as exc: + return RequestValidationResult([exc, ], None, None) + + operation_pattern = get_operation_pattern( + server.default_url, request.full_url_pattern + ) + + try: + operation = self.spec.get_operation( + operation_pattern, request.method) + # don't process if operation errors + except OpenAPIMappingError as exc: + return RequestValidationResult([exc, ], None, None) + + params, params_errors = self._get_parameters(request, operation) + body, body_errors = self._get_body(request, operation) + + errors = params_errors + body_errors + return RequestValidationResult(errors, body, params) + + def _get_parameters(self, request, operation): + errors = [] + + parameters = RequestParameters() + for param_name, param in iteritems(operation.parameters): + try: + raw_value = param.get_value(request) + except MissingParameter: + continue + except OpenAPIMappingError as exc: + errors.append(exc) + continue + + try: + value = param.unmarshal(raw_value) + except OpenAPIMappingError as exc: + errors.append(exc) + else: + parameters[param.location.value][param_name] = value + + return parameters, errors + + def _get_body(self, request, operation): + errors = [] + + if operation.request_body is None: + return None, errors + + body = None + try: + media_type = operation.request_body[request.mimetype] + except OpenAPIMappingError as exc: + errors.append(exc) + else: + try: + raw_body = operation.request_body.get_value(request) + except OpenAPIMappingError as exc: + errors.append(exc) + else: + try: + body = media_type.unmarshal(raw_body) + except OpenAPIMappingError as exc: + errors.append(exc) + + return body, errors diff --git a/openapi_core/validation/response/__init__.py b/openapi_core/validation/response/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/validation/response/models.py b/openapi_core/validation/response/models.py new file mode 100644 index 00000000..1a735c6d --- /dev/null +++ b/openapi_core/validation/response/models.py @@ -0,0 +1,10 @@ +"""OpenAPI core validation response models module""" +from openapi_core.validation.models import BaseValidationResult + + +class ResponseValidationResult(BaseValidationResult): + + def __init__(self, errors, data=None, headers=None): + super(ResponseValidationResult, self).__init__(errors) + self.data = data + self.headers = headers diff --git a/openapi_core/validation/response/validators.py b/openapi_core/validation/response/validators.py new file mode 100644 index 00000000..b926fb8a --- /dev/null +++ b/openapi_core/validation/response/validators.py @@ -0,0 +1,75 @@ +"""OpenAPI core validation response validators module""" +from openapi_core.schema.exceptions import OpenAPIMappingError +from openapi_core.validation.response.models import ResponseValidationResult +from openapi_core.validation.util import get_operation_pattern + + +class ResponseValidator(object): + + def __init__(self, spec): + self.spec = spec + + def validate(self, request, response): + try: + server = self.spec.get_server(request.full_url_pattern) + # don't process if server errors + except OpenAPIMappingError as exc: + return ResponseValidationResult([exc, ], None, None) + + operation_pattern = get_operation_pattern( + server.default_url, request.full_url_pattern + ) + + try: + operation = self.spec.get_operation( + operation_pattern, request.method) + # don't process if operation errors + except OpenAPIMappingError as exc: + return ResponseValidationResult([exc, ], None, None) + + try: + operation_response = operation.get_response( + str(response.status_code)) + # don't process if operation response errors + except OpenAPIMappingError as exc: + return ResponseValidationResult([exc, ], None, None) + + data, data_errors = self._get_data(response, operation_response) + + headers, headers_errors = self._get_headers( + response, operation_response) + + errors = data_errors + headers_errors + return ResponseValidationResult(errors, data, headers) + + def _get_data(self, response, operation_response): + errors = [] + + if not operation_response.content: + return None, errors + + data = None + try: + media_type = operation_response[response.mimetype] + except OpenAPIMappingError as exc: + errors.append(exc) + else: + try: + raw_data = operation_response.get_value(response) + except OpenAPIMappingError as exc: + errors.append(exc) + else: + try: + data = media_type.unmarshal(raw_data) + except OpenAPIMappingError as exc: + errors.append(exc) + + return data, errors + + def _get_headers(self, response, operation_response): + errors = [] + + # @todo: implement + headers = {} + + return headers, errors diff --git a/openapi_core/validation/util.py b/openapi_core/validation/util.py new file mode 100644 index 00000000..bbe0811c --- /dev/null +++ b/openapi_core/validation/util.py @@ -0,0 +1,12 @@ +"""OpenAPI core validation util module""" +from yarl import URL + + +def get_operation_pattern(server_url, request_url_pattern): + """Return an updated request URL pattern with the server URL removed.""" + if server_url[-1] == "/": + # operations have to start with a slash, so do not remove it + server_url = server_url[:-1] + if URL(server_url).is_absolute(): + return request_url_pattern.replace(server_url, "", 1) + return URL(request_url_pattern).path_qs.replace(server_url, "", 1) diff --git a/openapi_core/validators.py b/openapi_core/validators.py deleted file mode 100644 index 0f7fa6b4..00000000 --- a/openapi_core/validators.py +++ /dev/null @@ -1,211 +0,0 @@ -"""OpenAPI core validators module""" -from six import iteritems -from yarl import URL - -from openapi_core.exceptions import ( - OpenAPIMappingError, MissingParameter, MissingBody, InvalidResponse, -) - - -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 BaseValidationResult(object): - - def __init__(self, errors): - self.errors = errors - - def raise_for_errors(self): - for error in self.errors: - raise error - - -class RequestValidationResult(BaseValidationResult): - - def __init__(self, errors, body=None, parameters=None): - super(RequestValidationResult, self).__init__(errors) - self.body = body - self.parameters = parameters or RequestParameters() - - -class ResponseValidationResult(BaseValidationResult): - - def __init__(self, errors, data=None, headers=None): - super(ResponseValidationResult, self).__init__(errors) - self.data = data - self.headers = headers - - -def get_operation_pattern(server_url, request_url_pattern): - """Return an updated request URL pattern with the server URL removed.""" - if server_url[-1] == "/": - # operations have to start with a slash, so do not remove it - server_url = server_url[:-1] - if URL(server_url).is_absolute(): - return request_url_pattern.replace(server_url, "", 1) - return URL(request_url_pattern).path_qs.replace(server_url, "", 1) - - -class RequestValidator(object): - - def __init__(self, spec): - self.spec = spec - - def validate(self, request): - errors = [] - body = None - parameters = RequestParameters() - - try: - server = self.spec.get_server(request.full_url_pattern) - # don't process if server errors - except OpenAPIMappingError as exc: - errors.append(exc) - return RequestValidationResult(errors, body, parameters) - - operation_pattern = get_operation_pattern( - server.default_url, request.full_url_pattern - ) - - try: - operation = self.spec.get_operation( - operation_pattern, request.method) - # don't process if operation errors - except OpenAPIMappingError as exc: - errors.append(exc) - return RequestValidationResult(errors, body, parameters) - - for param_name, param in iteritems(operation.parameters): - try: - raw_value = self._get_raw_value(request, param) - except MissingParameter as exc: - if param.required: - errors.append(exc) - - if not param.schema or param.schema.default is None: - continue - raw_value = param.schema.default - - try: - value = param.unmarshal(raw_value) - except OpenAPIMappingError as exc: - errors.append(exc) - else: - parameters[param.location.value][param_name] = value - - if operation.request_body is not None: - try: - media_type = operation.request_body[request.mimetype] - except OpenAPIMappingError as exc: - errors.append(exc) - else: - try: - raw_body = self._get_raw_body(request) - except MissingBody as exc: - if operation.request_body.required: - errors.append(exc) - else: - try: - body = media_type.unmarshal(raw_body) - except OpenAPIMappingError as exc: - errors.append(exc) - - return RequestValidationResult(errors, body, parameters) - - def _get_raw_value(self, request, param): - location = request.parameters[param.location.value] - - try: - raw = location[param.name] - except KeyError: - raise MissingParameter( - "Missing required `{0}` parameter".format(param.name)) - - if param.aslist and param.explode: - return location.getlist(param.name) - - return raw - - def _get_raw_body(self, request): - if not request.body: - raise MissingBody("Missing required request body") - - return request.body - - -class ResponseValidator(object): - - def __init__(self, spec): - self.spec = spec - - def validate(self, request, response): - errors = [] - data = None - headers = {} - - try: - server = self.spec.get_server(request.full_url_pattern) - # don't process if server errors - except OpenAPIMappingError as exc: - errors.append(exc) - return ResponseValidationResult(errors, data, headers) - - operation_pattern = get_operation_pattern( - server.default_url, request.full_url_pattern - ) - - try: - operation = self.spec.get_operation( - operation_pattern, request.method) - # don't process if operation errors - except OpenAPIMappingError as exc: - errors.append(exc) - return ResponseValidationResult(errors, data, headers) - - try: - operation_response = operation.get_response( - str(response.status_code)) - # don't process if invalid response status code - except InvalidResponse as exc: - errors.append(exc) - return ResponseValidationResult(errors, data, headers) - - if operation_response.content: - try: - media_type = operation_response[response.mimetype] - except OpenAPIMappingError as exc: - errors.append(exc) - else: - try: - raw_data = self._get_raw_data(response) - except MissingBody as exc: - errors.append(exc) - else: - try: - data = media_type.unmarshal(raw_data) - except OpenAPIMappingError as exc: - errors.append(exc) - - return ResponseValidationResult(errors, data, headers) - - def _get_raw_data(self, response): - if not response.data: - raise MissingBody("Missing required response data") - - return response.data diff --git a/openapi_core/wrappers.py b/openapi_core/wrappers.py deleted file mode 100644 index 019e242d..00000000 --- a/openapi_core/wrappers.py +++ /dev/null @@ -1,142 +0,0 @@ -"""OpenAPI core wrappers module""" -import warnings - -from six.moves.urllib.parse import urljoin -from werkzeug.datastructures import ImmutableMultiDict - - -class BaseOpenAPIRequest(object): - - host_url = NotImplemented - path = NotImplemented - path_pattern = NotImplemented - method = NotImplemented - - parameters = NotImplemented - body = NotImplemented - - mimetype = NotImplemented - - @property - def full_url_pattern(self): - return urljoin(self.host_url, self.path_pattern) - - def get_body(self, spec): - warnings.warn( - "`get_body` method is deprecated. " - "Use RequestValidator instead.", - DeprecationWarning, - ) - # backward compatibility - from openapi_core.shortcuts import validate_body - return validate_body(spec, self, wrapper_class=None) - - def get_parameters(self, spec): - warnings.warn( - "`get_parameters` method is deprecated. " - "Use RequestValidator instead.", - DeprecationWarning, - ) - # backward compatibility - from openapi_core.shortcuts import validate_parameters - return validate_parameters(spec, self, wrapper_class=None) - - -class MockRequest(BaseOpenAPIRequest): - - def __init__( - self, host_url, method, path, path_pattern=None, args=None, - view_args=None, headers=None, cookies=None, data=None, - mimetype='application/json'): - self.host_url = host_url - self.path = path - self.path_pattern = path_pattern or path - self.method = method.lower() - - self.parameters = { - 'path': view_args or {}, - 'query': ImmutableMultiDict(args or []), - 'header': headers or {}, - 'cookie': cookies or {}, - } - - self.body = data or '' - - self.mimetype = mimetype - - -class FlaskOpenAPIRequest(BaseOpenAPIRequest): - - def __init__(self, request): - self.request = request - - @property - def host_url(self): - return self.request.host_url - - @property - def path(self): - return self.request.path - - @property - def method(self): - return self.request.method.lower() - - @property - def path_pattern(self): - if self.request.url_rule is None: - return self.path - - return self.request.url_rule.rule - - @property - def parameters(self): - return { - 'path': self.request.view_args, - 'query': self.request.args, - 'headers': self.request.headers, - 'cookies': self.request.cookies, - } - - @property - def body(self): - return self.request.data - - @property - def mimetype(self): - return self.request.mimetype - - -class BaseOpenAPIResponse(object): - - body = NotImplemented - status_code = NotImplemented - - mimetype = NotImplemented - - -class MockResponse(BaseOpenAPIRequest): - - def __init__(self, data, status_code=200, mimetype='application/json'): - self.data = data - - self.status_code = status_code - self.mimetype = mimetype - - -class FlaskOpenAPIResponse(BaseOpenAPIResponse): - - def __init__(self, response): - self.response = response - - @property - def data(self): - return self.response.data - - @property - def status_code(self): - return self.response._status_code - - @property - def mimetype(self): - return self.response.mimetype diff --git a/openapi_core/wrappers/__init__.py b/openapi_core/wrappers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openapi_core/wrappers/base.py b/openapi_core/wrappers/base.py new file mode 100644 index 00000000..395d16e1 --- /dev/null +++ b/openapi_core/wrappers/base.py @@ -0,0 +1,49 @@ +"""OpenAPI core wrappers module""" +import warnings + +from six.moves.urllib.parse import urljoin + + +class BaseOpenAPIRequest(object): + + host_url = NotImplemented + path = NotImplemented + path_pattern = NotImplemented + method = NotImplemented + + parameters = NotImplemented + body = NotImplemented + + mimetype = NotImplemented + + @property + def full_url_pattern(self): + return urljoin(self.host_url, self.path_pattern) + + def get_body(self, spec): + warnings.warn( + "`get_body` method is deprecated. " + "Use RequestValidator instead.", + DeprecationWarning, + ) + # backward compatibility + from openapi_core.shortcuts import validate_body + return validate_body(spec, self, wrapper_class=None) + + def get_parameters(self, spec): + warnings.warn( + "`get_parameters` method is deprecated. " + "Use RequestValidator instead.", + DeprecationWarning, + ) + # backward compatibility + from openapi_core.shortcuts import validate_parameters + return validate_parameters(spec, self, wrapper_class=None) + + +class BaseOpenAPIResponse(object): + + body = NotImplemented + status_code = NotImplemented + + mimetype = NotImplemented diff --git a/openapi_core/wrappers/flask.py b/openapi_core/wrappers/flask.py new file mode 100644 index 00000000..84bf0931 --- /dev/null +++ b/openapi_core/wrappers/flask.py @@ -0,0 +1,62 @@ +"""OpenAPI core wrappers module""" +from openapi_core.wrappers.base import BaseOpenAPIRequest, BaseOpenAPIResponse + + +class FlaskOpenAPIRequest(BaseOpenAPIRequest): + + def __init__(self, request): + self.request = request + + @property + def host_url(self): + return self.request.host_url + + @property + def path(self): + return self.request.path + + @property + def method(self): + return self.request.method.lower() + + @property + def path_pattern(self): + if self.request.url_rule is None: + return self.path + + return self.request.url_rule.rule + + @property + def parameters(self): + return { + 'path': self.request.view_args, + 'query': self.request.args, + 'headers': self.request.headers, + 'cookies': self.request.cookies, + } + + @property + def body(self): + return self.request.data + + @property + def mimetype(self): + return self.request.mimetype + + +class FlaskOpenAPIResponse(BaseOpenAPIResponse): + + def __init__(self, response): + self.response = response + + @property + def data(self): + return self.response.data + + @property + def status_code(self): + return self.response._status_code + + @property + def mimetype(self): + return self.response.mimetype diff --git a/openapi_core/wrappers/mock.py b/openapi_core/wrappers/mock.py new file mode 100644 index 00000000..9b034294 --- /dev/null +++ b/openapi_core/wrappers/mock.py @@ -0,0 +1,36 @@ +"""OpenAPI core wrappers module""" +from werkzeug.datastructures import ImmutableMultiDict + +from openapi_core.wrappers.base import BaseOpenAPIRequest, BaseOpenAPIResponse + + +class MockRequest(BaseOpenAPIRequest): + + def __init__( + self, host_url, method, path, path_pattern=None, args=None, + view_args=None, headers=None, cookies=None, data=None, + mimetype='application/json'): + self.host_url = host_url + self.path = path + self.path_pattern = path_pattern or path + self.method = method.lower() + + self.parameters = { + 'path': view_args or {}, + 'query': ImmutableMultiDict(args or []), + 'header': headers or {}, + 'cookie': cookies or {}, + } + + self.body = data or '' + + self.mimetype = mimetype + + +class MockResponse(BaseOpenAPIResponse): + + def __init__(self, data, status_code=200, mimetype='application/json'): + self.data = data + + self.status_code = status_code + self.mimetype = mimetype diff --git a/tests/integration/test_minimal.py b/tests/integration/test_minimal.py index deb96435..1dcc79d4 100644 --- a/tests/integration/test_minimal.py +++ b/tests/integration/test_minimal.py @@ -1,9 +1,9 @@ import pytest -from openapi_core.exceptions import InvalidOperation +from openapi_core.schema.operations.exceptions import InvalidOperation from openapi_core.shortcuts import create_spec -from openapi_core.validators import RequestValidator -from openapi_core.wrappers import MockRequest +from openapi_core.validation.request.validators import RequestValidator +from openapi_core.wrappers.mock import MockRequest class TestMinimal(object): diff --git a/tests/integration/test_petstore.py b/tests/integration/test_petstore.py index c1815892..3d3d9c68 100644 --- a/tests/integration/test_petstore.py +++ b/tests/integration/test_petstore.py @@ -2,22 +2,28 @@ import pytest from six import iteritems -from openapi_core.exceptions import ( - MissingParameter, InvalidContentType, InvalidServer, - UndefinedSchemaProperty, MissingProperty, - EmptyValue, InvalidMediaTypeValue, InvalidParameterValue, +from openapi_core.schema.media_types.exceptions import ( + InvalidContentType, InvalidMediaTypeValue, ) -from openapi_core.media_types import MediaType -from openapi_core.operations import Operation -from openapi_core.parameters import Parameter -from openapi_core.paths import Path -from openapi_core.request_bodies import RequestBody -from openapi_core.responses import Response -from openapi_core.schemas import Schema -from openapi_core.servers import Server, ServerVariable +from openapi_core.schema.media_types.models import MediaType +from openapi_core.schema.operations.models import Operation +from openapi_core.schema.parameters.exceptions import ( + MissingRequiredParameter, InvalidParameterValue, EmptyParameterValue, +) +from openapi_core.schema.parameters.models import Parameter +from openapi_core.schema.paths.models import Path +from openapi_core.schema.request_bodies.models import RequestBody +from openapi_core.schema.responses.models import Response +from openapi_core.schema.schemas.exceptions import ( + UndefinedSchemaProperty, MissingSchemaProperty, +) +from openapi_core.schema.schemas.models import Schema +from openapi_core.schema.servers.exceptions import InvalidServer +from openapi_core.schema.servers.models import Server, ServerVariable from openapi_core.shortcuts import create_spec -from openapi_core.validators import RequestValidator, ResponseValidator -from openapi_core.wrappers import MockRequest, MockResponse +from openapi_core.validation.request.validators import RequestValidator +from openapi_core.validation.response.validators import ResponseValidator +from openapi_core.wrappers.mock import MockRequest, MockResponse class TestPetstore(object): @@ -312,7 +318,7 @@ def test_get_pets_raises_missing_required_param(self, spec): path_pattern=path_pattern, ) - with pytest.raises(MissingParameter): + with pytest.raises(MissingRequiredParameter): request.get_parameters(spec) body = request.get_body(spec) @@ -331,7 +337,7 @@ def test_get_pets_empty_value(self, spec): path_pattern=path_pattern, args=query_params, ) - with pytest.raises(EmptyValue): + with pytest.raises(EmptyParameterValue): request.get_parameters(spec) body = request.get_body(spec) @@ -464,7 +470,7 @@ def test_post_pets_empty_body(self, spec, spec_dict): assert parameters == {} - with pytest.raises(MissingProperty): + with pytest.raises(MissingSchemaProperty): request.get_body(spec) def test_post_pets_extra_body_properties(self, spec, spec_dict): diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index d9816e2a..fe2d714c 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -1,14 +1,20 @@ import json import pytest -from openapi_core.exceptions import ( - InvalidServer, InvalidOperation, MissingParameter, - MissingBody, InvalidContentType, InvalidResponse, InvalidMediaTypeValue, - InvalidValue, +from openapi_core.schema.media_types.exceptions import ( + InvalidContentType, InvalidMediaTypeValue, ) +from openapi_core.schema.operations.exceptions import InvalidOperation +from openapi_core.schema.parameters.exceptions import MissingRequiredParameter +from openapi_core.schema.request_bodies.exceptions import MissingRequestBody +from openapi_core.schema.responses.exceptions import ( + MissingResponseContent, InvalidResponse, +) +from openapi_core.schema.servers.exceptions import InvalidServer from openapi_core.shortcuts import create_spec -from openapi_core.validators import RequestValidator, ResponseValidator -from openapi_core.wrappers import MockRequest, MockResponse +from openapi_core.validation.request.validators import RequestValidator +from openapi_core.validation.response.validators import ResponseValidator +from openapi_core.wrappers.mock import MockRequest, MockResponse class TestRequestValidator(object): @@ -52,7 +58,7 @@ def test_missing_parameter(self, validator): result = validator.validate(request) - assert type(result.errors[0]) == MissingParameter + assert type(result.errors[0]) == MissingRequiredParameter assert result.body is None assert result.parameters == { 'query': { @@ -88,7 +94,7 @@ def test_missing_body(self, validator): result = validator.validate(request) assert len(result.errors) == 1 - assert type(result.errors[0]) == MissingBody + assert type(result.errors[0]) == MissingRequestBody assert result.body is None assert result.parameters == {} @@ -183,7 +189,7 @@ def test_invalid_server(self, validator): assert len(result.errors) == 1 assert type(result.errors[0]) == InvalidServer assert result.data is None - assert result.headers == {} + assert result.headers is None def test_invalid_operation(self, validator): request = MockRequest(self.host_url, 'get', '/v1') @@ -194,7 +200,7 @@ def test_invalid_operation(self, validator): assert len(result.errors) == 1 assert type(result.errors[0]) == InvalidOperation assert result.data is None - assert result.headers == {} + assert result.headers is None def test_invalid_response(self, validator): request = MockRequest(self.host_url, 'get', '/v1/pets') @@ -205,7 +211,7 @@ def test_invalid_response(self, validator): assert len(result.errors) == 1 assert type(result.errors[0]) == InvalidResponse assert result.data is None - assert result.headers == {} + assert result.headers is None def test_invalid_content_type(self, validator): request = MockRequest(self.host_url, 'get', '/v1/pets') @@ -225,7 +231,7 @@ def test_missing_body(self, validator): result = validator.validate(request, response) assert len(result.errors) == 1 - assert type(result.errors[0]) == MissingBody + assert type(result.errors[0]) == MissingResponseContent assert result.data is None assert result.headers == {} @@ -256,7 +262,7 @@ def test_invalid_value(self, validator): result = validator.validate(request, response) assert len(result.errors) == 1 - assert type(result.errors[0]) == InvalidValue + assert type(result.errors[0]) == InvalidMediaTypeValue assert result.data is None assert result.headers == {} diff --git a/tests/integration/test_wrappers.py b/tests/integration/test_wrappers.py index b33505e9..29a357df 100644 --- a/tests/integration/test_wrappers.py +++ b/tests/integration/test_wrappers.py @@ -5,7 +5,9 @@ from werkzeug.routing import Map, Rule, Subdomain from werkzeug.test import create_environ -from openapi_core.wrappers import FlaskOpenAPIRequest, FlaskOpenAPIResponse +from openapi_core.wrappers.flask import ( + FlaskOpenAPIRequest, FlaskOpenAPIResponse, +) class TestFlaskOpenAPIRequest(object): diff --git a/tests/unit/test_operations.py b/tests/unit/schema/test_operations.py similarity index 95% rename from tests/unit/test_operations.py rename to tests/unit/schema/test_operations.py index 41bc8a99..bca1da45 100644 --- a/tests/unit/test_operations.py +++ b/tests/unit/schema/test_operations.py @@ -1,7 +1,7 @@ import mock import pytest -from openapi_core.operations import Operation +from openapi_core.schema.operations.models import Operation class TestSchemas(object): diff --git a/tests/unit/test_paramters.py b/tests/unit/schema/test_paramters.py similarity index 87% rename from tests/unit/test_paramters.py rename to tests/unit/schema/test_paramters.py index f4d3f01c..952e9563 100644 --- a/tests/unit/test_paramters.py +++ b/tests/unit/schema/test_paramters.py @@ -1,8 +1,8 @@ import pytest -from openapi_core.enums import ParameterStyle -from openapi_core.exceptions import EmptyValue -from openapi_core.parameters import Parameter +from openapi_core.schema.parameters.exceptions import EmptyParameterValue +from openapi_core.schema.parameters.enums import ParameterStyle +from openapi_core.schema.parameters.models import Parameter class TestParameterInit(object): @@ -59,7 +59,7 @@ def test_query_empty(self): param = Parameter('param', 'query') value = '' - with pytest.raises(EmptyValue): + with pytest.raises(EmptyParameterValue): param.unmarshal(value) def test_query_allow_empty_value(self): diff --git a/tests/unit/test_paths.py b/tests/unit/schema/test_paths.py similarity index 89% rename from tests/unit/test_paths.py rename to tests/unit/schema/test_paths.py index ec2ad219..f7de1d5c 100644 --- a/tests/unit/test_paths.py +++ b/tests/unit/schema/test_paths.py @@ -1,7 +1,7 @@ import mock import pytest -from openapi_core.paths import Path +from openapi_core.schema.paths.models import Path class TestPaths(object): diff --git a/tests/unit/test_request_bodies.py b/tests/unit/schema/test_request_bodies.py similarity index 88% rename from tests/unit/test_request_bodies.py rename to tests/unit/schema/test_request_bodies.py index 6f1da311..d47ccd21 100644 --- a/tests/unit/test_request_bodies.py +++ b/tests/unit/schema/test_request_bodies.py @@ -1,7 +1,7 @@ import mock import pytest -from openapi_core.request_bodies import RequestBody +from openapi_core.schema.request_bodies.models import RequestBody class TestRequestBodies(object): diff --git a/tests/unit/test_schemas.py b/tests/unit/schema/test_schemas.py similarity index 87% rename from tests/unit/test_schemas.py rename to tests/unit/schema/test_schemas.py index 7895a39c..423b0987 100644 --- a/tests/unit/test_schemas.py +++ b/tests/unit/schema/test_schemas.py @@ -1,8 +1,8 @@ import mock import pytest -from openapi_core.exceptions import InvalidValueType, InvalidValue -from openapi_core.schemas import Schema +from openapi_core.schema.schemas.exceptions import InvalidSchemaValue +from openapi_core.schema.schemas.models import Schema class TestSchemaIteritems(object): @@ -44,7 +44,7 @@ def test_string_none(self): schema = Schema('string') value = None - with pytest.raises(InvalidValueType): + with pytest.raises(InvalidSchemaValue): schema.unmarshal(value) def test_string_default(self): @@ -52,7 +52,7 @@ def test_string_default(self): schema = Schema('string', default=default_value) value = None - with pytest.raises(InvalidValueType): + with pytest.raises(InvalidSchemaValue): schema.unmarshal(value) def test_string_default_nullable(self): @@ -76,7 +76,7 @@ def test_integer_enum_invalid(self): schema = Schema('integer', enum=[1, 2, 3]) value = '123' - with pytest.raises(InvalidValue): + with pytest.raises(InvalidSchemaValue): schema.unmarshal(value) def test_integer_enum(self): @@ -92,7 +92,7 @@ def test_integer_default(self): schema = Schema('integer', default=default_value) value = None - with pytest.raises(InvalidValueType): + with pytest.raises(InvalidSchemaValue): schema.unmarshal(value) def test_integer_default_nullable(self): @@ -108,5 +108,5 @@ def test_integer_invalid(self): schema = Schema('integer') value = 'abc' - with pytest.raises(InvalidValueType): + with pytest.raises(InvalidSchemaValue): schema.unmarshal(value) diff --git a/tests/unit/test_specs.py b/tests/unit/schema/test_specs.py similarity index 87% rename from tests/unit/test_specs.py rename to tests/unit/schema/test_specs.py index a78a9509..cede1ca2 100644 --- a/tests/unit/test_specs.py +++ b/tests/unit/schema/test_specs.py @@ -1,9 +1,9 @@ import mock import pytest -from openapi_core.exceptions import InvalidOperation -from openapi_core.paths import Path -from openapi_core.specs import Spec +from openapi_core.schema.operations.exceptions import InvalidOperation +from openapi_core.schema.paths.models import Path +from openapi_core.schema.specs.models import Spec class TestSpecs(object):