"""Module for ISRC ownership related logic.""" from copy import deepcopy import uuid from oto import response from masters_registry.connectors import dynamodb from masters_registry.constant import bulk_tasks_const from masters_registry.constant import error from masters_registry.constant import field_const from masters_registry.constant import opcode_const from masters_registry.logic import bulk_tasks from masters_registry.logic import conflicts from masters_registry.models import bulk_tasks as bulk_tasks_model from masters_registry.models import ownership as ownership_model from masters_registry.models import track as track_model def bulk_get_ownership(isrcs, ignore_missing=False): """Get ownership information about given ISRCs. Args: isrcs (list): ['QA123, 'QA345'] Returns: response.Response: list of ISRC dicts or list of not found ISRCS """ found_isrc_dicts = ownership_model.get_existing_isrcs_in_active_table( isrcs) existing_isrcs = [d[field_const.ISRC] for d in found_isrc_dicts] not_found_isrcs = [i for i in isrcs if i not in existing_isrcs] if not_found_isrcs and not ignore_missing: return response.create_error_response( code=error.ISRC_NOT_FOUND, message=not_found_isrcs, status=404) return response.Response(found_isrc_dicts) def update_ownership( isrc, tuid, territories_to_update, existing_territories, correlation_id, user, daemon=False, source=field_const.MANUAL_EDIT_ISRC): """Update ownership of given territories. Args: isrc (str): international standard recording code. tuid (int): track unique identifier. territories_to_update (list): list of 2-code territories to add. existing_territories (dict): Value of `territories` field of ISRC item correlation_id (str): The correlation id for logging the microservice instance. user (str): The user id for audit table logging. daemon (bool): Whether function if called from a daemon process for instance a celery task. source (str): action code used in audit table for logging information about resolved conflicts. Returns: response.Response: the territories added. """ new_territories = add_tuid_to_territories( tuid, territories_to_update, existing_territories) dynamodb.update_isrc_item_field( isrc, field_const.TERRITORIES, new_territories) ownership_model.insert_audit_record( opcode_const.ADD, isrc, territories_to_update, correlation_id, user, tuid) created_conflict = conflicts.determine_conflict( new_territories, territories_to_update) if created_conflict: ownership_model.insert_audit_record( opcode=opcode_const.CONFLICT_CREATED, isrc=isrc, territories=territories_to_update, correlation_id=correlation_id, user=user, tuid=tuid, source=source, conflict=created_conflict) return response.Response( message={ field_const.TUID: tuid, field_const.TERRITORIES: territories_to_update}) def add_tuid_to_territories( tuid, territories_to_update, existing_territories): """Make new data for territories attribute of ISRC item in DynamoDB. Update territories with provided tuid. Args: tuid (int): tuid that should be added to given territories territories_to_update (list): 2-letter territory codes existing_territories (dict): value of territories attribute of ISRC Returns: dict: new value for territories attribute of ISRC item in DynamoDB """ new_territories = deepcopy(existing_territories) for territory_code in territories_to_update: existing_territory_entry = existing_territories.get(territory_code) new_territories[territory_code] = _add_tuid_to_territory( tuid, existing_territory_entry) return new_territories def _add_tuid_to_territory(tuid, existing_value=None): """Make new data for single territory. Adds tuid to given territory. Args: tuid (int): tuid that should be added to given territories existing_value (obj): value that given territory currently has, can be None if ownership for this territory was deleted before. Returns: list: new value for single territory """ value_exists = bool(existing_value) is_old_format_entry = isinstance(existing_value, dict) is_new_format_entry = isinstance(existing_value, list) if not value_exists: return [{field_const.TUID: tuid}] if is_old_format_entry: new_value = [] new_value.append(existing_value) # prevent duplicates from getting into data if tuid not in existing_value.values(): new_value.append({field_const.TUID: tuid}) return new_value elif is_new_format_entry: new_value = deepcopy(existing_value) new_tuid_data = {field_const.TUID: tuid} # prevent duplicates from getting into data if new_tuid_data not in new_value: new_value.append(new_tuid_data) return new_value else: raise ValueError( 'Unknown format of territory entry, {}'.format(existing_value)) def remove_ownership( isrc, tuid, territories_to_remove, existing_territories, correlation_id, user, daemon=False, source=field_const.MANUAL_EDIT_ISRC, opcode=opcode_const.REMOVE): """Remove ownership of given territories. Args: isrc (str): international standard recording code. tuid (int): track unique identifier. territories_to_remove (list): list of 2-code territories to remove. existing_territories (dict): Value of `territories` field of ISRC item correlation_id (str): The correlation id for logging the microservice instance. user (str): The user id for audit table logging. daemon (bool): Whether function if called from a daemon process for instance a celery task. source (str): action code used in audit table for logging information about resolved conflicts. opcode (str): The opcode determining the action entered into the audit table. Returns: dict: {updated_claimed_territories: [], resolved_conflict: {}} """ new_territories = remove_tuid_from_territories( tuid, territories_to_remove, existing_territories) dynamodb.update_isrc_item_field( isrc, field_const.TERRITORIES, new_territories) ownership_model.insert_audit_record( opcode, isrc, territories_to_remove, correlation_id, user, tuid=tuid) updated_ownership = ownership_model.get_ownership(isrc) resolved_conflict = conflicts.determine_resolved_conflict( existing_territories, updated_ownership.message[field_const.TERRITORIES]) if resolved_conflict: ownership_model.insert_audit_record( opcode=opcode_const.CONFLICT_RESOLVED, isrc=isrc, territories=territories_to_remove, correlation_id=correlation_id, user=user, tuid=tuid, source=source, conflict=resolved_conflict) updated_claimed_territories = [ t for t in updated_ownership.message[field_const.TERRITORIES]] return { field_const.UPDATED_CLAIMED_TERRITORIES: updated_claimed_territories, field_const.RESOLVED_CONFLICT: resolved_conflict } def remove_tuid_from_territories( tuid, territories_to_remove, existing_territories): """Make new data for territories attribute of ISRC item in DynamoDB. Remove tuid from given territories. Args: tuid (int): tuid that should be added to given territories territories_to_remove (list): 2-letter territory codes existing_territories (dict): value of territories attribute of ISRC Returns: dict: new value for territories attribute of ISRC item in DynamoDB """ new_territories = deepcopy(existing_territories) for territory_code in territories_to_remove: territory_data = new_territories.get(territory_code) if not territory_data: # in case given territory has been deleted already continue new_entry = _remove_tuid_from_territory( tuid, territory_data) if new_entry: new_territories[territory_code] = new_entry else: del new_territories[territory_code] return new_territories def _remove_tuid_from_territory(tuid, existing_value): """Make new data for single territory. Removes tuid from given territory. Args: tuid (int): tuid that should be added to given territories existing_value (obj): value that given territory currently has. It can be dict or list. Returns: list or None: new value for single territory, None if it should be deleted completely. """ is_old_format_entry = isinstance(existing_value, dict) is_new_format_entry = isinstance(existing_value, list) if is_old_format_entry: return None elif is_new_format_entry: new_value = [ entry for entry in existing_value if tuid not in entry.values()] return new_value else: raise ValueError( 'Unknown format of territory entry, {}'.format(existing_value)) def bulk_remove_ownership( *, isrcs, territories, account_type, account_id, orchard_user_id, correlation_id): """Remove ownership information from given territories. This means following: - get corresponding tuid for vendor_id/isrc pair - delete it from territories field of ISRC in masters active table Args: isrcs (list(str)): list of product codes territories (list(str)): list of 2-letter country codes account_type (str): vendor or subaccount account_id (int): Id of account orchard_user_id (str): Orchard user id correlation_id (str): The correlation id for logging Returns: response.Response: celery task id or errors """ isrcs_exist_in_db_response = get_isrcs_from_track_table(isrcs) if not isrcs_exist_in_db_response: return isrcs_exist_in_db_response isrcs_exist_in_dynamo_response = bulk_get_ownership(isrcs) if not isrcs_exist_in_dynamo_response: return isrcs_exist_in_dynamo_response isrcs_belong_to_account_response = get_tuid_isrc_pairs_for_account( isrcs, account_type, account_id) if not isrcs_belong_to_account_response: return isrcs_belong_to_account_response isrc_items = { item[field_const.ISRC]: item for item in isrcs_exist_in_dynamo_response.message} is_world_wide = len(territories) == 1 and territories[0] == field_const.WW isrcs_without_conflicts = [] isrc_conflicts_map = {} for isrc in isrcs: isrc_item = isrc_items[isrc] _, conflicting_territories = ( conflicts._split_isrc_territories_by_internal_conflict(isrc_item)) if is_world_wide: conflict_territories_to_remove = conflicting_territories else: conflict_territories_to_remove = [ t for t in territories if t in conflicting_territories] if conflict_territories_to_remove: isrc_conflicts_map[isrc] = conflict_territories_to_remove else: isrcs_without_conflicts.append(isrc) if isrcs_without_conflicts: return response.create_error_response( code=error.NO_INTERNAL_CONFLICTS, message=isrcs_without_conflicts) isrc_tuid_map = isrcs_belong_to_account_response.message for isrc in isrcs: corresponding_tuids = isrc_tuid_map[isrc] isrc_item = isrc_items[isrc] isrc_territories = isrc_conflicts_map[isrc] tuids_exist_response = tuids_exist_in_isrc_territories( corresponding_tuids, isrc_item, isrc_territories) if not tuids_exist_response: return tuids_exist_response # This local import is to avoid circular references in celery tasks from masters_registry.tasks import bulk task_id = bulk_tasks.create_task( correlation_id, orchard_user_id, bulk_tasks_const.BULK_RESOLVE_INTERNAL_CONFLICTS, len(isrcs), isrcs) bulk.bulk_resolve_conflicts.delay( dict(isrc_tuid_map), isrc_conflicts_map, orchard_user_id, task_id, correlation_id, account_id) return response.Response({field_const.TASK_ID: task_id}) def get_tuid_isrc_pairs_for_account(isrcs, account_type, account_id): """Get ISRC/tuid info from art_relations. Return error if one of ISRCs doesn't belong to account. Args: isrcs (list(str)): List of ISRC account_type (str): vendor or subaccount account_id (int): Id of account Returns: response.Response: dict of ISRC/tuid pairs, or error """ query_funcs = { 'vendor': track_model.get_tuid_isrc_for_vendor_id, 'subaccount': track_model.get_tuid_isrc_for_subaccount_id } retrieval_query = query_funcs[account_type] tuid_isrc_pairs = retrieval_query(isrcs, account_id) not_found_isrcs = [isrc for isrc in isrcs if isrc not in tuid_isrc_pairs] if not_found_isrcs: error_response = response.create_error_response( code=error.ISRC_NOT_OWNED_BY_ACCOUNT, message=not_found_isrcs, status=403) return error_response return response.Response(tuid_isrc_pairs) def get_isrcs_from_track_table(isrcs): """Confirm that given ISRCs exist in art_relations.track. Args: isrcs (list(str)): List of ISRC Returns: response.Response: isrcs or missing isrcs """ existing_isrcs = ownership_model.get_existing_isrcs_in_tracks(isrcs) missing_isrcs = [i for i in isrcs if i not in existing_isrcs] if missing_isrcs: return response.create_error_response( code=error.NOT_EXISTING_ISRCS_IN_TRACK, message=missing_isrcs, status=404) return response.Response(existing_isrcs) def _get_tuids_from_territory(territory_data): """Return list of tuids from given territory data. Because we can have territory data in dict(old)/list(new) forms, we need to extract tuids differently. Args: territory_data (object): dict or list depending on format Returns: list: tuids of given territory """ is_old_format = isinstance(territory_data, dict) is_new_format = isinstance(territory_data, list) if is_old_format: result = [territory_data[field_const.TUID]] elif is_new_format: result = [d[field_const.TUID] for d in territory_data] return result def tuids_exist_in_isrc_territories(tuids, isrc_item, territories): """Check whether tuids exist in given territories. TUIDs supposed to belong to same ISRC/vendor_id pair and represent same track on different albums. Args: tuids (list(int)): List of TUID for given ISRC/vendor_id pair isrc_item (dict): Entry from DynamoDB territories (list(str)): 2-letter territory codes """ for territory in territories: result = tuids_exist_in_territory(tuids, isrc_item, territory) if not result: return result return response.Response() def tuids_exist_in_territory(tuids, isrc_item, territory_code): """Check whether given tuid exists in given territory. There can be multiple tuids for ISRC/vendor_id combination. Args: tuids (list(int)): Track unique ids isrc_item (dict): Entry from DynamoDB territory_code (str): 2-letter territory code Returns: response.Response: empty message or errors if tuid not found """ territory_data = isrc_item[field_const.TERRITORIES].get(territory_code, []) territory_tuids = _get_tuids_from_territory(territory_data) tuid_exists = any(tuid in territory_tuids for tuid in tuids) if tuid_exists: return response.Response() error_message = error.NO_TUIDS_ON_GIVEN_TERRITORY.format( territory_code, isrc_item[field_const.ISRC]) return response.create_not_found_response(error_message) def bulk_remove_territories( items, account_type, account_id, correlation_id, orchard_user_id): """Start celery task to bulk remove a list of territories. Args: items (list): list of dicts with territories, isrc, tuid and conflict. account_type (str): vendor or subaccount account_id (int): account ID correlation_id: Correlation ID for logs orchard_user_id (str): Orchard user_id for checking feature flags Returns: response.Response: response containing celery task ID """ from masters_registry.tasks import bulk if not correlation_id: correlation_id = str(uuid.uuid1()) task_id = bulk_tasks_model.create_task( correlation_id=correlation_id, user_id=None, task_type=bulk_tasks_const.BULK_REMOVE_TERRITORIES, count=len(items), context=str(items), user_name=None, account_id=account_id, account_type=account_type) bulk.bulk_remove_territories.delay( items, account_type, account_id, correlation_id, task_id, orchard_user_id) return response.Response({field_const.TASK_ID: task_id})