"""Logic module for Action table.""" import datetime from flask import g from oto import response import sentry_sdk from conflict_manager import config from conflict_manager.constants import database as db_consts from conflict_manager.constants import error as error_consts from conflict_manager.constants import schema as schema_consts from conflict_manager.models import action as action_model from conflict_manager.models import fact_conflict from conflict_manager.models import fact_conflict_elasticsearch from conflict_manager.models import ows_masters_registry from conflict_manager.models import ows_sound_recordings def create_actions(account, actions_data, orchard_user_id): """Save conflict actions to DB. Args: account (namedtuple): User account data actions_data (dict): POST /action payload orchard_user_id (str): Orchard user_id for checking feature flags Returns: response.Response: result of operation """ action_date = datetime.datetime.utcnow() es_id, actions = _format_actions(actions_data, account, action_date) validation_response = validate_actions( account, actions_data[db_consts.ISRC], actions_data[schema_consts.CONFLICTING_OWNER], actions_data[db_consts.CONFLICT_DATE], actions, actions_data[db_consts.TUID]) if not validation_response: return validation_response created_action = action_model.create(actions) try: delete_response = fact_conflict_elasticsearch.delete_conflict( account, [es_id]) if delete_response.status != 200: return delete_response except Exception as e: if config.SENTRY_DSN: # Capture exceptions using Sentry when available sentry_sdk.capture_exception(e) return response.create_fatal_response(str(e)) _remove_ownership_for_release_conflicts(actions, account, orchard_user_id) return created_action def _remove_ownership_for_release_conflicts(actions, account, orchard_user_id): """Remove a ownership of released territories. Helper functions that calls ows_masters_registry.bulk_remove_territories Args: actions (list): list of actions formatted for saving in DB account (namedtuple): User account data orchard_user_id (str): Orchard user_id for checking feature flags """ release_ids = [ action[db_consts.CONFLICT_ID] for action in actions if action[db_consts.ACTION] == db_consts.RELEASE ] if release_ids: territories = fact_conflict.get_conflicts_territories(release_ids) ows_masters_registry.bulk_remove_territories( territories, account.type, account.id, correlation_id=None, orchard_user_id=orchard_user_id) _create_carveout_rules(territories, correlation_id=None) def _create_carveout_rules(territories_to_carveout, correlation_id): """Create carveout rules for released conflicts track.""" # it's only for youtube right now, # but it could be easily extend to tiktok as well services = ['youtube'] # getting existing rules for tracks, # we should merge and send all the rules, # in order to not overwrite the existing ones track_ids = list(set([item['tuid'] for item in territories_to_carveout])) if len(track_ids) == 0: return try: existing_rules = ows_sound_recordings.get_rules( track_ids, correlation_id).message updated_rules = {} for item in existing_rules: tuid = item['id'] updated_rules[tuid] = [ { 'policy': rule['policy'], 'service': rule['service'], 'territory': rule['territory'], 'start': rule['start'], 'end': rule['end'], } for rule in item.get('rules', []) ] for item in territories_to_carveout: tuid = item['tuid'] updated_rules[tuid] = updated_rules.get(tuid, []) for service in services: for territory in item.get('territories', []): # avoid duplicating carveout rules if any( rule.get('policy') == 'carveout' and rule.get('service') == service and rule.get('territory') == territory for rule in updated_rules[tuid] ): continue updated_rules[tuid].append({ 'policy': 'carveout', 'service': service, 'territory': territory, 'start': None, 'end': None }) ows_sound_recordings.bulk_create_fingerprint_rules( updated_rules, correlation_id) except Exception as e: if config.SENTRY_DSN: # Capture exceptions using Sentry when available sentry_sdk.capture_exception(e) def bulk_create_actions(account, payload_actions, orchard_user_id): """Save bulk conflict actions to DB. Args: account (namedtuple): User account data payload_actions (list): POST /action/bulk payload orchard_user_id (str): Orchard user_id for checking feature flags Returns: response.Response: result of operation """ formatted_actions = [] es_id_list = [] action_date = datetime.datetime.utcnow() for actions_data in payload_actions: es_id, actions = _format_actions(actions_data, account, action_date) formatted_actions.extend(actions) if es_id not in es_id_list: es_id_list.append(es_id) validation_response = validate_bulk_actions(account, payload_actions) if not validation_response: return validation_response number_of_rows = 0 for result in action_model.create(formatted_actions).message: try: number_of_rows += result[db_consts.NUMBER_OF_ROWS_INSERTED] except KeyError: number_of_rows += result[db_consts.NUMBER_OF_ROWS_UPDATED] _remove_ownership_for_release_conflicts( formatted_actions, account, orchard_user_id) try: delete_response = fact_conflict_elasticsearch.delete_conflict( account, es_id_list) if delete_response.status != 200: return delete_response except Exception as e: g.log.debug(e) if config.SENTRY_DSN: # Capture exceptions using Sentry when available sentry_sdk.capture_exception(e) return response.create_fatal_response(str(e)) return response.Response( {db_consts.NUMBER_OF_ROWS_INSERTED: number_of_rows}) def validate_bulk_actions(account, payload_actions): """Validate provided bulk actions data. Args: account (namedtuple): User account data payload_actions (list): POST /action/bulk payload Returns: response.Response: result of validation """ conflict_ids = fact_conflict.get_conflicts_ids_for_bulk_actions( account, payload_actions) def sort_key(action): return '{}{}{}{}'.format( action[schema_consts.ISRC], action[schema_consts.TUID], action[schema_consts.CONFLICTING_OWNER], action[schema_consts.CONFLICT_DATE]) existing_conflict_ids_map = {} for existing_conflict in conflict_ids: conflict_key = sort_key(existing_conflict) existing_conflict_ids_map[conflict_key] = set(map( int, existing_conflict[schema_consts.CONFLICT_IDS].split(',') )) action_conflict_ids_map = {} for action in payload_actions: release_action = action.get(schema_consts.RELEASE_ACTION, {}) assert_action = action.get(schema_consts.ASSERT_ACTION, {}) release_action_ids = set( release_action.get(schema_consts.CONFLICT_IDS, [])) assert_action_ids = set( assert_action.get(schema_consts.CONFLICT_IDS, [])) action_conflict_ids = release_action_ids.union(assert_action_ids) conflict_key = sort_key(action) if conflict_key not in action_conflict_ids_map: action_conflict_ids_map[conflict_key] = action_conflict_ids else: action_conflict_ids_map[conflict_key].update(action_conflict_ids) if len(action_conflict_ids_map) > len(existing_conflict_ids_map): return response.create_error_response( code=error_consts.ERROR_CODE_BAD_REQUEST, message=error_consts.BULK_ACTION_PAYLOAD_IS_TOO_LARGE) for action in payload_actions: conflict_key = sort_key(action) action_conflict_ids = action_conflict_ids_map.get( conflict_key, set() ) existing_conflict_ids = existing_conflict_ids_map.get( conflict_key, set() ) if not action_conflict_ids.issubset(existing_conflict_ids): return response.create_error_response( code=error_consts.ERROR_CODE_BAD_REQUEST, message=error_consts.INVALID_TERRITORIES_MSG) return response.Response() def validate_actions( account, isrc, conflicting_owner, conflict_date, actions, tuid): """Validate provided actions data. Args: account (namedtuple): User account data isrc (str): ISRC to filter conflicts by conflicting_owner (str): Owner who claimed rights for given ISRC conflict_date (str): Date when conflict was created actions (list): actions ready for saving into DB Returns: response.Response: result of validation """ conflict_ids_response = ( fact_conflict.get_conflict_ids_for_account_and_isrc( account, isrc, conflicting_owner, conflict_date, tuid)) if not conflict_ids_response: return conflict_ids_response existing_conflict_ids = set(conflict_ids_response.message['items']) actions_ids = {action['conflict_id'] for action in actions} if not actions_ids.issubset(existing_conflict_ids): return response.create_error_response( code=error_consts.ERROR_CODE_BAD_REQUEST, message=error_consts.INVALID_TERRITORIES_MSG) return response.Response(actions) def _format_actions(actions_data, account, action_date): """Convert actions payload from handler into suitable form for DB. This function works with ElasticSearch Id's. Args: actions_data (dict): dict with release and assert actions account (namedtuple): User account data action_date (datetime): action date Returns: list: ElasticSearch Id's in the action data list: actions ready for saving into DB """ actions = [] release_action = actions_data.get(schema_consts.RELEASE_ACTION, {}) release_conflict_ids = release_action.get(schema_consts.CONFLICT_IDS, []) es_id = actions_data.get(schema_consts.ELASTICSEARCH_ID, '') for conflict_id in release_conflict_ids: action_dict = _format_single_action( actions_data[schema_consts.TUID], conflict_id, es_id, db_consts.RELEASE, actions_data[schema_consts.RELEASE_ACTION], account, action_date) actions.append(action_dict) assert_action = actions_data.get(schema_consts.ASSERT_ACTION, {}) assert_conflict_ids = assert_action.get(schema_consts.CONFLICT_IDS, []) for conflict_id in assert_conflict_ids: action_dict = _format_single_action( actions_data[schema_consts.TUID], conflict_id, es_id, db_consts.ASSERT, actions_data[schema_consts.ASSERT_ACTION], account, action_date) actions.append(action_dict) return es_id, actions def _format_single_action( tuid, conflict_id, es_id, action_type, action_data, account, action_date): """Format single action for saving in DB. Args: tuid (int): id of the Track conflict_id (int): fact_conflict PK action_type (str): release or assert action_data (dict): rest of action-related information account (namedtuple): User account data action_date (datetime): action date Returns: dict: action formatted for saving in DB """ result = { db_consts.CONFLICT_ID: conflict_id, db_consts.ACTION: action_type, db_consts.ACTION_DATE: action_date.strftime( db_consts.SNOWFLAKE_DATE_FORMAT), db_consts.REASON: action_data[db_consts.REASON], db_consts.ADDITIONAL_INFORMATION: (action_data[db_consts.ADDITIONAL_INFORMATION]), db_consts.ACCOUNT_ID: account.id, db_consts.ACCOUNT_TYPE: account.type, db_consts.TUID: tuid, } result[db_consts.ELASTICSEARCH_ID] = es_id return result def _delete_conflict(account, args): """Delete a group of conflicts. Args: account (namedtuple): User account data args (ImmutableMultiDict): A list of request args. Returns: response.Response: items with pagination """ es_id = args.getlist('es_id') res = fact_conflict_elasticsearch.delete_conflict(account, es_id) return res