import numpy as np import pandas as pd import string import math from constants import knr_consts from config import config import sentry_sdk from datetime import datetime import re log = config.get_logger() sentry_sdk.init(config.SENTRY_DSN) def handle_validation_errors(invalid_rows, col_name, error_df): error_df = pd.concat([error_df, invalid_rows]) errors = [f'Invalid {col_name}'] * len(invalid_rows) return error_df, errors def is_empty_value(value): return value == '' or type(value) == float and math.isnan(float(value)) def valid_value(value: string, valid_items_arr, separator, mandatory, col_name): try: if is_empty_value(value): if not mandatory: return True if mandatory: return False value = str(value).replace(" ", "") if separator != "": arr = value.split(separator) # Remove any empty string items in array arr = list(filter(lambda x: x != "", arr)) if len(arr) == 0 and mandatory: return False invalid_strs = list(filter(lambda x: x.upper().translate(str.maketrans('', '', f'{string.punctuation}')).strip() not in valid_items_arr, arr)) return len(invalid_strs) == 0 if separator == "": x = value.upper().translate(str.maketrans('', '', f'{string.punctuation}')).strip() return x in valid_items_arr except Exception as e: log.error(f'Error validating {col_name}: {e}') def remove_non_numeric_chars(upc): return re.sub('[^0-9]', '', str(upc)).strip() def is_valid_length(value, length_arr): if is_empty_value(value): return False valid_length = [x == len(str(value).strip()) for x in length_arr] return any(valid_length) def valid_str_duration(duration: str): try: return bool(datetime.strptime(str(duration), '%H:%M:%S')) except Exception: return False def valid_duration(duration): if str(duration).isdigit() or (isinstance(duration, float) and not math.isnan(float(duration))) or valid_str_duration(duration): return True else: return False def performers_count(value, separator): if is_empty_value(value) or type(value) != str: return 0 arr = list(filter(lambda x: x != "", [x.strip() for x in value.split(separator)])) return len(arr) def validate_contributor_count(df: pd.DataFrame): try: performer_cols = df[['Contributor Legal Name', 'Contributor Type', 'Contributor Role(s)']].copy() performer_cols.columns = ["contrName", "contrType", "contrRole"] performer_cols = performer_cols.applymap(lambda x: performers_count(x, "|")) invalid_performer_columns = performer_cols.apply(lambda x: ~(x.contrName == x.contrType and x.contrType == x.contrRole), axis=1) return df.loc[invalid_performer_columns] except Exception as e: log.error(f'Error validating contributor count: {e}') def validate_collection_countries(countries): if is_empty_value(countries): return False arr = countries.split('|') for country in arr: if country.upper().strip() == knr_consts.NO_TERRITORY: return False if country.upper().strip() == 'WORLD': return True if is_valid_length(country, [2]): return True else: return False def validate_collection_countries_excluded(countries): if is_empty_value(countries): return True arr = countries.split('|') for country in arr: if country.upper().strip() == knr_consts.NO_TERRITORY: return True if country.upper().strip() == 'WORLD': return False if is_valid_length(country, [2]): return True else: return False def validate_mandatory_columns(ingest_template_df, error_df): validation_errors = [] error_row_indexes = [] # Validate Sales Start Date - Audio only # invalid_sales_start_dates = ingest_template_df.loc[pd.to_datetime(ingest_template_df.loc[:,'Sales Start Date'], errors="coerce").isna()] # error_df, sales_start_errs = handle_validation_errors(invalid_sales_start_dates, 'Sales Start Date', error_df) # for x in invalid_sales_start_dates.index: # error_row_indexes.append(x) # Validate Duration invalid_duration = ingest_template_df.loc[ingest_template_df.loc[:, 'Duration'].apply(lambda x: not valid_duration(x))] error_df, duration_errs = handle_validation_errors(invalid_duration, 'Duration', error_df) for x in invalid_duration.index: error_row_indexes.append(x) # Validate Product Format - Audio only # invalid_product_formats = ingest_template_df.loc[ingest_template_df.loc[:,'Product Format'].apply(lambda x: not valid_value(x, knr_consts.VALID_PRODUCT_FORMATS, "", True, "Product Format"))] # error_df, product_format_errs = handle_validation_errors(invalid_product_formats, 'Product Format', error_df) # for x in invalid_product_formats.index: # error_row_indexes.append(x) # Validate Collection Ownership Country invalid_countries = ingest_template_df.loc[ingest_template_df.loc[:,'Collection Ownership Country'].apply(lambda x: not validate_collection_countries(x))] error_df, countries_errs = handle_validation_errors(invalid_countries, 'Collection Ownership Country', error_df) for x in invalid_countries.index: error_row_indexes.append(x) validation_errors = duration_errs + countries_errs # validation_errors = sales_start_errs + duration_errs + product_format_errs + countries_errs return ingest_template_df, error_df, validation_errors, error_row_indexes def validate_non_mandatory_columns(ingest_template_df, error_df): non_mandatory_errors = [] error_row_indexes = [] # Validate Collection Ownership End Date invalid_rights_end_dates = ingest_template_df.loc[(pd.to_datetime(ingest_template_df.loc[:,'Collection Ownership End Date'], errors="coerce").isna()) & (~ingest_template_df.loc[:,'Collection Ownership End Date'].isnull())] error_df, rights_end_errs = handle_validation_errors(invalid_rights_end_dates, 'Collection Ownership End Date', error_df) for x in invalid_rights_end_dates.index: error_row_indexes.append(x) # Validate Collection Ownership Percentage of Rights (technically mandatory, but missing values are filled in during formatting) invalid_percentage = ingest_template_df.loc[ingest_template_df.loc[:, 'Collection Ownership Percentage of Rights'].apply(lambda x: not str(x).isdigit() or not 1 <= int(x) <= 100 )& (~ingest_template_df.loc[:,'Collection Ownership Percentage of Rights'].isnull())] error_df, percentage_errs = handle_validation_errors(invalid_percentage, 'Collection Ownership Percentage of Rights', error_df) for x in invalid_percentage.index: error_row_indexes.append(x) # Validate Collection Ownership Country - Exclusion invalid_country = ingest_template_df.loc[ingest_template_df.loc[:, 'Collection Ownership Country - Exclusion'].apply(lambda x: not validate_collection_countries_excluded(x) )] error_df, country_errs = handle_validation_errors(invalid_country, 'Collection Ownership Country - Exclusion', error_df) for x in invalid_country.index: error_row_indexes.append(x) non_mandatory_errors = rights_end_errs + percentage_errs + country_errs return ingest_template_df, error_df, non_mandatory_errors, error_row_indexes def validate_columns_with_set_length(ingest_template_df, error_df, columns): errors = [] error_row_indexes = [] for col in columns: # If column values should be digit-only, remove non-digit characters before validating length. This modifies the returned ingest_template_df so that we don't need to format again later on. if col[2]: ingest_template_df = ingest_template_df.assign(**{ col[0]: ingest_template_df.loc[:,col[0]].apply(lambda x: remove_non_numeric_chars(x)), }) invalid_lengths = ingest_template_df.loc[ingest_template_df.loc[:,col[0]].apply(lambda x: not is_valid_length(x, col[1]))] error_df, invalid_lengths_errs = handle_validation_errors(invalid_lengths, col[0], error_df) for x in invalid_lengths.index: error_row_indexes.append(x) errors = errors + invalid_lengths_errs return ingest_template_df, error_df, errors, error_row_indexes def contains_invalid_value(value, invalid_items, separator, mandatory): if is_empty_value(value): return mandatory arr = str(value).split(separator) invalid_strs = [] for item in arr: x = item.upper().translate(str.maketrans('', '', f'{string.punctuation} ')).strip() if x in invalid_items: invalid_strs.append(x) # remove all empty strings invalid_strs = list(filter(lambda x: x != "", invalid_strs)) return len(invalid_strs) >= 1 def is_valid_upc(value, length_arr): if is_empty_value(value): return True valid_length = [x == len(str(value).strip()) for x in length_arr] return any(valid_length) # Product columns are only required if a UPC is provided. UPCs are not mandatory for Video content. def validate_video_product_metadata(ingest_template_df, error_df): product_errors = [] error_row_indexes = [] # Validate UPC Length if provided invalid_upc_length = ingest_template_df.loc[ingest_template_df.loc[:,'UPC'].apply(lambda x: not is_valid_upc(x, [12, 13]))] error_df, invalid_upc_errs = handle_validation_errors(invalid_upc_length, 'UPC', error_df) for x in invalid_upc_length.index: error_row_indexes.append(x) product_errors = product_errors + invalid_upc_errs upc_exists = ingest_template_df.loc[ingest_template_df.loc[:,'UPC'].apply(lambda x: not is_empty_value(x))] for index_value in upc_exists.index: errored_row = ingest_template_df.filter(items=[index_value], axis=0) # Validate Product Format product_format = ingest_template_df.loc[index_value, 'Product Format'] valid_format = valid_value(product_format, knr_consts.VALID_VIDEO_PRODUCT_FORMATS, "", True, "Product Format") if not valid_format: error_df, product_format_errs = handle_validation_errors(errored_row, 'Product Format', error_df) error_row_indexes.append(index_value) product_errors = product_errors + product_format_errs # Validate Sales Start Date sales_date = ingest_template_df.loc[index_value, 'Sales Start Date'] invalid_sales_date = is_empty_value(sales_date) if invalid_sales_date: error_df, sales_start_errs = handle_validation_errors(errored_row, 'Sales Start Date', error_df) error_row_indexes.append(index_value) product_errors = product_errors + sales_start_errs # Validate P LINE Year length ingest_template_df = ingest_template_df.assign(**{ '(P) LINE Year': ingest_template_df.loc[:,'(P) LINE Year'].apply(lambda x: remove_non_numeric_chars(x)), }) pline_year = ingest_template_df.loc[index_value, '(P) LINE Year'] valid_pline_year = is_valid_length(pline_year, [4]) if not valid_pline_year: error_df, invalid_pline_errs = handle_validation_errors(errored_row, '(P) LINE Year', error_df) error_row_indexes.append(index_value) product_errors = product_errors + invalid_pline_errs # No-validation mandatory columns check for col_name in ['Product Primary Artists', 'Product Name', '(P) LINE Label', 'Product Code']: col_value = ingest_template_df.loc[index_value, col_name] if is_empty_value(col_value): error_df, mandatory_errs = handle_validation_errors(errored_row, col_name, error_df) error_row_indexes.append(index_value) product_errors = product_errors + mandatory_errs return ingest_template_df, error_df, product_errors, error_row_indexes return ingest_template_df, error_df, product_errors, error_row_indexes def validate_contributor_columns(ingest_template_df, error_df): # Validate Contributor Type invalid_performer_categories = ingest_template_df.loc[ingest_template_df.loc[:,'Contributor Type'].apply(lambda x: not valid_value(x, knr_consts.VALID_PERFORMER_CATEGORIES, "|", False, "Contributor Type"))] error_df, performer_cat_errs = handle_validation_errors(invalid_performer_categories, 'Contributor Type', error_df) # Validate that Contributor Names and Roles don't include types invalid_performer_names = ingest_template_df.loc[ingest_template_df.loc[:,'Contributor Legal Name'].apply(lambda x: contains_invalid_value(x, knr_consts.VALID_PERFORMER_CATEGORIES, "|", False))] error_df, performer_name_errs = handle_validation_errors(invalid_performer_names, 'Contributor Legal Name (includes a Contributor Type)', error_df) invalid_performer_roles = ingest_template_df.loc[ingest_template_df.loc[:,'Contributor Role(s)'].apply(lambda x: contains_invalid_value(x, knr_consts.VALID_PERFORMER_CATEGORIES, "|", False))] error_df, performer_role_errs = handle_validation_errors(invalid_performer_roles, 'Contributor Role(s) (includes a Contributor Type)', error_df) # Validate Contributor Counts (Names == Types == Roles) invalid_performer_counts = validate_contributor_count(ingest_template_df) error_df, performer_count_errs = handle_validation_errors(invalid_performer_counts, "Contributor Columns - number of Contributor Legal Names, Types, and Roles must be the same.", error_df) performer_error_rows = pd.concat([invalid_performer_categories, invalid_performer_names, invalid_performer_roles, invalid_performer_counts]) if len(performer_error_rows) > 0: ingest_template_df.loc[performer_error_rows.index] = ingest_template_df.loc[performer_error_rows.index].assign(**{ 'Contributor Legal Name': '', 'Contributor Type': '', 'Contributor Role(s)': '' }) contributor_errors = performer_cat_errs + performer_name_errs + performer_role_errs + performer_count_errs return ingest_template_df, error_df, contributor_errors def validate_isrc_column(ingest_template_df, error_df): errors = [] error_row_indexes = [] isrcs_to_exclude = ingest_template_df.loc[ingest_template_df.loc[:,'ISRC'].apply(lambda x: str(x).strip() in knr_consts.ISRCS_TO_EXCLUDE)] error_df, isrc_errs = handle_validation_errors(isrcs_to_exclude, 'ISRC - excluded bc of ownership clash', error_df) for x in isrcs_to_exclude.index: error_row_indexes.append(x) return ingest_template_df, error_df, isrc_errs, error_row_indexes