Merge pull request #183 from p1c2u/refactor/move-unmarshallers-to-subpackage

Move Unmarshallers to separate subpackage
This commit is contained in:
A 2020-01-23 21:44:56 +00:00 committed by GitHub
commit ca63475826
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 537 additions and 468 deletions

View file

@ -0,0 +1,5 @@
"""OpenAPI core exceptions module"""
class OpenAPIError(Exception):
pass

View file

@ -1,8 +1,5 @@
"""OpenAPI core schema exceptions module"""
class OpenAPIError(Exception):
pass
from openapi_core.exceptions import OpenAPIError
class OpenAPIMappingError(OpenAPIError):

View file

@ -0,0 +1,16 @@
"""OpenAPI core extensions generators module"""
from six import iteritems
from openapi_core.schema.extensions.models import Extension
class ExtensionsGenerator(object):
def __init__(self, dereferencer):
self.dereferencer = dereferencer
def generate(self, item_spec):
for field_name, value in iteritems(item_spec):
if not field_name.startswith('x-'):
continue
yield field_name, Extension(field_name, value)

View file

@ -0,0 +1,9 @@
"""OpenAPI core extensions models module"""
class Extension(object):
"""Represents an OpenAPI Extension."""
def __init__(self, field_name, value=None):
self.field_name = field_name
self.value = value

View file

@ -5,8 +5,9 @@ from json import loads
from openapi_core.schema.media_types.exceptions import InvalidMediaTypeValue
from openapi_core.schema.schemas.exceptions import (
CastError, ValidateError, UnmarshalError,
CastError, ValidateError,
)
from openapi_core.unmarshalling.schemas.exceptions import UnmarshalError
MEDIA_TYPE_DESERIALIZERS = {

View file

@ -11,8 +11,9 @@ from openapi_core.schema.parameters.exceptions import (
)
from openapi_core.schema.schemas.enums import SchemaType
from openapi_core.schema.schemas.exceptions import (
CastError, ValidateError, UnmarshalError,
CastError, ValidateError,
)
from openapi_core.unmarshalling.schemas.exceptions import UnmarshalError
log = logging.getLogger(__name__)
@ -81,11 +82,8 @@ class Parameter(object):
if self.required:
raise MissingRequiredParameter(self.name)
if not self.schema or self.schema.default is None:
raise MissingParameter(self.name)
return self.schema.default
if self.aslist and self.explode:
if hasattr(location, 'getall'):
return location.getall(self.name)

View file

@ -23,27 +23,6 @@ class ValidateError(OpenAPISchemaError):
pass
class UnmarshalError(OpenAPISchemaError):
"""Schema unmarshal operation error"""
pass
@attr.s(hash=True)
class UnmarshalValueError(UnmarshalError):
"""Failed to unmarshal value to type"""
value = attr.ib()
type = attr.ib()
original_exception = attr.ib(default=None)
def __str__(self):
return (
"Failed to unmarshal value {value} to type {type}: {exception}"
).format(
value=self.value, type=self.type,
exception=self.original_exception,
)
@attr.s(hash=True)
class InvalidSchemaValue(ValidateError):
value = attr.ib()
@ -61,48 +40,3 @@ class InvalidSchemaValue(ValidateError):
return (
"Value {value} not valid for schema of type {type}: {errors}"
).format(value=self.value, type=self.type, errors=self.schema_errors)
class UnmarshallerError(UnmarshalError):
"""Unmarshaller error"""
pass
@attr.s(hash=True)
class InvalidCustomFormatSchemaValue(UnmarshallerError):
"""Value failed to format with custom formatter"""
value = attr.ib()
type = attr.ib()
original_exception = attr.ib()
def __str__(self):
return (
"Failed to format value {value} to format {type}: {exception}"
).format(
value=self.value, type=self.type,
exception=self.original_exception,
)
@attr.s(hash=True)
class FormatterNotFoundError(UnmarshallerError):
"""Formatter not found to unmarshal"""
value = attr.ib()
type_format = attr.ib()
def __str__(self):
return (
"Formatter not found for {format} format "
"to unmarshal value {value}"
).format(format=self.type_format, value=self.value)
@attr.s(hash=True)
class UnmarshallerStrictTypeError(UnmarshallerError):
value = attr.ib()
types = attr.ib()
def __str__(self):
types = ', '.join(list(map(str, self.types)))
return "Value {value} is not one of types: {types}".format(
value=self.value, types=types)

View file

@ -4,9 +4,10 @@ import logging
from six import iteritems
from openapi_core.compat import lru_cache
from openapi_core.schema.extensions.generators import ExtensionsGenerator
from openapi_core.schema.properties.generators import PropertiesGenerator
from openapi_core.schema.schemas.models import Schema
from openapi_core.schema.schemas.types import Contribution
from openapi_core.schema.schemas.types import Contribution, NoValue
log = logging.getLogger(__name__)
@ -21,9 +22,8 @@ class SchemaFactory(object):
schema_type = schema_deref.get('type', None)
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)
default = schema_deref.get('default', NoValue)
properties_spec = schema_deref.get('properties', None)
items_spec = schema_deref.get('items', None)
nullable = schema_deref.get('nullable', False)
@ -47,6 +47,8 @@ class SchemaFactory(object):
min_properties = schema_deref.get('minProperties', None)
max_properties = schema_deref.get('maxProperties', None)
extensions = self.extensions_generator.generate(schema_deref)
properties = None
if properties_spec:
properties = self.properties_generator.generate(properties_spec)
@ -68,7 +70,7 @@ class SchemaFactory(object):
additional_properties = self.create(additional_properties_spec)
return Schema(
schema_type=schema_type, model=model, properties=properties,
schema_type=schema_type, properties=properties,
items=items, schema_format=schema_format, required=required,
default=default, nullable=nullable, enum=enum,
deprecated=deprecated, all_of=all_of, one_of=one_of,
@ -79,9 +81,15 @@ class SchemaFactory(object):
exclusive_maximum=exclusive_maximum,
exclusive_minimum=exclusive_minimum,
min_properties=min_properties, max_properties=max_properties,
extensions=extensions,
_source=schema_deref,
)
@property
@lru_cache()
def extensions_generator(self):
return ExtensionsGenerator(self.dereferencer)
@property
@lru_cache()
def properties_generator(self):

View file

@ -1,23 +1,22 @@
"""OpenAPI core schemas models module"""
import attr
import functools
import logging
from collections import defaultdict
import re
import warnings
from six import iteritems
from jsonschema.exceptions import ValidationError
from openapi_core.extensions.models.factories import ModelFactory
from openapi_core.schema.schemas._format import oas30_format_checker
from openapi_core.schema.schemas.enums import SchemaType
from openapi_core.schema.schemas.exceptions import (
CastError, InvalidSchemaValue,
UnmarshalValueError, UnmarshalError,
)
from openapi_core.schema.schemas.types import NoValue
from openapi_core.schema.schemas.util import forcebool
from openapi_core.schema.schemas.validators import OAS30Validator
from openapi_core.unmarshalling.schemas.exceptions import (
UnmarshalValueError,
)
log = logging.getLogger(__name__)
@ -37,20 +36,17 @@ class Schema(object):
SchemaType.BOOLEAN: forcebool,
}
DEFAULT_UNMARSHAL_CALLABLE_GETTER = {
}
def __init__(
self, schema_type=None, model=None, properties=None, items=None,
schema_format=None, required=None, default=None, nullable=False,
self, schema_type=None, properties=None, items=None,
schema_format=None, required=None, default=NoValue, nullable=False,
enum=None, deprecated=False, all_of=None, one_of=None,
additional_properties=True, min_items=None, max_items=None,
min_length=None, max_length=None, pattern=None, unique_items=False,
minimum=None, maximum=None, multiple_of=None,
exclusive_minimum=False, exclusive_maximum=False,
min_properties=None, max_properties=None, _source=None):
min_properties=None, max_properties=None, extensions=None,
_source=None):
self.type = SchemaType(schema_type)
self.model = model
self.properties = properties and dict(properties) or {}
self.items = items
self.format = schema_format
@ -79,6 +75,8 @@ class Schema(object):
self.max_properties = int(max_properties)\
if max_properties is not None else None
self.extensions = extensions and dict(extensions) or {}
self._all_required_properties_cache = None
self._all_optional_properties_cache = None
@ -95,6 +93,9 @@ class Schema(object):
def __getitem__(self, name):
return self.properties[name]
def has_default(self):
return self.default is not NoValue
def get_all_properties(self):
properties = self.properties.copy()
@ -108,32 +109,6 @@ class Schema(object):
all_properties = self.get_all_properties()
return set(all_properties.keys())
def get_all_required_properties(self):
if self._all_required_properties_cache is None:
self._all_required_properties_cache =\
self._get_all_required_properties()
return self._all_required_properties_cache
def _get_all_required_properties(self):
all_properties = self.get_all_properties()
required = self.get_all_required_properties_names()
return dict(
(prop_name, val)
for prop_name, val in iteritems(all_properties)
if prop_name in required
)
def get_all_required_properties_names(self):
required = self.required[:]
for subschema in self.all_of:
subschema_req = subschema.get_all_required_properties()
required += subschema_req
return set(required)
def get_cast_mapping(self):
mapping = self.TYPE_CAST_CALLABLE_GETTER.copy()
mapping.update({
@ -144,7 +119,7 @@ class Schema(object):
def cast(self, value):
"""Cast value from string to schema type"""
if value is None:
if value in (None, NoValue):
return value
cast_mapping = self.get_cast_mapping()
@ -158,28 +133,6 @@ class Schema(object):
def _cast_collection(self, value):
return list(map(self.items.cast, value))
def get_unmarshal_mapping(self, custom_formatters=None, strict=True):
primitive_unmarshallers = self.get_primitive_unmarshallers(
custom_formatters=custom_formatters)
primitive_unmarshallers_partial = dict(
(t, functools.partial(u, type_format=self.format, strict=strict))
for t, u in primitive_unmarshallers.items()
)
def pass_defaults(f):
return functools.partial(
f, custom_formatters=custom_formatters, strict=strict)
mapping = self.DEFAULT_UNMARSHAL_CALLABLE_GETTER.copy()
mapping.update(primitive_unmarshallers_partial)
mapping.update({
SchemaType.ANY: pass_defaults(self._unmarshal_any),
SchemaType.ARRAY: pass_defaults(self._unmarshal_collection),
SchemaType.OBJECT: pass_defaults(self._unmarshal_object),
})
return defaultdict(lambda: lambda x: x, mapping)
def get_validator(self, resolver=None):
return OAS30Validator(
self.__dict__,
@ -197,162 +150,13 @@ class Schema(object):
def unmarshal(self, value, custom_formatters=None, strict=True):
"""Unmarshal parameter from the value."""
if self.deprecated:
warnings.warn("The schema is deprecated", DeprecationWarning)
if value is None:
if not self.nullable:
raise UnmarshalError(
"Null value for non-nullable schema", value, self.type)
return self.default
if self.enum and value not in self.enum:
raise UnmarshalError("Invalid value for enum: {0}".format(value))
unmarshal_mapping = self.get_unmarshal_mapping(
custom_formatters=custom_formatters, strict=strict)
if self.type is not SchemaType.STRING and value == '':
return None
unmarshal_callable = unmarshal_mapping[self.type]
from openapi_core.unmarshalling.schemas.factories import (
SchemaUnmarshallersFactory,
)
unmarshallers_factory = SchemaUnmarshallersFactory(
custom_formatters)
unmarshaller = unmarshallers_factory.create(self)
try:
unmarshalled = unmarshal_callable(value)
return unmarshaller(value, strict=strict)
except ValueError as exc:
raise UnmarshalValueError(value, self.type, exc)
return unmarshalled
def get_primitive_unmarshallers(self, **options):
from openapi_core.schema.schemas.unmarshallers import (
StringUnmarshaller, BooleanUnmarshaller, IntegerUnmarshaller,
NumberUnmarshaller,
)
unmarshallers_classes = {
SchemaType.STRING: StringUnmarshaller,
SchemaType.BOOLEAN: BooleanUnmarshaller,
SchemaType.INTEGER: IntegerUnmarshaller,
SchemaType.NUMBER: NumberUnmarshaller,
}
unmarshallers = dict(
(t, klass(**options))
for t, klass in unmarshallers_classes.items()
)
return unmarshallers
def _unmarshal_any(self, value, custom_formatters=None, strict=True):
types_resolve_order = [
SchemaType.OBJECT, SchemaType.ARRAY, SchemaType.BOOLEAN,
SchemaType.INTEGER, SchemaType.NUMBER, SchemaType.STRING,
]
unmarshal_mapping = self.get_unmarshal_mapping()
if self.one_of:
result = None
for subschema in self.one_of:
try:
unmarshalled = subschema.unmarshal(
value, custom_formatters)
except UnmarshalError:
continue
else:
if result is not None:
log.warning("multiple valid oneOf schemas found")
continue
result = unmarshalled
if result is None:
log.warning("valid oneOf schema not found")
return result
else:
for schema_type in types_resolve_order:
unmarshal_callable = unmarshal_mapping[schema_type]
try:
return unmarshal_callable(value)
except (UnmarshalError, ValueError):
continue
log.warning("failed to unmarshal any type")
return value
def _unmarshal_collection(
self, value, custom_formatters=None, strict=True):
if not isinstance(value, (list, tuple)):
raise ValueError(
"Invalid value for collection: {0}".format(value))
f = functools.partial(
self.items.unmarshal,
custom_formatters=custom_formatters, strict=strict,
)
return list(map(f, value))
def _unmarshal_object(self, value, model_factory=None,
custom_formatters=None, strict=True):
if not isinstance(value, (dict, )):
raise ValueError("Invalid value for object: {0}".format(value))
model_factory = model_factory or ModelFactory()
if self.one_of:
properties = None
for one_of_schema in self.one_of:
try:
unmarshalled = self._unmarshal_properties(
value, one_of_schema,
custom_formatters=custom_formatters,
)
except (UnmarshalError, ValueError):
pass
else:
if properties is not None:
log.warning("multiple valid oneOf schemas found")
continue
properties = unmarshalled
if properties is None:
log.warning("valid oneOf schema not found")
else:
properties = self._unmarshal_properties(
value, custom_formatters=custom_formatters)
return model_factory.create(properties, name=self.model)
def _unmarshal_properties(self, value, one_of_schema=None,
custom_formatters=None, strict=True):
all_props = self.get_all_properties()
all_props_names = self.get_all_properties_names()
all_req_props_names = self.get_all_required_properties_names()
if one_of_schema is not None:
all_props.update(one_of_schema.get_all_properties())
all_props_names |= one_of_schema.\
get_all_properties_names()
all_req_props_names |= one_of_schema.\
get_all_required_properties_names()
value_props_names = value.keys()
extra_props = set(value_props_names) - set(all_props_names)
properties = {}
if self.additional_properties is not True:
for prop_name in extra_props:
prop_value = value[prop_name]
properties[prop_name] = self.additional_properties.unmarshal(
prop_value, custom_formatters=custom_formatters)
for prop_name, prop in iteritems(all_props):
try:
prop_value = value[prop_name]
except KeyError:
if not prop.nullable and not prop.default:
continue
prop_value = prop.default
properties[prop_name] = prop.unmarshal(
prop_value, custom_formatters=custom_formatters)
return properties

View file

@ -1,6 +1,9 @@
import attr
NoValue = object()
@attr.s(hash=True)
class Contribution(object):
src_prop_name = attr.ib()

View file

@ -1,101 +0,0 @@
from six import text_type, binary_type, integer_types
from openapi_core.schema.schemas.enums import SchemaFormat
from openapi_core.schema.schemas.exceptions import (
InvalidCustomFormatSchemaValue,
UnmarshallerStrictTypeError,
FormatterNotFoundError,
)
from openapi_core.schema.schemas.util import (
forcebool, format_date, format_datetime, format_byte, format_uuid,
format_number,
)
class StrictUnmarshaller(object):
STRICT_TYPES = ()
def __call__(self, value, type_format=SchemaFormat.NONE, strict=True):
if self.STRICT_TYPES and strict and not isinstance(
value, self.STRICT_TYPES):
raise UnmarshallerStrictTypeError(value, self.STRICT_TYPES)
return value
class PrimitiveTypeUnmarshaller(StrictUnmarshaller):
FORMATTERS = {
SchemaFormat.NONE: lambda x: x,
}
def __init__(self, custom_formatters=None):
if custom_formatters is None:
custom_formatters = {}
self.custom_formatters = custom_formatters
def __call__(self, value, type_format=SchemaFormat.NONE, strict=True):
value = super(PrimitiveTypeUnmarshaller, self).__call__(
value, type_format=type_format, strict=strict)
try:
schema_format = SchemaFormat(type_format)
except ValueError:
formatter = self.custom_formatters.get(type_format)
else:
formatters = self.get_formatters()
formatter = formatters.get(schema_format)
if formatter is None:
raise FormatterNotFoundError(value, type_format)
try:
return formatter(value)
except ValueError as exc:
raise InvalidCustomFormatSchemaValue(value, type_format, exc)
def get_formatters(self):
return self.FORMATTERS
class StringUnmarshaller(PrimitiveTypeUnmarshaller):
STRICT_TYPES = (text_type, binary_type)
FORMATTERS = {
SchemaFormat.NONE: text_type,
SchemaFormat.PASSWORD: text_type,
SchemaFormat.DATE: format_date,
SchemaFormat.DATETIME: format_datetime,
SchemaFormat.BINARY: binary_type,
SchemaFormat.UUID: format_uuid,
SchemaFormat.BYTE: format_byte,
}
class IntegerUnmarshaller(PrimitiveTypeUnmarshaller):
STRICT_TYPES = integer_types
FORMATTERS = {
SchemaFormat.NONE: int,
SchemaFormat.INT32: int,
SchemaFormat.INT64: int,
}
class NumberUnmarshaller(PrimitiveTypeUnmarshaller):
STRICT_TYPES = (float, ) + integer_types
FORMATTERS = {
SchemaFormat.NONE: format_number,
SchemaFormat.FLOAT: float,
SchemaFormat.DOUBLE: float,
}
class BooleanUnmarshaller(PrimitiveTypeUnmarshaller):
STRICT_TYPES = (bool, )
FORMATTERS = {
SchemaFormat.NONE: forcebool,
}

View file

@ -1,11 +1,7 @@
"""OpenAPI core schemas util module"""
from base64 import b64decode
import datetime
from distutils.util import strtobool
from six import string_types
from json import dumps
from six import string_types, text_type, integer_types
import strict_rfc3339
from uuid import UUID
def forcebool(val):
@ -17,29 +13,3 @@ def forcebool(val):
def dicthash(d):
return hash(dumps(d, sort_keys=True))
def format_date(value):
return datetime.datetime.strptime(value, '%Y-%m-%d').date()
def format_datetime(value):
timestamp = strict_rfc3339.rfc3339_to_timestamp(value)
return datetime.datetime.utcfromtimestamp(timestamp)
def format_uuid(value):
if isinstance(value, UUID):
return value
return UUID(value)
def format_byte(value, encoding='utf8'):
return text_type(b64decode(value), encoding)
def format_number(value):
if isinstance(value, integer_types + (float, )):
return value
return float(value)

View file

View file

@ -0,0 +1,66 @@
import attr
from openapi_core.exceptions import OpenAPIError
class UnmarshalError(OpenAPIError):
"""Schema unmarshal operation error"""
pass
class UnmarshallerError(UnmarshalError):
"""Unmarshaller error"""
pass
@attr.s(hash=True)
class UnmarshalValueError(UnmarshalError):
"""Failed to unmarshal value to type"""
value = attr.ib()
type = attr.ib()
original_exception = attr.ib(default=None)
def __str__(self):
return (
"Failed to unmarshal value {value} to type {type}: {exception}"
).format(
value=self.value, type=self.type,
exception=self.original_exception,
)
@attr.s(hash=True)
class InvalidCustomFormatSchemaValue(UnmarshallerError):
"""Value failed to format with custom formatter"""
value = attr.ib()
type = attr.ib()
original_exception = attr.ib()
def __str__(self):
return (
"Failed to format value {value} to format {type}: {exception}"
).format(
value=self.value, type=self.type,
exception=self.original_exception,
)
@attr.s(hash=True)
class FormatterNotFoundError(UnmarshallerError):
"""Formatter not found to unmarshal"""
type_format = attr.ib()
def __str__(self):
return "Formatter not found for {format} format".format(
format=self.type_format)
@attr.s(hash=True)
class UnmarshallerStrictTypeError(UnmarshallerError):
value = attr.ib()
types = attr.ib()
def __str__(self):
types = ', '.join(list(map(str, self.types)))
return "Value {value} is not one of types: {types}".format(
value=self.value, types=types)

View file

@ -0,0 +1,62 @@
import warnings
from openapi_core.schema.schemas.enums import SchemaType, SchemaFormat
from openapi_core.unmarshalling.schemas.exceptions import (
FormatterNotFoundError,
)
from openapi_core.unmarshalling.schemas.unmarshallers import (
StringUnmarshaller, IntegerUnmarshaller, NumberUnmarshaller,
BooleanUnmarshaller, ArrayUnmarshaller, ObjectUnmarshaller,
AnyUnmarshaller,
)
class SchemaUnmarshallersFactory(object):
PRIMITIVE_UNMARSHALLERS = {
SchemaType.STRING: StringUnmarshaller,
SchemaType.INTEGER: IntegerUnmarshaller,
SchemaType.NUMBER: NumberUnmarshaller,
SchemaType.BOOLEAN: BooleanUnmarshaller,
}
COMPLEX_UNMARSHALLERS = {
SchemaType.ARRAY: ArrayUnmarshaller,
SchemaType.OBJECT: ObjectUnmarshaller,
SchemaType.ANY: AnyUnmarshaller,
}
def __init__(self, custom_formatters=None):
if custom_formatters is None:
custom_formatters = {}
self.custom_formatters = custom_formatters
def create(self, schema, type_override=None):
"""Create unmarshaller from the schema."""
if schema.deprecated:
warnings.warn("The schema is deprecated", DeprecationWarning)
schema_type = type_override or schema.type
if schema_type in self.PRIMITIVE_UNMARSHALLERS:
klass = self.PRIMITIVE_UNMARSHALLERS[schema_type]
kwargs = dict(schema=schema)
elif schema_type in self.COMPLEX_UNMARSHALLERS:
klass = self.COMPLEX_UNMARSHALLERS[schema_type]
kwargs = dict(schema=schema, unmarshallers_factory=self)
formatter = self.get_formatter(klass.FORMATTERS, schema.format)
if formatter is None:
raise FormatterNotFoundError(schema.format)
return klass(formatter, **kwargs)
def get_formatter(self, formatters, type_format=SchemaFormat.NONE):
try:
schema_format = SchemaFormat(type_format)
except ValueError:
return self.custom_formatters.get(type_format)
else:
if schema_format == SchemaFormat.NONE:
return lambda x: x
return formatters.get(schema_format)

View file

@ -0,0 +1,240 @@
import logging
from six import text_type, binary_type, integer_types
from six import iteritems
from openapi_core.extensions.models.factories import ModelFactory
from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType
from openapi_core.schema.schemas.exceptions import (
ValidateError,
)
from openapi_core.schema.schemas.types import NoValue
from openapi_core.unmarshalling.schemas.exceptions import (
UnmarshalError,
InvalidCustomFormatSchemaValue,
UnmarshallerStrictTypeError,
)
from openapi_core.unmarshalling.schemas.util import (
forcebool, format_date, format_datetime, format_byte, format_uuid,
format_number,
)
log = logging.getLogger(__name__)
class StrictUnmarshaller(object):
STRICT_TYPES = ()
def __call__(self, value, strict=True):
if strict and not self._is_strict(value):
raise UnmarshallerStrictTypeError(value, self.STRICT_TYPES)
return value
def _is_strict(self, value):
if not self.STRICT_TYPES:
return True
return isinstance(value, self.STRICT_TYPES)
class PrimitiveTypeUnmarshaller(StrictUnmarshaller):
FORMATTERS = {}
def __init__(self, formatter, schema):
self.formatter = formatter
self.schema = schema
def __call__(self, value=NoValue, strict=True):
if value is NoValue:
value = self.schema.default
if value is None:
return
value = super(PrimitiveTypeUnmarshaller, self).__call__(
value, strict=strict)
return self.format(value)
def format(self, value):
try:
return self.formatter(value)
except ValueError as exc:
raise InvalidCustomFormatSchemaValue(
value, self.schema.format, exc)
class StringUnmarshaller(PrimitiveTypeUnmarshaller):
STRICT_TYPES = (text_type, binary_type)
FORMATTERS = {
SchemaFormat.NONE: text_type,
SchemaFormat.PASSWORD: text_type,
SchemaFormat.DATE: format_date,
SchemaFormat.DATETIME: format_datetime,
SchemaFormat.BINARY: binary_type,
SchemaFormat.UUID: format_uuid,
SchemaFormat.BYTE: format_byte,
}
class IntegerUnmarshaller(PrimitiveTypeUnmarshaller):
STRICT_TYPES = integer_types
FORMATTERS = {
SchemaFormat.NONE: int,
SchemaFormat.INT32: int,
SchemaFormat.INT64: int,
}
class NumberUnmarshaller(PrimitiveTypeUnmarshaller):
STRICT_TYPES = (float, ) + integer_types
FORMATTERS = {
SchemaFormat.NONE: format_number,
SchemaFormat.FLOAT: float,
SchemaFormat.DOUBLE: float,
}
class BooleanUnmarshaller(PrimitiveTypeUnmarshaller):
STRICT_TYPES = (bool, )
FORMATTERS = {
SchemaFormat.NONE: forcebool,
}
class ComplexUnmarshaller(PrimitiveTypeUnmarshaller):
def __init__(self, formatter, schema, unmarshallers_factory):
super(ComplexUnmarshaller, self).__init__(formatter, schema)
self.unmarshallers_factory = unmarshallers_factory
class ArrayUnmarshaller(ComplexUnmarshaller):
STRICT_TYPES = (list, tuple)
FORMATTERS = {}
@property
def items_unmarshaller(self):
return self.unmarshallers_factory.create(self.schema.items)
def __call__(self, value=NoValue, strict=True):
value = super(ArrayUnmarshaller, self).__call__(value, strict=strict)
self.unmarshallers_factory.create(self.schema.items)
return list(map(self.items_unmarshaller, value))
class ObjectUnmarshaller(ComplexUnmarshaller):
STRICT_TYPES = (dict, )
FORMATTERS = {}
@property
def model_factory(self):
return ModelFactory()
def __call__(self, value=NoValue, strict=True):
value = super(ObjectUnmarshaller, self).__call__(value, strict=strict)
if self.schema.one_of:
properties = None
for one_of_schema in self.schema.one_of:
try:
unmarshalled = self._unmarshal_properties(
value, one_of_schema, strict=strict)
except (UnmarshalError, ValueError):
pass
else:
if properties is not None:
log.warning("multiple valid oneOf schemas found")
continue
properties = unmarshalled
if properties is None:
log.warning("valid oneOf schema not found")
else:
properties = self._unmarshal_properties(value)
if 'x-model' in self.schema.extensions:
extension = self.schema.extensions['x-model']
return self.model_factory.create(properties, name=extension.value)
return properties
def _unmarshal_properties(
self, value=NoValue, one_of_schema=None, strict=True):
all_props = self.schema.get_all_properties()
all_props_names = self.schema.get_all_properties_names()
if one_of_schema is not None:
all_props.update(one_of_schema.get_all_properties())
all_props_names |= one_of_schema.\
get_all_properties_names()
value_props_names = value.keys()
extra_props = set(value_props_names) - set(all_props_names)
properties = {}
if self.schema.additional_properties is not True:
for prop_name in extra_props:
prop_value = value[prop_name]
properties[prop_name] = self.unmarshallers_factory.create(
self.schema.additional_properties)(
prop_value, strict=strict)
for prop_name, prop in iteritems(all_props):
try:
prop_value = value[prop_name]
except KeyError:
if prop.default is NoValue:
continue
prop_value = prop.default
properties[prop_name] = self.unmarshallers_factory.create(
prop)(prop_value, strict=strict)
return properties
class AnyUnmarshaller(ComplexUnmarshaller):
SCHEMA_TYPES_ORDER = [
SchemaType.OBJECT, SchemaType.ARRAY, SchemaType.BOOLEAN,
SchemaType.INTEGER, SchemaType.NUMBER, SchemaType.STRING,
]
def __call__(self, value=NoValue, strict=True):
one_of_schema = self._get_one_of_schema(value)
if one_of_schema:
return self.unmarshallers_factory.create(one_of_schema)(
value, strict=strict)
for schema_type in self.SCHEMA_TYPES_ORDER:
try:
unmarshaller = self.unmarshallers_factory.create(
self.schema, type_override=schema_type)
return unmarshaller(value, strict=strict)
except (UnmarshalError, ValueError):
continue
log.warning("failed to unmarshal any type")
return value
def _get_one_of_schema(self, value):
if not self.schema.one_of:
return
for subschema in self.schema.one_of:
try:
subschema.validate(value)
except ValidateError:
continue
else:
return subschema

View file

@ -0,0 +1,40 @@
"""OpenAPI core schemas util module"""
from base64 import b64decode
import datetime
from distutils.util import strtobool
from six import string_types, text_type, integer_types
import strict_rfc3339
from uuid import UUID
def forcebool(val):
if isinstance(val, string_types):
val = strtobool(val)
return bool(val)
def format_date(value):
return datetime.datetime.strptime(value, '%Y-%m-%d').date()
def format_datetime(value):
timestamp = strict_rfc3339.rfc3339_to_timestamp(value)
return datetime.datetime.utcfromtimestamp(timestamp)
def format_uuid(value):
if isinstance(value, UUID):
return value
return UUID(value)
def format_byte(value, encoding='utf8'):
return text_type(b64decode(value), encoding)
def format_number(value):
if isinstance(value, integer_types + (float, )):
return value
return float(value)

View file

@ -10,6 +10,18 @@ from six.moves.urllib.parse import urljoin
@attr.s
class RequestParameters(object):
"""OpenAPI request parameters dataclass.
Attributes:
path
Path parameters as dict.
query
Query string parameters as MultiDict. Must support getlist method.
header
Request headers as dict.
cookie
Request cookies as dict.
"""
path = attr.ib(factory=dict)
query = attr.ib(factory=ImmutableMultiDict)
header = attr.ib(factory=dict)

View file

@ -7,7 +7,7 @@ from openapi_core.schema.media_types.exceptions import (
)
from openapi_core.schema.operations.exceptions import InvalidOperation
from openapi_core.schema.parameters.exceptions import (
OpenAPIParameterError, MissingRequiredParameter,
OpenAPIParameterError, MissingRequiredParameter, MissingParameter,
)
from openapi_core.schema.paths.exceptions import InvalidPath
from openapi_core.schema.request_bodies.exceptions import MissingRequestBody
@ -74,9 +74,11 @@ class RequestValidator(object):
except MissingRequiredParameter as exc:
errors.append(exc)
continue
except OpenAPIParameterError:
except MissingParameter:
if not param.schema or not param.schema.has_default():
continue
casted = param.schema.default
else:
try:
casted = param.cast(raw_value)
except OpenAPIParameterError as exc:

View file

@ -278,6 +278,7 @@ components:
$ref: "#/components/schemas/Pet"
PetsData:
type: object
x-model: PetsData
required:
- data
properties:
@ -285,6 +286,7 @@ components:
$ref: "#/components/schemas/Pets"
PetData:
type: object
x-model: PetData
required:
- data
properties:
@ -292,6 +294,7 @@ components:
$ref: "#/components/schemas/Pet"
TagCreate:
type: object
x-model: TagCreate
required:
- name
properties:
@ -316,6 +319,7 @@ components:
message:
type: string
ExtendedError:
x-model: ExtendedError
allOf:
- $ref: "#/components/schemas/Error"
- type: object

View file

@ -7,11 +7,16 @@ import pytest
from openapi_core.extensions.models.models import Model
from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType
from openapi_core.schema.schemas.exceptions import (
InvalidSchemaValue, OpenAPISchemaError, UnmarshallerStrictTypeError,
UnmarshalValueError, UnmarshalError, InvalidCustomFormatSchemaValue,
FormatterNotFoundError,
InvalidSchemaValue, OpenAPISchemaError,
)
from openapi_core.schema.schemas.models import Schema
from openapi_core.schema.schemas.types import NoValue
from openapi_core.unmarshalling.schemas.exceptions import (
InvalidCustomFormatSchemaValue,
FormatterNotFoundError,
UnmarshalError,
UnmarshallerStrictTypeError,
)
from six import b, u
@ -50,9 +55,8 @@ class TestSchemaUnmarshal(object):
schema = Schema(schema_type)
value = ''
result = schema.unmarshal(value)
assert result is None
with pytest.raises(UnmarshallerStrictTypeError):
schema.unmarshal(value)
def test_string_valid(self):
schema = Schema('string')
@ -93,25 +97,19 @@ class TestSchemaUnmarshal(object):
with pytest.raises(UnmarshallerStrictTypeError):
schema.unmarshal(value)
def test_string_none(self):
schema = Schema('string')
value = None
with pytest.raises(UnmarshalError):
schema.unmarshal(value)
def test_string_default(self):
default_value = 'default'
schema = Schema('string', default=default_value)
value = None
value = NoValue
with pytest.raises(UnmarshalError):
schema.unmarshal(value)
result = schema.unmarshal(value)
def test_string_default_nullable(self):
default_value = 'default'
assert result == default_value
@pytest.mark.parametrize('default_value', ['default', None])
def test_string_default_nullable(self, default_value):
schema = Schema('string', default=default_value, nullable=True)
value = None
value = NoValue
result = schema.unmarshal(value)
@ -161,7 +159,7 @@ class TestSchemaUnmarshal(object):
schema = Schema('string', schema_format=unknown_format)
value = 'x'
with pytest.raises(OpenAPISchemaError):
with pytest.raises(FormatterNotFoundError):
schema.unmarshal(value)
def test_string_format_invalid_value(self):
@ -172,7 +170,7 @@ class TestSchemaUnmarshal(object):
with pytest.raises(
FormatterNotFoundError,
message=(
'Formatter not found for custom format to unmarshal value x'
'Formatter not found for custom format'
),
):
schema.unmarshal(value)
@ -215,21 +213,22 @@ class TestSchemaUnmarshal(object):
schema.unmarshal(value)
def test_integer_default(self):
default_value = '123'
default_value = 123
schema = Schema('integer', default=default_value)
value = None
value = NoValue
with pytest.raises(UnmarshalError):
schema.unmarshal(value)
result = schema.unmarshal(value)
assert result == default_value
def test_integer_default_nullable(self):
default_value = '123'
default_value = 123
schema = Schema('integer', default=default_value, nullable=True)
value = None
result = schema.unmarshal(value)
assert result == default_value
assert result is None
def test_integer_invalid(self):
schema = Schema('integer')
@ -250,14 +249,14 @@ class TestSchemaUnmarshal(object):
schema = Schema('array', items=Schema('string'))
value = '123'
with pytest.raises(UnmarshalValueError):
with pytest.raises(UnmarshallerStrictTypeError):
schema.unmarshal(value)
def test_array_of_integer_string_invalid(self):
schema = Schema('array', items=Schema('integer'))
value = '123'
with pytest.raises(UnmarshalValueError):
with pytest.raises(UnmarshallerStrictTypeError):
schema.unmarshal(value)
def test_boolean_valid(self):
@ -702,7 +701,7 @@ class TestSchemaValidate(object):
unknown_format = 'unknown'
schema = Schema('string', schema_format=unknown_format)
with pytest.raises(OpenAPISchemaError):
with pytest.raises(InvalidSchemaValue):
schema.validate(value)
@pytest.mark.parametrize('value', [u(""), u("a"), u("ab")])