# coding: utf-8

"""
    NetHSM

    All endpoints expect exactly the specified JSON. Additional properties will cause a Bad Request Error (400). All HTTP errors contain a JSON structure with an explanation of type string. All <a href=\"https://tools.ietf.org/html/rfc4648#section-4\">base64</a> encoded values are Big Endian.  # noqa: E501

    The version of the OpenAPI document: v1
    Generated by: https://openapi-generator.tech
"""

from collections import defaultdict
from datetime import date, datetime, timedelta  # noqa: F401
import functools
import decimal
import io
import re
import types
import typing
import uuid

from dateutil.parser.isoparser import isoparser, _takes_ascii
import frozendict

from pynitrokey.nethsm.client.exceptions import (
    ApiTypeError,
    ApiValueError,
)
from pynitrokey.nethsm.client.configuration import (
    Configuration,
)


class Unset(object):
    """
    An instance of this class is set as the default value for object type(dict) properties that are optional
    When a property has an unset value, that property will not be assigned in the dict
    """
    pass

unset = Unset()

none_type = type(None)
file_type = io.IOBase


class FileIO(io.FileIO):
    """
    A class for storing files
    Note: this class is not immutable
    """

    def __new__(cls, arg: typing.Union[io.FileIO, io.BufferedReader]):
        if isinstance(arg, (io.FileIO, io.BufferedReader)):
            if arg.closed:
                raise ApiValueError('Invalid file state; file is closed and must be open')
            arg.close()
            inst = super(FileIO, cls).__new__(cls, arg.name)
            super(FileIO, inst).__init__(arg.name)
            return inst
        raise ApiValueError('FileIO must be passed arg which contains the open file')

    def __init__(self, arg: typing.Union[io.FileIO, io.BufferedReader]):
        pass


def update(d: dict, u: dict):
    """
    Adds u to d
    Where each dict is defaultdict(set)
    """
    if not u:
        return d
    for k, v in u.items():
        if k not in d:
            d[k] = v
        else:
            d[k] = d[k] | v


class ValidationMetadata(frozendict.frozendict):
    """
    A class storing metadata that is needed to validate OpenApi Schema payloads
    """
    def __new__(
        cls,
        path_to_item: typing.Tuple[typing.Union[str, int], ...] = tuple(['args[0]']),
        from_server: bool = False,
        configuration: typing.Optional[Configuration] = None,
        seen_classes: typing.FrozenSet[typing.Type] = frozenset(),
        validated_path_to_schemas: typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Set[typing.Type]] = frozendict.frozendict()
    ):
        """
        Args:
            path_to_item: the path to the current data being instantiated.
                For {'a': [1]} if the code is handling, 1, then the path is ('args[0]', 'a', 0)
                This changes from location to location
            from_server: whether or not this data came form the server
                True when receiving server data
                False when instantiating model with client side data not form the server
                This does not change from location to location
            configuration: the Configuration instance to use
                This is needed because in Configuration:
                - one can disable validation checking
                This does not change from location to location
            seen_classes: when deserializing data that matches multiple schemas, this is used to store
                the schemas that have been traversed. This is used to stop processing when a cycle is seen.
                This changes from location to location
            validated_path_to_schemas: stores the already validated schema classes for a given path location
                This does not change from location to location
        """
        return super().__new__(
            cls,
            path_to_item=path_to_item,
            from_server=from_server,
            configuration=configuration,
            seen_classes=seen_classes,
            validated_path_to_schemas=validated_path_to_schemas
        )

    def validation_ran_earlier(self, cls: type) -> bool:
        validated_schemas = self.validated_path_to_schemas.get(self.path_to_item, set())
        validation_ran_earlier = validated_schemas and cls in validated_schemas
        if validation_ran_earlier:
            return True
        if cls in self.seen_classes:
            return True
        return False

    @property
    def path_to_item(self) -> typing.Tuple[typing.Union[str, int], ...]:
        return self.get('path_to_item')

    @property
    def from_server(self) -> bool:
        return self.get('from_server')

    @property
    def configuration(self) -> typing.Optional[Configuration]:
        return self.get('configuration')

    @property
    def seen_classes(self) -> typing.FrozenSet[typing.Type]:
        return self.get('seen_classes')

    @property
    def validated_path_to_schemas(self) -> typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Set[typing.Type]]:
        return self.get('validated_path_to_schemas')


class Singleton:
    """
    Enums and singletons are the same
    The same instance is returned for a given key of (cls, arg)
    """
    _instances = {}

    def __new__(cls, arg: typing.Any, **kwargs):
        """
        cls base classes: BoolClass, NoneClass, str, decimal.Decimal
        The 3rd key is used in the tuple below for a corner case where an enum contains integer 1
        However 1.0  can also be ingested into that enum schema because 1.0 == 1 and
        Decimal('1.0') == Decimal('1')
        But if we omitted the 3rd value in the key, then Decimal('1.0') would be stored as Decimal('1')
        and json serializing that instance would be '1' rather than the expected '1.0'
        Adding the 3rd value, the str of arg ensures that 1.0 -> Decimal('1.0') which is serialized as 1.0
        """
        key = (cls, arg, str(arg))
        if key not in cls._instances:
            if isinstance(arg, (none_type, bool, BoolClass, NoneClass)):
                inst = super().__new__(cls)
                cls._instances[key] = inst
            else:
                cls._instances[key] = super().__new__(cls, arg)
        return cls._instances[key]

    def __repr__(self):
        if isinstance(self, NoneClass):
            return f'<{self.__class__.__name__}: None>'
        elif isinstance(self, BoolClass):
            if bool(self):
                return f'<{self.__class__.__name__}: True>'
            return f'<{self.__class__.__name__}: False>'
        return f'<{self.__class__.__name__}: {super().__repr__()}>'


class classproperty:

    def __init__(self, fget):
        self.fget = fget

    def __get__(self, owner_self, owner_cls):
        return self.fget(owner_cls)


class NoneClass(Singleton):
    @classproperty
    def NONE(cls):
        return cls(None)

    def __bool__(self) -> bool:
        return False


class BoolClass(Singleton):
    @classproperty
    def TRUE(cls):
        return cls(True)

    @classproperty
    def FALSE(cls):
        return cls(False)

    @functools.lru_cache()
    def __bool__(self) -> bool:
        for key, instance in self._instances.items():
            if self is instance:
                return bool(key[1])
        raise ValueError('Unable to find the boolean value of this instance')


class MetaOapgTyped:
    exclusive_maximum: typing.Union[int, float]
    inclusive_maximum: typing.Union[int, float]
    exclusive_minimum: typing.Union[int, float]
    inclusive_minimum: typing.Union[int, float]
    max_items: int
    min_items: int
    discriminator: typing.Dict[str, typing.Dict[str, typing.Type['Schema']]]


    class properties:
        # to hold object properties
        pass

    additional_properties: typing.Optional[typing.Type['Schema']]
    max_properties: int
    min_properties: int
    all_of: typing.List[typing.Type['Schema']]
    one_of: typing.List[typing.Type['Schema']]
    any_of: typing.List[typing.Type['Schema']]
    not_schema: typing.Type['Schema']
    max_length: int
    min_length: int
    items: typing.Type['Schema']


class Schema:
    """
    the base class of all swagger/openapi schemas/models
    """
    __inheritable_primitive_types_set = {decimal.Decimal, str, tuple, frozendict.frozendict, FileIO, bytes, BoolClass, NoneClass}
    _types: typing.Set[typing.Type]
    MetaOapg = MetaOapgTyped

    @staticmethod
    def __get_valid_classes_phrase(input_classes):
        """Returns a string phrase describing what types are allowed"""
        all_classes = list(input_classes)
        all_classes = sorted(all_classes, key=lambda cls: cls.__name__)
        all_class_names = [cls.__name__ for cls in all_classes]
        if len(all_class_names) == 1:
            return "is {0}".format(all_class_names[0])
        return "is one of [{0}]".format(", ".join(all_class_names))

    @staticmethod
    def _get_class_oapg(item_cls: typing.Union[types.FunctionType, staticmethod, typing.Type['Schema']]) -> typing.Type['Schema']:
        if isinstance(item_cls, types.FunctionType):
            # referenced schema
            return item_cls()
        elif isinstance(item_cls, staticmethod):
            # referenced schema
            return item_cls.__func__()
        return item_cls

    @classmethod
    def __type_error_message(
        cls, var_value=None, var_name=None, valid_classes=None, key_type=None
    ):
        """
        Keyword Args:
            var_value (any): the variable which has the type_error
            var_name (str): the name of the variable which has the typ error
            valid_classes (tuple): the accepted classes for current_item's
                                      value
            key_type (bool): False if our value is a value in a dict
                             True if it is a key in a dict
                             False if our item is an item in a tuple
        """
        key_or_value = "value"
        if key_type:
            key_or_value = "key"
        valid_classes_phrase = cls.__get_valid_classes_phrase(valid_classes)
        msg = "Invalid type. Required {1} type {2} and " "passed type was {3}".format(
            var_name,
            key_or_value,
            valid_classes_phrase,
            type(var_value).__name__,
        )
        return msg

    @classmethod
    def __get_type_error(cls, var_value, path_to_item, valid_classes, key_type=False):
        error_msg = cls.__type_error_message(
            var_name=path_to_item[-1],
            var_value=var_value,
            valid_classes=valid_classes,
            key_type=key_type,
        )
        return ApiTypeError(
            error_msg,
            path_to_item=path_to_item,
            valid_classes=valid_classes,
            key_type=key_type,
        )

    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: ValidationMetadata,
    ) -> typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Set[typing.Union['Schema', str, decimal.Decimal, BoolClass, NoneClass, frozendict.frozendict, tuple]]]:
        """
        Schema _validate_oapg
        All keyword validation except for type checking was done in calling stack frames
        If those validations passed, the validated classes are collected in path_to_schemas

        Returns:
            path_to_schemas: a map of path to schemas

        Raises:
            ApiValueError: when a string can't be converted into a date or datetime and it must be one of those classes
            ApiTypeError: when the input type is not in the list of allowed spec types
        """
        base_class = type(arg)
        if base_class not in cls._types:
            raise cls.__get_type_error(
                arg,
                validation_metadata.path_to_item,
                cls._types,
                key_type=False,
            )

        path_to_schemas = {validation_metadata.path_to_item: set()}
        path_to_schemas[validation_metadata.path_to_item].add(cls)
        path_to_schemas[validation_metadata.path_to_item].add(base_class)
        return path_to_schemas

    @staticmethod
    def _process_schema_classes_oapg(
        schema_classes: typing.Set[typing.Union['Schema', str, decimal.Decimal, BoolClass, NoneClass, frozendict.frozendict, tuple]]
    ):
        """
        Processes and mutates schema_classes
        If a SomeSchema is a subclass of DictSchema then remove DictSchema because it is already included
        """
        if len(schema_classes) < 2:
            return
        if len(schema_classes) > 2 and UnsetAnyTypeSchema in schema_classes:
            schema_classes.remove(UnsetAnyTypeSchema)
        x_schema = schema_type_classes & schema_classes
        if not x_schema:
            return
        x_schema = x_schema.pop()
        if any(c is not x_schema and issubclass(c, x_schema) for c in schema_classes):
            # needed to not have a mro error in get_new_class
            schema_classes.remove(x_schema)

    @classmethod
    def __get_new_cls(
        cls,
        arg,
        validation_metadata: ValidationMetadata
    ) -> typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Type['Schema']]:
        """
        Make a new dynamic class and return an instance of that class
        We are making an instance of cls, but instead of making cls
        make a new class, new_cls
        which includes dynamic bases including cls
        return an instance of that new class

        Dict property + List Item Assignment Use cases:
        1. value is NOT an instance of the required schema class
            the value is validated by _validate_oapg
            _validate_oapg returns a key value pair
            where the key is the path to the item, and the value will be the required manufactured class
            made out of the matching schemas
        2. value is an instance of the the correct schema type
            the value is NOT validated by _validate_oapg, _validate_oapg only checks that the instance is of the correct schema type
            for this value, _validate_oapg does NOT return an entry for it in _path_to_schemas
            and in list/dict _get_items_oapg,_get_properties_oapg the value will be directly assigned
            because value is of the correct type, and validation was run earlier when the instance was created
        """
        _path_to_schemas = {}
        if validation_metadata.validated_path_to_schemas:
            update(_path_to_schemas, validation_metadata.validated_path_to_schemas)
        if not validation_metadata.validation_ran_earlier(cls):
            other_path_to_schemas = cls._validate_oapg(arg, validation_metadata=validation_metadata)
            update(_path_to_schemas, other_path_to_schemas)
        # loop through it make a new class for each entry
        # do not modify the returned result because it is cached and we would be modifying the cached value
        path_to_schemas = {}
        for path, schema_classes in _path_to_schemas.items():
            """
            Use cases
            1. N number of schema classes + enum + type != bool/None, classes in path_to_schemas: tuple/frozendict.frozendict/str/Decimal/bytes/FileIo
                needs Singleton added
            2. N number of schema classes + enum + type == bool/None, classes in path_to_schemas: BoolClass/NoneClass
                Singleton already added
            3. N number of schema classes, classes in path_to_schemas: BoolClass/NoneClass/tuple/frozendict.frozendict/str/Decimal/bytes/FileIo
            """
            cls._process_schema_classes_oapg(schema_classes)
            enum_schema = any(
                issubclass(this_cls, EnumBase) for this_cls in schema_classes)
            inheritable_primitive_type = schema_classes.intersection(cls.__inheritable_primitive_types_set)
            chosen_schema_classes = schema_classes - inheritable_primitive_type
            suffix = tuple(inheritable_primitive_type)
            if enum_schema and suffix[0] not in {NoneClass, BoolClass}:
                suffix = (Singleton,) + suffix

            used_classes = tuple(sorted(chosen_schema_classes, key=lambda a_cls: a_cls.__name__)) + suffix
            mfg_cls = get_new_class(class_name='DynamicSchema', bases=used_classes)
            path_to_schemas[path] = mfg_cls

        return path_to_schemas

    @classmethod
    def _get_new_instance_without_conversion_oapg(
        cls,
        arg: typing.Any,
        path_to_item: typing.Tuple[typing.Union[str, int], ...],
        path_to_schemas: typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Type['Schema']]
    ):
        # We have a Dynamic class and we are making an instance of it
        if issubclass(cls, frozendict.frozendict) and issubclass(cls, DictBase):
            properties = cls._get_properties_oapg(arg, path_to_item, path_to_schemas)
            return super(Schema, cls).__new__(cls, properties)
        elif issubclass(cls, tuple) and issubclass(cls, ListBase):
            items = cls._get_items_oapg(arg, path_to_item, path_to_schemas)
            return super(Schema, cls).__new__(cls, items)
        """
        str = openapi str, date, and datetime
        decimal.Decimal = openapi int and float
        FileIO = openapi binary type and the user inputs a file
        bytes = openapi binary type and the user inputs bytes
        """
        return super(Schema, cls).__new__(cls, arg)

    @classmethod
    def from_openapi_data_oapg(
        cls,
        arg: typing.Union[
            str,
            date,
            datetime,
            int,
            float,
            decimal.Decimal,
            bool,
            None,
            'Schema',
            dict,
            frozendict.frozendict,
            tuple,
            list,
            io.FileIO,
            io.BufferedReader,
            bytes
        ],
        _configuration: typing.Optional[Configuration]
    ):
        """
        Schema from_openapi_data_oapg
        """
        from_server = True
        validated_path_to_schemas = {}
        arg = cast_to_allowed_types(arg, from_server, validated_path_to_schemas)
        validation_metadata = ValidationMetadata(
            from_server=from_server, configuration=_configuration, validated_path_to_schemas=validated_path_to_schemas)
        path_to_schemas = cls.__get_new_cls(arg, validation_metadata)
        new_cls = path_to_schemas[validation_metadata.path_to_item]
        new_inst = new_cls._get_new_instance_without_conversion_oapg(
            arg,
            validation_metadata.path_to_item,
            path_to_schemas
        )
        return new_inst

    @staticmethod
    def __get_input_dict(*args, **kwargs) -> frozendict.frozendict:
        input_dict = {}
        if args and isinstance(args[0], (dict, frozendict.frozendict)):
            input_dict.update(args[0])
        if kwargs:
            input_dict.update(kwargs)
        return frozendict.frozendict(input_dict)

    @staticmethod
    def __remove_unsets(kwargs):
        return {key: val for key, val in kwargs.items() if val is not unset}

    def __new__(cls, *args: typing.Union[dict, frozendict.frozendict, list, tuple, decimal.Decimal, float, int, str, date, datetime, bool, None, 'Schema'], _configuration: typing.Optional[Configuration] = None, **kwargs: typing.Union[dict, frozendict.frozendict, list, tuple, decimal.Decimal, float, int, str, date, datetime, bool, None, 'Schema', Unset]):
        """
        Schema __new__

        Args:
            args (int/float/decimal.Decimal/str/list/tuple/dict/frozendict.frozendict/bool/None): the value
            kwargs (str, int/float/decimal.Decimal/str/list/tuple/dict/frozendict.frozendict/bool/None): dict values
            _configuration: contains the Configuration that enables json schema validation keywords
                like minItems, minLength etc

        Note: double underscores are used here because pycharm thinks that these variables
        are instance properties if they are named normally :(
        """
        __kwargs = cls.__remove_unsets(kwargs)
        if not args and not __kwargs:
            raise TypeError(
                'No input given. args or kwargs must be given.'
            )
        if not __kwargs and args and not isinstance(args[0], dict):
            __arg = args[0]
        else:
            __arg = cls.__get_input_dict(*args, **__kwargs)
        __from_server = False
        __validated_path_to_schemas = {}
        __arg = cast_to_allowed_types(
            __arg, __from_server, __validated_path_to_schemas)
        __validation_metadata = ValidationMetadata(
            configuration=_configuration, from_server=__from_server, validated_path_to_schemas=__validated_path_to_schemas)
        __path_to_schemas = cls.__get_new_cls(__arg, __validation_metadata)
        __new_cls = __path_to_schemas[__validation_metadata.path_to_item]
        return __new_cls._get_new_instance_without_conversion_oapg(
            __arg,
            __validation_metadata.path_to_item,
            __path_to_schemas
        )

    def __init__(
        self,
        *args: typing.Union[
            dict, frozendict.frozendict, list, tuple, decimal.Decimal, float, int, str, date, datetime, bool, None, 'Schema'],
        _configuration: typing.Optional[Configuration] = None,
        **kwargs: typing.Union[
            dict, frozendict.frozendict, list, tuple, decimal.Decimal, float, int, str, date, datetime, bool, None, 'Schema', Unset
        ]
    ):
        """
        this is needed to fix 'Unexpected argument' warning in pycharm
        this code does nothing because all Schema instances are immutable
        this means that all input data is passed into and used in new, and after the new instance is made
        no new attributes are assigned and init is not used
        """
        pass

"""
import itertools
data_types = ('None', 'FrozenDict', 'Tuple', 'Str', 'Decimal', 'Bool')
type_to_cls = {
    'None': 'NoneClass',
    'FrozenDict': 'frozendict.frozendict',
    'Tuple': 'tuple',
    'Str': 'str',
    'Decimal': 'decimal.Decimal',
    'Bool': 'BoolClass'
}
cls_tuples = [v for v in itertools.combinations(data_types, 5)]
typed_classes = [f"class {''.join(cls_tuple)}Mixin({', '.join(type_to_cls[typ] for typ in cls_tuple)}):\n    pass" for cls_tuple in cls_tuples]
for cls in typed_classes:
    print(cls)
object_classes = [f"{''.join(cls_tuple)}Mixin = object" for cls_tuple in cls_tuples]
for cls in object_classes:
    print(cls)
"""
if typing.TYPE_CHECKING:
    # qty 1
    NoneMixin = NoneClass
    FrozenDictMixin = frozendict.frozendict
    TupleMixin = tuple
    StrMixin = str
    DecimalMixin = decimal.Decimal
    BoolMixin = BoolClass
    BytesMixin = bytes
    FileMixin = FileIO
    # qty 2
    class BinaryMixin(bytes, FileIO):
        pass
    class NoneFrozenDictMixin(NoneClass, frozendict.frozendict):
        pass
    class NoneTupleMixin(NoneClass, tuple):
        pass
    class NoneStrMixin(NoneClass, str):
        pass
    class NoneDecimalMixin(NoneClass, decimal.Decimal):
        pass
    class NoneBoolMixin(NoneClass, BoolClass):
        pass
    class FrozenDictTupleMixin(frozendict.frozendict, tuple):
        pass
    class FrozenDictStrMixin(frozendict.frozendict, str):
        pass
    class FrozenDictDecimalMixin(frozendict.frozendict, decimal.Decimal):
        pass
    class FrozenDictBoolMixin(frozendict.frozendict, BoolClass):
        pass
    class TupleStrMixin(tuple, str):
        pass
    class TupleDecimalMixin(tuple, decimal.Decimal):
        pass
    class TupleBoolMixin(tuple, BoolClass):
        pass
    class StrDecimalMixin(str, decimal.Decimal):
        pass
    class StrBoolMixin(str, BoolClass):
        pass
    class DecimalBoolMixin(decimal.Decimal, BoolClass):
        pass
    # qty 3
    class NoneFrozenDictTupleMixin(NoneClass, frozendict.frozendict, tuple):
        pass
    class NoneFrozenDictStrMixin(NoneClass, frozendict.frozendict, str):
        pass
    class NoneFrozenDictDecimalMixin(NoneClass, frozendict.frozendict, decimal.Decimal):
        pass
    class NoneFrozenDictBoolMixin(NoneClass, frozendict.frozendict, BoolClass):
        pass
    class NoneTupleStrMixin(NoneClass, tuple, str):
        pass
    class NoneTupleDecimalMixin(NoneClass, tuple, decimal.Decimal):
        pass
    class NoneTupleBoolMixin(NoneClass, tuple, BoolClass):
        pass
    class NoneStrDecimalMixin(NoneClass, str, decimal.Decimal):
        pass
    class NoneStrBoolMixin(NoneClass, str, BoolClass):
        pass
    class NoneDecimalBoolMixin(NoneClass, decimal.Decimal, BoolClass):
        pass
    class FrozenDictTupleStrMixin(frozendict.frozendict, tuple, str):
        pass
    class FrozenDictTupleDecimalMixin(frozendict.frozendict, tuple, decimal.Decimal):
        pass
    class FrozenDictTupleBoolMixin(frozendict.frozendict, tuple, BoolClass):
        pass
    class FrozenDictStrDecimalMixin(frozendict.frozendict, str, decimal.Decimal):
        pass
    class FrozenDictStrBoolMixin(frozendict.frozendict, str, BoolClass):
        pass
    class FrozenDictDecimalBoolMixin(frozendict.frozendict, decimal.Decimal, BoolClass):
        pass
    class TupleStrDecimalMixin(tuple, str, decimal.Decimal):
        pass
    class TupleStrBoolMixin(tuple, str, BoolClass):
        pass
    class TupleDecimalBoolMixin(tuple, decimal.Decimal, BoolClass):
        pass
    class StrDecimalBoolMixin(str, decimal.Decimal, BoolClass):
        pass
    # qty 4
    class NoneFrozenDictTupleStrMixin(NoneClass, frozendict.frozendict, tuple, str):
        pass
    class NoneFrozenDictTupleDecimalMixin(NoneClass, frozendict.frozendict, tuple, decimal.Decimal):
        pass
    class NoneFrozenDictTupleBoolMixin(NoneClass, frozendict.frozendict, tuple, BoolClass):
        pass
    class NoneFrozenDictStrDecimalMixin(NoneClass, frozendict.frozendict, str, decimal.Decimal):
        pass
    class NoneFrozenDictStrBoolMixin(NoneClass, frozendict.frozendict, str, BoolClass):
        pass
    class NoneFrozenDictDecimalBoolMixin(NoneClass, frozendict.frozendict, decimal.Decimal, BoolClass):
        pass
    class NoneTupleStrDecimalMixin(NoneClass, tuple, str, decimal.Decimal):
        pass
    class NoneTupleStrBoolMixin(NoneClass, tuple, str, BoolClass):
        pass
    class NoneTupleDecimalBoolMixin(NoneClass, tuple, decimal.Decimal, BoolClass):
        pass
    class NoneStrDecimalBoolMixin(NoneClass, str, decimal.Decimal, BoolClass):
        pass
    class FrozenDictTupleStrDecimalMixin(frozendict.frozendict, tuple, str, decimal.Decimal):
        pass
    class FrozenDictTupleStrBoolMixin(frozendict.frozendict, tuple, str, BoolClass):
        pass
    class FrozenDictTupleDecimalBoolMixin(frozendict.frozendict, tuple, decimal.Decimal, BoolClass):
        pass
    class FrozenDictStrDecimalBoolMixin(frozendict.frozendict, str, decimal.Decimal, BoolClass):
        pass
    class TupleStrDecimalBoolMixin(tuple, str, decimal.Decimal, BoolClass):
        pass
    # qty 5
    class NoneFrozenDictTupleStrDecimalMixin(NoneClass, frozendict.frozendict, tuple, str, decimal.Decimal):
        pass
    class NoneFrozenDictTupleStrBoolMixin(NoneClass, frozendict.frozendict, tuple, str, BoolClass):
        pass
    class NoneFrozenDictTupleDecimalBoolMixin(NoneClass, frozendict.frozendict, tuple, decimal.Decimal, BoolClass):
        pass
    class NoneFrozenDictStrDecimalBoolMixin(NoneClass, frozendict.frozendict, str, decimal.Decimal, BoolClass):
        pass
    class NoneTupleStrDecimalBoolMixin(NoneClass, tuple, str, decimal.Decimal, BoolClass):
        pass
    class FrozenDictTupleStrDecimalBoolMixin(frozendict.frozendict, tuple, str, decimal.Decimal, BoolClass):
        pass
    # qty 6
    class NoneFrozenDictTupleStrDecimalBoolMixin(NoneClass, frozendict.frozendict, tuple, str, decimal.Decimal, BoolClass):
        pass
    # qty 8
    class NoneFrozenDictTupleStrDecimalBoolFileBytesMixin(NoneClass, frozendict.frozendict, tuple, str, decimal.Decimal, BoolClass, FileIO, bytes):
        pass
else:
    # qty 1
    class NoneMixin:
        _types = {NoneClass}
    class FrozenDictMixin:
        _types = {frozendict.frozendict}
    class TupleMixin:
        _types = {tuple}
    class StrMixin:
        _types = {str}
    class DecimalMixin:
        _types = {decimal.Decimal}
    class BoolMixin:
        _types = {BoolClass}
    class BytesMixin:
        _types = {bytes}
    class FileMixin:
        _types = {FileIO}
    # qty 2
    class BinaryMixin:
        _types = {bytes, FileIO}
    class NoneFrozenDictMixin:
        _types = {NoneClass, frozendict.frozendict}
    class NoneTupleMixin:
        _types = {NoneClass, tuple}
    class NoneStrMixin:
        _types = {NoneClass, str}
    class NoneDecimalMixin:
        _types = {NoneClass, decimal.Decimal}
    class NoneBoolMixin:
        _types = {NoneClass, BoolClass}
    class FrozenDictTupleMixin:
        _types = {frozendict.frozendict, tuple}
    class FrozenDictStrMixin:
        _types = {frozendict.frozendict, str}
    class FrozenDictDecimalMixin:
        _types = {frozendict.frozendict, decimal.Decimal}
    class FrozenDictBoolMixin:
        _types = {frozendict.frozendict, BoolClass}
    class TupleStrMixin:
        _types = {tuple, str}
    class TupleDecimalMixin:
        _types = {tuple, decimal.Decimal}
    class TupleBoolMixin:
        _types = {tuple, BoolClass}
    class StrDecimalMixin:
        _types = {str, decimal.Decimal}
    class StrBoolMixin:
        _types = {str, BoolClass}
    class DecimalBoolMixin:
        _types = {decimal.Decimal, BoolClass}
    # qty 3
    class NoneFrozenDictTupleMixin:
        _types = {NoneClass, frozendict.frozendict, tuple}
    class NoneFrozenDictStrMixin:
        _types = {NoneClass, frozendict.frozendict, str}
    class NoneFrozenDictDecimalMixin:
        _types = {NoneClass, frozendict.frozendict, decimal.Decimal}
    class NoneFrozenDictBoolMixin:
        _types = {NoneClass, frozendict.frozendict, BoolClass}
    class NoneTupleStrMixin:
        _types = {NoneClass, tuple, str}
    class NoneTupleDecimalMixin:
        _types = {NoneClass, tuple, decimal.Decimal}
    class NoneTupleBoolMixin:
        _types = {NoneClass, tuple, BoolClass}
    class NoneStrDecimalMixin:
        _types = {NoneClass, str, decimal.Decimal}
    class NoneStrBoolMixin:
        _types = {NoneClass, str, BoolClass}
    class NoneDecimalBoolMixin:
        _types = {NoneClass, decimal.Decimal, BoolClass}
    class FrozenDictTupleStrMixin:
        _types = {frozendict.frozendict, tuple, str}
    class FrozenDictTupleDecimalMixin:
        _types = {frozendict.frozendict, tuple, decimal.Decimal}
    class FrozenDictTupleBoolMixin:
        _types = {frozendict.frozendict, tuple, BoolClass}
    class FrozenDictStrDecimalMixin:
        _types = {frozendict.frozendict, str, decimal.Decimal}
    class FrozenDictStrBoolMixin:
        _types = {frozendict.frozendict, str, BoolClass}
    class FrozenDictDecimalBoolMixin:
        _types = {frozendict.frozendict, decimal.Decimal, BoolClass}
    class TupleStrDecimalMixin:
        _types = {tuple, str, decimal.Decimal}
    class TupleStrBoolMixin:
        _types = {tuple, str, BoolClass}
    class TupleDecimalBoolMixin:
        _types = {tuple, decimal.Decimal, BoolClass}
    class StrDecimalBoolMixin:
        _types = {str, decimal.Decimal, BoolClass}
    # qty 4
    class NoneFrozenDictTupleStrMixin:
        _types = {NoneClass, frozendict.frozendict, tuple, str}
    class NoneFrozenDictTupleDecimalMixin:
        _types = {NoneClass, frozendict.frozendict, tuple, decimal.Decimal}
    class NoneFrozenDictTupleBoolMixin:
        _types = {NoneClass, frozendict.frozendict, tuple, BoolClass}
    class NoneFrozenDictStrDecimalMixin:
        _types = {NoneClass, frozendict.frozendict, str, decimal.Decimal}
    class NoneFrozenDictStrBoolMixin:
        _types = {NoneClass, frozendict.frozendict, str, BoolClass}
    class NoneFrozenDictDecimalBoolMixin:
        _types = {NoneClass, frozendict.frozendict, decimal.Decimal, BoolClass}
    class NoneTupleStrDecimalMixin:
        _types = {NoneClass, tuple, str, decimal.Decimal}
    class NoneTupleStrBoolMixin:
        _types = {NoneClass, tuple, str, BoolClass}
    class NoneTupleDecimalBoolMixin:
        _types = {NoneClass, tuple, decimal.Decimal, BoolClass}
    class NoneStrDecimalBoolMixin:
        _types = {NoneClass, str, decimal.Decimal, BoolClass}
    class FrozenDictTupleStrDecimalMixin:
        _types = {frozendict.frozendict, tuple, str, decimal.Decimal}
    class FrozenDictTupleStrBoolMixin:
        _types = {frozendict.frozendict, tuple, str, BoolClass}
    class FrozenDictTupleDecimalBoolMixin:
        _types = {frozendict.frozendict, tuple, decimal.Decimal, BoolClass}
    class FrozenDictStrDecimalBoolMixin:
        _types = {frozendict.frozendict, str, decimal.Decimal, BoolClass}
    class TupleStrDecimalBoolMixin:
        _types = {tuple, str, decimal.Decimal, BoolClass}
    # qty 5
    class NoneFrozenDictTupleStrDecimalMixin:
        _types = {NoneClass, frozendict.frozendict, tuple, str, decimal.Decimal}
    class NoneFrozenDictTupleStrBoolMixin:
        _types = {NoneClass, frozendict.frozendict, tuple, str, BoolClass}
    class NoneFrozenDictTupleDecimalBoolMixin:
        _types = {NoneClass, frozendict.frozendict, tuple, decimal.Decimal, BoolClass}
    class NoneFrozenDictStrDecimalBoolMixin:
        _types = {NoneClass, frozendict.frozendict, str, decimal.Decimal, BoolClass}
    class NoneTupleStrDecimalBoolMixin:
        _types = {NoneClass, tuple, str, decimal.Decimal, BoolClass}
    class FrozenDictTupleStrDecimalBoolMixin:
        _types = {frozendict.frozendict, tuple, str, decimal.Decimal, BoolClass}
    # qty 6
    class NoneFrozenDictTupleStrDecimalBoolMixin:
        _types = {NoneClass, frozendict.frozendict, tuple, str, decimal.Decimal, BoolClass}
    # qty 8
    class NoneFrozenDictTupleStrDecimalBoolFileBytesMixin:
        _types = {NoneClass, frozendict.frozendict, tuple, str, decimal.Decimal, BoolClass, FileIO, bytes}


class ValidatorBase:
    @staticmethod
    def _is_json_validation_enabled_oapg(schema_keyword, configuration=None):
        """Returns true if JSON schema validation is enabled for the specified
        validation keyword. This can be used to skip JSON schema structural validation
        as requested in the configuration.
        Note: the suffix _oapg stands for openapi python (experimental) generator and
        it has been added to prevent collisions with other methods and properties

        Args:
            schema_keyword (string): the name of a JSON schema validation keyword.
            configuration (Configuration): the configuration class.
        """

        return (configuration is None or
            not hasattr(configuration, '_disabled_client_side_validations') or
            schema_keyword not in configuration._disabled_client_side_validations)

    @staticmethod
    def _raise_validation_errror_message_oapg(value, constraint_msg, constraint_value, path_to_item, additional_txt=""):
        raise ApiValueError(
            "Invalid value `{value}`, {constraint_msg} `{constraint_value}`{additional_txt} at {path_to_item}".format(
                value=value,
                constraint_msg=constraint_msg,
                constraint_value=constraint_value,
                additional_txt=additional_txt,
                path_to_item=path_to_item,
            )
        )


class EnumBase:
    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: ValidationMetadata,
    ) -> typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Set[typing.Union['Schema', str, decimal.Decimal, BoolClass, NoneClass, frozendict.frozendict, tuple]]]:
        """
        EnumBase _validate_oapg
        Validates that arg is in the enum's allowed values
        """
        try:
            cls.MetaOapg.enum_value_to_name[arg]
        except KeyError:
            raise ApiValueError("Invalid value {} passed in to {}, allowed_values={}".format(arg, cls, cls.MetaOapg.enum_value_to_name.keys()))
        return super()._validate_oapg(arg, validation_metadata=validation_metadata)


class BoolBase:
    def is_true_oapg(self) -> bool:
        """
        A replacement for x is True
        True if the instance is a BoolClass True Singleton
        """
        if not issubclass(self.__class__, BoolClass):
            return False
        return bool(self)

    def is_false_oapg(self) -> bool:
        """
        A replacement for x is False
        True if the instance is a BoolClass False Singleton
        """
        if not issubclass(self.__class__, BoolClass):
            return False
        return bool(self) is False


class NoneBase:
    def is_none_oapg(self) -> bool:
        """
        A replacement for x is None
        True if the instance is a NoneClass None Singleton
        """
        if issubclass(self.__class__, NoneClass):
            return True
        return False


class StrBase(ValidatorBase):
    MetaOapg: MetaOapgTyped

    @property
    def as_str_oapg(self) -> str:
        return self

    @property
    def as_date_oapg(self) -> date:
        raise Exception('not implemented')

    @property
    def as_datetime_oapg(self) -> datetime:
        raise Exception('not implemented')

    @property
    def as_decimal_oapg(self) -> decimal.Decimal:
        raise Exception('not implemented')

    @property
    def as_uuid_oapg(self) -> uuid.UUID:
        raise Exception('not implemented')

    @classmethod
    def __check_str_validations(
        cls,
        arg: str,
        validation_metadata: ValidationMetadata
    ):
        if not hasattr(cls, 'MetaOapg'):
            return
        if (cls._is_json_validation_enabled_oapg('maxLength', validation_metadata.configuration) and
                hasattr(cls.MetaOapg, 'max_length') and
                len(arg) > cls.MetaOapg.max_length):
            cls._raise_validation_errror_message_oapg(
                value=arg,
                constraint_msg="length must be less than or equal to",
                constraint_value=cls.MetaOapg.max_length,
                path_to_item=validation_metadata.path_to_item
            )

        if (cls._is_json_validation_enabled_oapg('minLength', validation_metadata.configuration) and
                hasattr(cls.MetaOapg, 'min_length') and
                len(arg) < cls.MetaOapg.min_length):
            cls._raise_validation_errror_message_oapg(
                value=arg,
                constraint_msg="length must be greater than or equal to",
                constraint_value=cls.MetaOapg.min_length,
                path_to_item=validation_metadata.path_to_item
            )

        if (cls._is_json_validation_enabled_oapg('pattern', validation_metadata.configuration) and
                hasattr(cls.MetaOapg, 'regex')):
            for regex_dict in cls.MetaOapg.regex:
                flags = regex_dict.get('flags', 0)
                if not re.search(regex_dict['pattern'], arg, flags=flags):
                    if flags != 0:
                        # Don't print the regex flags if the flags are not
                        # specified in the OAS document.
                        cls._raise_validation_errror_message_oapg(
                            value=arg,
                            constraint_msg="must match regular expression",
                            constraint_value=regex_dict['pattern'],
                            path_to_item=validation_metadata.path_to_item,
                            additional_txt=" with flags=`{}`".format(flags)
                        )
                    cls._raise_validation_errror_message_oapg(
                        value=arg,
                        constraint_msg="must match regular expression",
                        constraint_value=regex_dict['pattern'],
                        path_to_item=validation_metadata.path_to_item
                    )

    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: ValidationMetadata,
    ) -> typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Set[typing.Union['Schema', str, decimal.Decimal, BoolClass, NoneClass, frozendict.frozendict, tuple]]]:
        """
        StrBase _validate_oapg
        Validates that validations pass
        """
        if isinstance(arg, str):
            cls.__check_str_validations(arg, validation_metadata)
        return super()._validate_oapg(arg, validation_metadata=validation_metadata)


class UUIDBase:
    @property
    @functools.lru_cache()
    def as_uuid_oapg(self) -> uuid.UUID:
        return uuid.UUID(self)

    @classmethod
    def __validate_format(cls, arg: typing.Optional[str], validation_metadata: ValidationMetadata):
        if isinstance(arg, str):
            try:
                uuid.UUID(arg)
                return True
            except ValueError:
                raise ApiValueError(
                    "Invalid value '{}' for type UUID at {}".format(arg, validation_metadata.path_to_item)
                )

    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: typing.Optional[ValidationMetadata] = None,
    ):
        """
        UUIDBase _validate_oapg
        """
        cls.__validate_format(arg, validation_metadata=validation_metadata)
        return super()._validate_oapg(arg, validation_metadata=validation_metadata)


class CustomIsoparser(isoparser):

    @_takes_ascii
    def parse_isodatetime(self, dt_str):
        components, pos = self._parse_isodate(dt_str)
        if len(dt_str) > pos:
            if self._sep is None or dt_str[pos:pos + 1] == self._sep:
                components += self._parse_isotime(dt_str[pos + 1:])
            else:
                raise ValueError('String contains unknown ISO components')

        if len(components) > 3 and components[3] == 24:
            components[3] = 0
            return datetime(*components) + timedelta(days=1)

        if len(components) <= 3:
            raise ValueError('Value is not a datetime')

        return datetime(*components)

    @_takes_ascii
    def parse_isodate(self, datestr):
        components, pos = self._parse_isodate(datestr)

        if len(datestr) > pos:
            raise ValueError('String contains invalid time components')

        if len(components) > 3:
            raise ValueError('String contains invalid time components')

        return date(*components)


DEFAULT_ISOPARSER = CustomIsoparser()


class DateBase:
    @property
    @functools.lru_cache()
    def as_date_oapg(self) -> date:
        return DEFAULT_ISOPARSER.parse_isodate(self)

    @classmethod
    def __validate_format(cls, arg: typing.Optional[str], validation_metadata: ValidationMetadata):
        if isinstance(arg, str):
            try:
                DEFAULT_ISOPARSER.parse_isodate(arg)
                return True
            except ValueError:
                raise ApiValueError(
                    "Value does not conform to the required ISO-8601 date format. "
                    "Invalid value '{}' for type date at {}".format(arg, validation_metadata.path_to_item)
                )

    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: typing.Optional[ValidationMetadata] = None,
    ):
        """
        DateBase _validate_oapg
        """
        cls.__validate_format(arg, validation_metadata=validation_metadata)
        return super()._validate_oapg(arg, validation_metadata=validation_metadata)


class DateTimeBase:
    @property
    @functools.lru_cache()
    def as_datetime_oapg(self) -> datetime:
        return DEFAULT_ISOPARSER.parse_isodatetime(self)

    @classmethod
    def __validate_format(cls, arg: typing.Optional[str], validation_metadata: ValidationMetadata):
        if isinstance(arg, str):
            try:
                DEFAULT_ISOPARSER.parse_isodatetime(arg)
                return True
            except ValueError:
                raise ApiValueError(
                    "Value does not conform to the required ISO-8601 datetime format. "
                    "Invalid value '{}' for type datetime at {}".format(arg, validation_metadata.path_to_item)
                )

    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: ValidationMetadata,
    ):
        """
        DateTimeBase _validate_oapg
        """
        cls.__validate_format(arg, validation_metadata=validation_metadata)
        return super()._validate_oapg(arg, validation_metadata=validation_metadata)


class DecimalBase:
    """
    A class for storing decimals that are sent over the wire as strings
    These schemas must remain based on StrBase rather than NumberBase
    because picking base classes must be deterministic
    """

    @property
    @functools.lru_cache()
    def as_decimal_oapg(self) -> decimal.Decimal:
        return decimal.Decimal(self)

    @classmethod
    def __validate_format(cls, arg: typing.Optional[str], validation_metadata: ValidationMetadata):
        if isinstance(arg, str):
            try:
                decimal.Decimal(arg)
                return True
            except decimal.InvalidOperation:
                raise ApiValueError(
                    "Value cannot be converted to a decimal. "
                    "Invalid value '{}' for type decimal at {}".format(arg, validation_metadata.path_to_item)
                )

    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: ValidationMetadata,
    ):
        """
        DecimalBase _validate_oapg
        """
        cls.__validate_format(arg, validation_metadata=validation_metadata)
        return super()._validate_oapg(arg, validation_metadata=validation_metadata)


class NumberBase(ValidatorBase):
    MetaOapg: MetaOapgTyped

    @property
    def as_int_oapg(self) -> int:
        try:
            return self._as_int
        except AttributeError:
            """
            Note: for some numbers like 9.0 they could be represented as an
            integer but our code chooses to store them as
            >>> Decimal('9.0').as_tuple()
            DecimalTuple(sign=0, digits=(9, 0), exponent=-1)
            so we can tell that the value came from a float and convert it back to a float
            during later serialization
            """
            if self.as_tuple().exponent < 0:
                # this could be represented as an integer but should be represented as a float
                # because that's what it was serialized from
                raise ApiValueError(f'{self} is not an integer')
            self._as_int = int(self)
            return self._as_int

    @property
    def as_float_oapg(self) -> float:
        try:
            return self._as_float
        except AttributeError:
            if self.as_tuple().exponent >= 0:
                raise ApiValueError(f'{self} is not an float')
            self._as_float = float(self)
            return self._as_float

    @classmethod
    def __check_numeric_validations(
        cls,
        arg,
        validation_metadata: ValidationMetadata
    ):
        if not hasattr(cls, 'MetaOapg'):
            return
        if cls._is_json_validation_enabled_oapg('multipleOf',
                                      validation_metadata.configuration) and hasattr(cls.MetaOapg, 'multiple_of'):
            multiple_of_value = cls.MetaOapg.multiple_of
            if (not (float(arg) / multiple_of_value).is_integer()):
                # Note 'multipleOf' will be as good as the floating point arithmetic.
                cls._raise_validation_errror_message_oapg(
                    value=arg,
                    constraint_msg="value must be a multiple of",
                    constraint_value=multiple_of_value,
                    path_to_item=validation_metadata.path_to_item
                )

        checking_max_or_min_values = any(
            hasattr(cls.MetaOapg, validation_key) for validation_key in {
                'exclusive_maximum',
                'inclusive_maximum',
                'exclusive_minimum',
                'inclusive_minimum',
            }
        )
        if not checking_max_or_min_values:
            return

        if (cls._is_json_validation_enabled_oapg('exclusiveMaximum', validation_metadata.configuration) and
                hasattr(cls.MetaOapg, 'exclusive_maximum') and
                arg >= cls.MetaOapg.exclusive_maximum):
            cls._raise_validation_errror_message_oapg(
                value=arg,
                constraint_msg="must be a value less than",
                constraint_value=cls.MetaOapg.exclusive_maximum,
                path_to_item=validation_metadata.path_to_item
            )

        if (cls._is_json_validation_enabled_oapg('maximum', validation_metadata.configuration) and
                hasattr(cls.MetaOapg, 'inclusive_maximum') and
                arg > cls.MetaOapg.inclusive_maximum):
            cls._raise_validation_errror_message_oapg(
                value=arg,
                constraint_msg="must be a value less than or equal to",
                constraint_value=cls.MetaOapg.inclusive_maximum,
                path_to_item=validation_metadata.path_to_item
            )

        if (cls._is_json_validation_enabled_oapg('exclusiveMinimum', validation_metadata.configuration) and
                hasattr(cls.MetaOapg, 'exclusive_minimum') and
                arg <= cls.MetaOapg.exclusive_minimum):
            cls._raise_validation_errror_message_oapg(
                value=arg,
                constraint_msg="must be a value greater than",
                constraint_value=cls.MetaOapg.exclusive_maximum,
                path_to_item=validation_metadata.path_to_item
            )

        if (cls._is_json_validation_enabled_oapg('minimum', validation_metadata.configuration) and
                hasattr(cls.MetaOapg, 'inclusive_minimum') and
                arg < cls.MetaOapg.inclusive_minimum):
            cls._raise_validation_errror_message_oapg(
                value=arg,
                constraint_msg="must be a value greater than or equal to",
                constraint_value=cls.MetaOapg.inclusive_minimum,
                path_to_item=validation_metadata.path_to_item
            )

    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: ValidationMetadata,
    ) -> typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Set[typing.Union['Schema', str, decimal.Decimal, BoolClass, NoneClass, frozendict.frozendict, tuple]]]:
        """
        NumberBase _validate_oapg
        Validates that validations pass
        """
        if isinstance(arg, decimal.Decimal):
            cls.__check_numeric_validations(arg, validation_metadata)
        return super()._validate_oapg(arg, validation_metadata=validation_metadata)


class ListBase(ValidatorBase):
    MetaOapg: MetaOapgTyped

    @classmethod
    def __validate_items(cls, list_items, validation_metadata: ValidationMetadata):
        """
        Ensures that:
        - values passed in for items are valid
        Exceptions will be raised if:
        - invalid arguments were passed in

        Args:
            list_items: the input list of items

        Raises:
            ApiTypeError - for missing required arguments, or for invalid properties
        """

        # if we have definitions for an items schema, use it
        # otherwise accept anything
        item_cls = getattr(cls.MetaOapg, 'items', UnsetAnyTypeSchema)
        item_cls = cls._get_class_oapg(item_cls)
        path_to_schemas = {}
        for i, value in enumerate(list_items):
            item_validation_metadata = ValidationMetadata(
                from_server=validation_metadata.from_server,
                configuration=validation_metadata.configuration,
                path_to_item=validation_metadata.path_to_item+(i,),
                validated_path_to_schemas=validation_metadata.validated_path_to_schemas
            )
            if item_validation_metadata.validation_ran_earlier(item_cls):
                continue
            other_path_to_schemas = item_cls._validate_oapg(
                value, validation_metadata=item_validation_metadata)
            update(path_to_schemas, other_path_to_schemas)
        return path_to_schemas

    @classmethod
    def __check_tuple_validations(
            cls, arg,
            validation_metadata: ValidationMetadata):
        if not hasattr(cls, 'MetaOapg'):
            return
        if (cls._is_json_validation_enabled_oapg('maxItems', validation_metadata.configuration) and
                hasattr(cls.MetaOapg, 'max_items') and
                len(arg) > cls.MetaOapg.max_items):
            cls._raise_validation_errror_message_oapg(
                value=arg,
                constraint_msg="number of items must be less than or equal to",
                constraint_value=cls.MetaOapg.max_items,
                path_to_item=validation_metadata.path_to_item
            )

        if (cls._is_json_validation_enabled_oapg('minItems', validation_metadata.configuration) and
                hasattr(cls.MetaOapg, 'min_items') and
                len(arg) < cls.MetaOapg.min_items):
            cls._raise_validation_errror_message_oapg(
                value=arg,
                constraint_msg="number of items must be greater than or equal to",
                constraint_value=cls.MetaOapg.min_items,
                path_to_item=validation_metadata.path_to_item
            )

        if (cls._is_json_validation_enabled_oapg('uniqueItems', validation_metadata.configuration) and
                hasattr(cls.MetaOapg, 'unique_items') and cls.MetaOapg.unique_items and arg):
            unique_items = set(arg)
            if len(arg) > len(unique_items):
                cls._raise_validation_errror_message_oapg(
                    value=arg,
                    constraint_msg="duplicate items were found, and the tuple must not contain duplicates because",
                    constraint_value='unique_items==True',
                    path_to_item=validation_metadata.path_to_item
                )

    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: ValidationMetadata,
    ):
        """
        ListBase _validate_oapg
        We return dynamic classes of different bases depending upon the inputs
        This makes it so:
        - the returned instance is always a subclass of our defining schema
            - this allows us to check type based on whether an instance is a subclass of a schema
        - the returned instance is a serializable type (except for None, True, and False) which are enums

        Returns:
            new_cls (type): the new class

        Raises:
            ApiValueError: when a string can't be converted into a date or datetime and it must be one of those classes
            ApiTypeError: when the input type is not in the list of allowed spec types
        """
        if isinstance(arg, tuple):
            cls.__check_tuple_validations(arg, validation_metadata)
        _path_to_schemas = super()._validate_oapg(arg, validation_metadata=validation_metadata)
        if not isinstance(arg, tuple):
            return _path_to_schemas
        updated_vm = ValidationMetadata(
            configuration=validation_metadata.configuration,
            from_server=validation_metadata.from_server,
            path_to_item=validation_metadata.path_to_item,
            seen_classes=validation_metadata.seen_classes | frozenset({cls}),
            validated_path_to_schemas=validation_metadata.validated_path_to_schemas
        )
        other_path_to_schemas = cls.__validate_items(arg, validation_metadata=updated_vm)
        update(_path_to_schemas, other_path_to_schemas)
        return _path_to_schemas

    @classmethod
    def _get_items_oapg(
        cls: 'Schema',
        arg: typing.List[typing.Any],
        path_to_item: typing.Tuple[typing.Union[str, int], ...],
        path_to_schemas: typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Type['Schema']]
    ):
        '''
        ListBase _get_items_oapg
        '''
        cast_items = []

        for i, value in enumerate(arg):
            item_path_to_item = path_to_item + (i,)
            item_cls = path_to_schemas[item_path_to_item]
            new_value = item_cls._get_new_instance_without_conversion_oapg(
                value,
                item_path_to_item,
                path_to_schemas
            )
            cast_items.append(new_value)

        return cast_items


class Discriminable:
    MetaOapg: MetaOapgTyped

    @classmethod
    def _ensure_discriminator_value_present_oapg(cls, disc_property_name: str, validation_metadata: ValidationMetadata, *args):
        if not args or args and disc_property_name not in args[0]:
            # The input data does not contain the discriminator property
            raise ApiValueError(
                "Cannot deserialize input data due to missing discriminator. "
                "The discriminator property '{}' is missing at path: {}".format(disc_property_name, validation_metadata.path_to_item)
            )

    @classmethod
    def get_discriminated_class_oapg(cls, disc_property_name: str, disc_payload_value: str):
        """
        Used in schemas with discriminators
        """
        if not hasattr(cls.MetaOapg, 'discriminator'):
            return None
        disc = cls.MetaOapg.discriminator()
        if disc_property_name not in disc:
            return None
        discriminated_cls = disc[disc_property_name].get(disc_payload_value)
        if discriminated_cls is not None:
            return discriminated_cls
        if not hasattr(cls, 'MetaOapg'):
            return None
        elif not (
            hasattr(cls.MetaOapg, 'all_of') or
            hasattr(cls.MetaOapg, 'one_of') or
            hasattr(cls.MetaOapg, 'any_of')
        ):
            return None
        # TODO stop traveling if a cycle is hit
        if hasattr(cls.MetaOapg, 'all_of'):
            for allof_cls in cls.MetaOapg.all_of():
                discriminated_cls = allof_cls.get_discriminated_class_oapg(
                    disc_property_name=disc_property_name, disc_payload_value=disc_payload_value)
                if discriminated_cls is not None:
                    return discriminated_cls
        if hasattr(cls.MetaOapg, 'one_of'):
            for oneof_cls in cls.MetaOapg.one_of():
                discriminated_cls = oneof_cls.get_discriminated_class_oapg(
                    disc_property_name=disc_property_name, disc_payload_value=disc_payload_value)
                if discriminated_cls is not None:
                    return discriminated_cls
        if hasattr(cls.MetaOapg, 'any_of'):
            for anyof_cls in cls.MetaOapg.any_of():
                discriminated_cls = anyof_cls.get_discriminated_class_oapg(
                    disc_property_name=disc_property_name, disc_payload_value=disc_payload_value)
                if discriminated_cls is not None:
                    return discriminated_cls
        return None


class DictBase(Discriminable, ValidatorBase):

    @classmethod
    def __validate_arg_presence(cls, arg):
        """
        Ensures that:
        - all required arguments are passed in
        - the input variable names are valid
            - present in properties or
            - accepted because additionalProperties exists
        Exceptions will be raised if:
        - invalid arguments were passed in
            - a var_name is invalid if additional_properties == NotAnyTypeSchema
            and var_name not in properties.__annotations__
        - required properties were not passed in

        Args:
            arg: the input dict

        Raises:
            ApiTypeError - for missing required arguments, or for invalid properties
        """
        seen_required_properties = set()
        invalid_arguments = []
        required_property_names = getattr(cls.MetaOapg, 'required', set())
        additional_properties = getattr(cls.MetaOapg, 'additional_properties', UnsetAnyTypeSchema)
        properties = getattr(cls.MetaOapg, 'properties', {})
        property_annotations = getattr(properties, '__annotations__', {})
        for property_name in arg:
            if property_name in required_property_names:
                seen_required_properties.add(property_name)
            elif property_name in property_annotations:
                continue
            elif additional_properties is not NotAnyTypeSchema:
                continue
            else:
                invalid_arguments.append(property_name)
        missing_required_arguments = list(required_property_names - seen_required_properties)
        if missing_required_arguments:
            missing_required_arguments.sort()
            raise ApiTypeError(
                "{} is missing {} required argument{}: {}".format(
                    cls.__name__,
                    len(missing_required_arguments),
                    "s" if len(missing_required_arguments) > 1 else "",
                    missing_required_arguments
                )
            )
        if invalid_arguments:
            invalid_arguments.sort()
            raise ApiTypeError(
                "{} was passed {} invalid argument{}: {}".format(
                    cls.__name__,
                    len(invalid_arguments),
                    "s" if len(invalid_arguments) > 1 else "",
                    invalid_arguments
                )
            )

    @classmethod
    def __validate_args(cls, arg, validation_metadata: ValidationMetadata):
        """
        Ensures that:
        - values passed in for properties are valid
        Exceptions will be raised if:
        - invalid arguments were passed in

        Args:
            arg: the input dict

        Raises:
            ApiTypeError - for missing required arguments, or for invalid properties
        """
        path_to_schemas = {}
        additional_properties = getattr(cls.MetaOapg, 'additional_properties', UnsetAnyTypeSchema)
        properties = getattr(cls.MetaOapg, 'properties', {})
        property_annotations = getattr(properties, '__annotations__', {})
        for property_name, value in arg.items():
            path_to_item = validation_metadata.path_to_item+(property_name,)
            if property_name in property_annotations:
                schema = property_annotations[property_name]
            elif additional_properties is not NotAnyTypeSchema:
                if additional_properties is UnsetAnyTypeSchema:
                    """
                    If additionalProperties is unset and this path_to_item does not yet have
                    any validations on it, validate it.
                    If it already has validations on it, skip this validation.
                    """
                    if path_to_item in path_to_schemas:
                        continue
                schema = additional_properties
            else:
                raise ApiTypeError('Unable to find schema for value={} in class={} at path_to_item={}'.format(
                    value, cls, validation_metadata.path_to_item+(property_name,)
                ))
            schema = cls._get_class_oapg(schema)
            arg_validation_metadata = ValidationMetadata(
                from_server=validation_metadata.from_server,
                configuration=validation_metadata.configuration,
                path_to_item=path_to_item,
                validated_path_to_schemas=validation_metadata.validated_path_to_schemas
            )
            if arg_validation_metadata.validation_ran_earlier(schema):
                continue
            other_path_to_schemas = schema._validate_oapg(value, validation_metadata=arg_validation_metadata)
            update(path_to_schemas, other_path_to_schemas)
        return path_to_schemas

    @classmethod
    def __check_dict_validations(
        cls,
        arg,
        validation_metadata: ValidationMetadata
    ):
        if not hasattr(cls, 'MetaOapg'):
            return
        if (cls._is_json_validation_enabled_oapg('maxProperties', validation_metadata.configuration) and
                hasattr(cls.MetaOapg, 'max_properties') and
                len(arg) > cls.MetaOapg.max_properties):
            cls._raise_validation_errror_message_oapg(
                value=arg,
                constraint_msg="number of properties must be less than or equal to",
                constraint_value=cls.MetaOapg.max_properties,
                path_to_item=validation_metadata.path_to_item
            )

        if (cls._is_json_validation_enabled_oapg('minProperties', validation_metadata.configuration) and
                hasattr(cls.MetaOapg, 'min_properties') and
                len(arg) < cls.MetaOapg.min_properties):
            cls._raise_validation_errror_message_oapg(
                value=arg,
                constraint_msg="number of properties must be greater than or equal to",
                constraint_value=cls.MetaOapg.min_properties,
                path_to_item=validation_metadata.path_to_item
            )

    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: ValidationMetadata,
    ):
        """
        DictBase _validate_oapg
        We return dynamic classes of different bases depending upon the inputs
        This makes it so:
        - the returned instance is always a subclass of our defining schema
            - this allows us to check type based on whether an instance is a subclass of a schema
        - the returned instance is a serializable type (except for None, True, and False) which are enums

        Returns:
            new_cls (type): the new class

        Raises:
            ApiValueError: when a string can't be converted into a date or datetime and it must be one of those classes
            ApiTypeError: when the input type is not in the list of allowed spec types
        """
        if isinstance(arg, frozendict.frozendict):
            cls.__check_dict_validations(arg, validation_metadata)
        _path_to_schemas = super()._validate_oapg(arg, validation_metadata=validation_metadata)
        if not isinstance(arg, frozendict.frozendict):
            return _path_to_schemas
        cls.__validate_arg_presence(arg)
        other_path_to_schemas = cls.__validate_args(arg, validation_metadata=validation_metadata)
        update(_path_to_schemas, other_path_to_schemas)
        try:
            discriminator = cls.MetaOapg.discriminator()
        except AttributeError:
            return _path_to_schemas
        # discriminator exists
        disc_prop_name = list(discriminator.keys())[0]
        cls._ensure_discriminator_value_present_oapg(disc_prop_name, validation_metadata, arg)
        discriminated_cls = cls.get_discriminated_class_oapg(
            disc_property_name=disc_prop_name, disc_payload_value=arg[disc_prop_name])
        if discriminated_cls is None:
            raise ApiValueError(
                "Invalid discriminator value was passed in to {}.{} Only the values {} are allowed at {}".format(
                    cls.__name__,
                    disc_prop_name,
                    list(discriminator[disc_prop_name].keys()),
                    validation_metadata.path_to_item + (disc_prop_name,)
                )
            )
        updated_vm = ValidationMetadata(
            configuration=validation_metadata.configuration,
            from_server=validation_metadata.from_server,
            path_to_item=validation_metadata.path_to_item,
            seen_classes=validation_metadata.seen_classes | frozenset({cls}),
            validated_path_to_schemas=validation_metadata.validated_path_to_schemas
        )
        if updated_vm.validation_ran_earlier(discriminated_cls):
            return _path_to_schemas
        other_path_to_schemas = discriminated_cls._validate_oapg(arg, validation_metadata=updated_vm)
        update(_path_to_schemas, other_path_to_schemas)
        return _path_to_schemas

    @classmethod
    def _get_properties_oapg(
        cls,
        arg: typing.Dict[str, typing.Any],
        path_to_item: typing.Tuple[typing.Union[str, int], ...],
        path_to_schemas: typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Type['Schema']]
    ):
        """
        DictBase _get_properties_oapg, this is how properties are set
        These values already passed validation
        """
        dict_items = {}

        for property_name_js, value in arg.items():
            property_path_to_item = path_to_item + (property_name_js,)
            property_cls = path_to_schemas[property_path_to_item]
            new_value = property_cls._get_new_instance_without_conversion_oapg(
                value,
                property_path_to_item,
                path_to_schemas
            )
            dict_items[property_name_js] = new_value

        return dict_items

    def __setattr__(self, name: str, value: typing.Any):
        if not isinstance(self, FileIO):
            raise AttributeError('property setting not supported on immutable instances')

    def __getattr__(self, name: str):
        """
        for instance.name access
        Properties are only type hinted for required properties
        so that hasattr(instance, 'optionalProp') is False when that key is not present
        """
        if not isinstance(self, frozendict.frozendict):
            return super().__getattr__(name)
        if name not in self.__class__.__annotations__:
            raise AttributeError(f"{self} has no attribute '{name}'")
        try:
            value = self[name]
            return value
        except KeyError as ex:
            raise AttributeError(str(ex))

    def __getitem__(self, name: str):
        """
        dict_instance[name] accessor
        key errors thrown
        """
        if not isinstance(self, frozendict.frozendict):
            return super().__getattr__(name)
        return super().__getitem__(name)

    def get_item_oapg(self, name: str) -> typing.Union['AnyTypeSchema', Unset]:
        # dict_instance[name] accessor
        if not isinstance(self, frozendict.frozendict):
            raise NotImplementedError()
        try:
            return super().__getitem__(name)
        except KeyError:
            return unset


def cast_to_allowed_types(
    arg: typing.Union[str, date, datetime, uuid.UUID, decimal.Decimal, int, float, None, dict, frozendict.frozendict, list, tuple, bytes, Schema, io.FileIO, io.BufferedReader],
    from_server: bool,
    validated_path_to_schemas: typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Set[typing.Union['Schema', str, decimal.Decimal, BoolClass, NoneClass, frozendict.frozendict, tuple]]],
    path_to_item: typing.Tuple[typing.Union[str, int], ...] = tuple(['args[0]']),
) -> typing.Union[frozendict.frozendict, tuple, decimal.Decimal, str, bytes, BoolClass, NoneClass, FileIO]:
    """
    Casts the input payload arg into the allowed types
    The input validated_path_to_schemas is mutated by running this function

    When from_server is False then
    - date/datetime is cast to str
    - int/float is cast to Decimal

    If a Schema instance is passed in it is converted back to a primitive instance because
    One may need to validate that data to the original Schema class AND additional different classes
    those additional classes will need to be added to the new manufactured class for that payload
    If the code didn't do this and kept the payload as a Schema instance it would fail to validate to other
    Schema classes and the code wouldn't be able to mfg a new class that includes all valid schemas
    TODO: store the validated schema classes in validation_metadata

    Args:
        arg: the payload
        from_server: whether this payload came from the server or not
        validated_path_to_schemas: a dict that stores the validated classes at any path location in the payload
    """
    if isinstance(arg, Schema):
        # store the already run validations
        schema_classes = set()
        source_schema_was_unset = len(arg.__class__.__bases__) == 2 and UnsetAnyTypeSchema in arg.__class__.__bases__
        if not source_schema_was_unset:
            """
            Do not include UnsetAnyTypeSchema and its base class because
            it did not exist in the original spec schema definition
            It was added to ensure that all instances are of type Schema and the allowed base types
            """
            for cls in arg.__class__.__bases__:
                if cls is Singleton:
                    # Skip Singleton
                    continue
                schema_classes.add(cls)
        validated_path_to_schemas[path_to_item] = schema_classes

    type_error = ApiTypeError(f"Invalid type. Required value type is str and passed type was {type(arg)} at {path_to_item}")
    if isinstance(arg, str):
        return str(arg)
    elif isinstance(arg, (dict, frozendict.frozendict)):
        return frozendict.frozendict({key: cast_to_allowed_types(val, from_server, validated_path_to_schemas, path_to_item + (key,)) for key, val in arg.items()})
    elif isinstance(arg, (bool, BoolClass)):
        """
        this check must come before isinstance(arg, (int, float))
        because isinstance(True, int) is True
        """
        if arg:
            return BoolClass.TRUE
        return BoolClass.FALSE
    elif isinstance(arg, int):
        return decimal.Decimal(arg)
    elif isinstance(arg, float):
        decimal_from_float = decimal.Decimal(arg)
        if decimal_from_float.as_integer_ratio()[1] == 1:
            # 9.0 -> Decimal('9.0')
            # 3.4028234663852886e+38 -> Decimal('340282346638528859811704183484516925440.0')
            return decimal.Decimal(str(decimal_from_float)+'.0')
        return decimal_from_float
    elif isinstance(arg, (tuple, list)):
        return tuple([cast_to_allowed_types(item, from_server, validated_path_to_schemas, path_to_item + (i,)) for i, item in enumerate(arg)])
    elif isinstance(arg, (none_type, NoneClass)):
        return NoneClass.NONE
    elif isinstance(arg, (date, datetime)):
        if not from_server:
            return arg.isoformat()
        raise type_error
    elif isinstance(arg, uuid.UUID):
        if not from_server:
            return str(arg)
        raise type_error
    elif isinstance(arg, decimal.Decimal):
        return decimal.Decimal(arg)
    elif isinstance(arg, bytes):
        return bytes(arg)
    elif isinstance(arg, (io.FileIO, io.BufferedReader)):
        return FileIO(arg)
    raise ValueError('Invalid type passed in got input={} type={}'.format(arg, type(arg)))


class ComposedBase(Discriminable):

    @classmethod
    def __get_allof_classes(cls, arg, validation_metadata: ValidationMetadata):
        path_to_schemas = defaultdict(set)
        for allof_cls in cls.MetaOapg.all_of():
            if validation_metadata.validation_ran_earlier(allof_cls):
                continue
            other_path_to_schemas = allof_cls._validate_oapg(arg, validation_metadata=validation_metadata)
            update(path_to_schemas, other_path_to_schemas)
        return path_to_schemas

    @classmethod
    def __get_oneof_class(
        cls,
        arg,
        discriminated_cls,
        validation_metadata: ValidationMetadata,
    ):
        oneof_classes = []
        path_to_schemas = defaultdict(set)
        for oneof_cls in cls.MetaOapg.one_of():
            if oneof_cls in path_to_schemas[validation_metadata.path_to_item]:
                oneof_classes.append(oneof_cls)
                continue
            if validation_metadata.validation_ran_earlier(oneof_cls):
                oneof_classes.append(oneof_cls)
                continue
            try:
                path_to_schemas = oneof_cls._validate_oapg(arg, validation_metadata=validation_metadata)
            except (ApiValueError, ApiTypeError) as ex:
                if discriminated_cls is not None and oneof_cls is discriminated_cls:
                    raise ex
                continue
            oneof_classes.append(oneof_cls)
        if not oneof_classes:
            raise ApiValueError(
                "Invalid inputs given to generate an instance of {}. None "
                "of the oneOf schemas matched the input data.".format(cls)
            )
        elif len(oneof_classes) > 1:
            raise ApiValueError(
                "Invalid inputs given to generate an instance of {}. Multiple "
                "oneOf schemas {} matched the inputs, but a max of one is allowed.".format(cls, oneof_classes)
            )
        # exactly one class matches
        return path_to_schemas

    @classmethod
    def __get_anyof_classes(
        cls,
        arg,
        discriminated_cls,
        validation_metadata: ValidationMetadata
    ):
        anyof_classes = []
        path_to_schemas = defaultdict(set)
        for anyof_cls in cls.MetaOapg.any_of():
            if validation_metadata.validation_ran_earlier(anyof_cls):
                anyof_classes.append(anyof_cls)
                continue

            try:
                other_path_to_schemas = anyof_cls._validate_oapg(arg, validation_metadata=validation_metadata)
            except (ApiValueError, ApiTypeError) as ex:
                if discriminated_cls is not None and anyof_cls is discriminated_cls:
                    raise ex
                continue
            anyof_classes.append(anyof_cls)
            update(path_to_schemas, other_path_to_schemas)
        if not anyof_classes:
            raise ApiValueError(
                "Invalid inputs given to generate an instance of {}. None "
                "of the anyOf schemas matched the input data.".format(cls)
            )
        return path_to_schemas

    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: ValidationMetadata,
    ) -> typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Set[typing.Union['Schema', str, decimal.Decimal, BoolClass, NoneClass, frozendict.frozendict, tuple]]]:
        """
        ComposedBase _validate_oapg
        We return dynamic classes of different bases depending upon the inputs
        This makes it so:
        - the returned instance is always a subclass of our defining schema
            - this allows us to check type based on whether an instance is a subclass of a schema
        - the returned instance is a serializable type (except for None, True, and False) which are enums

        Returns:
            new_cls (type): the new class

        Raises:
            ApiValueError: when a string can't be converted into a date or datetime and it must be one of those classes
            ApiTypeError: when the input type is not in the list of allowed spec types
        """
        # validation checking on types, validations, and enums
        path_to_schemas = super()._validate_oapg(arg, validation_metadata=validation_metadata)

        updated_vm = ValidationMetadata(
            configuration=validation_metadata.configuration,
            from_server=validation_metadata.from_server,
            path_to_item=validation_metadata.path_to_item,
            seen_classes=validation_metadata.seen_classes | frozenset({cls}),
            validated_path_to_schemas=validation_metadata.validated_path_to_schemas
        )

        # process composed schema
        discriminator = None
        if hasattr(cls, 'MetaOapg') and hasattr(cls.MetaOapg, 'discriminator'):
            discriminator = cls.MetaOapg.discriminator()
        discriminated_cls = None
        if discriminator and arg and isinstance(arg, frozendict.frozendict):
            disc_property_name = list(discriminator.keys())[0]
            cls._ensure_discriminator_value_present_oapg(disc_property_name, updated_vm, arg)
            # get discriminated_cls by looking at the dict in the current class
            discriminated_cls = cls.get_discriminated_class_oapg(
                disc_property_name=disc_property_name, disc_payload_value=arg[disc_property_name])
            if discriminated_cls is None:
                raise ApiValueError(
                    "Invalid discriminator value '{}' was passed in to {}.{} Only the values {} are allowed at {}".format(
                        arg[disc_property_name],
                        cls.__name__,
                        disc_property_name,
                        list(discriminator[disc_property_name].keys()),
                        updated_vm.path_to_item + (disc_property_name,)
                    )
                )

        if hasattr(cls, 'MetaOapg') and hasattr(cls.MetaOapg, 'all_of'):
            other_path_to_schemas = cls.__get_allof_classes(arg, validation_metadata=updated_vm)
            update(path_to_schemas, other_path_to_schemas)
        if hasattr(cls, 'MetaOapg') and hasattr(cls.MetaOapg, 'one_of'):
            other_path_to_schemas = cls.__get_oneof_class(
                arg,
                discriminated_cls=discriminated_cls,
                validation_metadata=updated_vm
            )
            update(path_to_schemas, other_path_to_schemas)
        if hasattr(cls, 'MetaOapg') and hasattr(cls.MetaOapg, 'any_of'):
            other_path_to_schemas = cls.__get_anyof_classes(
                arg,
                discriminated_cls=discriminated_cls,
                validation_metadata=updated_vm
            )
            update(path_to_schemas, other_path_to_schemas)
        not_cls = None
        if hasattr(cls, 'MetaOapg') and hasattr(cls.MetaOapg, 'not_schema'):
            not_cls = cls.MetaOapg.not_schema
            not_cls = cls._get_class_oapg(not_cls)
        if not_cls:
            other_path_to_schemas = None
            not_exception = ApiValueError(
                "Invalid value '{}' was passed in to {}. Value is invalid because it is disallowed by {}".format(
                    arg,
                    cls.__name__,
                    not_cls.__name__,
                )
            )
            if updated_vm.validation_ran_earlier(not_cls):
                raise not_exception

            try:
                other_path_to_schemas = not_cls._validate_oapg(arg, validation_metadata=updated_vm)
            except (ApiValueError, ApiTypeError):
                pass
            if other_path_to_schemas:
                raise not_exception

        if discriminated_cls is not None and not updated_vm.validation_ran_earlier(discriminated_cls):
            # TODO use an exception from this package here
            assert discriminated_cls in path_to_schemas[updated_vm.path_to_item]
        return path_to_schemas


# DictBase, ListBase, NumberBase, StrBase, BoolBase, NoneBase
class ComposedSchema(
    ComposedBase,
    DictBase,
    ListBase,
    NumberBase,
    StrBase,
    BoolBase,
    NoneBase,
    Schema,
    NoneFrozenDictTupleStrDecimalBoolMixin
):
    @classmethod
    def from_openapi_data_oapg(cls, *args: typing.Any, _configuration: typing.Optional[Configuration] = None, **kwargs):
        if not args:
            if not kwargs:
                raise ApiTypeError('{} is missing required input data in args or kwargs'.format(cls.__name__))
            args = (kwargs, )
        return super().from_openapi_data_oapg(args[0], _configuration=_configuration)


class ListSchema(
    ListBase,
    Schema,
    TupleMixin
):

    @classmethod
    def from_openapi_data_oapg(cls, arg: typing.List[typing.Any], _configuration: typing.Optional[Configuration] = None):
        return super().from_openapi_data_oapg(arg, _configuration=_configuration)

    def __new__(cls, arg: typing.Union[typing.List[typing.Any], typing.Tuple[typing.Any]], **kwargs: Configuration):
        return super().__new__(cls, arg, **kwargs)


class NoneSchema(
    NoneBase,
    Schema,
    NoneMixin
):

    @classmethod
    def from_openapi_data_oapg(cls, arg: None, _configuration: typing.Optional[Configuration] = None):
        return super().from_openapi_data_oapg(arg, _configuration=_configuration)

    def __new__(cls, arg: None, **kwargs: Configuration):
        return super().__new__(cls, arg, **kwargs)


class NumberSchema(
    NumberBase,
    Schema,
    DecimalMixin
):
    """
    This is used for type: number with no format
    Both integers AND floats are accepted
    """

    @classmethod
    def from_openapi_data_oapg(cls, arg: typing.Union[int, float], _configuration: typing.Optional[Configuration] = None):
        return super().from_openapi_data_oapg(arg, _configuration=_configuration)

    def __new__(cls, arg: typing.Union[decimal.Decimal, int, float], **kwargs: Configuration):
        return super().__new__(cls, arg, **kwargs)


class IntBase:
    @property
    def as_int_oapg(self) -> int:
        try:
            return self._as_int
        except AttributeError:
            self._as_int = int(self)
            return self._as_int

    @classmethod
    def __validate_format(cls, arg: typing.Optional[decimal.Decimal], validation_metadata: ValidationMetadata):
        if isinstance(arg, decimal.Decimal):

            denominator = arg.as_integer_ratio()[-1]
            if denominator != 1:
                raise ApiValueError(
                    "Invalid value '{}' for type integer at {}".format(arg, validation_metadata.path_to_item)
                )

    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: ValidationMetadata,
    ):
        """
        IntBase _validate_oapg
        TODO what about types = (int, number) -> IntBase, NumberBase? We could drop int and keep number only
        """
        cls.__validate_format(arg, validation_metadata=validation_metadata)
        return super()._validate_oapg(arg, validation_metadata=validation_metadata)


class IntSchema(IntBase, NumberSchema):

    @classmethod
    def from_openapi_data_oapg(cls, arg: int, _configuration: typing.Optional[Configuration] = None):
        return super().from_openapi_data_oapg(arg, _configuration=_configuration)

    def __new__(cls, arg: typing.Union[decimal.Decimal, int], **kwargs: Configuration):
        return super().__new__(cls, arg, **kwargs)


class Int32Base:
    __inclusive_minimum = decimal.Decimal(-2147483648)
    __inclusive_maximum = decimal.Decimal(2147483647)

    @classmethod
    def __validate_format(cls, arg: typing.Optional[decimal.Decimal], validation_metadata: ValidationMetadata):
        if isinstance(arg, decimal.Decimal) and arg.as_tuple().exponent == 0:
            if not cls.__inclusive_minimum <= arg <= cls.__inclusive_maximum:
                raise ApiValueError(
                    "Invalid value '{}' for type int32 at {}".format(arg, validation_metadata.path_to_item)
                )

    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: ValidationMetadata,
    ):
        """
        Int32Base _validate_oapg
        """
        cls.__validate_format(arg, validation_metadata=validation_metadata)
        return super()._validate_oapg(arg, validation_metadata=validation_metadata)


class Int32Schema(
    Int32Base,
    IntSchema
):
    pass


class Int64Base:
    __inclusive_minimum = decimal.Decimal(-9223372036854775808)
    __inclusive_maximum = decimal.Decimal(9223372036854775807)

    @classmethod
    def __validate_format(cls, arg: typing.Optional[decimal.Decimal], validation_metadata: ValidationMetadata):
        if isinstance(arg, decimal.Decimal) and arg.as_tuple().exponent == 0:
            if not cls.__inclusive_minimum <= arg <= cls.__inclusive_maximum:
                raise ApiValueError(
                    "Invalid value '{}' for type int64 at {}".format(arg, validation_metadata.path_to_item)
                )

    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: ValidationMetadata,
    ):
        """
        Int64Base _validate_oapg
        """
        cls.__validate_format(arg, validation_metadata=validation_metadata)
        return super()._validate_oapg(arg, validation_metadata=validation_metadata)


class Int64Schema(
    Int64Base,
    IntSchema
):
    pass


class Float32Base:
    __inclusive_minimum = decimal.Decimal(-3.4028234663852886e+38)
    __inclusive_maximum = decimal.Decimal(3.4028234663852886e+38)

    @classmethod
    def __validate_format(cls, arg: typing.Optional[decimal.Decimal], validation_metadata: ValidationMetadata):
        if isinstance(arg, decimal.Decimal):
            if not cls.__inclusive_minimum <= arg <= cls.__inclusive_maximum:
                raise ApiValueError(
                    "Invalid value '{}' for type float at {}".format(arg, validation_metadata.path_to_item)
                )

    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: ValidationMetadata,
    ):
        """
        Float32Base _validate_oapg
        """
        cls.__validate_format(arg, validation_metadata=validation_metadata)
        return super()._validate_oapg(arg, validation_metadata=validation_metadata)


class Float32Schema(
    Float32Base,
    NumberSchema
):

    @classmethod
    def from_openapi_data_oapg(cls, arg: float, _configuration: typing.Optional[Configuration] = None):
        return super().from_openapi_data_oapg(arg, _configuration=_configuration)


class Float64Base:
    __inclusive_minimum = decimal.Decimal(-1.7976931348623157E+308)
    __inclusive_maximum = decimal.Decimal(1.7976931348623157E+308)

    @classmethod
    def __validate_format(cls, arg: typing.Optional[decimal.Decimal], validation_metadata: ValidationMetadata):
        if isinstance(arg, decimal.Decimal):
            if not cls.__inclusive_minimum <= arg <= cls.__inclusive_maximum:
                raise ApiValueError(
                    "Invalid value '{}' for type double at {}".format(arg, validation_metadata.path_to_item)
                )

    @classmethod
    def _validate_oapg(
        cls,
        arg,
        validation_metadata: ValidationMetadata,
    ):
        """
        Float64Base _validate_oapg
        """
        cls.__validate_format(arg, validation_metadata=validation_metadata)
        return super()._validate_oapg(arg, validation_metadata=validation_metadata)

class Float64Schema(
    Float64Base,
    NumberSchema
):

    @classmethod
    def from_openapi_data_oapg(cls, arg: float, _configuration: typing.Optional[Configuration] = None):
        # todo check format
        return super().from_openapi_data_oapg(arg, _configuration=_configuration)


class StrSchema(
    StrBase,
    Schema,
    StrMixin
):
    """
    date + datetime string types must inherit from this class
    That is because one can validate a str payload as both:
    - type: string (format unset)
    - type: string, format: date
    """

    @classmethod
    def from_openapi_data_oapg(cls, arg: str, _configuration: typing.Optional[Configuration] = None) -> 'StrSchema':
        return super().from_openapi_data_oapg(arg, _configuration=_configuration)

    def __new__(cls, arg: typing.Union[str, date, datetime, uuid.UUID], **kwargs: Configuration):
        return super().__new__(cls, arg, **kwargs)


class UUIDSchema(UUIDBase, StrSchema):

    def __new__(cls, arg: typing.Union[str, uuid.UUID], **kwargs: Configuration):
        return super().__new__(cls, arg, **kwargs)


class DateSchema(DateBase, StrSchema):

    def __new__(cls, arg: typing.Union[str, date], **kwargs: Configuration):
        return super().__new__(cls, arg, **kwargs)


class DateTimeSchema(DateTimeBase, StrSchema):

    def __new__(cls, arg: typing.Union[str, datetime], **kwargs: Configuration):
        return super().__new__(cls, arg, **kwargs)


class DecimalSchema(DecimalBase, StrSchema):

    def __new__(cls, arg: str, **kwargs: Configuration):
        """
        Note: Decimals may not be passed in because cast_to_allowed_types is only invoked once for payloads
        which can be simple (str) or complex (dicts or lists with nested values)
        Because casting is only done once and recursively casts all values prior to validation then for a potential
        client side Decimal input if Decimal was accepted as an input in DecimalSchema then one would not know
        if one was using it for a StrSchema (where it should be cast to str) or one is using it for NumberSchema
        where it should stay as Decimal.
        """
        return super().__new__(cls, arg, **kwargs)


class BytesSchema(
    Schema,
    BytesMixin
):
    """
    this class will subclass bytes and is immutable
    """
    def __new__(cls, arg: bytes, **kwargs: Configuration):
        return super(Schema, cls).__new__(cls, arg)


class FileSchema(
    Schema,
    FileMixin
):
    """
    This class is NOT immutable
    Dynamic classes are built using it for example when AnyType allows in binary data
    Al other schema classes ARE immutable
    If one wanted to make this immutable one could make this a DictSchema with required properties:
    - data = BytesSchema (which would be an immutable bytes based schema)
    - file_name = StrSchema
    and cast_to_allowed_types would convert bytes and file instances into dicts containing data + file_name
    The downside would be that data would be stored in memory which one may not want to do for very large files

    The developer is responsible for closing this file and deleting it

    This class was kept as mutable:
    - to allow file reading and writing to disk
    - to be able to preserve file name info
    """

    def __new__(cls, arg: typing.Union[io.FileIO, io.BufferedReader], **kwargs: Configuration):
        return super(Schema, cls).__new__(cls, arg)


class BinaryBase:
    pass


class BinarySchema(
    ComposedBase,
    BinaryBase,
    Schema,
    BinaryMixin
):
    class MetaOapg:
        @staticmethod
        def one_of():
            return [
                BytesSchema,
                FileSchema,
            ]

    def __new__(cls, arg: typing.Union[io.FileIO, io.BufferedReader, bytes], **kwargs: Configuration):
        return super().__new__(cls, arg)


class BoolSchema(
    BoolBase,
    Schema,
    BoolMixin
):

    @classmethod
    def from_openapi_data_oapg(cls, arg: bool, _configuration: typing.Optional[Configuration] = None):
        return super().from_openapi_data_oapg(arg, _configuration=_configuration)

    def __new__(cls, arg: bool, **kwargs: ValidationMetadata):
        return super().__new__(cls, arg, **kwargs)


class AnyTypeSchema(
    DictBase,
    ListBase,
    NumberBase,
    StrBase,
    BoolBase,
    NoneBase,
    Schema,
    NoneFrozenDictTupleStrDecimalBoolFileBytesMixin
):
    # Python representation of a schema defined as true or {}
    pass


class UnsetAnyTypeSchema(AnyTypeSchema):
    # Used when additionalProperties/items was not explicitly defined and a defining schema is needed
    pass


class NotAnyTypeSchema(
    ComposedSchema,
):
    """
    Python representation of a schema defined as false or {'not': {}}
    Does not allow inputs in of AnyType
    Note: validations on this class are never run because the code knows that no inputs will ever validate
    """

    class MetaOapg:
        not_schema = AnyTypeSchema

    def __new__(
        cls,
        *args,
        _configuration: typing.Optional[Configuration] = None,
    ) -> 'NotAnyTypeSchema':
        return super().__new__(
            cls,
            *args,
            _configuration=_configuration,
        )


class DictSchema(
    DictBase,
    Schema,
    FrozenDictMixin
):
    @classmethod
    def from_openapi_data_oapg(cls, arg: typing.Dict[str, typing.Any], _configuration: typing.Optional[Configuration] = None):
        return super().from_openapi_data_oapg(arg, _configuration=_configuration)

    def __new__(cls, *args: typing.Union[dict, frozendict.frozendict], **kwargs: typing.Union[dict, frozendict.frozendict, list, tuple, decimal.Decimal, float, int, str, date, datetime, bool, None, bytes, Schema, Unset, ValidationMetadata]):
        return super().__new__(cls, *args, **kwargs)


schema_type_classes = {NoneSchema, DictSchema, ListSchema, NumberSchema, StrSchema, BoolSchema, AnyTypeSchema}


@functools.lru_cache()
def get_new_class(
    class_name: str,
    bases: typing.Tuple[typing.Type[typing.Union[Schema, typing.Any]], ...]
) -> typing.Type[Schema]:
    """
    Returns a new class that is made with the subclass bases
    """
    new_cls: typing.Type[Schema] = type(class_name, bases, {})
    return new_cls


LOG_CACHE_USAGE = False


def log_cache_usage(cache_fn):
    if LOG_CACHE_USAGE:
        print(cache_fn.__name__, cache_fn.cache_info())
