openapi-core/openapi_core/schema/schemas/models.py

408 lines
15 KiB
Python
Raw Normal View History

2018-04-17 12:18:40 +00:00
"""OpenAPI core schemas models module"""
2018-08-24 14:57:41 +00:00
import attr
import functools
2017-09-21 11:51:37 +00:00
import logging
from collections import defaultdict
2018-08-22 10:51:06 +00:00
from datetime import date, datetime
2019-02-26 16:49:25 +00:00
from uuid import UUID
import re
2017-10-17 13:23:26 +00:00
import warnings
2018-08-17 14:54:01 +00:00
from six import iteritems, integer_types, binary_type, text_type
2019-09-03 00:38:19 +00:00
from jsonschema.exceptions import ValidationError
2017-09-21 11:51:37 +00:00
2018-04-17 12:18:40 +00:00
from openapi_core.extensions.models.factories import ModelFactory
2019-09-03 00:38:19 +00:00
from openapi_core.schema.schemas._format import oas30_format_checker
from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType
2018-04-18 10:39:03 +00:00
from openapi_core.schema.schemas.exceptions import (
InvalidSchemaValue, UndefinedSchemaProperty, MissingSchemaProperty,
2018-08-05 12:40:34 +00:00
OpenAPISchemaError, NoOneOfSchema, MultipleOneOfSchema, NoValidSchema,
2018-09-12 16:43:31 +00:00
UndefinedItemsSchema, InvalidCustomFormatSchemaValue, InvalidSchemaProperty,
2019-05-23 11:48:45 +00:00
UnmarshallerStrictTypeError,
2018-04-18 10:39:03 +00:00
)
2018-08-22 10:51:06 +00:00
from openapi_core.schema.schemas.util import (
2019-03-07 21:55:27 +00:00
forcebool, format_date, format_datetime, format_byte, format_uuid,
2019-05-21 11:54:13 +00:00
format_number,
2018-08-22 10:51:06 +00:00
)
2018-08-17 14:54:01 +00:00
from openapi_core.schema.schemas.validators import (
2019-09-03 00:38:19 +00:00
TypeValidator, AttributeValidator, OAS30Validator,
2018-08-17 14:54:01 +00:00
)
2017-09-21 11:51:37 +00:00
log = logging.getLogger(__name__)
2018-09-05 11:39:10 +00:00
2018-08-24 14:57:41 +00:00
@attr.s
2018-09-05 11:39:10 +00:00
class Format(object):
unmarshal = attr.ib()
2018-08-24 14:57:41 +00:00
validate = attr.ib()
2018-02-28 12:01:05 +00:00
2017-09-21 11:51:37 +00:00
class Schema(object):
"""Represents an OpenAPI Schema."""
2019-09-02 22:14:37 +00:00
TYPE_CAST_CALLABLE_GETTER = {
SchemaType.INTEGER: int,
SchemaType.NUMBER: float,
SchemaType.BOOLEAN: forcebool,
}
DEFAULT_UNMARSHAL_CALLABLE_GETTER = {
2018-04-17 12:18:40 +00:00
}
2018-08-24 14:57:41 +00:00
STRING_FORMAT_CALLABLE_GETTER = {
2018-09-05 11:39:10 +00:00
SchemaFormat.NONE: Format(text_type, TypeValidator(text_type)),
Add missing STRING_FORMAT_CALLABLE_GETTER: SchemaFormat.PASSWORD `password` is a valid OpenAPIv3 string format, that is used as a UI hint for frontend clients to mask the input field. It was already present in the `SchemaFormat` enum, but it was not handled in `_unmarshal_string` that uses `STRING_FORMAT_CALLABLE_GETTER` to decide how to unmarshal a string, which would result in an error like this one: ``` Traceback (most recent call last): [... snip ...] File ".venv/lib/python3.7/site-packages/openapi_core/validation/request/validators.py", line 37, in validate body, body_errors = self._get_body(request, operation) File ".venv/lib/python3.7/site-packages/openapi_core/validation/request/validators.py", line 82, in _get_body body = media_type.unmarshal(raw_body, self.custom_formatters) File ".venv/lib/python3.7/site-packages/openapi_core/schema/media_types/models.py", line 45, in unmarshal unmarshalled = self.schema.unmarshal(deserialized, custom_formatters=custom_formatters) File ".venv/lib/python3.7/site-packages/openapi_core/schema/schemas/models.py", line 189, in unmarshal casted = self.cast(value, custom_formatters=custom_formatters, strict=strict) File ".venv/lib/python3.7/site-packages/openapi_core/schema/schemas/models.py", line 179, in cast return cast_callable(value) File ".venv/lib/python3.7/site-packages/openapi_core/schema/schemas/models.py", line 295, in _unmarshal_object value, custom_formatters=custom_formatters) File ".venv/lib/python3.7/site-packages/openapi_core/schema/schemas/models.py", line 335, in _unmarshal_properties prop_value, custom_formatters=custom_formatters) File ".venv/lib/python3.7/site-packages/openapi_core/schema/schemas/models.py", line 189, in unmarshal casted = self.cast(value, custom_formatters=custom_formatters, strict=strict) File ".venv/lib/python3.7/site-packages/openapi_core/schema/schemas/models.py", line 179, in cast return cast_callable(value) File ".venv/lib/python3.7/site-packages/openapi_core/schema/schemas/models.py", line 295, in _unmarshal_object value, custom_formatters=custom_formatters) File ".venv/lib/python3.7/site-packages/openapi_core/schema/schemas/models.py", line 335, in _unmarshal_properties prop_value, custom_formatters=custom_formatters) File ".venv/lib/python3.7/site-packages/openapi_core/schema/schemas/models.py", line 189, in unmarshal casted = self.cast(value, custom_formatters=custom_formatters, strict=strict) File ".venv/lib/python3.7/site-packages/openapi_core/schema/schemas/models.py", line 179, in cast return cast_callable(value) File ".venv/lib/python3.7/site-packages/openapi_core/schema/schemas/models.py", line 215, in _unmarshal_string formatstring = self.STRING_FORMAT_CALLABLE_GETTER[schema_format] KeyError: <SchemaFormat.PASSWORD: 'password'> ```
2019-04-26 19:22:54 +00:00
SchemaFormat.PASSWORD: Format(text_type, TypeValidator(text_type)),
2019-02-26 16:49:25 +00:00
SchemaFormat.DATE: Format(
format_date, TypeValidator(date, exclude=datetime)),
2018-09-05 11:39:10 +00:00
SchemaFormat.DATETIME: Format(format_datetime, TypeValidator(datetime)),
SchemaFormat.BINARY: Format(binary_type, TypeValidator(binary_type)),
SchemaFormat.UUID: Format(format_uuid, TypeValidator(UUID)),
2019-03-07 21:55:27 +00:00
SchemaFormat.BYTE: Format(format_byte, TypeValidator(text_type)),
2018-08-22 10:51:06 +00:00
}
2019-05-21 11:54:13 +00:00
NUMBER_FORMAT_CALLABLE_GETTER = {
SchemaFormat.NONE: Format(format_number, TypeValidator(
integer_types + (float, ), exclude=bool)),
SchemaFormat.FLOAT: Format(float, TypeValidator(float)),
SchemaFormat.DOUBLE: Format(float, TypeValidator(float)),
}
2018-08-22 09:05:15 +00:00
TYPE_VALIDATOR_CALLABLE_GETTER = {
SchemaType.ANY: lambda x: True,
2018-08-17 14:54:01 +00:00
SchemaType.BOOLEAN: TypeValidator(bool),
SchemaType.INTEGER: TypeValidator(integer_types, exclude=bool),
2019-05-21 11:54:13 +00:00
SchemaType.NUMBER: TypeValidator(
integer_types + (float, ), exclude=bool),
2018-08-22 10:51:06 +00:00
SchemaType.STRING: TypeValidator(
text_type, date, datetime, binary_type, UUID),
2018-08-17 14:54:01 +00:00
SchemaType.ARRAY: TypeValidator(list, tuple),
2018-08-22 09:05:15 +00:00
SchemaType.OBJECT: AttributeValidator('__dict__'),
2018-08-17 14:54:01 +00:00
}
2017-09-21 11:51:37 +00:00
def __init__(
2018-04-04 10:26:21 +00:00
self, schema_type=None, model=None, properties=None, items=None,
2017-11-14 13:36:05 +00:00
schema_format=None, required=None, default=None, nullable=False,
2018-05-30 10:15:17 +00:00
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,
2019-09-03 00:38:19 +00:00
min_properties=None, max_properties=None, _source=None):
2018-08-02 18:30:51 +00:00
self.type = SchemaType(schema_type)
self.model = model
2017-09-21 11:51:37 +00:00
self.properties = properties and dict(properties) or {}
self.items = items
2018-05-30 08:41:34 +00:00
self.format = schema_format
2017-11-06 16:50:00 +00:00
self.required = required or []
2017-09-25 14:15:00 +00:00
self.default = default
2017-10-17 13:02:21 +00:00
self.nullable = nullable
2017-10-17 13:23:26 +00:00
self.enum = enum
2017-10-17 13:33:46 +00:00
self.deprecated = deprecated
2017-11-06 16:50:00 +00:00
self.all_of = all_of and list(all_of) or []
2018-05-25 15:32:09 +00:00
self.one_of = one_of and list(one_of) or []
2018-05-30 10:15:17 +00:00
self.additional_properties = additional_properties
self.min_items = int(min_items) if min_items is not None else None
self.max_items = int(max_items) if max_items is not None else None
self.min_length = int(min_length) if min_length is not None else None
self.max_length = int(max_length) if max_length is not None else None
self.pattern = pattern and re.compile(pattern) or None
self.unique_items = unique_items
self.minimum = int(minimum) if minimum is not None else None
self.maximum = int(maximum) if maximum is not None else None
self.multiple_of = int(multiple_of)\
if multiple_of is not None else None
self.exclusive_minimum = exclusive_minimum
self.exclusive_maximum = exclusive_maximum
self.min_properties = int(min_properties)\
if min_properties is not None else None
self.max_properties = int(max_properties)\
if max_properties is not None else None
2018-05-25 15:32:09 +00:00
self._all_required_properties_cache = None
self._all_optional_properties_cache = None
2017-09-21 11:51:37 +00:00
2019-09-03 00:38:19 +00:00
self._source = _source
2019-09-11 21:47:16 +00:00
@property
def __dict__(self):
return self._source or self.to_dict()
def to_dict(self):
from openapi_core.schema.schemas.factories import SchemaDictFactory
return SchemaDictFactory().create(self)
2017-09-21 11:51:37 +00:00
def __getitem__(self, name):
return self.properties[name]
2017-11-06 16:50:00 +00:00
def get_all_properties(self):
properties = self.properties.copy()
for subschema in self.all_of:
subschema_props = subschema.get_all_properties()
properties.update(subschema_props)
return properties
2018-05-25 15:32:09 +00:00
def get_all_properties_names(self):
all_properties = self.get_all_properties()
return set(all_properties.keys())
def get_all_required_properties(self):
2018-05-25 15:32:09 +00:00
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)
2018-08-05 12:40:34 +00:00
for prop_name, val in iteritems(all_properties)
2018-05-25 15:32:09 +00:00
if prop_name in required
)
def get_all_required_properties_names(self):
2018-07-15 21:22:44 +00:00
required = self.required[:]
for subschema in self.all_of:
subschema_req = subschema.get_all_required_properties()
required += subschema_req
2018-05-25 15:32:09 +00:00
return set(required)
2019-09-02 22:14:37 +00:00
def are_additional_properties_allowed(self, one_of_schema=None):
return (
(self.additional_properties is not False) and
(one_of_schema is None or
one_of_schema.additional_properties is not False)
)
def get_cast_mapping(self):
mapping = self.TYPE_CAST_CALLABLE_GETTER.copy()
mapping.update({
SchemaType.ARRAY: self._cast_collection,
})
return defaultdict(lambda: lambda x: x, mapping)
def cast(self, value):
"""Cast value from string to schema type"""
if value is None:
return value
cast_mapping = self.get_cast_mapping()
cast_callable = cast_mapping[self.type]
try:
return cast_callable(value)
except ValueError:
raise InvalidSchemaValue(
"Failed to cast value {value} to type {type}", value, self.type)
def _cast_collection(self, value):
return list(map(self.items.cast, value))
def get_unmarshal_mapping(self, custom_formatters=None, strict=True):
2019-05-23 11:48:45 +00:00
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()
)
2018-09-05 11:39:10 +00:00
pass_defaults = lambda f: functools.partial(
2019-03-22 01:51:47 +00:00
f, custom_formatters=custom_formatters, strict=strict)
2019-09-02 22:14:37 +00:00
mapping = self.DEFAULT_UNMARSHAL_CALLABLE_GETTER.copy()
2019-05-23 11:48:45 +00:00
mapping.update(primitive_unmarshallers_partial)
mapping.update({
2018-09-05 11:39:10 +00:00
SchemaType.ANY: pass_defaults(self._unmarshal_any),
SchemaType.ARRAY: pass_defaults(self._unmarshal_collection),
SchemaType.OBJECT: pass_defaults(self._unmarshal_object),
})
2017-09-21 11:51:37 +00:00
return defaultdict(lambda: lambda x: x, mapping)
2019-09-03 00:38:19 +00:00
def get_validator(self, resolver=None):
return OAS30Validator(
2019-09-11 21:47:16 +00:00
self.__dict__, resolver=resolver, format_checker=oas30_format_checker)
2019-09-03 00:38:19 +00:00
def validate(self, value, resolver=None):
validator = self.get_validator(resolver=resolver)
try:
return validator.validate(value)
except ValidationError:
# TODO: pass validation errors
raise InvalidSchemaValue("Value not valid for schema", value, self.type)
2019-09-02 22:14:37 +00:00
def unmarshal(self, value, custom_formatters=None, strict=True):
"""Unmarshal parameter from the value."""
if self.deprecated:
warnings.warn("The schema is deprecated", DeprecationWarning)
2017-09-21 11:51:37 +00:00
if value is None:
2017-10-17 13:02:21 +00:00
if not self.nullable:
raise InvalidSchemaValue("Null value for non-nullable schema", value, self.type)
2017-10-17 13:02:21 +00:00
return self.default
2017-09-21 11:51:37 +00:00
2019-05-23 11:48:45 +00:00
if self.enum and value not in self.enum:
raise InvalidSchemaValue(
"Value {value} not in enum choices: {type}", value, self.enum)
2019-09-02 22:14:37 +00:00
unmarshal_mapping = self.get_unmarshal_mapping(
2019-03-22 01:51:47 +00:00
custom_formatters=custom_formatters, strict=strict)
2017-09-21 11:51:37 +00:00
if self.type is not SchemaType.STRING and value == '':
2017-09-21 11:51:37 +00:00
return None
2019-09-02 22:14:37 +00:00
unmarshal_callable = unmarshal_mapping[self.type]
2017-09-21 11:51:37 +00:00
try:
2019-09-02 22:14:37 +00:00
unmarshalled = unmarshal_callable(value)
2019-05-23 11:48:45 +00:00
except UnmarshallerStrictTypeError:
raise InvalidSchemaValue(
"Value {value} is not of type {type}", value, self.type)
2017-09-21 11:51:37 +00:00
except ValueError:
2018-04-18 10:39:03 +00:00
raise InvalidSchemaValue(
2019-09-03 00:38:19 +00:00
"Failed to unmarshal value {value} to type {type}", value, self.type)
2017-09-21 11:51:37 +00:00
2019-09-02 22:14:37 +00:00
return unmarshalled
2017-09-21 11:51:37 +00:00
2019-05-23 11:48:45 +00:00
def get_primitive_unmarshallers(self, **options):
from openapi_core.schema.schemas.unmarshallers import (
StringUnmarshaller, BooleanUnmarshaller, IntegerUnmarshaller,
NumberUnmarshaller,
)
2019-05-21 11:54:13 +00:00
2019-05-23 11:48:45 +00:00
unmarshallers_classes = {
SchemaType.STRING: StringUnmarshaller,
SchemaType.BOOLEAN: BooleanUnmarshaller,
SchemaType.INTEGER: IntegerUnmarshaller,
SchemaType.NUMBER: NumberUnmarshaller,
}
2019-03-22 01:51:47 +00:00
2019-05-23 11:48:45 +00:00
unmarshallers = dict(
(t, klass(**options))
for t, klass in unmarshallers_classes.items()
)
2019-03-22 01:51:47 +00:00
2019-05-23 11:48:45 +00:00
return unmarshallers
2019-03-22 01:51:47 +00:00
def _unmarshal_any(self, value, custom_formatters=None, strict=True):
2018-08-05 12:40:34 +00:00
types_resolve_order = [
SchemaType.OBJECT, SchemaType.ARRAY, SchemaType.BOOLEAN,
SchemaType.INTEGER, SchemaType.NUMBER, SchemaType.STRING,
]
2019-09-02 22:14:37 +00:00
unmarshal_mapping = self.get_unmarshal_mapping()
2019-03-02 20:44:01 +00:00
if self.one_of:
result = None
for subschema in self.one_of:
try:
2019-09-02 22:14:37 +00:00
unmarshalled = subschema.unmarshal(value, custom_formatters)
2019-03-02 20:44:01 +00:00
except (OpenAPISchemaError, TypeError, ValueError):
continue
else:
if result is not None:
raise MultipleOneOfSchema(self.type)
2019-09-02 22:14:37 +00:00
result = unmarshalled
2019-03-02 20:44:01 +00:00
if result is None:
raise NoOneOfSchema(self.type)
return result
else:
for schema_type in types_resolve_order:
2019-09-02 22:14:37 +00:00
unmarshal_callable = unmarshal_mapping[schema_type]
2019-03-02 20:44:01 +00:00
try:
2019-09-02 22:14:37 +00:00
return unmarshal_callable(value)
2019-05-23 11:48:45 +00:00
except UnmarshallerStrictTypeError:
continue
2019-09-03 00:38:19 +00:00
except (OpenAPISchemaError, TypeError):
2019-03-02 20:44:01 +00:00
continue
2018-08-05 12:40:34 +00:00
raise NoValidSchema(value)
2018-08-05 12:40:34 +00:00
2019-03-22 01:51:47 +00:00
def _unmarshal_collection(self, value, custom_formatters=None, strict=True):
if not isinstance(value, (list, tuple)):
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
f = functools.partial(
self.items.unmarshal,
custom_formatters=custom_formatters, strict=strict,
)
2018-08-24 14:57:41 +00:00
return list(map(f, value))
2018-09-05 11:39:10 +00:00
def _unmarshal_object(self, value, model_factory=None,
2019-03-22 01:51:47 +00:00
custom_formatters=None, strict=True):
2018-04-23 18:50:29 +00:00
if not isinstance(value, (dict, )):
raise InvalidSchemaValue("Value {value} is not of type {type}", value, self.type)
2018-08-21 17:33:24 +00:00
model_factory = model_factory or ModelFactory()
2018-05-25 15:32:09 +00:00
if self.one_of:
properties = None
for one_of_schema in self.one_of:
try:
found_props = self._unmarshal_properties(
2018-09-05 11:39:10 +00:00
value, one_of_schema, custom_formatters=custom_formatters)
2018-05-25 15:32:09 +00:00
except OpenAPISchemaError:
pass
else:
if properties is not None:
raise MultipleOneOfSchema(self.type)
2018-05-25 15:32:09 +00:00
properties = found_props
if properties is None:
raise NoOneOfSchema(self.type)
2018-05-25 15:32:09 +00:00
else:
2018-09-05 11:39:10 +00:00
properties = self._unmarshal_properties(
value, custom_formatters=custom_formatters)
2018-05-25 15:32:09 +00:00
2018-08-21 17:33:24 +00:00
return model_factory.create(properties, name=self.model)
2017-09-25 14:15:00 +00:00
2018-09-05 11:39:10 +00:00
def _unmarshal_properties(self, value, one_of_schema=None,
2019-03-22 01:51:47 +00:00
custom_formatters=None, strict=True):
2018-05-25 15:32:09 +00:00
all_props = self.get_all_properties()
all_props_names = self.get_all_properties_names()
all_req_props_names = self.get_all_required_properties_names()
2017-09-25 14:15:00 +00:00
2018-05-25 15:32:09 +00:00
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)
extra_props_allowed = self.are_additional_properties_allowed(
one_of_schema)
if extra_props and not extra_props_allowed:
raise UndefinedSchemaProperty(extra_props)
2017-09-25 14:15:00 +00:00
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)
2018-05-30 10:15:17 +00:00
2018-05-25 15:32:09 +00:00
for prop_name, prop in iteritems(all_props):
2017-09-25 14:15:00 +00:00
try:
prop_value = value[prop_name]
except KeyError:
2018-05-25 15:32:09 +00:00
if prop_name in all_req_props_names:
raise MissingSchemaProperty(prop_name)
2017-10-17 13:02:21 +00:00
if not prop.nullable and not prop.default:
continue
2017-09-25 14:15:00 +00:00
prop_value = prop.default
2018-09-12 16:43:31 +00:00
try:
properties[prop_name] = prop.unmarshal(
prop_value, custom_formatters=custom_formatters)
except OpenAPISchemaError as exc:
raise InvalidSchemaProperty(prop_name, exc)
2018-08-21 17:33:24 +00:00
2018-05-25 15:32:09 +00:00
return properties