class EngineObjectsDataImporterModelBase(EngineObjectsModelBase):
    __swagger_json__ = extend_swagger_json(
        EngineObjectsModelBase.__swagger_json__,
        __file__
    )

    @classmethod
    async def post_import_data_job(cls, req, session):
        session = cls._copy_session(session)
        engine_object = await cls._get_engine_object(req, session)

        if engine_object is None:
            return cls._build_response(404)

        jobs_id = cls._get_jobs_id_importer(engine_object)
        response = cls._create_job(cls._run_import_data_job, jobs_id, req, session, engine_object)
        return response

    @classmethod
    async def _get_engine_object(cls, req, session):
        id_ = req.path_params['id']
        engine_objects = await cls.get(session, {'id': id_}, todict=False)

        if not engine_objects:
            return None

        return engine_objects[0]

    @classmethod
    def _get_jobs_id_importer(cls, engine_object):
        return cls._get_jobs_id(engine_object) + '_importer'

    @classmethod
    def _get_jobs_id(cls, engine_object):
        return '{}_{}_{}'.format(
            engine_object.strategy.name,
            engine_object.type,
            engine_object.id
        )

    @classmethod
    def _run_import_data_job(cls, req, session, engine_object):
        items_model = cls._get_items_model(engine_object)
        engine_object = cls.get_engine_object_instance(engine_object)
        return engine_object.get_data(
            items_model,
            session
        )

    @classmethod
    def _get_items_model(cls, engine_object):
        return get_items_model(
            engine_object.item_type.todict(),
            engine_object.store_id
        )

    @classmethod
    def get_engine_object_instance(cls, engine_object):
        strategy_class = engine_object.strategy.get_class()
        object_class = strategy_class.object_types[engine_object.type]
        return object_class(engine_object.todict(), cls.__data_path__)

    @classmethod
    async def get_import_data_job(cls, req, session):
        engine_object = await cls._get_engine_object(req, session)
        if engine_object is None:
            return cls._build_response(404)

        jobs_id = cls._get_jobs_id_importer(engine_object)
        return await cls._get_job(jobs_id, req, session) class EngineObjectsExporterModelBase(EngineObjectsDataImporterModelBase):
    __swagger_json__ = extend_swagger_json(
        EngineObjectsDataImporterModelBase.__swagger_json__,
        __file__
    )

    @classmethod
    async def post_export_job(cls, req, session):
        session = cls._copy_session(session)
        engine_object = await cls._get_engine_object(req, session)

        if engine_object is None:
            return cls._build_response(404)

        jobs_id = cls._get_jobs_id_exporter(engine_object)
        return cls._create_job(cls._run_export_objects_job, jobs_id, req, session, engine_object)

    @classmethod
    def _get_jobs_id_exporter(cls, engine_object):
        return cls._get_jobs_id(engine_object) + '_exporter'

    @classmethod
    def _run_export_objects_job(cls, req, session, engine_object):
        import_data = req.query.get('import_data')
        items_model = cls._get_items_model(engine_object)
        engine_object = cls.get_engine_object_instance(engine_object)

        if import_data:
            importer_result = engine_object.get_data(items_model, session)
            exporter_result = engine_object.export(items_model, session)
            return {
                'importer': importer_result,
                'exporter': exporter_result
            }

        else:
            return engine_object.export(items_model, session)

    @classmethod
    async def get_export_job(cls, req, session):
        engine_object = await cls._get_engine_object(req, session)
        if engine_object is None:
            return cls._build_response(404)

        jobs_id = cls._get_jobs_id_exporter(engine_object)
        return await cls._get_job(jobs_id, req, session) class EngineObjectsModelBase(AbstractConcreteBase):
    __tablename__ = 'engine_objects'
    __swagger_json__ = get_swagger_json(__file__)
    __table_args__ = (sa.UniqueConstraint('name', 'type', 'store_id'),)

    id = sa.Column(sa.Integer, primary_key=True)
    type = sa.Column(sa.String(255), nullable=False)
    name = sa.Column(sa.String(255), nullable=False)
    configuration_json = sa.Column(sa.Text, nullable=False)

    @declared_attr
    def strategy_id(cls):
        return sa.Column(sa.ForeignKey('engine_strategies.id'), nullable=False)

    @declared_attr
    def store_id(cls):
        return sa.Column(sa.ForeignKey('stores.id'), nullable=False)

    @declared_attr
    def item_type_id(cls):
        return sa.Column(sa.ForeignKey('item_types.id'), nullable=False)

    @declared_attr
    def strategy(cls):
        return sa.orm.relationship('EngineStrategiesModel')

    @declared_attr
    def item_type(cls):
        return sa.orm.relationship('ItemTypesModel')

    @declared_attr
    def store(cls):
        return sa.orm.relationship('StoresModel')

    @property
    def configuration(self):
        if not hasattr(self, '_configuration'):
            self._configuration = ujson.loads(self.configuration_json)
        return self._configuration

    async def _setattr(self, attr_name, value, session, input_):
        if attr_name == 'configuration':
            value = ujson.dumps(value)
            attr_name = 'configuration_json'

        if attr_name == 'strategy_id':
            value = {'id': value}
            attr_name = 'strategy'

        await super()._setattr(attr_name, value, session, input_)

    def _format_output_json(self, dict_inst, schema):
        if schema.get('configuration') is not False:
            dict_inst.pop('configuration_json')
            dict_inst['configuration'] = self.configuration

    async def _validate(self, session, input_):
        # disable validation when the operation was did by the engine model
        if (isinstance(input_, list) and 'objects' in input_[0]) or \
                (isinstance(input_, dict) and 'objects' in input_):
            return

        strategy_class = self.strategy.get_class()

        if self.type not in strategy_class.object_types:
            raise SwaggerItModelError(
                "Invalid object type '{}'".format(self.type),
                instance=input_
            )

        object_schema = strategy_class.configuration_schema['properties'][self.type]

        if 'definitions' in strategy_class.configuration_schema:
            object_schema['definitions'] = strategy_class.configuration_schema['definitions']

        jsonschema.validate(self.configuration, object_schema) class EngineStrategiesModelBase(AbstractConcreteBase):
    __tablename__ = 'engine_strategies'
    __swagger_json__ = get_swagger_json(__file__)
    _jobs = dict()
    __table_args__ = (sa.UniqueConstraint('class_name', 'class_module'),)

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String(255), unique=True, nullable=False)
    class_name = sa.Column(sa.String(255), nullable=False)
    class_module = sa.Column(sa.String(255), nullable=False)

    async def _validate(self, session, input_):
        self.get_class()

    @classmethod
    def _get_class(cls, strategy):
        return ModuleObjectLoader.load({
            'path': strategy['class_module'],
            'object_name': strategy['class_name']
        })

    def get_class(self):
        if not hasattr(self, '_class'):
            self._class = type(self)._get_class(self.todict({'object_types': False}))

        return self._class

    @property
    def object_types(self):
        return self.get_class().object_types.keys()

    def _format_output_json(self, dict_inst, schema):
        if schema.get('object_types') is not False:
            dict_inst['object_types'] = self.object_types

    @classmethod
    def get_instance(cls, engine, items_model=None):
        return cls._get_class(engine['strategy'])(engine, items_model) class EnginesModelBase(AbstractConcreteBase):
    __tablename__ = 'engines'
    __swagger_json__ = get_swagger_json(__file__)
    _jobs = dict()
    __table_args__ = (sa.UniqueConstraint('name', 'store_id'),)

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String(255), nullable=False)

    @declared_attr
    def strategy_id(cls):
        return sa.Column(sa.ForeignKey('engine_strategies.id'), nullable=False)

    @declared_attr
    def store_id(cls):
        return sa.Column(sa.ForeignKey('stores.id'), nullable=False)

    @declared_attr
    def item_type_id(cls):
        return sa.Column(sa.ForeignKey('item_types.id'), nullable=False)

    @declared_attr
    def item_type(cls):
        return sa.orm.relationship('ItemTypesModel')

    @declared_attr
    def store(cls):
        return sa.orm.relationship('StoresModel')

    @declared_attr
    def strategy(cls):
        return sa.orm.relationship('EngineStrategiesModel')

    @declared_attr
    def objects(cls):
        return sa.orm.relationship('EngineObjectsModel', uselist=True,
                                   secondary='engines_objects')

    @property
    def variables(self):
        return self.strategy_instance.get_variables()

    @property
    def strategy_instance(self):
        if not hasattr(self, '_strategy_instance'):
            self._strategy_instance = \
                type(self.strategy).get_instance(self._todict_when_new())

        return self._strategy_instance

    def _todict_when_new(self):
        objects = []
        self_dict = self.todict({'variables': False})
        strategy_dict = self_dict['strategy']

        for obj in self.objects:
            obj_dict = obj.todict()
            obj_dict['strategy'] = strategy_dict
            objects.append(obj_dict)

        self_dict['objects'] = objects
        return self_dict

    async def _setattr(self, attr_name, value, session, input_):
        if attr_name == 'item_type_id':
            value = {'id': value}
            attr_name = 'item_type'

        if attr_name == 'strategy_id':
            value = {'id': value}
            attr_name = 'strategy'

        if attr_name == 'store_id':
            value = {'id': value}
            attr_name = 'store'

        await super()._setattr(attr_name, value, session, input_)

    async def _validate(self, session, input_):
        strategy_class = self.strategy.get_class()

        for obj in self.objects:
            if obj.type not in strategy_class.object_types:
                raise SwaggerItModelError(
                    "Invalid object type '{}'".format(obj.type),
                    instance=input_
                )

        props_sequence = [
            ('strategy_id', self.strategy.id),
            ('item_type_id',self.item_type.id),
            ('store_id', self.store.id)
        ]

        for obj in self.objects:
            for obj_prop_name, self_prop_value in props_sequence:
                value = getattr(obj, obj_prop_name)

                if value is None:
                    setattr(obj, obj_prop_name, self_prop_value)

                elif value != self_prop_value:
                    raise SwaggerItModelError(
                    "Invalid object '{}' with value '{}'. "\
                    "This value must be the same as '{}'".format(
                        obj_prop_name,
                        value,
                        self_prop_value
                    ),
                    instance=input_
                )

        self.strategy_instance.validate_config()

    async def init(self, session, input_=None, **kwargs):
        for object_ in kwargs.get('objects', []):
            object_['item_type_id'] = kwargs.get('item_type_id', self.item_type_id)
            object_['strategy_id'] = kwargs.get('strategy_id', self.strategy_id)
            object_['store_id'] = kwargs.get('store_id', self.store_id)

        await super().init(session, input_=input_, **kwargs)


    def _format_output_json(self, dict_inst, schema):
        if schema.get('variables') is not False:
            dict_inst['variables'] = self.variables def build_engines_objects_table(metadata, **kwargs):
    return sa.Table(
        'engines_objects', metadata,
        sa.Column('engine_id', sa.Integer, sa.ForeignKey('engines.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True),
        sa.Column('object_id', sa.Integer, sa.ForeignKey('engine_objects.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True),
        **kwargs) class ExternalVariablesModelBase(AbstractConcreteBase):
    __tablename__ = 'external_variables'
    __swagger_json__ = get_swagger_json(__file__)
    __table_args__ = (sa.UniqueConstraint('name', 'store_id'),)

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String(255), nullable=False)

    @declared_attr
    def store_id(cls):
        return sa.Column(sa.ForeignKey('stores.id'), nullable=False) class ItemTypesDataFileImporterModelBase(ItemTypesModelBase):
    __swagger_json__ = extend_swagger_json(
        ItemTypesModelBase.__swagger_json__,
        __file__
    )

    @classmethod
    async def post_import_data_file_job(cls, req, session):
        content_type = req.headers.get('content-type')
        if content_type != 'application/zip' and content_type != 'application/gzip':
            raise SwaggerItModelError("Invalid content type '{}'".format(content_type))

        stream = BytesIO()
        async for line in req.body:
            stream.write(line)
        stream.seek(0)

        return await cls._create_job(
            cls._run_import_data_file_job,
            req, session, '_importer',
            stream=stream, content_type=content_type)

    @classmethod
    async def _create_job(cls, func, req, session, jobs_id_prefix, stream=None, content_type=None):
        store_id = req.query['store_id']
        store_items_model = await cls._get_store_items_model(req, session)
        if store_items_model is None:
            return cls._build_response(404)

        session = cls._copy_session(session)
        jobs_id = store_items_model.__key__ + jobs_id_prefix
        return super()._create_job(
            func, jobs_id,
            req, session,
            store_items_model, store_id,
            stream=stream, content_type=content_type
        )

    @classmethod
    def _run_import_data_file_job(
            cls, req, session, store_items_model,
            store_id, stream, content_type):
        upload_file = req.query.get('upload_file', True)
        if upload_file:
            cls._put_file_on_s3(stream, store_items_model, session, store_id)
            stream.seek(0)

        result = cls._update_items_from_zipped_file(stream, store_items_model, content_type, session)

        gc.collect()
        return result

    @classmethod
    def _put_file_on_s3(cls, stream, store_items_model, session, store_id):
        cls._logger.info("Started put file on S3 for '{}'".format(store_items_model.__key__))

        store = cls._run_coro(
            cls.get_model('stores').get(session, [{'id': store_id}]),
            session
        )[0]

        s3_bucket = store['configuration']['aws']['s3']['bucket']
        access_key_id = store['configuration']['aws'].get('access_key_id')
        secret_access_key = store['configuration']['aws'].get('secret_access_key')
        s3_key = '{}.zip'.format(store_items_model.__key__)

        boto3.resource(
            's3',
            aws_access_key_id=access_key_id,
            aws_secret_access_key=secret_access_key
        ).Bucket(s3_bucket).put_object(Body=stream, Key=s3_key)

        cls._logger.info("Finished put file on S3 for '{}'".format(store_items_model.__key__))

    @classmethod
    def _run_coro(cls, coro, session):
        return run_coro(coro, session)

    @classmethod
    def _update_items_from_zipped_file(cls, stream, store_items_model, content_type, session):
        feed = cls._unzip_file(content_type, stream)
        result = cls._update_items_from_file(feed, store_items_model, session)

        feed.close()

        if os.path.isfile(feed.name):
            os.remove(feed.name)

        return result

    @classmethod
    def _unzip_file(cls, content_type, stream=None, mode=None):
        tempfile = NamedTemporaryFile(delete=False)
        filename = tempfile.name
        kwargs = {'mode': mode} if mode else {}
        if stream is not None:
            tempfile.write(stream.read())

        tempfile.close()

        if content_type.endswith('gzip'):
            zfile = GzipFile(filename, **kwargs)

        else:
            zfile = ZipFile(filename, **kwargs)
            zfile = zfile.open(zfile.namelist()[0])

        return zfile


    @classmethod
    def _update_items_from_file(cls, feed, store_items_model, session):
        cls._logger.info(
            "Started update items from file for '{}'".format(store_items_model.__key__)
        )
        warning_message = "Invalid line for model '{}': ".format(store_items_model.__key__) + '{}'
        validator = ItemValidator(store_items_model.item_type['schema'])
        new_keys = set()
        success_lines = 0
        errors_lines = 0
        empty_lines = 0
        old_keys = cls._run_coro(
            session.redis_bind.hkeys(store_items_model.__key__),
            session
        )
        old_keys = set(old_keys)
        lines = []

        if hasattr(store_items_model, 'pre_process_feed'):
            feed = store_items_model.pre_process_feed(feed, session)

        for line in feed:
            try:
                line = cls._try_to_process_line(line, validator, store_items_model)
                if line is None:
                    empty_lines += 1
                    continue
                else:
                    success_lines += 1
                    new_keys.add(store_items_model.get_instance_key(line))
                    lines.append(line)

                    if len(lines) == 1000:
                        cls._run_coro(
                            store_items_model.insert(session, lines, skip_validation=True),
                            session
                        )
                        lines = []

            except Exception as error:
                errors_lines += 1
                cls._logger.warning(warning_message.format(line))
                cls._logger.warning(error)
                continue

        if lines:
            cls._run_coro(
                store_items_model.insert(session, lines, skip_validation=True),
                session
            )

        del lines
        old_keys.difference_update(new_keys)

        if old_keys:
            cls._run_coro(
                session.redis_bind.hdel(store_items_model.__key__, *old_keys),
                session
            )

        cls._run_coro(
            cls._set_stock_filter(store_items_model, session),
            session
        )

        cls._logger.info(
            "Finished update items from file for '{}'".format(store_items_model.__key__)
        )

        return {
            'success_lines': success_lines,
            'errors_lines': errors_lines,
            'empty_lines': empty_lines
        }

    @classmethod
    def _try_to_process_line(cls, line, validator, store_items_model):
        line = line.strip()

        if isinstance(line, bytes):
            line = line.decode()

        if not line:
            return None

        line = ujson.loads(line)
        validator.validate(line)

        return line

    @classmethod
    async def get_import_data_file_job(cls, req, session):
        return await cls._get_job('_importer', req, session)

    @classmethod
    async def _get_job(cls, sufix, req, session):
        store_items_model = await cls._get_store_items_model(req, session)
        if store_items_model is None:
            return cls._build_response(404)

        jobs_id = store_items_model.__key__ + sufix
        return await super()._get_job(jobs_id, req, session) class ItemTypesFiltersUpdaterModelBase(ItemTypesDataFileImporterModelBase):
    __swagger_json__ = extend_swagger_json(
        ItemTypesDataFileImporterModelBase.__swagger_json__,
        __file__
    )

    @classmethod
    async def post_update_filters_job(cls, req, session):
        return await cls._create_job(cls._run_update_filters_job, req, session, '_updater')

    @classmethod
    async def _run_update_filters_job(cls, req, session, store_items_model, store_id, **kwargs):
        cls._logger.info("Started update filters for '{}'".format(store_items_model.__key__))

        return await cls.update_enabled_filters(store_items_model, session, store_id)

    @classmethod
    async def get_update_filters_job(cls, req, session):
        return await cls._get_job('_updater', req, session)

    @classmethod
    async def update_enabled_filters(cls, store_items_model, session, store_id):
        items_indices_map = store_items_model.indices_map
        items_indices_map_ret = await items_indices_map.update(session)
        items_indices_map_len = await items_indices_map.get_length(session)

        filters_factory = cls.get_model('slot_filters').__factory__
        enabled_filters = await cls._get_enabled_filters(store_items_model, session, store_id)
        filters_ret = dict()
        items_indices_map_dict = await items_indices_map.get_all(session)
        items = await cls._get_items_with_indices_and_stock(
            store_items_model, session, items_indices_map_dict
        )

        stock_filter = BooleanFilterBy(store_items_model, 'stock')
        await stock_filter.update(session, items, items_indices_map_len)

        for slot_filter, schema in enabled_filters:
            filter_ = filters_factory.make(
                store_items_model, slot_filter,
                schema, slot_filter['skip_values']
            )
            filters_ret[filter_.name] = await filter_.update(session, items, items_indices_map_len)

        cls._logger.info("Finished update filters for '{}'".format(store_items_model.__key__))
        return {'items_indices_map': items_indices_map_ret, 'filters': filters_ret}

    @classmethod
    async def _get_enabled_filters(cls, store_items_model, session, store_id):
        slots_model = cls.get_model('slots')
        slots = await slots_model.get(session, **{'store_id': store_id})
        filters_external_variables = []

        # used to aggregate filters inclusive and exclusive with same property
        filters_names_set = set()

        for slot in slots:
            if store_items_model.item_type['id'] == slot['engine']['item_type_id']:
                for slot_filter in slot['slot_filters']:
                    filter_name = slot_filter['property_name']
                    if filter_name not in filters_names_set:
                        schema = \
                            store_items_model.item_type['schema']['properties'][filter_name]
                        filters_external_variables.append((slot_filter, schema))
                        filters_names_set.add(filter_name)

        return filters_external_variables

    @classmethod
    async def _get_items_with_indices_and_stock(cls, store_items_model, session, items_indices_map_dict):
        items = []
        page = 1
        items_part = await store_items_model.get(session, page=page, items_per_page=100000)

        while items_part:
            items.extend(items_part)
            page += 1
            items_part = await store_items_model.get(session, page=page, items_per_page=100000)

        for item in items:
            item_key = store_items_model.get_instance_key(item)
            index = items_indices_map_dict.get(item_key)
            if index is not None:
                item['index'] = index
                item['stock'] = True

        return items class ItemTypesModelBase(_ItemTypesModelBase, _StoreItemsOperationsMixin):
    pass def build_item_types_stores_table(metadata, **kwargs):
    return sa.Table(
        'item_types_stores', metadata,
        sa.Column('item_type_id', sa.Integer, sa.ForeignKey('item_types.id', ondelete='CASCADE', onupdate='CASCADE'), primary_key=True),
        sa.Column('store_id', sa.Integer, sa.ForeignKey('stores.id', ondelete='CASCADE', onupdate='CASCADE'), primary_key=True),
        **kwargs) class ABTestUsersModelBase(AbstractConcreteBase):
    __tablename__ = 'ab_test_users'

    id = sa.Column(sa.Integer, primary_key=True)

    @declared_attr
    def variation_id(cls):
        return sa.Column(sa.ForeignKey('variations.id'), nullable=False) class PlacementsModelBase(AbstractConcreteBase):
    __tablename__ = 'placements'
    __swagger_json__ = get_swagger_json(__file__)

    hash = sa.Column(sa.String(255), unique=True, nullable=False)
    small_hash = sa.Column(sa.String(255), primary_key=True)
    name = sa.Column(sa.String(255), nullable=False)
    ab_testing = sa.Column(sa.Boolean, default=False)
    show_details = sa.Column(sa.Boolean, default=True)
    distribute_items = sa.Column(sa.Boolean, default=False)
    is_redirect = sa.Column(sa.Boolean, default=False)

    @declared_attr
    def store_id(cls):
        return sa.Column(sa.ForeignKey('stores.id'), nullable=False)

    @declared_attr
    def variations(cls):
        return sa.orm.relationship('VariationsModel', uselist=True, passive_deletes=True)

    async def init(self, session, input_=None, **kwargs):
        await super().init(session, input_=input_, **kwargs)
        self._set_hash()

    def _set_hash(self):
        if self.name and not self.hash:
            hash_ = hashlib.new('ripemd160')
            hash_.update(self.name.encode() + bytes(self.store_id))
            self.hash = hash_.hexdigest()

    def __setattr__(self, name, value):
        if name == 'hash':
            self.small_hash = value[:5]

        super().__setattr__(name, value)

    @classmethod
    async def get_items(cls, req, session):
        placement = await cls._get_placement(req, session)
        if placement is None:
            return cls._build_response(404)

        explict_fallbacks = req.query.pop('explict_fallbacks', False)
        input_external_variables = req.query
        show_details = req.query.pop('show_details', placement.get('show_details'))
        distribute_items = placement.get('distribute_items')
        recos = slots = []
        recos_key = 'slots'
        slots_coros = []

        for slot in placement['variations'][0]['slots']:
            coro = cls._get_slot_recos_async(slot, input_external_variables,
                                             session, show_details)
            slots_coros.append(Task(coro))

        for coro in slots_coros:
            slots.append(await coro)

        valid_slots = []
        for slot in slots:
            slot_recos = slot['items']
            has_fallback = [True for fallback in slot_recos['fallbacks'] if fallback]
            if slot_recos['main'] or has_fallback:
                valid_slots.append(slot)

        if not valid_slots:
            return cls._build_response(404)

        if not explict_fallbacks or placement['is_redirect']:
            for slot in slots:
                slot['items'] = cls._get_all_slot_recos(slot['items'])

            if distribute_items:
                recos_key = 'distributed_items'
                recos = cls._get_all_recos_from_slots(slots)
                recos = cls._distribute_items(recos)

        if placement['is_redirect']:
            return cls._build_redirect_response(recos, distribute_items, req)

        else:
            placement = {'name': placement['name'], 'small_hash': placement['small_hash']}
            placement[recos_key] = recos

            return cls._build_recos_response(placement)

    @classmethod
    async def _get_placement(cls, req, session):
        small_hash = req.path_params['small_hash']
        placements = await cls.get(session, {'small_hash': small_hash})

        if not placements:
            return None

        return placements[0]

    @classmethod
    async def _get_slot_recos_async(cls, slot, input_external_variables, session, show_details):
        slot_recos = {'fallbacks': []}
        slot_recos['main'] = \
            await cls._get_recos_by_slot(slot, input_external_variables, session, show_details)

        await cls._get_fallbacks_recos(slot_recos, slot, input_external_variables, session, show_details)

        slot = {'name': slot['name'], 'item_type': slot['engine']['item_type']['name']}
        slot['items'] = slot_recos

        return slot

    @classmethod
    async def _get_recos_by_slot(cls, slot, input_external_variables, session, show_details, max_items=None):
        try:
            engine = slot['engine']
            items_model = get_items_model(engine['item_type'], engine['store_id'])
            engine_vars = cls._get_slot_variables(slot, input_external_variables)
            filters = cls._get_slot_filters(slot, input_external_variables, items_model)
            engine_strategy_model = cls.get_model('engine_strategies')
            strategy_instance = engine_strategy_model.get_instance(engine, items_model)
            max_items = slot['max_items'] if max_items is None else max_items

            return await strategy_instance.get_items(
                session, filters, max_items, show_details, items_model, **engine_vars)

        except Exception as error:
            cls._logger.debug('Input Variables:\n' + ujson.dumps(input_external_variables, indent=4))
            cls._logger.exception('Slot:\n' + ujson.dumps(slot, indent=4))
            return []

    @classmethod
    def _get_slot_variables(cls, slot, input_external_variables):
        engine_vars = dict()

        for slot_var in slot['slot_variables']:
            var_name = slot_var['external_variable']['name']
            var_engine_name = slot_var['engine_variable_name']

            if var_name in input_external_variables:
                var_value = input_external_variables[var_name]
                schema = cls._get_external_variable_schema(slot, var_engine_name)

                if schema is not None:
                    engine_vars[var_engine_name] = JsonBuilder.build(var_value, schema)

        return engine_vars

    @classmethod
    def _get_external_variable_schema(cls, slot, var_name):
        for var in slot['engine']['variables']:
            if var['name'] == var_name:
                return var['schema']

    @classmethod
    def _get_slot_filters(cls, slot, input_external_variables, items_model):
        filters = dict()

        for slot_filter in slot['slot_filters']:
            var_name = slot_filter['external_variable']['name']
            prop_name = slot_filter['property_name']
            is_overridable = slot_filter['override'] and slot_filter['override_value']

            if var_name in input_external_variables:
                if is_overridable:
                    var_value = slot_filter['override_value']
                else:
                    var_value = input_external_variables[var_name]

                cls._set_filter(filters, slot_filter, slot, var_value, items_model)

            elif is_overridable:
                var_value = slot_filter['override_value']
                cls._set_filter(filters, slot_filter, slot, var_value, items_model)

        return filters

    @classmethod
    def _set_filter(cls, filters, slot_filter, slot, var_value, items_model):
        factory = cls.get_model('slot_filters').__factory__
        filter_schema, input_schema = \
            cls._get_filter_and_input_schema(slot['engine'], slot_filter)

        if filter_schema is not None and input_schema is not None:
            filter_ = factory.make(items_model, slot_filter, filter_schema)
            filters[filter_] = JsonBuilder.build(var_value, input_schema)

    @classmethod
    def _get_filter_and_input_schema(cls, engine, slot_filter):
        for var in engine['item_type']['available_filters']:
            if var['name'] == slot_filter['property_name']:
                if slot_filter['type_id'] == 'item_property_value' or \
                        slot_filter['type_id'] == 'item_property_value_index':
                    input_schema = {'type': 'array', 'items': {'type': 'string'}}

                elif var['schema'].get('type') != 'array':
                    input_schema = {'type': 'array', 'items': var['schema']}

                else:
                    input_schema = var['schema']

                filter_schema = var['schema']
                return filter_schema, input_schema

        return None, None

    @classmethod
    async def _get_fallbacks_recos(cls, slot_recos, slot, input_external_variables, session, show_details):
        if len(slot_recos['main']) != slot['max_items']:
            for fallback in slot['fallbacks']:
                fallbacks_recos_size = \
                    sum([len(fallback) for fallback in slot_recos['fallbacks']])
                max_items = slot['max_items'] - len(slot_recos['main']) - fallbacks_recos_size
                if max_items == 0:
                    break

                fallback_recos = await cls._get_recos_by_slot(
                    fallback, input_external_variables, session, show_details, max_items)
                all_recos = cls._get_all_slot_recos(slot_recos)
                fallback_recos = cls._unique_recos(fallback_recos, all_recos)
                slot_recos['fallbacks'].append(fallback_recos)

    @classmethod
    def _get_all_slot_recos(cls, slot_recos):
        all_recos = list(slot_recos['main'])
        [all_recos.extend(fallback_recos) for fallback_recos in slot_recos['fallbacks']]
        return all_recos

    @classmethod
    def _get_all_recos_from_slots(cls, slots):
        recos = []
        for slot in slots:
            for reco in slot['items']:
                reco['type'] = slot['item_type']

            recos.append(slot['items'])
        return recos

    @classmethod
    def _distribute_items(cls, recos_list, random=True):
        total_length = sum([len(recos) for recos in recos_list])
        total_items = []
        initial_pos = 0

        for recos in recos_list:
            if not recos:
                continue

            step = int(total_length / len(recos))

            if random:
                initial_pos = random_.randint(0, step-1)

            positions = range(initial_pos, total_length, step)
            zip_items_positions = zip(recos, positions)
            total_items.extend(zip_items_positions)

        random_.shuffle(total_items)
        sorted_items = sorted(total_items, key=(lambda each: each[1]))

        return [i[0] for i in sorted_items]

    @classmethod
    def _unique_recos(cls, recos, all_recos):
        unique = list()
        [unique.append(reco) for reco in recos if not all_recos.count(reco)]
        return unique

    @classmethod
    def _build_redirect_response(cls, recos, distribute_items, req):
        slot_idx = req.query.get('slot_idx')
        item_idx = req.query.get('item_idx')

        if item_idx is None:
            message = {
                'message': "Query argument 'item_idx' "\
                           "is mandatory when 'is_redirect' is true."
            }
            return cls._build_response(400, body=cls._pack_obj(message))

        if (slot_idx is None and distribute_items is False):
            message = {
                'message': "Query argument 'slot_idx' "\
                           "is mandatory when 'distribute_items' is false."
            }
            return cls._build_response(400, body=cls._pack_obj(message))

        elif (slot_idx is not None and distribute_items is True):
            message = {
                'message': "Query argument 'slot_idx' "\
                           "can't be setted when 'distribute_items' is true."
            }
            return cls._build_response(400, body=cls._pack_obj(message))

        if distribute_items is True:
            get_item = lambda recos: recos[item_idx]
        else:
            get_item = lambda recos: recos[slot_idx]['items'][item_idx]

        try:
            location = get_item(recos)

        except IndexError:
            return cls._build_response(404)

        else:
            return cls._build_response(302, headers={'Location': str(location)})

    @classmethod
    def _build_recos_response(cls, recos):
        headers = {'Content-Type': 'application/json'}
        return cls._build_response(200, body=cls._pack_obj(recos), headers=headers) class VariationsModelBase(AbstractConcreteBase):
    __tablename__ = 'variations'
    __table_args__ = (sa.UniqueConstraint('name', 'placement_hash'),)

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String(255), nullable=False)
    weight = sa.Column(sa.Float)

    @declared_attr
    def placement_hash(cls):
        return sa.Column(sa.ForeignKey('placements.hash', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)

    @declared_attr
    def slots(cls):
        return sa.orm.relationship('SlotsModel',
                uselist=True, secondary='variations_slots') def build_variations_slots_table(metadata, **kwargs):
    return sa.Table(
        'variations_slots', metadata,
        sa.Column('variation_id', sa.Integer, sa.ForeignKey('variations.id', ondelete='CASCADE', onupdate='CASCADE'), primary_key=True),
        sa.Column('slot_id', sa.Integer, sa.ForeignKey('slots.id', ondelete='CASCADE', onupdate='CASCADE'), primary_key=True),
        **kwargs) class SlotFiltersModelBase(AbstractConcreteBase):
    __tablename__ = 'slot_filters'
    __factory__ = FiltersFactory
    __table_args__ = (sa.UniqueConstraint('is_inclusive', 'property_name', 'type_id', 'slot_id'),)

    id = sa.Column(sa.Integer, primary_key=True)
    is_inclusive = sa.Column(sa.Boolean, default=True, nullable=False)
    property_name = sa.Column(sa.String(255), nullable=False)
    type_id = sa.Column(sa.String(255), nullable=False)
    override = sa.Column(sa.Boolean, default=False)
    override_value_json = sa.Column(sa.Text)
    skip_values_json = sa.Column(sa.Text)

    @property
    def override_value(self):
        if not hasattr(self, '_override_value'):
            self._override_value = \
                ujson.loads(self.override_value_json) if self.override_value_json is not None else None
        return self._override_value

    @property
    def skip_values(self):
        if not hasattr(self, '_skip_values'):
            self._skip_values = \
                ujson.loads(self.skip_values_json) if self.skip_values_json is not None else None
        return self._skip_values

    async def _setattr(self, attr_name, value, session, input_):
        if attr_name == 'skip_values':
            value = ujson.dumps(value)
            attr_name = 'skip_values_json'

        if attr_name == 'override_value':
            value = ujson.dumps(value)
            attr_name = 'override_value_json'

        await super()._setattr(attr_name, value, session, input_)

    def _format_output_json(self, dict_inst, schema):
        if schema.get('skip_values') is not False:
            dict_inst.pop('skip_values_json')
            dict_inst['skip_values'] = self.skip_values

        if schema.get('override_value') is not False:
            dict_inst.pop('override_value_json')
            dict_inst['override_value'] = self.override_value

        if schema.get('type') is not False:
            dict_inst['type'] = self.type

    @declared_attr
    def external_variable_id(cls):
        return sa.Column(sa.ForeignKey('external_variables.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)

    @declared_attr
    def slot_id(cls):
        return sa.Column(sa.ForeignKey('slots.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)

    @declared_attr
    def external_variable(cls):
        return sa.orm.relationship('ExternalVariablesModel')

    @property
    def type(self):
        return type(self).__factory__.get_filter_type(self.type_id)

    async def _validate(self, session, input_):
        self.type class SlotsModelBase(AbstractConcreteBase):
    __tablename__ = 'slots'
    __swagger_json__ = get_swagger_json(__file__)

    id = sa.Column(sa.Integer, primary_key=True)
    max_items = sa.Column(sa.Integer, nullable=False)
    name = sa.Column(sa.String(255), nullable=False)

    @declared_attr
    def engine_id(cls):
        return sa.Column(sa.ForeignKey('engines.id'), nullable=False)

    @declared_attr
    def store_id(cls):
        return sa.Column(sa.ForeignKey('stores.id'), nullable=False)

    @declared_attr
    def engine(cls):
        return sa.orm.relationship('EnginesModel')

    @declared_attr
    def slot_variables(cls):
        return sa.orm.relationship('SlotVariablesModel', uselist=True, passive_deletes=True)

    @declared_attr
    def slot_filters(cls):
        return sa.orm.relationship('SlotFiltersModel', uselist=True, passive_deletes=True)

    @declared_attr
    def fallbacks(cls):
        return sa.orm.relationship('SlotsModel',
                                   uselist=True, remote_side='SlotsModel.id',
                                   secondary='slots_fallbacks',
                                   primaryjoin='slots_fallbacks.c.slot_id == SlotsModel.id',
                                   secondaryjoin='slots_fallbacks.c.fallback_id == SlotsModel.id')

    async def init(self, session, input_=None, **kwargs):
        await super().init(session, input_=input_, **kwargs)
        self._validate_fallbacks(input_)
        self._validate_slot_variables(input_)
        self._validate_slot_filters(input_)

    def _validate_fallbacks(self, input_):
        for fallback in self.fallbacks:
            if fallback.id == self.id:
                raise SwaggerItModelError(
                    "a Engine Manager can't fallback itself", input_)

            if fallback.engine.item_type_id != self.engine.item_type_id:
                raise SwaggerItModelError(
                    "Cannot set a fallback with different items types", input_)

    def _validate_slot_variables(self, input_):
        if self.engine is not None:
            engine = self.engine.todict()
            engine_variables_set = set([var['name'] for var in engine['variables']])
            message = "Invalid slot variable with 'engine_variable_name' attribute value '{}'"
            schema = {'available_variables': engine['variables']}

            for slot_variable in self.slot_variables:
                var_name = slot_variable.engine_variable_name
                if var_name not in engine_variables_set:
                    raise ValidationError(
                        message.format(var_name),
                        instance=input_, schema=schema
                    )

    def _validate_slot_filters(self, input_):
        if self.engine is not None:
            engine = self.engine.todict()
            available_filters = engine['item_type']['available_filters']
            available_filters_set = set([filter_['name'] for filter_ in available_filters])
            schema = {'available_filters': available_filters}
            message = "Invalid slot filter with 'property_name' attribute value '{}'"

            for slot_filter in self.slot_filters:
                filter_name = slot_filter.property_name
                if filter_name not in available_filters_set:
                    raise ValidationError(
                        message.format(filter_name),
                        instance=input_, schema=schema
                    )

    async def _setattr(self, attr_name, value, session, input_):
        if attr_name == 'engine_id':
            value = {'id': value}
            attr_name = 'engine'

        if attr_name == 'slot_variables':
            for engine_var in value:
                if 'external_variable_id' in engine_var:
                    var = {'id': engine_var.pop('external_variable_id')}
                    engine_var['external_variable'] = var

        await super()._setattr(attr_name, value, session, input_)

    def _format_output_json(self, dict_inst, schema):
        if schema.get('fallbacks') is not False:
            for fallback in dict_inst.get('fallbacks'):
                fallback.pop('fallbacks') class SlotVariablesModelBase(AbstractConcreteBase):
    __tablename__ = 'slot_variables'
    __table_args__ = (sa.UniqueConstraint('engine_variable_name', 'slot_id'),)

    id = sa.Column(sa.Integer, primary_key=True)
    override = sa.Column(sa.Boolean, default=False)
    override_value_json = sa.Column(sa.Text)
    engine_variable_name = sa.Column(sa.String(255), nullable=False)

    @property
    def override_value(self):
        if not hasattr(self, '_override_value'):
            self._override_value = \
                ujson.loads(self.override_value_json) if self.override_value_json is not None else None
        return self._override_value

    async def _setattr(self, attr_name, value, session, input_):
        if attr_name == 'override_value':
            value = ujson.dumps(value)
            attr_name = 'override_value_json'

        await super()._setattr(attr_name, value, session, input_)

    def _format_output_json(self, dict_inst, schema):
        if schema.get('override_value') is not False:
            dict_inst.pop('override_value_json')
            dict_inst['override_value'] = self.override_value

    @declared_attr
    def external_variable_id(cls):
        return sa.Column(sa.ForeignKey('external_variables.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)

    @declared_attr
    def slot_id(cls):
        return sa.Column(sa.ForeignKey('slots.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)

    @declared_attr
    def external_variable(cls):
        return sa.orm.relationship('ExternalVariablesModel') def build_slots_fallbacks_table(metadata, **kwargs):
    return sa.Table("slots_fallbacks", metadata,
        sa.Column("slot_id", sa.Integer, sa.ForeignKey(
            "slots.id", ondelete='CASCADE', onupdate='CASCADE'), primary_key=True),
        sa.Column("fallback_id", sa.Integer, sa.ForeignKey(
            "slots.id", ondelete='CASCADE', onupdate='CASCADE'), primary_key=True),
        **kwargs) class StoresModelBase(AbstractConcreteBase):
    __tablename__ = 'stores'
    __swagger_json__ = get_swagger_json(__file__)
    __table_args__ = (sa.UniqueConstraint('name', 'country'),)

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String(255), nullable=False)
    country = sa.Column(sa.String(255), nullable=False)
    configuration_json = sa.Column(sa.Text, nullable=False)

    @property
    def configuration(self):
        if not hasattr(self, '_configuration'):
            self._configuration = ujson.loads(self.configuration_json)
        return self._configuration

    async def _setattr(self, attr_name, value, session, input_):
        if attr_name == 'configuration':
            value = ujson.dumps(value)
            attr_name = 'configuration_json'

        await super()._setattr(attr_name, value, session, input_)

    def _format_output_json(self, dict_inst, schema):
        if schema.get('configuration') is not False:
            config = dict_inst.pop('configuration_json')
            dict_inst['configuration'] = self.configuration class GrantsModelBase(AbstractConcreteBase):
    __tablename__ = 'grants'
    __table_args__ = (sa.UniqueConstraint('uri_id', 'method_id'),)
    __swagger_json__ = get_swagger_json(__file__, 'grants_swagger.json')

    id = sa.Column(sa.Integer, primary_key=True)

    @declared_attr
    def uri_id(cls):
        return sa.Column(sa.ForeignKey('uris.id'))

    @declared_attr
    def method_id(cls):
        return sa.Column(sa.ForeignKey('methods.id'))

    @declared_attr
    def uri(cls):
        return sa.orm.relationship('URIsModel')

    @declared_attr
    def method(cls):
        return sa.orm.relationship('MethodsModel') class MethodsModelBase(AbstractConcreteBase):
    __tablename__ = 'methods'
    __swagger_json__ = get_swagger_json(__file__, 'methods_swagger.json')

    id = sa.Column(sa.Integer, primary_key=True)
    method = sa.Column(sa.String(10), unique=True, nullable=False) class URIsModelBase(AbstractConcreteBase):
    __tablename__ = 'uris'
    __swagger_json__ = get_swagger_json(__file__, 'uris_swagger.json')

    id = sa.Column(sa.Integer, primary_key=True)
    uri = sa.Column(sa.String(255), unique=True, nullable=False) class UsersModelBase(AbstractConcreteBase):
    __tablename__ = 'users'
    __swagger_json__ = get_swagger_json(__file__)

    id = sa.Column(sa.String(255), primary_key=True)
    name = sa.Column(sa.String(255), unique=True, nullable=False)
    email = sa.Column(sa.String(255), unique=True, nullable=False)
    password = sa.Column(sa.String(255), nullable=False)
    admin = sa.Column(sa.Boolean, default=False)

    @declared_attr
    def grants(cls):
        return sa.orm.relationship('GrantsModel', uselist=True, secondary='users_grants')

    @declared_attr
    def stores(cls):
        return sa.orm.relationship('StoresModel', uselist=True, secondary='users_stores')

    @classmethod
    async def authorize(cls, session, authorization, url, method):
        try:
            authorization = b64decode(authorization).decode()
        except binascii.Error:
            return None
        except UnicodeDecodeError:
            return None

        if not ':' in authorization:
            return None

        user = await cls.get(session, {'id': authorization})
        user = user[0] if user else user
        if user and user.get('admin'):
            session.user = user
            return True

        elif user:
            if method == 'OPTIONS':
                return True

            for grant in user['grants']:
                grant_uri = grant['uri']['uri']
                if (grant_uri == url or re.match(grant_uri, url)) \
                        and grant['method']['method'].lower() == method.lower():
                    session.user = user
                    return True

            return False

    @classmethod
    async def insert(cls, session, objs, commit=True, todict=True):
        objs = cls._to_list(objs)
        await cls._set_objs_ids_and_grant(objs, session)
        return await type(cls).insert(cls, session, objs, commit, todict)

    @classmethod
    async def _set_objs_ids_and_grant(cls, objs, session):
        objs = cls._to_list(objs)

        patch_method = await cls.get_model('methods').get(session, ids={'method': 'patch'}, todict=False)
        if not patch_method:
            patch_method = await cls.get_model('methods').insert(session, [{'method': 'patch'}], todict=False)
        patch_method = patch_method[0]

        get_method = await cls.get_model('methods').get(session, ids={'method': 'get'}, todict=False)
        if not get_method:
            get_method = await cls.get_model('methods').insert(session, [{'method': 'get'}], todict=False)
        get_method = get_method[0]

        for obj in objs:
            new_grants = []
            user_uri = '/users/{}'.format(obj['email'])

            uri = await cls.get_model('uris').get(session, ids={'uri': user_uri}, todict=False)
            if not uri:
                uri = await cls.get_model('uris').insert(session, [{'uri': user_uri}], todict=False)
            uri = uri[0]

            grant = await cls.get_model('grants').get(session, {'uri_id': uri.id, 'method_id': patch_method.id}, todict=False)
            if grant:
                grant = grant[0].todict()
            else:
                grant = {'uri_id': uri.id, 'method_id': patch_method.id, '_operation': 'insert'}
            new_grants.append(grant)

            grant = await cls.get_model('grants').get(session, {'uri_id': uri.id, 'method_id': get_method.id}, todict=False)
            if grant:
                grant = grant[0].todict()
            else:
                grant = {'uri_id': uri.id, 'method_id': get_method.id, '_operation': 'insert'}
            new_grants.append(grant)

            obj['id'] = '{}:{}'.format(obj['email'], obj['password'])
            grants = obj.get('grants', [])
            grants.extend(new_grants)
            obj['grants'] = grants

    @classmethod
    async def update(cls, session, objs, commit=True, todict=True, ids=None, ids_keys=None):
        if not ids:
            ids = []
            objs = cls._to_list(objs)
            for obj in objs:
                id_ = obj.get('id')
                email = obj.get('email')
                if id_ is not None:
                    ids.append({'id': id_})
                    ids_keys = ('id',)
                elif email is not None:
                    ids.append({'email': email})
                    ids_keys = ('email',)

        insts = await type(cls).update(cls, session, objs, commit=False,
                            todict=False, ids=ids, ids_keys=ids_keys)
        cls._set_insts_ids(insts)

        if commit:
            await session.commit()
        return cls._build_todict_list(insts) if todict else insts

    @classmethod
    def _set_insts_ids(cls, insts):
        insts = cls._to_list(insts)
        for inst in insts:
            inst.id = '{}:{}'.format(inst.email, inst.password) def build_users_grants_table(metadata, **kwargs):
    return sa.Table(
        'users_grants', metadata,
        sa.Column('user_id', sa.String(255), sa.ForeignKey('users.id', ondelete='CASCADE', onupdate='CASCADE'), primary_key=True),
        sa.Column('grant_id', sa.Integer, sa.ForeignKey('grants.id', ondelete='CASCADE', onupdate='CASCADE'), primary_key=True),
        **kwargs) def build_users_stores_table(metadata, **kwargs):
    return sa.Table(
        'users_stores', metadata,
        sa.Column('user_id', sa.String(255), sa.ForeignKey('users.id', ondelete='CASCADE', onupdate='CASCADE'), primary_key=True),
        sa.Column('store_id', sa.Integer, sa.ForeignKey('stores.id', ondelete='CASCADE', onupdate='CASCADE'), primary_key=True),
        **kwargs)
from myreco.engine_objects.data_importer.model import \
    EngineObjectsDataImporterModelBase
from myreco.engine_objects.exporter.model import EngineObjectsExporterModelBase
from myreco.engine_objects.model import EngineObjectsModelBase
from myreco.engine_strategies.model import EngineStrategiesModelBase
from myreco.engines.model import EnginesModelBase, build_engines_objects_table
from myreco.external_variables.model import ExternalVariablesModelBase
from myreco.item_types.data_file_importer.model import \
    ItemTypesDataFileImporterModelBase
from myreco.item_types.filters_updater.model import \
    ItemTypesFiltersUpdaterModelBase
from myreco.item_types.model import (ItemTypesModelBase,
                                     build_item_types_stores_table)
from myreco.placements.models import (ABTestUsersModelBase,
                                      PlacementsModelBase, VariationsModelBase,
                                      build_variations_slots_table)
from myreco.slots.models import (SlotFiltersModelBase, SlotsModelBase,
                                 SlotVariablesModelBase,
                                 build_slots_fallbacks_table)
from myreco.stores.model import StoresModelBase
from myreco.users.models import (GrantsModelBase, MethodsModelBase,
                                 URIsModelBase, UsersModelBase,
                                 build_users_grants_table,
                                 build_users_stores_table)
from swaggerit.models.orm.factory import FactoryOrmModels
# MIT License

# Copyright (c) 2016 Diogo Dutra <dutradda@gmail.com>

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.




class FactoryError(Exception):
    pass


class ModelsFactory(object):
    engine_strategies_base = EngineStrategiesModelBase
    engine_objects_recommender_base = EngineObjectsModelBase
    engine_objects_data_importer_base = EngineObjectsDataImporterModelBase
    engine_objects_exporter_base = EngineObjectsExporterModelBase
    engines_base = EnginesModelBase
    slots_base = SlotsModelBase
    slot_variables_base = SlotVariablesModelBase
    slot_filters_base = SlotFiltersModelBase
    item_types_recommender_base = ItemTypesModelBase
    item_types_data_importer_base = ItemTypesDataFileImporterModelBase
    item_types_objects_exporter_base = ItemTypesFiltersUpdaterModelBase
    placements_base = PlacementsModelBase
    variations_base = VariationsModelBase

    ab_test_users_base = ABTestUsersModelBase