"""set-release-correction.""" import json import uuid from typing import Dict, List from ddex_ingester_common.constants.roles import \ DDEX_ARTIST_ROLE_REQUIRED_GENRES from ddex_ingester_common.constants.status import (IN_CONTENT, PRODUCT_UNSUBMITTED, SUBMITTED) from ddex_ingester_common.constants.swb_deal_types import FOR_DISTRIBUTION from ddex_ingester_common.helpers.list import have_common_elements from ddex_ingester_common.helpers.s3_ddex import load_ddex_json from ddex_ingester_common.lambda_exceptions import UnsubmitException from ddex_ingester_common.logging import utils as logging_utils from ddex_ingester_common.models.s3 import body as S3Context from ddex_ingester_common.models.state_machine import \ body as StateMachineContext from ddex_ingester_common.models.state_machine.error_correction import \ ErrorCorrection from ddex_ingester_common.release_correction.release_correction_constants import \ RELEASE_CORRECTION_PRODUCT_ARTIST_FIELDS as RC_ARTIST_FIELDS # noqa from ddex_ingester_common.release_correction.release_correction_diffs import \ ReleaseCorrectionDiffDetail from ddex_ingester_common.release_correction.release_corrections_s3 import ( load_rc_json, write_rc_json) from ddex_ingester_common.schemas.s3_schema import S3Schema from ddex_ingester_common.schemas.state_machine_schema import \ StateMachineSchema from humps import decamelize import config from config import graphql_gateway from constants import queries logger = logging_utils.get_logger(config.app_logger) def handler(event, context): """Lambda entry point.""" s3_context = S3Schema().load(load_ddex_json(event)) context = StateMachineSchema().load(event) s3_release_corrections = load_rc_json(event) correlation_id = context.correlation_id or str(uuid.uuid4()) context.correlation_id = correlation_id 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 ) graphql_gateway.set_headers( { 'Orchard-User-Id': config.OA_USER, 'Correlation-Id': correlation_id, } ) graphql_result = get_product_metadata(context) enrich_release_correction(context, graphql_result) if should_create_release_correction(context, s3_context): logger.info('Creating Release Correction.') if context.error_correction: # Error correction already exists on Product display_status = context.product.display_status logger.info(f'Product display status: {display_status}') if display_status == SUBMITTED: logger.info('Unsubmitting release correction on Product') unsubmit_product(context.product.product_id) # If the submit was successful the display status will change product_data = get_product_metadata(context) display_status = product_data.get('displayStatus') context.product.display_status = display_status logger.info( f'Updated the context display status to {display_status}') # noqa else: create_release_correction(context) """ There can be situations where NFD=SWBDUMMY is here because assets have been updated. If we're in this code path because of that, we want to avoid creating any release correction details for it. Assets will be handled later in their respective lambdas. """ if context.product.not_for_distribution == FOR_DISTRIBUTION: create_release_correction_details( context, s3_context, s3_release_corrections) return StateMachineSchema().dump(context) def get_product_metadata(context: StateMachineContext) -> Dict: """Retrieve product metadata from GraphQL.""" upc = context.product.upc result = graphql_gateway.execute( queries.GET_PRODUCT_BY_UPC, {'upc': upc} )['data']['productByUpc'] logger.info(f'Graphql result: {result}') return result def should_create_release_correction( context: StateMachineContext, s3_context: S3Context) -> bool: """Determine if an error correction should be created.""" if context.product.status != IN_CONTENT: return False if context.product.not_for_distribution == FOR_DISTRIBUTION: # This is because we want carveouts and sales start date etc # that are not error correction fields to get a second look in # the release approval queue. # This may make a product look like a blank error correction BUT a # delivery needs to take place for these fields to get propogated # to stores. return True for track in s3_context.tracks: if track.asset: return True if s3_context.product.artwork: return True return False def unsubmit_product(product_id: int) -> Dict: """Unsubmit product.""" logger.info( f'Running unsubmit with product_id: {product_id}' ) result = graphql_gateway.execute( queries.UNSUBMIT_PRODUCT, { 'productId': product_id } )['data']['unsubmitProduct'] if result.get('result') != PRODUCT_UNSUBMITTED: raise UnsubmitException( f'Unsubmit failed for product_id: {product_id}' ) return result def create_release_correction(context: StateMachineContext): """Create an error correction.""" product_id = context.product.product_id logger.info(f'Creating Release Correction for {product_id}') result = graphql_gateway.execute( queries.CREATE_PRODUCT_CORRECTION, {'data': {'productId': product_id}} )['data']['createProductCorrection'] release_correction_id = result['releaseCorrectionId'] if context.error_correction: context.error_correction.release_correction_id = \ release_correction_id else: ec = { 'release_correction_id': release_correction_id, 'release_id': product_id, 'items': [] } context.error_correction = ErrorCorrection(**ec) def enrich_release_correction(context: StateMachineContext, result: Dict): """Enrich context release_correction.""" # FIXME: This needs updated. release_correction = result.get('releaseCorrection') logger.info(f'Found release correction: {release_correction}') if release_correction: # We don't fetch items at this point to avoid bloating the context. # The schema still requires an items key, if you do not define the # key you will encounter an unhelpful TypeError exception. release_correction['items'] = [] context.error_correction =\ ErrorCorrection(**decamelize(release_correction)) def create_release_correction_details( context: StateMachineContext, s3_context: S3Context, rc_details: Dict): """Create RC Details for product data.""" if not rc_details.get('changes'): return rc_updates = [] artist_fields = {key: [] for key in RC_ARTIST_FIELDS.keys()} check_genre_required_fields(artist_fields, s3_context.product.genres) for rc in rc_details.get('changes'): # Unpack list back into NamedTuple detail = ReleaseCorrectionDiffDetail(*rc) if detail.accept_update: logger.info( f'Adding RC Detail for {detail.field_name}. ' f'Field data: {detail.new}') # Artists require special formatting, so grab the data. if detail.field_name in artist_fields: artist_fields[detail.field_name].append(*detail.new) else: rc_updates.append({ 'tableName': detail.db_table_name, 'fieldName': detail.db_field_name, 'keyValue': json.dumps(detail.new) }) # If we have any, parse that Artist data before adding to updates. for key, value in artist_fields.items(): if value: # A new value of None means we should clear the field if value == [None]: value = [] correction_object = { 'tableName': RC_ARTIST_FIELDS[key].db_table_name, 'fieldName': RC_ARTIST_FIELDS[key].db_field_name, 'keyValue': json.dumps(value) } rc_updates.append(correction_object) if rc_updates: rc_details['changes'] = remove_processed_details(rc_details['changes']) event_details = {'bucket': context.bucket, 'key': context.key} logger.info(f'Writing RC details back to S3: {rc_details}') write_rc_json(event_details, rc_details) product_id = context.product.product_id payload = { 'productId': product_id, 'releaseCorrectionId': context.error_correction.release_correction_id, # noqa 'corrections': rc_updates } logger.info(f'Creating Release Correction Details for {product_id}' f' With details: {payload}') response = graphql_gateway.execute( queries.CREATE_PRODUCT_CORRECTION_DETAIL, payload )['data']['createProductCorrectionDetail'] logger.info(f'RC Creation response: {response}') def remove_processed_details(rc_details: List) -> List: """Remove all processed Release Correction details.""" not_processed = [] for detail in rc_details: rcd = ReleaseCorrectionDiffDetail(*detail) # Assume all acceptable updates have been processed. if not rcd.accept_update: not_processed.append(detail) return not_processed def check_genre_required_fields(fields, genres): """Remove fields that do not apply to the genre.""" for role in DDEX_ARTIST_ROLE_REQUIRED_GENRES: required_genres = DDEX_ARTIST_ROLE_REQUIRED_GENRES[role] genre_names = [genre.genre for genre in genres] # Applies if any product genre matches one of the required genres # If no genre matches, the role does not apply. Remove the role if not have_common_elements(genre_names, required_genres): fields.pop(role.lower())