"""Lambda function module.""" from typing import List import uuid import config from config import graphql_gateway from constants import queries from constants.constants import WORKSTATION_VALIDATION_RULE_ID from constants.validation_errors import WAIT_FOR_ASSET_PROCESSING_ERRORS from ddex_ingester_common.constants.catalog_ingestion import WARNING from ddex_ingester_common.helpers.catalog_ingestion import ( CatalogIngestionValidationResult ) from ddex_ingester_common.lambda_exceptions import ValidateProductException from ddex_ingester_common.logging import utils as logging_utils from ddex_ingester_common.schemas.state_machine_schema import ( StateMachineSchema ) from lambdacommon.graphql import graphql logger = logging_utils.get_logger(config.app_logger) def handler(event, context): """Validate product handler.""" context = StateMachineSchema().load(event) correlation_id = event.get('correlation_id') or str(uuid.uuid4()) logging_utils.update_logger_correlation_id(logger, correlation_id) logging_utils.update_logger_with_message_ids( logger, context.message_id, context.message_thread_id, context.execution_name ) logger.info(f'Triggered validate_product: {event}') graphql_gateway.set_headers( { 'Orchard-User-Id': config.OA_USER, 'Correlation-Id': correlation_id, } ) product_id = context.product.product_id errors = [] try: validate_result = graphql_gateway.execute( queries.validate_product, {'productId': str(product_id)} )['data']['product'] if not validate_result['validation']['isValid']: product_errors = validate_result['validation']['errors'] if product_errors: logger.info(f'Product errors: {product_errors}') results = [ CatalogIngestionValidationResult( state_machine_name=context.state_machine_name, state_machine_execution_name=context.execution_name, validation_rule_id=WORKSTATION_VALIDATION_RULE_ID, response=WARNING, message=error['reason'], category=error['code'] ) for error in product_errors ] config.catalog_ingestion_session.add(results) product_errors = flatten_errors(product_errors) errors.append(product_errors) for track in validate_result['tracks']: isrc = track['isrc'] if not track['validation']['isValid']: track_errors = track['validation']['errors'] # no warnings at the time of this implementation # track_warnings = track['validation']['warnings'] if track_errors: logger.info(f'Track errors: {track_errors}') results = [ CatalogIngestionValidationResult( state_machine_name=context.state_machine_name, state_machine_execution_name=context.execution_name, # noqa validation_rule_id=WORKSTATION_VALIDATION_RULE_ID, response=WARNING, isrc=isrc, message=error['reason'], category=error['code'] ) for error in track_errors ] config.catalog_ingestion_session.add(results) track_errors = flatten_errors(track_errors, isrc) errors.append(track_errors) config.catalog_ingestion_session.save() except graphql.GraphQLError as err: raise ValidateProductException('Graphql error') from err # The lambda after this one is CheckErrors # It checks for the 'errors' key in the context # If the key is present it means that the ingest failed serialized_context = StateMachineSchema().dump(context) if errors: check_for_asset_validation_errors(context, errors) context_errors = { 'Error': 'ProductValidationException', 'ErrorMessage': errors } if not serialized_context['errors']: serialized_context['errors'] = context_errors else: serialized_context['errors'].append(context_errors) elif not serialized_context['errors']: serialized_context.pop('errors') return serialized_context def check_for_asset_validation_errors( context, errors: List[str]) -> int: """Check for known asset validation errors that we should ignore. If the ingestion gets to validate_product before the assets have enough time to process we might get certain known validation errors. Those errors go away if we just wait for the assets to finish processing. This function checks for those errors and if they are present it sets the validate_product_retry_counter variable to tell terraform it needs to wait and retry the Lambda. The validate_product_retry_counter variable also keeps track of how many times the Lambda has been retried. We use this counter instead of an exception because we don't want this validation failure to be treated like an exception since it's not a critical failure. """ for error in errors: for known_asset_error in WAIT_FOR_ASSET_PROCESSING_ERRORS: if known_asset_error in error: if not context.validate_product_retry_counter: context.validate_product_retry_counter = 1 return else: context.validate_product_retry_counter =\ context.validate_product_retry_counter + 1 return context.validate_product_retry_counter = 0 def flatten_errors(errors, isrc=None): """Convert nested list to one dimension error list. Args: errors (list): list of errors isrc (str): ISRC Returns: list """ flattened_errors = [] for key, value in enumerate(errors): if isinstance(value, list): flattened_errors += flatten_errors(value, isrc) else: message = value['reason'] if isrc: message = f'ISRC: {isrc}: {message}' flattened_errors.append(message) return flattened_errors