"""GraphqlRouter connections.""" from datetime import datetime from gql.transport.exceptions import TransportServerError from gql.transport.exceptions import TransportQueryError from soundrecording_utils.metadata.types import DeliveryType from ..common.exceptions import exceptions from ..common.connectors import graphql_gateway import config OSR_DELIVERY_HISTORY = """ query DeliveryHistoryByOsrIds($ids: [ID!]!, $filters: DeliveryHistoryFilters) { deliveryHistoryByOsrIds(ids: $ids, filters: $filters) { orchardSoundRecording { id } orchardSoundRecordingVersion { versionId } status creationDate type stepFunctionName } } """ TRACKS_QUERY = """ query Track( $isrc: String!) { isrc(isrc: $isrc) { tracks { tracks { trackName labelSoundRecording { label { name } } product { subgenre { genre { name } } } participations { participant { name uuid } participated_as } } } } } """ HISTORY_QUERY = """ query OrchardSoundRecording($id: ID!) { orchardSoundRecording(id: $id) { deliveryHistory( filters: { status: [SUCCESS] type: [FULL_DELIVERY, METADATA_UPDATE, TAKEDOWN_DELIVERY] service: ["TikTok (Audio Fingerprinting)"] } ) { creationDate stepFunctionName message } } } """ def _raise_graphql_error(response): """Raise exception for GraphQL errors in response.""" msg = response['errors'][0]['message'] raise Exception(f'GraphQL query failed: {msg}') def _execute_tracks_query(isrc): """Execute gql tracks query. Args: isrc (str): ISRC for the track Returns: list: dicts of tracks """ headers = { 'apollographql-client-name': config.APPLICATION_NAME, 'apollographql-client-version': '1', 'Orchard-Identity-Id': config.TRACKS_IDENTITY_ID, 'Orchard-Profile-Id': config.TRACKS_PROFILE_ID, 'Orchard-Profile-Type': config.TRACKS_PROFILE_TYPE, 'Cache-Control': 'no-cache' } params = {'isrc': isrc} return graphql_gateway.make_request(headers, TRACKS_QUERY, params) def _most_recent_delivery(deliveries): """Return most recent delivery.""" ordered = sorted( deliveries, key=lambda item: datetime.fromisoformat(item['creationDate']) ) return ordered.pop() def _execute_query(the_query, params): """Execute provided gql query with provided params. Args: osr_id (string): the orchard sound recording id Returns: list: dicts in format of orchard sound recording delivery history """ headers = { 'apollographql-client-name': config.APPLICATION_NAME, 'apollographql-client-version': '1', 'Orchard-Identity-Id': config.SR_IDENTITY_ID, 'Orchard-Profile-Id': config.SR_PROFILE_ID, 'Orchard-Profile-Type': config.SR_PROFILE_TYPE, 'orchard-roles': config.SR_PROFILE_ROLE, 'Cache-Control': 'no-cache' } return graphql_gateway.make_request(headers, the_query, params) def _execute_history_query(sound_recording_id): """ Execute gql history query. Args: sound_recording_id (str): ID for the sound recording Returns: data containing delivery history """ params = {'id': sound_recording_id} return _execute_query(HISTORY_QUERY, params) def get_track(isrc): """Get metadata about the first track found by its ISRC. Args: isrc (str): ISRC for the track Returns: track: track data """ try: data = _execute_tracks_query(isrc) except TransportServerError as e: if e.code in (504, 503, 502): message_error = f'Unexpected response code from GraphQL Gateway HTTP:{e.code}' # noqa:E501 raise exceptions.RetryableException(message_error) else: raise e except TransportQueryError as e: raise exceptions.RetryableException(str(e)) tracks = data.get('isrc', {}).get('tracks', {}).get('tracks', None) if tracks and len(tracks) > 0: return tracks[0] return None def get_last_delivered_xml_location(sound_recording_id): """Get the last delivered XML location for a sound recording. Args: sound_recording_id (str): ID for the sound recording Returns: string: S3 location of last delivered XML """ try: data = _execute_history_query(sound_recording_id) except TransportServerError as e: if e.code in (504, 503, 502): message_error = f'Unexpected response code from GraphQL Gateway HTTP:{e.code}' raise exceptions.RetryableException(message_error) else: raise e except TransportQueryError as e: raise exceptions.RetryableException(str(e)) entries = data.get('orchardSoundRecording', {}).get('deliveryHistory', None) if not entries: return None last_entry = sorted(entries, key=lambda item: datetime.fromisoformat(item['creationDate'])).pop() step_function_name = last_entry.get('stepFunctionName', None) file_names = last_entry.get('message', {}).get('details', {}).get('filenames', []) if not file_names: return None return step_function_name + '/' + file_names[0] def get_delivery_histories(osr_ids, store_name) -> dict: """ Get delivery histories for OSR IDs. Args: osr_ids (list): the orchard sound recording ids store_name (string): the name of the store Returns: dict: { osr_id: { 'type': last_delivery_type, 'has_full_delivery': bool } } """ try: params = {'ids': osr_ids, 'filters': {'service': store_name, 'status': 'SUCCESS'}} response = _execute_query(OSR_DELIVERY_HISTORY, params) if response.get('errors', None): _raise_graphql_error(response) histories = response.get('deliveryHistoryByOsrIds', []) if not histories or len(histories) == 0: return {} # making a map of osr_id to its delivery history records mapped: dict[str, list[dict[str, str | None]]] = {} for item in histories: osr_version = item.get('orchardSoundRecordingVersion') or {} mapped.setdefault(item['orchardSoundRecording']['id'], []).append({ 'creationDate': item['creationDate'], 'status': item['status'], 'type': item['type'], 'stepFunctionName': item['stepFunctionName'], 'versionId': osr_version.get('versionId') }) # preparing the final result of mapping osr_id to its last delivery type and full delivery status result = {} for osr_id in mapped: history = mapped[osr_id] most_recent_record = _most_recent_delivery(history) has_full_delivery = any( record['type'] == DeliveryType.FULL_DELIVERY.value and not (record['stepFunctionName'] or '').endswith('-False') # noqa:E501 for record in history ) result[osr_id] = { 'type': most_recent_record['type'], 'status': most_recent_record['status'], 'has_full_delivery': has_full_delivery, 'last_delivered_version_id': most_recent_record.get('versionId') } return result except Exception as e: raise e