"""Config for Amazon Music Ingestion Workflow.""" import datetime import os import re feed_name = 'amazon_music' secrets_path = 'amazon_music' feed_version = '1.0' storeid = 187 licensors = ['theorchard', 'sme', 'awal', 'altafonte'] DBT_REQUIRED_LICENSORS = ['theorchard', 'sme'] SOURCE_SFTP = 'sftp' SOURCE_DATAPULSE = 'datapulse' sources = [SOURCE_SFTP, SOURCE_DATAPULSE] # Licensors mapped from datapulse ENTITY_NAME column. DATAPULSE_LICENSORS = ['theorchard', 'sme'] # Maps the amazon_music licensor to the datapulse ENTITY_NAME value. DATAPULSE_ENTITY_NAME_BY_LICENSOR = { 'sme': 'Sony', 'theorchard': 'ZQOR', 'awal': 'ZQAWA', 'red': 'ZQRED', } # Maps amazon_music report_name to the datapulse SERVICE partition value. DATAPULSE_SERVICE_BY_REPORT = { 'unlimited': 'Unlimited', 'prime': 'Prime', 'adsupported': 'FreeTier', } # Single unified staging_raw table loaded from the datapulse Snowflake tables. # Holds all reports (services) and licensors for a given date in one place, # replacing the per-report staging_raw_amazon_ tables. DATAPULSE_STAGING_RAW_TABLE = 'staging_raw_amazon_music' reports = { 'unlimited': { 'feedid': 37, 'staging_raw_table': 'staging_raw_amazon_unlimited', 'cucumber_drop_feed_name': 'AmazonUnlimited', 'cucumber_feed_name': 'AmazonUnlimitedV2', 'awal_ftp_folder_name': 'Unlimited', 'awal_report_name_in_filename': 'Unlimited', 'altafonte_ftp_folder_name': 'Unlimited', 'altafonte_report_name_in_filename': 'Unlimited', 'unstable_countries': []}, 'prime': { 'feedid': 35, 'staging_raw_table': 'staging_raw_amazon_prime', 'cucumber_feed_name': 'AmazonPrime', 'awal_ftp_folder_name': 'Prime', 'awal_report_name_in_filename': 'Prime', 'altafonte_ftp_folder_name': 'Prime', 'altafonte_report_name_in_filename': 'Prime', 'unstable_countries': []}, 'adsupported': { 'feedid': 36, 'staging_raw_table': 'staging_raw_amazon_adsupported', 'cucumber_feed_name': 'AmazonAdSupported', 'awal_ftp_folder_name': 'Ad-Supported', 'awal_report_name_in_filename': 'ADS', 'altafonte_ftp_folder_name': 'Ad-Supported', 'altafonte_report_name_in_filename': 'ADS', 'unstable_countries': []}, } # execution of the flow can take up to 2 hours # so let's clean up for period 3 times more MAX_EXECUTION_TIMEOUT = 3 * 2 * 3600 DATE_FORMAT = '%Y-%m-%d' # This is original file template for The Orchard. file_template = ( '{org}_{country}_{date:%Y%m%d}_Daily_{report_type}_Report.txt.zip') file_name_regexp = re.compile( r'(?P[A-Z]{4,5})_(?P[A-Z_]{2,6})_(?P[\d]+)_Daily_' r'(?P[a-zA-Z]+)_Report.txt.zip') # This is file template will be for other licensors in archive ../clean folder. gz_file_template = ( '{org}_{country}_{date}_Daily_{report_type}_Report.txt.gz') table_name_template = ( '{feed_name}_daily_{licensor}_{report_type}_report_{org}_{country}_{date}') # default report types report_types = ['Playlist', 'Activity', 'User'] SME_AWS_ACCESS_KEY_ID_NAME = 'SME_AWS_ACCESS_KEY_ID' SME_AWS_SECRET_ACCESS_KEY_NAME = 'SME_AWS_SECRET_ACCESS_KEY' # example filename in sme bucket: # PCO3_A_AU_20190220_20190220_Activity_20190221.txt.zip sme_file_name_regexp = re.compile( r'(?P[A-Z\d]{4})_A_(?P[A-Z_]{2,6})_' r'(?P[\d]+)_(?P[\d]+)_' r'(?P[a-zA-Z]+)_(?P[\d]+).txt.zip') licensors_config = { 'theorchard': { 'report_types': report_types, 'file_name_regexp': file_name_regexp, 'file_template': file_template }, 'sme': { 'report_types': report_types, # filename example in sme bucket: # PCO3_A_AU_20190220_20190220_Activity_20190221.txt.zip 'file_name_regexp': sme_file_name_regexp, 'file_template': file_template }, 'awal': { # there is only one type of files for awal 'report_types': ['Activity'], # filename example on awal ftp # ZQAWA_Daily_Unlimited_Activity_20220710_US.txt.zip 'file_name_regexp': re.compile( r'(?P[A-Z]{5})_Daily_(?P[a-zA-Z]+)_Activity' r'_(?P[\d]+)_(?P[A-Z_]{2,6}).txt.zip'), # original file_template on awal ftp 'file_template': ( '{org}_Daily_{report}_Activity_{date:%Y%m%d}_{country}.txt.zip'), 'org': 'ZQAWA' }, 'altafonte': { # there is only one type of files for altafonte 'report_types': ['Activity'], # filename example on altafonte ftp # ZQALT_Daily_Unlimited_Activity_20250122_US.txt.zip 'file_name_regexp': re.compile( r'(?P[A-Z]{5})_Daily_(?P[a-zA-Z]+)_Activity' r'_(?P[\d]+)_(?P[A-Z_]{2,6}).txt.zip'), # original file_template on altafonte ftp 'file_template': ( '{org}_Daily_{report}_Activity_{date:%Y%m%d}_{country}.txt.zip'), 'org': 'ZQALT' }, } # bucket for dropped data drop_bucket = os.environ.get('FEED_INGESTION_DROP_BUCKET', 'dev-feed-drop') sme_drop_bucket = os.environ.get('SME_FEED_INGESTION_DROP_BUCKET', 'sme-ca-prod-partners') # bucket for archived data archive_bucket = os.environ.get('FEED_INGESTION_DATA_BUCKET', 'dev-cucumbers') awal_drop_bucket = os.environ.get( 'FEED_INGESTION_AWAL_DROP_BUCKET', 'prod-orcd-awal-drop') awal_s3_folder = 'amazon_usage' awal_s3_drop_bucket = ( f's3://{awal_drop_bucket}/AWAL_TO_ORCH/' f'ANALYTICS_BACKFILL/{awal_s3_folder}/') awal_s3_path = ( 's3://{drop_bucket}/AWAL_TO_ORCH/ANALYTICS_BACKFILL/{awal_s3_folder}/' 'version={version}/year={date:%Y}/month={date:%Y%m}/day={date:%Y%m%d}/' 'service={report_type}/iso_territory_code={country_code}/') altafonte_drop_bucket = os.environ.get( 'FEED_INGESTION_ALTAFONTE_DROP_BUCKET', 'prod-orcd-ftp') altafonte_s3_folder = 'Amazon' altafonte_s3_drop_bucket = ( f's3://{altafonte_drop_bucket}/ftp/' f'altafonte/{altafonte_s3_folder}/sales/') altafonte_s3_path = ( 's3://{drop_bucket}/ftp/altafonte/{altafonte_s3_folder}/sales/' 'version={version}/year={date:%Y}/month={date:%Y%m}/day={date:%Y%m%d}/' 'service={report_type}/iso_territory_code={country_code}/') s3 = { 'drop_path': { 'sme': 's3://{s3_bucket}/amazon/in/{{report_location}}' '/{{date:%Y/%m/%d}}/'.format(s3_bucket=sme_drop_bucket), 'theorchard': ( 's3://{s3_bucket}/feed-drop/{{cucumber_feed_name}}/'.format( s3_bucket=drop_bucket)), # source for awal files is sftp, it is just placeholder 'awal': 's3://placeholder', # source for altafonte files is sftp, it is just placeholder 'altafonte': 's3://placeholder' }, 'archive_path': ( 's3://{s3_bucket}/{{cucumber_feed_name}}/archives/' '{{date:%Y-%m-%d}}/{{licensor}}/').format(s3_bucket=archive_bucket), 'clean_dir': 'clean/' } # sftp config for awal data sftp = { 'host': os.environ.get( 'AMAZON_MUSIC_AWAL_SFTP_HOST', 'prod.reporting.amazonmusiccatalog.com'), 'username': 'AMAZON_MUSIC_AWAL_SFTP_USERNAME', 'password': 'AMAZON_MUSIC_AWAL_SFTP_PASSWORD', 'port': int(os.environ.get('AMAZON_MUSIC_AWAL_SFTP_PORT', 22)), 'path': 'sales/{country}/Daily/{awal_ftp_folder_name}/' } # sftp config for altafonte data sftp_altafonte = { 'host': os.environ.get( 'AMAZON_MUSIC_ALTAFONTE_SFTP_HOST', 'digital-music-ftp.amazonmp3.com'), 'username': 'AMAZON_MUSIC_ALTAFONTE_SFTP_USERNAME', 'password': 'AMAZON_MUSIC_ALTAFONTE_SFTP_PASSWORD', 'port': int(os.environ.get('AMAZON_MUSIC_ALTAFONTE_SFTP_PORT', 22)), 'path': 'sales/{country}/Daily/{altafonte_ftp_folder_name}/' } aggregated_staging_raw_table = 'staging_raw_amazon_aggregated_streams' FEED_DELAY = 3 # Number of days for monitoring MONITORING_PERIOD_OF_UNSTABLE_COUNTRIES = 15 # A country becomes stable if the ratio of stable days to the number of # monitoring days is more than this value REQUIRED_PERCENT_OF_STABLE_DAYS = 0.8 UNEXPECTED_FILES_NOTIFICATION_SUBJECT = ( 'Alert: Amazon {report} Analytics ({licensor}). ' 'Unexpected files were found in S3 location.') UNEXPECTED_FILES_NOTIFICATION_MESSAGE = ('Unexpected files:\n{}') NEW_STABLE_COUNTRIES_NOTIFICATION_SUBJECT = ( 'Alert: Amazon {report} Analytics ({licensor}). ' 'New stable countries were detected.') NEW_STABLE_COUNTRIES_NOTIFICATION_MESSAGE = 'Country stability:\n{}' # config for the update_dim_tables task dimension_tables = { 'tables_to_update': [ 'dim_sourcetype', 'dim_subscriptiontype', 'dim_subscriptionpaytier', 'dim_playlist', 'dim_user' ], 'include_to_report': [ 'dim_sourcetype', 'dim_subscriptiontype', 'dim_subscriptionpaytier' ], 'sns_topic': os.environ.get( 'FEED_INGESTION_SNS_TOPIC', 'arn:aws:sns:us-east-1:437795906767:dev-swf-feed-ingestion:' 'e17576f8-b905-45a9-b863-0eaceca54f4d') } dimension_tables_awal = { 'tables_to_update': ['dim_sourcetype', 'dim_playlist', 'dim_user'], 'include_to_report': ['dim_sourcetype'], 'sns_topic': os.environ.get( 'FEED_INGESTION_SNS_TOPIC', 'arn:aws:sns:us-east-1:437795906767:dev-swf-feed-ingestion:' 'e17576f8-b905-45a9-b863-0eaceca54f4d') } dimension_tables_altafonte = { 'tables_to_update': ['dim_sourcetype', 'dim_playlist', 'dim_user'], 'include_to_report': ['dim_sourcetype'], 'sns_topic': os.environ.get( 'FEED_INGESTION_SNS_TOPIC', 'arn:aws:sns:us-east-1:437795906767:dev-swf-feed-ingestion:' 'e17576f8-b905-45a9-b863-0eaceca54f4d') } # Dimension tables for the datapulse source. # The _datapulse queries read from the # unified staging_raw_amazon_music table and apply the legacy value mappings. dimension_tables_datapulse = { 'tables_to_update': [ 'dim_sourcetype_datapulse', 'dim_subscriptiontype_datapulse', 'dim_subscriptionpaytier_datapulse', 'dim_playlist_datapulse', 'dim_user_datapulse' ], 'include_to_report': [ 'dim_sourcetype_datapulse', 'dim_subscriptiontype_datapulse', 'dim_subscriptionpaytier_datapulse' ], 'sns_topic': dimension_tables['sns_topic'] } # The maximum allowed number of errors in the file snowflake_error_limit = 5 # The maximum allowed number of errors in the file during file preparation malformed_rows_limit = 20 # NOTE: Amazon includes two rows at the beginning and at the end of file # that do not match the columns expected for the file; skip them. SKIP_LINE_COLUMNS = 4 def parse_date(date_str): """Parse date from YYYY-MM-DD and return dattime object.""" return datetime.datetime.strptime(date_str, DATE_FORMAT) jenkins_config = { 'feeds_required_for_jenkins_build': [ '_'.join([feed_name, licensor, report_name]) for licensor in DBT_REQUIRED_LICENSORS for report_name in reports ], 'jenkins_username': os.environ.get('JENKINS_USER', 'jenkinsjobrunner'), 'jenkins_secrets_path': 'swf_feed_ingestion', 'jenkins_url': 'https://scheduler.theorchard.io', 'jenkins_job': 'dbt-scheduler-analytics-pipeline', 'jenkins_job_params': {'TRIGGERED_BY': feed_name} }