"""Validation rules.""" import re from typing import Dict, NamedTuple from constants import rule_error_codes from constants import rules as rule_constants from constants.accepted_values import VALID_UPDATE_INDICATORS from constants.orchard_pricing_family import ORCHARD_PRICING_FAMILY from ddex_ingester_common.constants.catalog_ingestion import ( REJECT, SUCCESS, WARNING, ) from ddex_ingester_common.constants.ddex_providers import AWAL from ddex_ingester_common.constants.roles import ( DDEX_ARTIST_ROLE_TO_ORCHARD_ARTIST_TYPE, DDEX_ARTIST_ROLE_TO_ORCHARD_PERFORMER_ROLE, DDEX_ARTIST_ROLE_TO_ORCHARD_WRITER, DDEX_RESOURCE_CONTRIBUTOR_ROLE_TO_ORCHARD_PARTICIPANT_ROLE, DDEX_RESOURCE_CONTRIBUTOR_ROLE_TO_ORCHARD_PERFORMER_ROLE, DDEX_VIDEO_ROLE_TO_ORCHARD_ARTIST_TYPE, ) from ddex_ingester_common.helpers import metadata as metadata_helper from ddex_ingester_common.lambda_exceptions import ( InvalidGenreException, InvalidSubgenreException ) from ddex_ingester_common.utils import genre_mapping as genre_mapping_util from workstation_rules import ORCHARD_VALIDATION_RULES class RuleResult(NamedTuple): """A result from a rule.""" validation_rule_id: int response: str = SUCCESS message: str = None def require_party_id(data: Dict) -> RuleResult: """Rule for validating a valid party id.""" party_id = data.get('party_id') if not party_id: return RuleResult( rule_error_codes.INVALID_PARTY_ID, REJECT, 'Party Id is missing' ) if party_id not in rule_constants.VALID_PARTY_IDS: return RuleResult( rule_error_codes.INVALID_PARTY_ID, REJECT, f'Invalid Party Id: {party_id}' ) return RuleResult(rule_error_codes.INVALID_PARTY_ID) def require_message_id(data: Dict) -> RuleResult: """Rule for requiring a message id.""" if not data.get('message_id'): return RuleResult( rule_error_codes.MISSING_MESSAGE_ID, REJECT, 'Message Id is missing' ) return RuleResult(rule_error_codes.MISSING_MESSAGE_ID) def require_product(data: Dict) -> RuleResult: """Rule for requiring a product.""" if not data.get('product'): return RuleResult( rule_error_codes.MISSING_PRODUCT, REJECT, 'Product is missing' ) return RuleResult(rule_error_codes.MISSING_PRODUCT) def require_product_name(data: Dict) -> RuleResult: """Rule for requiring a product name.""" if not data['product'].get('product_name'): return RuleResult( rule_error_codes.MISSING_PRODUCT_NAME, REJECT, 'Product Name is missing' ) return RuleResult(rule_error_codes.MISSING_PRODUCT_NAME) def require_product_release_reference(data: Dict) -> RuleResult: """Rule for requiring a product release reference.""" if not data['product'].get('release_reference'): return RuleResult( rule_error_codes.MISSING_PRODUCT_RELEASE_REFERENCE, REJECT, 'Product Release Reference is missing' ) return RuleResult(rule_error_codes.MISSING_PRODUCT_RELEASE_REFERENCE) def require_product_display_artist_name(data: Dict) -> RuleResult: """Rule for requiring a product display artist name.""" if not data['product'].get('display_artist_name'): return RuleResult( rule_error_codes.MISSING_PRODUCT_DISPLAY_ARTIST_NAME, REJECT, 'Product Display Artist Name is missing' ) return RuleResult(rule_error_codes.MISSING_PRODUCT_DISPLAY_ARTIST_NAME) def require_product_release_type(data: Dict) -> RuleResult: """Rule for requiring a product release type.""" if not data['product'].get('release_type'): return RuleResult( rule_error_codes.MISSING_PRODUCT_RELEASE_TYPE, REJECT, 'Product Release Type is missing' ) return RuleResult(rule_error_codes.MISSING_PRODUCT_RELEASE_TYPE) def require_product_is_main_release(data: Dict) -> RuleResult: """Rule for requiring a product is main release.""" if type(data['product'].get('is_main_release')) is not bool: return RuleResult( rule_error_codes.MISSING_PRODUCT_IS_MAIN_RELEASE, REJECT, 'Product Main Release is missing' ) return RuleResult(rule_error_codes.MISSING_PRODUCT_IS_MAIN_RELEASE) def require_product_upc(data: Dict) -> RuleResult: """Rule for requiring a product UPC.""" if not data['product'].get('upc'): return RuleResult( rule_error_codes.MISSING_PRODUCT_UPC, REJECT, 'Product UPC is missing' ) return RuleResult(rule_error_codes.MISSING_PRODUCT_UPC) def require_valid_awal_genre(data: Dict) -> RuleResult: """Validate genre/subgenre.""" genres = data['product'].get('genres') if not genres: return RuleResult( rule_error_codes.MISSING_GENRE, REJECT, 'Genre is missing' ) genre = genres[0].get('genre') subgenre = genres[0].get('subgenre') if not subgenre: return RuleResult( rule_error_codes.MISSING_SUBGENRE, REJECT, 'Subgenre is missing' ) try: genre_mapping_util.get_orchard_genre_mapping(genre, subgenre) except InvalidGenreException as err: return RuleResult( rule_error_codes.INVALID_GENRE, REJECT, str(err) ) except InvalidSubgenreException as err: return RuleResult( rule_error_codes.INVALID_SUBGENRE, REJECT, str(err) ) return RuleResult(rule_error_codes.MISSING_GENRE) def require_valid_pline(data: Dict) -> RuleResult: """Validate track p-line.""" tracks = data['tracks'] for track in tracks: p_line = track.get('p_line') if p_line and not re.match('[0-9]{4}[ ]+\\S+', p_line, flags=re.ASCII): return RuleResult( rule_error_codes.INVALID_PLINE, REJECT, f'Invalid P-Line on track ' f'{track["volume"]}-{track["sequence_number"]}') return RuleResult(rule_error_codes.INVALID_PLINE) def require_valid_cline(data: Dict) -> RuleResult: """Validate product c-line.""" c_line = data['product'].get('c_line') if c_line and not re.match('[0-9]{4}[ ]+\\S+', c_line, flags=re.ASCII): return RuleResult( rule_error_codes.INVALID_CLINE, REJECT, 'Invalid product C-Line' ) return RuleResult(rule_error_codes.INVALID_CLINE) def validate_product_localization_language_codes(data: Dict) -> RuleResult: """Validate product localized title language codes.""" localized_titles = data['product'].get('localized_titles') return validate_language_codes( localized_titles, rule_error_codes.DUPLICATE_LOCALIZED_PRODUCT_LANGUAGE_CODE, 'localized product title' ) def validate_product_localized_titles(data: Dict) -> RuleResult: """Check product localized titles for missing data.""" localized_titles = data['product'].get('localized_titles') for localized_title in localized_titles or []: result = validate_localized_title(localized_title, 'Product') if result: return result return RuleResult(rule_error_codes.MISSING_LOCALIZED_TITLE_DATA) def validate_track_localized_titles(data: Dict) -> RuleResult: """Check track localized titles for missing data.""" for track in data.get('tracks') or []: localized_titles = track.get('localized_titles') for localized_title in localized_titles or []: result = validate_localized_title(localized_title, 'Track') if result: return result return RuleResult(rule_error_codes.MISSING_LOCALIZED_TITLE_DATA) def validate_localized_title(localized_title: Dict, field: str) -> RuleResult: """Check localized title for missing data.""" if not localized_title.get('title'): return RuleResult( rule_error_codes.MISSING_LOCALIZED_TITLE_DATA, REJECT, f'{field} Localization Title is missing' ) if not localized_title.get('language_code'): return RuleResult( rule_error_codes.MISSING_LOCALIZED_TITLE_DATA, REJECT, f'{field} Localization Language Code is missing' ) return None def validate_product_localized_artists(data: Dict) -> RuleResult: """Check product localized artists for missing data.""" for display_artist in data['product'].get('display_artists') or []: for localized_name in display_artist.get('localized_names') or []: result = validate_localized_artist(localized_name, 'Product') if result: return result return RuleResult(rule_error_codes.MISSING_LOCALIZED_ARTIST_DATA) def validate_track_localized_artists(data: Dict) -> RuleResult: """Check track localized artists for missing data.""" for track in data.get('tracks') or []: for display_artist in track.get('display_artists') or []: for localized_name in display_artist.get('localized_names') or []: result = validate_localized_artist(localized_name, 'Track') if result: return result return RuleResult(rule_error_codes.MISSING_LOCALIZED_ARTIST_DATA) def validate_localized_artist( localized_artist: Dict, field: str) -> RuleResult: """Check localized artist for missing data.""" if not localized_artist.get('name'): return RuleResult( rule_error_codes.MISSING_LOCALIZED_ARTIST_DATA, REJECT, f'{field} Artist Localization Name is missing' ) if not localized_artist.get('language_code'): return RuleResult( rule_error_codes.MISSING_LOCALIZED_ARTIST_DATA, REJECT, f'{field} Artist Localization Language Code is missing' ) return None def validate_language_codes( localized_titles: Dict, validation_rule_id: int, field_name: str) -> RuleResult: """Validate language codes.""" if localized_titles: language_ids = [] for localized_title in localized_titles: language_id = metadata_helper.get_language_id( localized_title['language_code']) if language_id in language_ids: return RuleResult( validation_rule_id, REJECT, f'Duplicate language code' f' {localized_title["language_code"]}' f' not allowed for localization' ) language_ids.append(language_id) if (rule_constants.CHINESE_SIMPLIFIED in language_ids and rule_constants.CHINESE_TRADITIONAL not in language_ids ) or ( rule_constants.CHINESE_SIMPLIFIED not in language_ids and rule_constants.CHINESE_TRADITIONAL in language_ids): return RuleResult( rule_error_codes.MISSING_CHINESE_SIMPLIFIED_OR_TRADITIONAL, WARNING, f'Both Simplified and Traditional Chinese must be present for' f' {field_name}' ) return RuleResult(validation_rule_id) def validate_product_display_artist_language_codes(data: Dict) -> RuleResult: """Validate product localized title language codes.""" display_artists = data['product'].get('display_artists') result = RuleResult( rule_error_codes.DUPLICATE_LOCALIZED_PRODUCT_ARTIST_LANGUAGE_CODE) for display_artist in display_artists: result = validate_language_codes( display_artist.get('localized_names'), rule_error_codes.DUPLICATE_LOCALIZED_PRODUCT_ARTIST_LANGUAGE_CODE, f'localized product display artist {display_artist["name"]}' ) if result.response == REJECT: return result return result def require_track_name(data: Dict) -> RuleResult: """Rule for requiring a track name.""" for track in data.get('tracks') or []: if not track.get('track_name'): return RuleResult( rule_error_codes.MISSING_TRACK_NAME, REJECT, 'Track Name is missing' ) return RuleResult(rule_error_codes.MISSING_TRACK_NAME) def require_track_isrc(data: Dict) -> RuleResult: """Rule for requiring a track ISRC.""" for track in data.get('tracks') or []: if not track.get('isrc'): return RuleResult( rule_error_codes.MISSING_TRACK_ISRC, REJECT, 'Track ISRC is missing' ) return RuleResult(rule_error_codes.MISSING_TRACK_ISRC) def require_track_unique_isrc(data: Dict) -> RuleResult: """Rule for requiring a track ISRC to be unique.""" existing_isrc = set() for track in data.get('tracks') or []: track_isrc = track.get('isrc') if track_isrc in existing_isrc: return RuleResult( rule_error_codes.DUPLICATE_TRACK_ISRC, REJECT, f'Duplicate track ISRC {track_isrc}' ) else: existing_isrc.add(track_isrc) return RuleResult(rule_error_codes.DUPLICATE_TRACK_ISRC) def require_track_sequence_number(data: Dict) -> RuleResult: """Rule for requiring a track sequence number.""" for track in data.get('tracks') or []: if not track.get('sequence_number'): return RuleResult( rule_error_codes.MISSING_TRACK_SEQUENCE_NUMBER, REJECT, 'Track Sequence Number is missing' ) return RuleResult(rule_error_codes.MISSING_TRACK_SEQUENCE_NUMBER) def require_track_volume(data: Dict) -> RuleResult: """Rule for requiring a track volume.""" for track in data.get('tracks') or []: if not track.get('volume'): return RuleResult( rule_error_codes.MISSING_TRACK_VOLUME, REJECT, 'Track Volume is missing' ) return RuleResult(rule_error_codes.MISSING_TRACK_VOLUME) def require_track_release_type(data: Dict) -> RuleResult: """Rule for requiring a track release type.""" for track in data.get('tracks') or []: if not track.get('release_type'): return RuleResult( rule_error_codes.MISSING_TRACK_RELEASE_TYPE, REJECT, 'Track Release Type is missing' ) return RuleResult(rule_error_codes.MISSING_TRACK_RELEASE_TYPE) def require_track_release_reference(data: Dict) -> RuleResult: """Rule for requiring a track release reference.""" for track in data.get('tracks') or []: if not track.get('release_reference'): return RuleResult( rule_error_codes.MISSING_TRACK_RELEASE_REFERENCE, REJECT, 'Track Release Reference is missing' ) return RuleResult(rule_error_codes.MISSING_TRACK_RELEASE_REFERENCE) def require_track_resource_reference(data: Dict) -> RuleResult: """Rule for requiring a track resource reference.""" for track in data.get('tracks') or []: if not track.get('resource_reference'): return RuleResult( rule_error_codes.MISSING_TRACK_RESOURCE_REFERENCE, REJECT, 'Track Resource Reference is missing' ) return RuleResult(rule_error_codes.MISSING_TRACK_RESOURCE_REFERENCE) def validate_track_display_artist_language_codes(data: Dict) -> RuleResult: """Validate track localized title language codes.""" result = RuleResult( rule_error_codes.DUPLICATE_LOCALIZED_TRACK_ARTIST_LANGUAGE_CODE) if data['tracks']: for track in data['tracks']: display_artists = track.get('display_artists') for display_artist in display_artists: result = validate_language_codes( display_artist.get('localized_names'), rule_error_codes.DUPLICATE_LOCALIZED_TRACK_ARTIST_LANGUAGE_CODE, # noqa f'localized product display artist ' f'{display_artist["name"]}' ) if result.response == REJECT: return result return result def validate_track_localization_language_codes(data: Dict) -> RuleResult: """Validate track localized title language codes.""" result = RuleResult( rule_error_codes.DUPLICATE_LOCALIZED_TRACK_LANGUAGE_CODE) if data['tracks']: for track in data['tracks']: localized_titles = track.get('localized_titles') result = validate_language_codes( localized_titles, rule_error_codes.DUPLICATE_LOCALIZED_TRACK_LANGUAGE_CODE, track['track_name'] ) if result.response == REJECT: return result return result def require_orchard_label(data: Dict) -> RuleResult: """Require orchard label ID.""" if not data['product'].get('vendor_id'): return RuleResult( rule_error_codes.MISSING_ORCHARD_LABEL, REJECT, 'Orchard Label ID is missing' ) return RuleResult(rule_error_codes.MISSING_ORCHARD_LABEL) def require_project_from_ext_meta(data: Dict) -> RuleResult: """Require project data from extended metadata.""" if not data.get('project'): return RuleResult( rule_error_codes.MISSING_PROJECT_DATA, REJECT, 'Project section is missing' ) if not data['project'].get('project_code'): return RuleResult( rule_error_codes.MISSING_PROJECT_CODE, REJECT, 'Project code is missing' ) if not data['project'].get('artist'): return RuleResult( rule_error_codes.MISSING_PROJECT_ARTIST, REJECT, 'Project artist is missing' ) return RuleResult(rule_error_codes.MISSING_PROJECT_DATA) def require_video_name(data: Dict) -> RuleResult: """Rule for requiring a video name.""" if data.get('video'): if not data['video'].get('video_name'): return RuleResult( rule_error_codes.MISSING_VIDEO_NAME, REJECT, 'Video Name is missing' ) return RuleResult(rule_error_codes.MISSING_VIDEO_NAME) def require_video_isrc(data: Dict) -> RuleResult: """Rule for requiring a video ISRC.""" if data.get('video'): if not data['video'].get('isrc'): return RuleResult( rule_error_codes.MISSING_VIDEO_ISRC, REJECT, 'Video ISRC is missing' ) return RuleResult(rule_error_codes.MISSING_VIDEO_ISRC) def require_video_release_type(data: Dict) -> RuleResult: """Rule for requiring a video release type.""" if data.get('video'): if not data['video'].get('release_type'): return RuleResult( rule_error_codes.MISSING_VIDEO_RELEASE_TYPE, REJECT, 'Video Release Type is missing' ) return RuleResult(rule_error_codes.MISSING_VIDEO_RELEASE_TYPE) def require_video_release_reference(data: Dict) -> RuleResult: """Rule for requiring a video release reference.""" if data.get('video'): if not data['video'].get('release_reference'): return RuleResult( rule_error_codes.MISSING_VIDEO_RELEASE_REFERENCE, REJECT, 'Video Release Reference is missing' ) return RuleResult(rule_error_codes.MISSING_VIDEO_RELEASE_REFERENCE) def validate_video_p_line(data: Dict) -> RuleResult: """Rule for requiring a video pline to be complete or null.""" if data.get('video'): if not data['video'].get('p_line'): return RuleResult( rule_error_codes.MISSING_VIDEO_PLINE_DATA, REJECT, 'Video PLine is missing from XML' ) if not data['video']['p_line'].get('line'): return RuleResult( rule_error_codes.MISSING_VIDEO_PLINE_DATA, REJECT, 'Video PLine Text is missing' ) if not data['video']['p_line'].get('year'): return RuleResult( rule_error_codes.MISSING_VIDEO_PLINE_DATA, REJECT, 'Video PLine Year is missing' ) return RuleResult(rule_error_codes.MISSING_VIDEO_PLINE_DATA) def validate_video_c_line(data: Dict) -> RuleResult: """Rule for requiring a video cline to be complete or null.""" if data.get('video'): if not data['video'].get('c_line'): return RuleResult( rule_error_codes.MISSING_VIDEO_CLINE_DATA, REJECT, 'Video CLine is missing from XML' ) if not data['video']['c_line'].get('line'): return RuleResult( rule_error_codes.MISSING_VIDEO_CLINE_DATA, REJECT, 'Video CLine Text is missing' ) if not data['video']['c_line'].get('year'): return RuleResult( rule_error_codes.MISSING_VIDEO_CLINE_DATA, REJECT, 'Video CLine Year is missing' ) return RuleResult(rule_error_codes.MISSING_VIDEO_CLINE_DATA) def validate_product_participants(data: Dict) -> RuleResult: """Check product participants for missing data.""" for participant in data['product'].get('display_artists') or []: result = validate_participant(participant, 'Product') if result: return result return RuleResult(rule_error_codes.MISSING_PARTICIPANT_DATA) def validate_track_participants(data: Dict) -> RuleResult: """Check track participants for missing data.""" for track in data.get('tracks') or []: for participant in track.get('display_artists') or []: result = validate_participant(participant, 'Track') if result: return result for participant in track.get('resource_contributors') or []: result = validate_participant(participant, 'Track') if result: return result return RuleResult(rule_error_codes.MISSING_PARTICIPANT_DATA) def validate_video_participants(data: Dict) -> RuleResult: """Check video participants for missing data.""" if data.get('video'): for participant in data['video'].get('display_artists') or []: result = validate_participant(participant, 'Video') if result: return result for participant in data['video'].get('resource_contributors') or []: result = validate_participant(participant, 'Video') if result: return result return RuleResult(rule_error_codes.MISSING_PARTICIPANT_DATA) def validate_participant(participant: Dict, field: str) -> RuleResult: """Check participant for missing data.""" if not participant.get('name'): return RuleResult( rule_error_codes.MISSING_PARTICIPANT_DATA, REJECT, f'{field} Participant Name is missing' ) if not participant.get('sequence_number'): return RuleResult( rule_error_codes.MISSING_PARTICIPANT_DATA, REJECT, f'{field} Participant Sequence Number is missing' ) return None def validate_deal_release_references(data: Dict) -> RuleResult: """Check deals for missing data.""" for deal in data.get('deals') or []: if not deal.get('release_references'): return RuleResult( rule_error_codes.MISSING_DEAL_RELEASE_REFERENCES, REJECT, 'Deal Release References are missing' ) return RuleResult(rule_error_codes.MISSING_DEAL_RELEASE_REFERENCES) def validate_deal_terms(data: Dict) -> RuleResult: """Check deal terms for missing data.""" for deal in data.get('deals') or []: if not deal.get('deal_terms'): return RuleResult( rule_error_codes.MISSING_DEAL_TERMS, REJECT, 'Deal Terms are missing' ) return RuleResult(rule_error_codes.MISSING_DEAL_TERMS) def validate_deal_term_commercial_model_types(data: Dict) -> RuleResult: """Check deal terms for missing data.""" for deal in data.get('deals') or []: for deal_term in deal.get('deal_terms') or []: if not deal_term.get('commercial_model_types'): return RuleResult( rule_error_codes.MISSING_DEAL_TERM_COMMERCIAL_MODEL_TYPES, REJECT, 'Deal Term Commercial Model Types are missing' ) return RuleResult(rule_error_codes.MISSING_DEAL_TERM_COMMERCIAL_MODEL_TYPES) # noqa def validate_deal_term_usage_type(data: Dict) -> RuleResult: """Check deal terms for missing data.""" for deal in data.get('deals') or []: for deal_term in deal.get('deal_terms') or []: if not deal_term.get('usage_type'): return RuleResult( rule_error_codes.MISSING_DEAL_TERM_USAGE_TYPE, REJECT, 'Deal Term Usage Type is missing' ) return RuleResult(rule_error_codes.MISSING_DEAL_TERM_USAGE_TYPE) def validate_deal_term_territories(data: Dict) -> RuleResult: """Check deal terms for missing data.""" for deal in data.get('deals') or []: for deal_term in deal.get('deal_terms') or []: if not deal_term.get('territories') and \ not deal_term.get('excluded_territories'): return RuleResult( rule_error_codes.MISSING_DEAL_TERM_TERRITORIES, REJECT, 'Deal Term Territories are missing' ) return RuleResult(rule_error_codes.MISSING_DEAL_TERM_TERRITORIES) def validate_instant_gratification_references(data: Dict) -> RuleResult: """Check instant gratification for missing data.""" for deal in data.get('deals') or []: for deal_term in deal.get('deal_terms') or []: if deal_term.get('instant_gratifications'): if not deal_term['instant_gratifications'].get('references'): return RuleResult( rule_error_codes.MISSING_INSTANT_GRATIFICATION_REFERENCES, # noqa REJECT, 'Instant Gratification References are missing' ) return RuleResult(rule_error_codes.MISSING_INSTANT_GRATIFICATION_REFERENCES) # noqa def validate_update_indicator(data: Dict) -> RuleResult: """Check update indicator for invalid data.""" update_indicator = data.get('update_indicator') if update_indicator and update_indicator not in VALID_UPDATE_INDICATORS: return RuleResult( rule_error_codes.INVALID_UPDATE_INDICATOR, REJECT, 'Invalid value for update indicator' ) return RuleResult(rule_error_codes.INVALID_UPDATE_INDICATOR) def validate_product_price_type(data: Dict) -> RuleResult: """Check product album price type for invalid data.""" deal_terms = next(( deal['deal_terms'] for deal in data['deals'] if 'R0' in deal['release_references'] or 'R1' in deal['release_references']), [] ) price_type = next(( deal_term['price_type'] for deal_term in deal_terms if deal_term['price_type']), None ) # Allow null values if not price_type: return RuleResult(rule_error_codes.INVALID_PRODUCT_PRICE_TYPE) if not str.isdigit(price_type): return RuleResult( rule_error_codes.INVALID_PRODUCT_PRICE_TYPE, REJECT, f'Product price type "{price_type}" is not an integer' ) pricing_tiers = ORCHARD_PRICING_FAMILY[rule_constants.MUSIC_ALBUM] for pricing_tier in pricing_tiers: if int(pricing_tier['id']) == int(price_type): return RuleResult(rule_error_codes.INVALID_PRODUCT_PRICE_TYPE) return RuleResult( rule_error_codes.INVALID_PRODUCT_PRICE_TYPE, REJECT, f'Product price type "{price_type}" is not valid' ) def validate_track_price_type(data: Dict) -> RuleResult: """Check product track price type for invalid data.""" pricing_tiers = ORCHARD_PRICING_FAMILY[rule_constants.MUSIC_TRACK] for track in data['tracks']: isrc = track['isrc'] deal_terms = next(( deal['deal_terms'] for deal in data['deals'] if track['release_reference'] in deal['release_references']), [] ) price_type = next(( deal_term['price_type'] for deal_term in deal_terms if deal_term['price_type']), None ) # Allow null values if not price_type: continue if not str.isdigit(price_type): return RuleResult( rule_error_codes.INVALID_TRACK_PRICE_TYPE, REJECT, f'On track {isrc} price type "{price_type}" is not an integer' ) valid_price_type = False for pricing_tier in pricing_tiers: if int(pricing_tier['id']) == int(price_type): valid_price_type = True if not valid_price_type: return RuleResult( rule_error_codes.INVALID_TRACK_PRICE_TYPE, REJECT, f'On track {isrc} price type "{price_type}" is not valid' ) return RuleResult(rule_error_codes.INVALID_TRACK_PRICE_TYPE) def validate_roles(data: Dict) -> RuleResult: """Apply Orchard validation to track publishing.""" VALID_ROLES = set( list(DDEX_ARTIST_ROLE_TO_ORCHARD_ARTIST_TYPE.keys()) + list(DDEX_ARTIST_ROLE_TO_ORCHARD_WRITER.keys()) + list(DDEX_ARTIST_ROLE_TO_ORCHARD_PERFORMER_ROLE.keys()) + list(DDEX_RESOURCE_CONTRIBUTOR_ROLE_TO_ORCHARD_PERFORMER_ROLE.keys()) + list(DDEX_RESOURCE_CONTRIBUTOR_ROLE_TO_ORCHARD_PARTICIPANT_ROLE.keys()) # noqa + list(DDEX_VIDEO_ROLE_TO_ORCHARD_ARTIST_TYPE.keys()) ) artist = data.get('project', {}).get('artist') for role in artist.get('roles') or []: if role not in VALID_ROLES: return RuleResult( rule_error_codes.INVALID_ROLE, WARNING, f'Invalid role {role} on project artist {artist.get("name")}') for artist in data['product'].get('display_artists') or []: for role in artist.get('roles') or []: if role not in VALID_ROLES: return RuleResult( rule_error_codes.INVALID_ROLE, WARNING, f'Invalid role {role} on product artist ' f'{artist.get("name")}') video = data.get('video') or {} video_artists = video.get('display_artists') or [] video_artists.extend(video.get('resource_contributors') or []) for artist in video_artists: for role in artist.get('roles') or []: if role not in VALID_ROLES: return RuleResult( rule_error_codes.INVALID_ROLE, WARNING, f'Invalid role {role} on video artist ' f'{artist.get("name")}') for track in data['tracks']: track_artists = track.get('display_artists') or [] track_artists.extend(track.get('resource_contributors') or []) for artist in track_artists: for role in artist.get('roles') or []: if role not in VALID_ROLES: return RuleResult( rule_error_codes.INVALID_ROLE, WARNING, f'Invalid role {role} on artist {artist.get("name")} ' f'on track {track.get("isrc")}') return RuleResult(rule_error_codes.INVALID_ROLE) COMMON_RULES = [ require_party_id, require_message_id, require_product, require_product_name, require_product_release_reference, require_product_display_artist_name, require_product_release_type, require_product_is_main_release, require_product_upc, validate_product_participants, validate_product_localized_titles, validate_product_localized_artists, validate_product_localization_language_codes, validate_product_display_artist_language_codes, require_track_name, require_track_isrc, require_track_unique_isrc, require_track_sequence_number, require_track_volume, require_track_release_type, require_track_release_reference, require_track_resource_reference, validate_track_participants, validate_track_localized_titles, validate_track_localized_artists, require_video_name, require_video_isrc, require_video_release_type, require_video_release_reference, validate_video_participants, validate_video_p_line, validate_video_c_line, require_valid_pline, require_valid_cline, validate_deal_release_references, validate_deal_terms, validate_deal_term_commercial_model_types, validate_deal_term_usage_type, validate_deal_term_territories, validate_instant_gratification_references, validate_update_indicator, require_project_from_ext_meta, validate_roles, ] # Configure the rules we want to execute per ingestion source RULES_PER_INGESTION_SOURCE = { AWAL: [ *COMMON_RULES, *ORCHARD_VALIDATION_RULES, require_valid_awal_genre, require_orchard_label, validate_product_price_type, validate_track_price_type ] }