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""" """OpenAPI core schema exceptions module"""
from openapi_core.exceptions import OpenAPIError
class OpenAPIError(Exception):
pass
class OpenAPIMappingError(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.media_types.exceptions import InvalidMediaTypeValue
from openapi_core.schema.schemas.exceptions import ( from openapi_core.schema.schemas.exceptions import (
CastError, ValidateError, UnmarshalError, CastError, ValidateError,
) )
from openapi_core.unmarshalling.schemas.exceptions import UnmarshalError
MEDIA_TYPE_DESERIALIZERS = { 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.enums import SchemaType
from openapi_core.schema.schemas.exceptions import ( from openapi_core.schema.schemas.exceptions import (
CastError, ValidateError, UnmarshalError, CastError, ValidateError,
) )
from openapi_core.unmarshalling.schemas.exceptions import UnmarshalError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -81,10 +82,7 @@ class Parameter(object):
if self.required: if self.required:
raise MissingRequiredParameter(self.name) raise MissingRequiredParameter(self.name)
if not self.schema or self.schema.default is None: raise MissingParameter(self.name)
raise MissingParameter(self.name)
return self.schema.default
if self.aslist and self.explode: if self.aslist and self.explode:
if hasattr(location, 'getall'): if hasattr(location, 'getall'):

View file

@ -23,27 +23,6 @@ class ValidateError(OpenAPISchemaError):
pass 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) @attr.s(hash=True)
class InvalidSchemaValue(ValidateError): class InvalidSchemaValue(ValidateError):
value = attr.ib() value = attr.ib()
@ -61,48 +40,3 @@ class InvalidSchemaValue(ValidateError):
return ( return (
"Value {value} not valid for schema of type {type}: {errors}" "Value {value} not valid for schema of type {type}: {errors}"
).format(value=self.value, type=self.type, errors=self.schema_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 six import iteritems
from openapi_core.compat import lru_cache 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.properties.generators import PropertiesGenerator
from openapi_core.schema.schemas.models import Schema 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__) log = logging.getLogger(__name__)
@ -21,9 +22,8 @@ class SchemaFactory(object):
schema_type = schema_deref.get('type', None) schema_type = schema_deref.get('type', None)
schema_format = schema_deref.get('format') schema_format = schema_deref.get('format')
model = schema_deref.get('x-model', None)
required = schema_deref.get('required', False) 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) properties_spec = schema_deref.get('properties', None)
items_spec = schema_deref.get('items', None) items_spec = schema_deref.get('items', None)
nullable = schema_deref.get('nullable', False) nullable = schema_deref.get('nullable', False)
@ -47,6 +47,8 @@ class SchemaFactory(object):
min_properties = schema_deref.get('minProperties', None) min_properties = schema_deref.get('minProperties', None)
max_properties = schema_deref.get('maxProperties', None) max_properties = schema_deref.get('maxProperties', None)
extensions = self.extensions_generator.generate(schema_deref)
properties = None properties = None
if properties_spec: if properties_spec:
properties = self.properties_generator.generate(properties_spec) properties = self.properties_generator.generate(properties_spec)
@ -68,7 +70,7 @@ class SchemaFactory(object):
additional_properties = self.create(additional_properties_spec) additional_properties = self.create(additional_properties_spec)
return Schema( return Schema(
schema_type=schema_type, model=model, properties=properties, schema_type=schema_type, properties=properties,
items=items, schema_format=schema_format, required=required, items=items, schema_format=schema_format, required=required,
default=default, nullable=nullable, enum=enum, default=default, nullable=nullable, enum=enum,
deprecated=deprecated, all_of=all_of, one_of=one_of, deprecated=deprecated, all_of=all_of, one_of=one_of,
@ -79,9 +81,15 @@ class SchemaFactory(object):
exclusive_maximum=exclusive_maximum, exclusive_maximum=exclusive_maximum,
exclusive_minimum=exclusive_minimum, exclusive_minimum=exclusive_minimum,
min_properties=min_properties, max_properties=max_properties, min_properties=min_properties, max_properties=max_properties,
extensions=extensions,
_source=schema_deref, _source=schema_deref,
) )
@property
@lru_cache()
def extensions_generator(self):
return ExtensionsGenerator(self.dereferencer)
@property @property
@lru_cache() @lru_cache()
def properties_generator(self): def properties_generator(self):

View file

@ -1,23 +1,22 @@
"""OpenAPI core schemas models module""" """OpenAPI core schemas models module"""
import attr import attr
import functools
import logging import logging
from collections import defaultdict from collections import defaultdict
import re import re
import warnings
from six import iteritems
from jsonschema.exceptions import ValidationError 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._format import oas30_format_checker
from openapi_core.schema.schemas.enums import SchemaType from openapi_core.schema.schemas.enums import SchemaType
from openapi_core.schema.schemas.exceptions import ( from openapi_core.schema.schemas.exceptions import (
CastError, InvalidSchemaValue, 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.util import forcebool
from openapi_core.schema.schemas.validators import OAS30Validator from openapi_core.schema.schemas.validators import OAS30Validator
from openapi_core.unmarshalling.schemas.exceptions import (
UnmarshalValueError,
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -37,20 +36,17 @@ class Schema(object):
SchemaType.BOOLEAN: forcebool, SchemaType.BOOLEAN: forcebool,
} }
DEFAULT_UNMARSHAL_CALLABLE_GETTER = {
}
def __init__( def __init__(
self, schema_type=None, model=None, properties=None, items=None, self, schema_type=None, properties=None, items=None,
schema_format=None, required=None, default=None, nullable=False, schema_format=None, required=None, default=NoValue, nullable=False,
enum=None, deprecated=False, all_of=None, one_of=None, enum=None, deprecated=False, all_of=None, one_of=None,
additional_properties=True, min_items=None, max_items=None, additional_properties=True, min_items=None, max_items=None,
min_length=None, max_length=None, pattern=None, unique_items=False, min_length=None, max_length=None, pattern=None, unique_items=False,
minimum=None, maximum=None, multiple_of=None, minimum=None, maximum=None, multiple_of=None,
exclusive_minimum=False, exclusive_maximum=False, 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.type = SchemaType(schema_type)
self.model = model
self.properties = properties and dict(properties) or {} self.properties = properties and dict(properties) or {}
self.items = items self.items = items
self.format = schema_format self.format = schema_format
@ -79,6 +75,8 @@ class Schema(object):
self.max_properties = int(max_properties)\ self.max_properties = int(max_properties)\
if max_properties is not None else None if max_properties is not None else None
self.extensions = extensions and dict(extensions) or {}
self._all_required_properties_cache = None self._all_required_properties_cache = None
self._all_optional_properties_cache = None self._all_optional_properties_cache = None
@ -95,6 +93,9 @@ class Schema(object):
def __getitem__(self, name): def __getitem__(self, name):
return self.properties[name] return self.properties[name]
def has_default(self):
return self.default is not NoValue
def get_all_properties(self): def get_all_properties(self):
properties = self.properties.copy() properties = self.properties.copy()
@ -108,32 +109,6 @@ class Schema(object):
all_properties = self.get_all_properties() all_properties = self.get_all_properties()
return set(all_properties.keys()) 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): def get_cast_mapping(self):
mapping = self.TYPE_CAST_CALLABLE_GETTER.copy() mapping = self.TYPE_CAST_CALLABLE_GETTER.copy()
mapping.update({ mapping.update({
@ -144,7 +119,7 @@ class Schema(object):
def cast(self, value): def cast(self, value):
"""Cast value from string to schema type""" """Cast value from string to schema type"""
if value is None: if value in (None, NoValue):
return value return value
cast_mapping = self.get_cast_mapping() cast_mapping = self.get_cast_mapping()
@ -158,28 +133,6 @@ class Schema(object):
def _cast_collection(self, value): def _cast_collection(self, value):
return list(map(self.items.cast, 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): def get_validator(self, resolver=None):
return OAS30Validator( return OAS30Validator(
self.__dict__, self.__dict__,
@ -197,162 +150,13 @@ class Schema(object):
def unmarshal(self, value, custom_formatters=None, strict=True): def unmarshal(self, value, custom_formatters=None, strict=True):
"""Unmarshal parameter from the value.""" """Unmarshal parameter from the value."""
if self.deprecated: from openapi_core.unmarshalling.schemas.factories import (
warnings.warn("The schema is deprecated", DeprecationWarning) SchemaUnmarshallersFactory,
if value is None: )
if not self.nullable: unmarshallers_factory = SchemaUnmarshallersFactory(
raise UnmarshalError( custom_formatters)
"Null value for non-nullable schema", value, self.type) unmarshaller = unmarshallers_factory.create(self)
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]
try: try:
unmarshalled = unmarshal_callable(value) return unmarshaller(value, strict=strict)
except ValueError as exc: except ValueError as exc:
raise UnmarshalValueError(value, self.type, 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 import attr
NoValue = object()
@attr.s(hash=True) @attr.s(hash=True)
class Contribution(object): class Contribution(object):
src_prop_name = attr.ib() 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""" """OpenAPI core schemas util module"""
from base64 import b64decode
import datetime
from distutils.util import strtobool from distutils.util import strtobool
from six import string_types
from json import dumps from json import dumps
from six import string_types, text_type, integer_types
import strict_rfc3339
from uuid import UUID
def forcebool(val): def forcebool(val):
@ -17,29 +13,3 @@ def forcebool(val):
def dicthash(d): def dicthash(d):
return hash(dumps(d, sort_keys=True)) 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 @attr.s
class RequestParameters(object): 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) path = attr.ib(factory=dict)
query = attr.ib(factory=ImmutableMultiDict) query = attr.ib(factory=ImmutableMultiDict)
header = attr.ib(factory=dict) 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.operations.exceptions import InvalidOperation
from openapi_core.schema.parameters.exceptions import ( from openapi_core.schema.parameters.exceptions import (
OpenAPIParameterError, MissingRequiredParameter, OpenAPIParameterError, MissingRequiredParameter, MissingParameter,
) )
from openapi_core.schema.paths.exceptions import InvalidPath from openapi_core.schema.paths.exceptions import InvalidPath
from openapi_core.schema.request_bodies.exceptions import MissingRequestBody from openapi_core.schema.request_bodies.exceptions import MissingRequestBody
@ -74,14 +74,16 @@ class RequestValidator(object):
except MissingRequiredParameter as exc: except MissingRequiredParameter as exc:
errors.append(exc) errors.append(exc)
continue continue
except OpenAPIParameterError: except MissingParameter:
continue if not param.schema or not param.schema.has_default():
continue
try: casted = param.schema.default
casted = param.cast(raw_value) else:
except OpenAPIParameterError as exc: try:
errors.append(exc) casted = param.cast(raw_value)
continue except OpenAPIParameterError as exc:
errors.append(exc)
continue
try: try:
unmarshalled = param.unmarshal( unmarshalled = param.unmarshal(

View file

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

View file

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