"""Lambda function module.""" import json import re import uuid import boto3 from ddex_ingester_common.constants.content_type import \ INSTRUMENTAL_CONTENT_TYPES from ddex_ingester_common.constants.ddex_providers import RISING_88 from ddex_ingester_common.constants.language_codes import \ INSTRUMENTAL_LANGUAGE_CODE from ddex_ingester_common.constants.release_type import VIDEO_RELEASE_TYPES from ddex_ingester_common.constants.us_publishing_obligation import \ DEFAULT_US_PUBLISHING_OBLIGATION_AWAL from ddex_ingester_common.helpers.rds import run_rds_query from ddex_ingester_common.helpers.s3_ddex import load_ddex_json from ddex_ingester_common.lambda_exceptions import LambdaException from ddex_ingester_common.logging import utils as logging_utils from ddex_ingester_common.models.s3.body import Body as S3Body from ddex_ingester_common.models.s3.store import Store from ddex_ingester_common.models.state_machine.body import \ Body as StateMachineBody from ddex_ingester_common.schemas.s3_schema import S3Schema from ddex_ingester_common.schemas.state_machine_schema import \ StateMachineSchema from ddex_ingester_common.utils import genre_mapping as genre_mapping_util from ddex_ingester_common.utils.prep_ddex import (prep_forbidden_sequence, prep_localizations, prep_track_name_length, prep_video_content_language) import config from constants.product_type_store_carveouts import ( AUDIO, DEFAULT_PRODUCT_TYPE_STORE_CARVEOUTS, PRODUCT_TYPE_STORE_CARVEOUTS, VIDEO) from constants.release_type import ALBUM_RELEASE_TYPE, AUDIO_BOOK_RELEASE_TYPE from constants.sql_queries import (CLEAR_PREVIOUS_ADDED_STORE_CARVEOUTS, INSERT_INTO_ADDED_STORE_CARVEOUTS) from helpers.upc_remap import (create_placeholder_audio_product, create_placeholder_project, create_placeholder_video_product, get_project_id, get_remapped_upc_rows, update_remapped_upc) logger = logging_utils.get_logger(config.app_logger) # TODO This lambda should be called prep_awal_ddex def handler(event, context): """Prep AWAL DDEX data with extra info, for instance genre/subgenre.""" s3_context = S3Schema().load(load_ddex_json(event)) context = StateMachineSchema().load(event) correlation_id = context.correlation_id or str(uuid.uuid4()) context.correlation_id = correlation_id logging_utils.update_logger_correlation_id(logger, correlation_id) logger.info(f'Triggered prep_ddex: {event}') logging_utils.update_logger_with_message_ids( logger, s3_context.message_id, s3_context.message_thread_id, s3_context.execution_name ) config.graphql_gateway.set_headers( { 'Orchard-User-Id': config.OA_USER, 'Correlation-Id': correlation_id, } ) update_context_with_genre_ids(context, s3_context) update_context_with_not_for_distribution(context, s3_context) prep_localizations(context.ddex_provider, s3_context, logger) prep_lyrics_language(s3_context) prep_content_type(s3_context) prep_us_publishing_obligation(s3_context) prep_audio_book_format(context, s3_context) prep_store_carveouts(context, s3_context) prep_alphanumeric_product_code(context, s3_context) prep_track_name_length(s3_context) prep_forbidden_sequence(s3_context) remap_upc(context, s3_context) if context.product.release_type in VIDEO_RELEASE_TYPES: prep_video_content_language(s3_context) logger.info('Saving s3 context JSON') save_s3_context(context, s3_context) return StateMachineSchema().dump(context) def update_context_with_genre_ids( context: StateMachineBody, s3_context: S3Body): """Update context with genre ids.""" genres = s3_context.product.genres if not genres: return genre = genres[0].genre subgenre = genres[0].subgenre # For 88 Rising we do not raise an exception and continue the execution raise_exception = context.ddex_provider != RISING_88 if genre: genre_mapping = genre_mapping_util.get_orchard_genre_mapping( config.graphql_gateway, genre, subgenre, raise_exception=raise_exception ) if genre_mapping: context.product.genre_id = genre_mapping['genre_id'] context.product.subgenre_id = genre_mapping['subgenre_id'] def prep_lyrics_language(s3_context: S3Body): """Prep lyrics language within the context.""" # Instrumental tracks do not have a lyrics language for track in s3_context.tracks or []: if not track.lyrics_language: if track.content_type in INSTRUMENTAL_CONTENT_TYPES or \ track.is_instrumental: track.lyrics_language = INSTRUMENTAL_LANGUAGE_CODE else: track.lyrics_language = \ s3_context.product.metadata_language def prep_content_type(s3_context: S3Body): """Prep track content type within the context.""" for track in s3_context.tracks or []: if not track.content_type and track.is_instrumental: track.content_type = INSTRUMENTAL_CONTENT_TYPES[0] def prep_us_publishing_obligation(s3_context: S3Body): """Prep US Publishing Obligation within the context.""" for track in s3_context.tracks or []: if not track.us_publishing_obligation: track.us_publishing_obligation = DEFAULT_US_PUBLISHING_OBLIGATION_AWAL # noqa def update_context_with_not_for_distribution( context: StateMachineBody, s3_context: S3Body): """Update context with Not For Distribution.""" s3_nfd = s3_context.product.not_for_distribution context.product.not_for_distribution = s3_nfd if s3_nfd else 'N' def prep_audio_book_format(context: StateMachineBody, s3_context: S3Body): """Update context and S3 context with audio book format.""" if context.product.release_type == AUDIO_BOOK_RELEASE_TYPE or \ s3_context.product.release_type == AUDIO_BOOK_RELEASE_TYPE: context.product.release_type = ALBUM_RELEASE_TYPE s3_context.product.release_type = ALBUM_RELEASE_TYPE def prep_store_carveouts(context: StateMachineBody, s3_context: S3Body): """Add store carveouts depending on the product type.""" if not s3_context.product: return if not s3_context.product.stores: s3_context.product.stores = [] product_type = AUDIO if context.product.release_type in VIDEO_RELEASE_TYPES: product_type = VIDEO if context.ddex_provider in PRODUCT_TYPE_STORE_CARVEOUTS: product_type_carveouts =\ PRODUCT_TYPE_STORE_CARVEOUTS[context.ddex_provider][product_type] else: product_type_carveouts =\ DEFAULT_PRODUCT_TYPE_STORE_CARVEOUTS[product_type] ddex_carveouts = [] added_carveouts = [] for store in s3_context.product.stores or []: if store.store_id: if store.carved_out: ddex_carveouts.append(store.store_id) elif store.store_id in product_type_carveouts: store.carved_out = True added_carveouts.append(store.store_id) for store_id in product_type_carveouts: if store_id not in ddex_carveouts and \ store_id not in added_carveouts: added_carveouts.append(store_id) s3_context.product.stores.append( Store( store_id=store_id, carved_out=True )) run_rds_query( logger, config.RDS_HOST, config.RDS_DB_NAME, config.RDS_RW_USER, config.secrets_manager_client.get_cred('rds_read_write_password'), CLEAR_PREVIOUS_ADDED_STORE_CARVEOUTS, context.product.upc or context.product.grid, ) for store_id in added_carveouts: query_args = ( context.product.upc or context.product.grid, int(store_id) ) run_rds_query( logger, config.RDS_HOST, config.RDS_DB_NAME, config.RDS_RW_USER, config.secrets_manager_client.get_cred('rds_read_write_password'), INSERT_INTO_ADDED_STORE_CARVEOUTS, query_args, ) def prep_alphanumeric_product_code( context: StateMachineBody, s3_context: S3Body): """Replace non alphanumeric characters with - . Error message from Workstation validation: Product code must only contain letters, numbers, and hyphens. """ if context.product.release_type in VIDEO_RELEASE_TYPES: if s3_context.video.product_code: s3_context.video.product_code = re.sub( r'[^0-9a-zA-Z]+', '-', s3_context.video.product_code ) else: if s3_context.product.catalog_number: s3_context.product.catalog_number = re.sub( r'[^0-9a-zA-Z]+', '-', s3_context.product.catalog_number ) def remap_upc(context: StateMachineBody, s3_context: S3Body): """Remap the UPC if it's already used by another product.""" logger.info('Checking for UPC conflict on prep_awal_ddex') if not context.product or not s3_context.product \ or not context.product.upc: logger.info('No UPC found, skipped remap_upc') return rows = get_remapped_upc_rows(logger, context) # If there is no entry in the DB for this UPC, do nothing if not rows: return # There should only be one row for each (UPC, DDEX Provider) set if len(rows) > 1: raise LambdaException(f'Multiple UPC mappings found: {rows}') mapping_vendor_id = rows[0].get('vendor_id') if context.product.vendor_id != mapping_vendor_id: raise LambdaException( f'DDEX vendor id {context.product.vendor_id} does not match ' f'the RDS UPC mapping vendor id {mapping_vendor_id}') remapped_upc = rows[0].get('remapped_upc') if not remapped_upc: logger.info('UPC remap not found. Generating new UPC') project_id = get_project_id(logger, s3_context) if not project_id: project_id = create_placeholder_project( logger, s3_context, video=(context.product.release_type in VIDEO_RELEASE_TYPES) ) if context.product.release_type in VIDEO_RELEASE_TYPES: remapped_upc = create_placeholder_video_product( logger, s3_context, project_id) else: remapped_upc = create_placeholder_audio_product( logger, context, project_id) update_remapped_upc(logger, context, remapped_upc) # Manufacturer UPC stores the original UPC s3_context.product.manufacturer_upc = context.product.upc context.product.upc = remapped_upc s3_context.product.upc = remapped_upc def save_s3_context(context: StateMachineBody, s3_context: S3Body): """Save s3 context.""" s3_client = boto3.client('s3') json_file_path = f'{context.key}parsed_ddex.json' s3_client.put_object( Bucket=context.bucket, Key=json_file_path, Body=json.dumps(S3Schema().dump(s3_context)).encode(encoding='UTF-8') )