from apollo_utils.service.exceptions import BadRequest, NotFound from marshmallow import Schema from sqlalchemy.future import select from typing import Awaitable, Callable, Type from server.constants.core import CheckUniqueMode from server.constants.messages import MessageUniqueCheckError from server.db.models import Application, Base from server.publishers.clients.sns.core import sns_publish_bulk from server.schemas.messages.utils import ok_and_failed, prepare_request_bulk_data, prepare_response_bulk_data, \ unique_check_failed_item async def create_multiple_messages_scenario( model: Base, data: dict, app_slug: str, code: str = None, # unfilled for events bulk creation only public: bool = False, publish_schema: Type[Schema] = None, publisher: Awaitable and Callable = sns_publish_bulk, check_unique_mode: CheckUniqueMode = CheckUniqueMode.INACTIVE, check_unique_key: str = None, ) -> dict: """ Create multiple 'model' messages based on the 'data' passed and optionally publish corresponding events to a bus. Args: model: model of a message to create. data: request data with 'data' attribute to get BulkField deserialized content from. app_slug: application to create messages for. code: Message code. public: flag - should message-related events be published or not. publish_schema: schema cls to dump a message object to an event. publisher: async function to publish message-related events to a bus. Returns: dict with 'ok' and 'failed' items lists. """ if public and not (publisher or publish_schema): raise NotImplemented( f"Can not create and publish {model.__name__} instances, " f"because publisher is not configured." ) if check_unique_mode and not check_unique_key: raise NotImplemented( f"Can not check uniqueness of {model.__name__} data, " f"because 'check_unique_key' is not configured." ) valid_data, validation_failed_items = data["data"] if not valid_data: return ok_and_failed(failed=validation_failed_items) app = await Application.get(slug=app_slug) if not app: raise NotFound(f"An application with the slug {app_slug} was not found.") valid_index_to_item_map, unique_failed_items, unique_value_to_index_map = prepare_request_bulk_data( items_list=valid_data, extra_data=dict(public=public, app_slug=app_slug), check_unique_key=check_unique_key ) if check_unique_key: if check_unique_mode is CheckUniqueMode.RAISE and unique_failed_items: raise BadRequest( f"Uniqueness check by {check_unique_key} was failed on the request uniqueness validation " f"step for the following items:\n{unique_failed_items}." ) if not valid_index_to_item_map: return ok_and_failed(failed=validation_failed_items + unique_failed_items) db_duplicated_keys = await model.get_bulk_unique( model.q_filter( select(getattr(model, check_unique_key)), filters=(getattr(model, check_unique_key).in_(list(unique_value_to_index_map.keys())),), app_slug=app_slug, ) ) for k in db_duplicated_keys: idx = unique_value_to_index_map[k] valid_index_to_item_map.pop(idx) unique_failed_items.append( unique_check_failed_item( index=idx, key=check_unique_key, value=k, error_type=MessageUniqueCheckError.DUPLICATED_DB_DATA ) ) if check_unique_mode is CheckUniqueMode.RAISE and unique_failed_items: raise BadRequest( f"Uniqueness check by {check_unique_key} was failed on the db uniqueness validation " f"step for the following items:\n{unique_failed_items}." ) if not valid_index_to_item_map: return ok_and_failed(failed=validation_failed_items + unique_failed_items) messages = await model.create_bulk(list(valid_index_to_item_map.values())) id_to_error = None if public: params = dict(many=True) if code: params["context"] = {"code": code} try: public_messages = publish_schema(**params).dump(messages) id_to_error = await publisher(public_messages) except Exception: await model.delete(filters=(model.id.in_([m.id for m in messages]),)) raise if id_to_error: await model.delete(filters=(model.id.in_(list(id_to_error.keys())),)) ok_items, publishing_failed_items = prepare_response_bulk_data( messages, passed_attrs=("id",), index_list=list(valid_index_to_item_map.keys()), id_to_error=id_to_error, id_attr_name="id", ) return ok_and_failed(ok=ok_items, failed=(validation_failed_items + unique_failed_items + publishing_failed_items))