"""Logic for determining internal conflicts.""" from collections import abc from collections import defaultdict from datetime import datetime import itertools from operator import itemgetter from oto import response from masters_registry.constant import field_const from masters_registry.constant import opcode_const from masters_registry.models import ownership def get_conflicts(ownership_territories): """Get internal conflicts. Args: ownership_territories (dict): territory ownership items Returns: list: conflicts list Example: [ (tuid1, tuid2), (tuid1, tuid3), (tuid3, tuid4), ] """ conflicts = set() for tuid_items in ownership_territories.values(): # skip old format items if isinstance(tuid_items, abc.Mapping): continue tuids = sorted([item['tuid'] for item in tuid_items], reverse=True) if len(tuids) > 1: conflicts.update(itertools.combinations(tuids, 2)) return conflicts def get_conflicts_history_for_ownership(ownership_info): """Extend ownership with information when conflicts were created. Args: ownership_info (dict): ownership info for ISRC Returns: Response """ audit_records = ownership.get_ownership_audit( ownership_info[field_const.ISRC]) if not audit_records.message: return audit_records conflict_codes = set(( opcode_const.CONFLICT_CREATED, opcode_const.CONFLICT_RESOLVED)) history = {} audit_records = audit_records.message audit_records.sort(key=itemgetter(field_const.TIMESTAMP), reverse=True) audit_records = list(filter( lambda r: r[field_const.OPCODE] in conflict_codes, audit_records)) for conflict in get_conflicts( ownership_info[field_const.TERRITORIES]): conflict_key = '{0}V{1}'.format(conflict[0], conflict[1]) history[conflict_key] = _get_conflict_history(conflict, audit_records) ownership_info[field_const.CONFLICTS_HISTORY] = history return response.Response() def _get_conflict_history(conflict, audit_records): """Helper function for getting history for a single conflict Args: conflict (tuple): tuids in conflict audit_records (dict): ownership audit information Returns: dict Example: { 'datetime': '2018-04-13T10:38:39.090141', 'created_by': 10718589 } """ conflict = set(conflict) date = None tuid = None for record in audit_records: opcode = record[field_const.OPCODE] conflicting_tuid = record.get(field_const.CONFLICT, {}).get( field_const.CONFLICTING_TUID, []) conflicting_tuid = set(conflicting_tuid) if opcode == opcode_const.CONFLICT_CREATED: if conflict.issubset(conflicting_tuid): date = datetime.utcfromtimestamp( float(record[field_const.TIMESTAMP]) / 1000).isoformat() tuid = list(conflict)[0] elif opcode == opcode_const.CONFLICT_RESOLVED: if not conflict.issubset(conflicting_tuid): # conflict was resolved # so we don't have to go deeper into history break return {field_const.DATETIME: date, field_const.CREATED_BY: tuid} def determine_conflict(updated_territories, territories_added=None): """Determine if there was an internal conflict created. Args: updated_territories (dict): updated list of territory ownership items. territories_added: (list): list of territories that were added to the registry. Returns: dict: conflict information or empty dict if there is no conflict. Example: { 'conflicting_tuid': [1323123123, 2323232323], 'status': 'CONFLICT_CREATED' } """ conflicts_result = {} conflict_territories = {} conflict_tuids = set() if not territories_added: territories_added = updated_territories for territory in territories_added: tuid_items = updated_territories.get(territory, []) # skip old format items if isinstance(tuid_items, abc.Mapping): continue tuids = [item['tuid'] for item in tuid_items] if len(tuids) > 1: conflict_territories[territory] = tuids conflict_tuids.update(tuids) if not conflict_territories: return conflicts_result conflicts_result = { 'conflicting_tuid': list(conflict_tuids), 'status': opcode_const.CONFLICT_CREATED } return conflicts_result def determine_resolved_conflict( original_territories, updated_territories): """Determine if an internal conflict was resolved. Args: original_territories (dict): original ownership record from DynamoDB. updated_territories (dict): updated ownership record from DynamoDB. Returns: dict: resolved conflicts information or empty dict if there were no conflicts, or no existing conflicts were resolved. Example: { 'conflicting_tuid': [1323123123], 'status': 'CONFLICT_RESOLVED', 'resolved': 1 } """ resolved_response = {} existing_conflict = determine_conflict(original_territories) if not existing_conflict: return resolved_response resolved_conflict = determine_conflict(updated_territories) conflicting_tuid = resolved_conflict.get('conflicting_tuid', []) number_of_resolved = ( len(existing_conflict['conflicting_tuid']) - len(conflicting_tuid)) # Number of conflicts resolved is not equal to number of tuids. if number_of_resolved > 1: number_of_resolved -= 1 resolved_response = { 'conflicting_tuid': conflicting_tuid, 'status': opcode_const.CONFLICT_RESOLVED, 'resolved': number_of_resolved } return resolved_response def check_conflicts_with_the_same_label(tuid, territories, ownership_info): """Determine if there are internal conflicts between tuids that belongs to the same label. Checks if any territory from the given territories list is already claimed by a tuid that belongs to the same vendor as the given tuid. Args: tuid (int): track unique identifier territories (list): list of 2-code territories ownership_info (dict): ownership info for isrc Returns: (list): list of 2-code territories that are internal conflicts between TUIDs that belongs to the same label """ ownership_territories = ownership_info.get(field_const.TERRITORIES, {}) conflicting_territories = _get_conflicting_territories( territories, ownership_territories) if not conflicting_territories: return [] conflict_territories_info = _get_tuids_for_conflicting_territories( conflicting_territories, ownership_territories) vendors = _get_vendors_info(tuid, conflict_territories_info) return _get_territories_conflicts_with_the_same_label( tuid, conflict_territories_info, vendors) def _get_conflicting_territories(territories, ownership_territories): """Helper function that returns territories form the given list that already claimed by any tuid. Args: territories (set): set of 2-code territories ownership_territories (dict): 'territories' part from ownership info Returns: set: {territories_codes} """ return [t for t in ownership_territories if t in territories] def _get_tuids_for_conflicting_territories(territories, ownership_territories): """Helper functions that returns list of tuids for every given territory. For given territories it converts ownership_territories form: { 'CA': [{'tuid': 1},{'tuid': 2}] 'FR': { 'tuid': 1} } into: { 'CA': [1,2] 'FR': [1] } Args: territories (set): set of 2-code territories ownership_territories (dict): 'territories' part from ownership info Returns: dict: {country_code: [tuids]} """ conflict_territories_info = {} for territory in territories: territory_info = ownership_territories[territory] if isinstance(territory_info, abc.Mapping): tuids = [int(territory_info[field_const.TUID])] else: tuids = [t[field_const.TUID] for t in territory_info] conflict_territories_info[territory] = tuids return conflict_territories_info def _get_vendors_info(tuid, conflict_territories_info): """Helper function that returns vendor info. It gets dict that maps tuid to vendor_id for the given tuid plus every tuid from conflict_territories_info Args: tuid (int): track unique identifier conflict_territories_info (dict): {country_code: [tuids]} Returns: dict: {tuid: vendor_id} """ distinct_tuids = {tuid} for tuids in conflict_territories_info.values(): distinct_tuids.update(tuids) tracks = ownership.get_tracks(list(distinct_tuids)).message vendors = {tuid: tracks[tuid]['vendor_id'] for tuid in distinct_tuids} return vendors def _get_territories_conflicts_with_the_same_label( tuid, conflict_territories_info, vendors): """Helper function that returns list of territories that are claimed by a tuid the belongs to the same label as the given tuid. Args: tuid (int): track unique identifier conflict_territories_info (dict): {country_code: [tuids]} vendors (dict): dict that maps tuid to vendor_id Returns: list: [territories_codes] """ claimed_territories = [] vendor = vendors[tuid] for territory, tuids in conflict_territories_info.items(): if tuid in tuids: # territory already claimed by tuid - no conflict continue territory_vendors = {vendors[tuid] for tuid in tuids} is_claimed_by_the_same_label = vendor in territory_vendors if is_claimed_by_the_same_label: claimed_territories.append(territory) return claimed_territories def _split_isrc_territories_by_internal_conflict(isrc_item): """Separate territories that have internal conflict from the rest. Args: isrc_item (dict): ISRC item from DynamoDB Returns: (list, list): lists of territories """ territories = [] conflicting_territories = [] for territory_code, value in isrc_item[field_const.TERRITORIES].items(): has_internal_conflict = len(value) > 1 if has_internal_conflict: conflicting_territories.append(territory_code) else: territories.append(territory_code) return territories, conflicting_territories def filter_lock_territories_without_conflict( isrc_items, territories_to_lock): """Filter territories_to_lock that have internal conflict. Args: isrc_items (list(dict)): list of ISRC items from DynamoDB territories_to_lock (list(str)): list of ISO-3166-1 or fully-qualified territories Returns: dict: Sorted ISRCs """ result = defaultdict(dict) for isrc_item in isrc_items: isrc = isrc_item[field_const.ISRC] territories, conflicting_territories = ( _split_isrc_territories_by_internal_conflict(isrc_item)) if territories: result['accepted_isrcs'][isrc] = territories lock_territories_with_conflict = [ t for t in territories_to_lock if t in conflicting_territories] if lock_territories_with_conflict: result['conflicting_isrcs'][isrc] = lock_territories_with_conflict return dict(result)