"""Application configuration.""" import base64 import logging import os from os.path import abspath, dirname, join, pardir import boto3 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from dotenv import load_dotenv from owsrequest.constants import headers as owsrequest_headers # Load environment variables from a .env file if present dotenv_path = abspath(join(dirname(__file__), pardir, ".env")) load_dotenv(dotenv_path) # Service information SERVICE_NAME = "ows-analytics" SERVICE_VERSION = "1.0.0" # Swagger documentation path SWAGGER_FILE_PATH = "spec/ows-analytics-1.0.0.yaml" # Environment DEV_ENVIRONMENT = "dev" QA_ENVIRONMENT = "qa" PROD_ENVIRONMENT = "prod" TEST_ENVIRONMENT = "test" ENVIRONMENT = os.environ.get("Environment", DEV_ENVIRONMENT) ONLY_LOG_ACCESS_ERRORS = True # secrets manager secrets_manager = boto3.session.Session().client( "secretsmanager", region_name="us-east-1" ) SECRET_PREFIX = f"{ENVIRONMENT}/{SERVICE_NAME}/" def get_secret(secret_name): response = {"SecretString": None} try: response = secrets_manager.get_secret_value( SecretId=SECRET_PREFIX + secret_name ) except Exception as e: logging.warning(f"Error retrieving secret {secret_name}: {e}") return response["SecretString"] # Mock grass headers for local dev DEV_AUTH = { owsrequest_headers.GRASS_ACCOUNT_TYPE: os.environ.get("GRASS_ACCOUNT_TYPE") or None, owsrequest_headers.GRASS_ACCOUNT_ID: ( os.environ.get("GRASS_ACCOUNT_ID") and str(os.environ.get("GRASS_ACCOUNT_ID")) ) or None, owsrequest_headers.ORCHARD_USER_ID: ( os.environ.get("ORCHARD_USER_ID") and str(os.environ.get("ORCHARD_USER_ID")) ) or None, } # Mock grass headers for local dev DEV_PROFILE_AUTH = { owsrequest_headers.ORCHARD_PROFILE_TYPE: os.environ.get("LOCAL_PROFILE_TYPE") or None, owsrequest_headers.ORCHARD_PROFILE_ID: ( os.environ.get("LOCAL_PROFILE_ID") and str(os.environ.get("LOCAL_PROFILE_ID")) ) or None, } # Dev configuration if ENVIRONMENT == DEV_ENVIRONMENT: # Redis configuration REDIS_HOST = os.environ.get("REDIS_HOST") REDIS_PORT = os.environ.get("REDIS_PORT") # Sentry SENTRY = os.environ.get("SENTRY_DSN") or None # Snowflake SNOWFLAKE_ACCOUNT = os.environ.get("SNOWFLAKE_ACCOUNT", "test") SNOWFLAKE_USER = os.environ.get("SNOWFLAKE_USER", "test") SNOWFLAKE_ROLE = os.environ.get("SNOWFLAKE_ROLE", "test") SNOWFLAKE_WAREHOUSE = os.environ.get("SNOWFLAKE_WAREHOUSE", "test") SNOWFLAKE_SCHEMA = os.environ.get("SNOWFLAKE_SCHEMA", "DEV") SNOWFLAKE_DATABASE = os.environ.get("SNOWFLAKE_DATABASE", "test") else: # Redis configuration REDIS_HOST = get_secret("REDIS_HOST") REDIS_PORT = get_secret("REDIS_PORT") # Sentry SENTRY = get_secret("SENTRY_DSN") # Snowflake SNOWFLAKE_ACCOUNT = get_secret("SNOWFLAKE_ACCOUNT") SNOWFLAKE_USER = get_secret("SNOWFLAKE_USER") SNOWFLAKE_ROLE = get_secret("SNOWFLAKE_ROLE") SNOWFLAKE_WAREHOUSE = get_secret("SNOWFLAKE_WAREHOUSE") SNOWFLAKE_SCHEMA = get_secret("SNOWFLAKE_SCHEMA") SNOWFLAKE_DATABASE = get_secret("SNOWFLAKE_DATABASE") # Loggers LOGGER_LEVEL = logging.WARNING if ENVIRONMENT == PROD_ENVIRONMENT else logging.INFO LOGGER_NAME = "ows1" # DynamoDB ANALYTICS_METADATA_TABLE = os.environ.get("ANALYTICS_METADATA_TABLE") # Base URL BASE_URL = os.environ.get("INTEGRATION_TESTS_BASE_URL") or ( "https://qa-ows-analytics.theorchard.io" if ENVIRONMENT == QA_ENVIRONMENT else "http://localhost:5000" ) # Endpoint URLs def _concat(a, b): return f"{a}{b}" def _base(path): return _concat(BASE_URL, path) HEALTH_CHECK_PATH = "/hello/" # we need a trailing slash due to load balancer HEALTH_CHECK_URL = _base(HEALTH_CHECK_PATH) ACCOUNT_METRICS_PATH = "/account/" def _account_metrics(path): return _concat(ACCOUNT_METRICS_PATH, path) ACCOUNT_PRODUCTS_PATH = _account_metrics("/products") ACCOUNT_PRODUCTS_URL = _base(ACCOUNT_PRODUCTS_PATH) ACCOUNT_METRICS_TIME_SERIES_PATH = _account_metrics("/timeseries") ACCOUNT_METRICS_TIME_SERIES_URL = _base(ACCOUNT_METRICS_TIME_SERIES_PATH) ACCOUNT_METRICS_SUMMARY_PATH = _account_metrics("/summary") ACCOUNT_METRICS_SUMMARY_URL = _base(ACCOUNT_METRICS_SUMMARY_PATH) ACCOUNT_TOP_CONTENT_PATH = _account_metrics("/top-content") ACCOUNT_TOP_CONTENT_URL = _base(ACCOUNT_TOP_CONTENT_PATH) ACCOUNT_VIDEO_METRICS_TIME_SERIES_PATH = _account_metrics("/video/timeseries") ACCOUNT_VIDEO_METRICS_TIME_SERIES_URL = _base(ACCOUNT_VIDEO_METRICS_TIME_SERIES_PATH) ACCOUNT_VIDEO_METRICS_SUMMARY_PATH = _account_metrics("/video/summary") ACCOUNT_VIDEO_METRICS_SUMMARY_URL = _base(ACCOUNT_VIDEO_METRICS_SUMMARY_PATH) FEEDS_PATH = "/feeds" FEEDS_URL = _base(FEEDS_PATH) FEEDS_V2_PATH = "/feeds_v2" FEEDS_V2_URL = _base(FEEDS_V2_PATH) PARTICIPANT_PATH = "/participant/" def _participant(path): return _concat(PARTICIPANT_PATH, path) PARTICIPANT_TRACK_STREAMS_ALL_PATH = _participant("/track-streams-all") PARTICIPANT_TRACK_STREAMS_ALL_URL = _base(PARTICIPANT_TRACK_STREAMS_ALL_PATH) PARTICIPANT_SUMMARY_PATH = _participant("/summary") PARTICIPANT_TIMESERIES_PATH = _participant("/timeseries") PARTICIPANT_DEMOGRAPHICS_PATH = _participant("/demographics") PARTICIPANT_AGGREGATED_STREAMS_PATH = _participant("/aggregated-streams") PARTICIPANT_AGGREGATED_STREAMS_URL = _base(PARTICIPANT_AGGREGATED_STREAMS_PATH) PARTICIPANT_TRACK_STREAMS_STORE_PATH = _participant("/track-streams-by-store") PARTICIPANT_TRACK_STREAMS_STORE_URL = _base(PARTICIPANT_TRACK_STREAMS_STORE_PATH) PRODUCT_PATH = "/product/" PRODUCT_URL = _base(PRODUCT_PATH) def _product(path): return _concat(PRODUCT_PATH, path) PRODUCT_GROWTH_PERIODS_BULK_PATH = "/product/growth-periods-bulk" PRODUCT_BULK_GROWTH_PERIODS_URL = _base(PRODUCT_GROWTH_PERIODS_BULK_PATH) PRODUCT_TIME_SERIES_PATH = _product("/timeseries") PRODUCT_TIME_SERIES_URL = _base(PRODUCT_TIME_SERIES_PATH) PRODUCT_SUMMARY_PATH = _product("/summary") PRODUCT_SUMMARY_URL = _base(PRODUCT_SUMMARY_PATH) PRODUCT_AGGREGATED_STREAMS_PATH = _product("/aggregated-streams") PRODUCT_AGGREGATED_STREAMS_URL = _base(PRODUCT_AGGREGATED_STREAMS_PATH) PRODUCT_AGGREGATE_STREAMS_PATH = _product("/aggregate-streams") PRODUCT_AGGREGATE_STREAMS_URL = _base(PRODUCT_AGGREGATE_STREAMS_PATH) PRODUCT_METADATA_PATH = _product("/metadata") PRODUCT_METADATA_URL = _base(PRODUCT_METADATA_PATH) PRODUCT_METRICS_BY_TRACK_PATH = _product("/metrics-by-track") PRODUCT_METRICS_BY_TRACK_URL = _base(PRODUCT_METRICS_BY_TRACK_PATH) # https://workstation.theorchard.com/ Top Tracks widget in the bottom right TOP_SOUND_RECORDINGS_PATH = "/top-sound-recordings" TOP_SOUND_RECORDINGS_URL = _base(TOP_SOUND_RECORDINGS_PATH) TOP_SOUND_RECORDING_FAMILIES_IDS_PATH = "/top-sound-recording-families-ids" TOP_SOUND_RECORDING_FAMILIES_IDS_URL = _base(TOP_SOUND_RECORDING_FAMILIES_IDS_PATH) TOP_SOUND_RECORDING_FAMILIES_BY_IDS_PATH = "/top-sound-recording-families-by-ids" TOP_SOUND_RECORDING_FAMILIES_BY_IDS_URL = _base( TOP_SOUND_RECORDING_FAMILIES_BY_IDS_PATH ) TOP_ACCOUNTS_METRICS_PATH = "/top-accounts-metrics" TOP_ACCOUNTS_METRICS_URL = _base(TOP_ACCOUNTS_METRICS_PATH) TOP_METRICS_PATH = "/top-metrics" TOP_METRICS_URL = _base(TOP_METRICS_PATH) MARKET_RANKS_PATH = "/market-ranks" MARKET_RANKS_URL = _base(MARKET_RANKS_PATH) TADAS_TRENDS_PATH = "/tadas-trends" TADAS_TRENDS_URL = _base(TADAS_TRENDS_PATH) TADAS_TREND_GLOBALSOUNDRECORDING_BY_ISRC_PATH = ( "/tadas/trend-globalsoundrecording-by-isrc/" ) TADAS_TREND_GLOBALSOUNDRECORDING_BY_ISRC_URL = _base( TADAS_TREND_GLOBALSOUNDRECORDING_BY_ISRC_PATH ) TADAS_DATA_AVAILABILITY_PATH = "/tadas/data-availability" REP_OWNERS_PATH = "/tadas/rep-owners" REP_OWNERS_URL = _base(REP_OWNERS_PATH) TOP_VIDEO_METRICS_PATH = "/top-video-metrics" TOP_VIDEO_METRICS_URL = _base(TOP_VIDEO_METRICS_PATH) TOP_CHANNEL_METRICS_PATH = "/top-channel-metrics" TOP_CHANNEL_METRICS_URL = _base(TOP_CHANNEL_METRICS_PATH) SOUND_RECORDING_PATH = "/sound-recording/" SOUND_RECORDING_URL = _base(SOUND_RECORDING_PATH) HIGHWATERMARK_PATH = "/highwatermark" HIGHWATERMARK_URL = _base(HIGHWATERMARK_PATH) STORES_PATH = "/stores" STORES_URL = _base(STORES_PATH) STORE_OUTAGES_PATH = "/store-outages" STORE_OUTAGES_URL = _base(STORE_OUTAGES_PATH) def _sound_recording(path): return _concat(SOUND_RECORDING_PATH, path) SOUND_RECORDING_TIMESERIES_PATH = _sound_recording("/timeseries") SOUND_RECORDING_TIMESERIES_URL = _base(SOUND_RECORDING_TIMESERIES_PATH) SOUND_RECORDING_SUMMARY_PATH = _sound_recording("/summary") SOUND_RECORDING_SUMMARY_URL = _base(SOUND_RECORDING_SUMMARY_PATH) SOUND_RECORDING_RELATED_VIDEOS_BY_ISRC_PATH = _sound_recording( "/related-videos-by-isrc" ) SOUND_RECORDING_RELATED_VIDEOS_BY_ISRC_URL = _base( SOUND_RECORDING_RELATED_VIDEOS_BY_ISRC_PATH ) SOUND_RECORDING_METADATA_PATH = _sound_recording("/metadata") SOUND_RECORDING_METADATA_URL = _base(SOUND_RECORDING_METADATA_PATH) STREAMS_BREAKDOWN_PATH = _sound_recording("/streams-breakdown") STREAMS_BREAKDOWN_URL = _base(STREAMS_BREAKDOWN_PATH) STREAMS_BY_STORE_PATH = _sound_recording("/streams-by-store") STREAMS_BY_STORE_URL = _base(STREAMS_BY_STORE_PATH) STREAMS_ALL_PATH = _sound_recording("/streams-all") STREAMS_ALL_URL = _base(STREAMS_ALL_PATH) STREAMS_BULK_PATH = "/sound-recording/streams" AGGREGATE_STREAMS_PATH = "/sound-recording/aggregate-streams" SOUND_RECORDING_AGGREGATED_STREAMS_PATH = _sound_recording("/aggregated-streams") SOUND_RECORDING_AGGREGATED_STREAMS_URL = _base(SOUND_RECORDING_AGGREGATED_STREAMS_PATH) PARTICIPANT_METRICS_PATH = "/participant-metrics" PARTICIPANT_METRICS_URL = _base(PARTICIPANT_METRICS_PATH) PRODUCT_METRICS_PATH = "/product-metrics" PRODUCT_METRICS_URL = _base(PRODUCT_METRICS_PATH) SOUND_RECORDING_TIKTOK_TIMESERIES_PATH = _sound_recording("/tiktok/timeseries") SOUND_RECORDING_TIKTOK_TIMESERIES_URL = _base(SOUND_RECORDING_TIKTOK_TIMESERIES_PATH) SOUND_RECORDING_TIKTOK_SUMMARY_PATH = _sound_recording("/tiktok/summary") SOUND_RECORDING_TIKTOK_SUMMARY_URL = _base(SOUND_RECORDING_TIKTOK_SUMMARY_PATH) SOUND_RECORDING_TIKTOK_AGGREGATED_SUMMARY_PATH = _sound_recording( "/tiktok/aggregated-summary" ) SOUND_RECORDING_TIKTOK_AGGREGATED_SUMMARY_URL = _base( SOUND_RECORDING_TIKTOK_AGGREGATED_SUMMARY_PATH ) SOUND_RECORDING_TIKTOK_AGGREGATED_TIMESERIES_PATH = _sound_recording( "/tiktok/aggregated-timeseries" ) SOUND_RECORDING_TIKTOK_AGGREGATED_TIMESERIES_URL = _base( SOUND_RECORDING_TIKTOK_AGGREGATED_TIMESERIES_PATH ) VIDEOS_PATH = "/videos/" def _videos(path): return _concat(VIDEOS_PATH, path) CHANNELS_PATH = "/channels/" def _channels(path): return _concat(CHANNELS_PATH, path) TOP_MARKETS_PATH = _sound_recording("/top-markets") TOP_MARKETS_URL = _base(TOP_MARKETS_PATH) TOP_COUNTRIES_VIDEOS_PATH = _videos("/top-countries") TOP_COUNTRIES_VIDEOS_URL = _base(TOP_COUNTRIES_VIDEOS_PATH) TOP_COUNTRIES_CHANNELS_PATH = _channels("/top-countries") TOP_COUNTRIES_CHANNELS_URL = _base(TOP_COUNTRIES_CHANNELS_PATH) TOP_VIDEO_TRAFFIC_SOURCES_PATH = _videos("/top-traffic-sources") TOP_VIDEO_TRAFFIC_SOURCES_URL = _base(TOP_VIDEO_TRAFFIC_SOURCES_PATH) TOP_CHANNEL_TRAFFIC_SOURCES_PATH = _channels("/top-traffic-sources") TOP_CHANNEL_TRAFFIC_SOURCES_URL = _base(TOP_CHANNEL_TRAFFIC_SOURCES_PATH) TOP_CHANNEL_VIDEOS_PATH = _channels("/top-videos") TOP_CHANNEL_VIDEOS_URL = _base(TOP_CHANNEL_VIDEOS_PATH) DEMOGRAPHICS_PATH = _sound_recording("/demographics") DEMOGRAPHICS_URL = _base(DEMOGRAPHICS_PATH) DEMOGRAPHICS_2_PATH = "/demographics" DEMOGRAPHICS_2_URL = _base(DEMOGRAPHICS_2_PATH) VIDEO_METRICS_BULK_PATH = "/video-metrics-bulk" ALL_TIME_VIDEO_METRICS_PATH = "/all-time-video-metrics/" CHANNEL_METRICS_PATH = "/channel-metrics/" CHANNEL_METRICS_URL = _base(CHANNEL_METRICS_PATH) ALL_TIME_CHANNEL_METRICS_PATH = "/all-time-channel-metrics/" ALL_TIME_CHANNEL_METRICS_URL = _base(ALL_TIME_CHANNEL_METRICS_PATH) UGC_VIDEO_METRICS_PATH = "/ugc-video-metrics" UGC_VIDEO_METRICS_URL = _base(UGC_VIDEO_METRICS_PATH) SNOWFLAKE_POOL_SIZE = 15 # UWSGI number of threads SNOWFLAKE_POOL_RECYCLE = 4 * 55 * 60 # Snowflake timeout: ~4 hours SNOWFLAKE_POOL_MAX_OVERFLOW = 5 SNOWFLAKE_POOL_PRE_PING = False SNOWFLAKE_POOL_RESET_ON_RETURN = None if ENVIRONMENT == DEV_ENVIRONMENT: try: SNOWFLAKE_PRIVATE_KEY_PATH = os.environ["HOME"] + "/.ssh/snowflake/rsa_key.p8" SNOWFLAKE_KEY_PASSPHRASE = os.environ.get("SNOWFLAKE_KEY_PASSPHRASE", "") with open(SNOWFLAKE_PRIVATE_KEY_PATH, "rb") as key: p_key = serialization.load_pem_private_key( key.read(), password=( SNOWFLAKE_KEY_PASSPHRASE.encode() if SNOWFLAKE_KEY_PASSPHRASE else None ), backend=default_backend(), ) pkb = p_key.private_bytes( encoding=serialization.Encoding.DER, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), ) SNOWFLAKE_CONNECT_ARGS = {"private_key": pkb} except Exception: SNOWFLAKE_CONNECT_ARGS = {} else: # For QA and PROD environments we use Secrets Manager SNOWFLAKE_PRIVATE_KEY = get_secret("SNOWFLAKE_PRIVATE_KEY") or None if ( not SNOWFLAKE_PRIVATE_KEY or SNOWFLAKE_PRIVATE_KEY == "dummy" ): # default value set by Terraform when you create a secret # noqa SNOWFLAKE_CONNECT_ARGS = {} else: SNOWFLAKE_CONNECT_ARGS = { "private_key": base64.b64decode( bytes(SNOWFLAKE_PRIVATE_KEY, encoding="utf-8") ) } logging.info("Snowflake private key is set from Secrets Manager.") STORES = { 1: "Apple Music", 4: "Napster", 11: "TDC Play", 187: "Amazon Music", 213: "Beatport", 286: "Spotify", 348: "Deezer", 424: "Rdio", 440: "Simfy GmbH", 453: "Youtube", 496: "Google Play", 708: "Pandora", 1202: "TikTok", 1405: "VKontakte", 1406: "OK", 1503: "LINE Japan", 1505: "SoundCloud", 1600: "AWA", 1601: "Kuwo", 1602: "Kugou", 1603: "QQ", 9999: "iTunes", 45302: "YouTube Claim", } FEEDS = { 1: { "storename": "Spotify", "storeid": 286, "distributors": ["theorchard", "sme"], "name": "Spotify Streams", "update_threshold_days": 1, }, 3: { "storename": "Pandora", "storeid": 708, "distributors": ["theorchard", "sme"], "name": "Pandora", "update_threshold_days": 2, }, 4: { "storename": "Apple Music", "storeid": 1, "distributors": ["theorchard", "sme"], "name": "Apple Music", "update_threshold_days": 2, }, 5: { "storename": "Apple Music", "storeid": 1, "distributors": ["theorchard", "sme"], "name": "Apple Music Non-Royalty Bearing Streams", "update_threshold_days": 2, }, 6: { "storename": "YouTube", "storeid": 453, "distributors": ["theorchard"], "name": "YouTube Weekly", "update_threshold_days": 3, }, 7: { "storename": "Spotify", "storeid": 286, "distributors": ["theorchard", "sme"], "name": "Spotify Aggregated Streams", "update_threshold_days": 1, }, 9: { "storename": "Deezer", "storeid": 348, "distributors": ["theorchard"], "name": "Deezer", "update_threshold_days": 1, }, 10: { "storename": "Google Play", "storeid": 496, "distributors": ["theorchard"], "name": "Google Play Music", "update_threshold_days": 1, }, 11: { "storename": "Napster", "storeid": 4, "distributors": ["theorchard", "sme"], "name": "Napster (Rhapsody)", "update_threshold_days": 2, }, 12: { "storename": "YouTube Movies", "storeid": 463, "distributors": ["theorchard"], "name": "YouTube VOD Weekly", "update_threshold_days": 3, }, 13: { "storename": "Apple Music", "storeid": 1, "distributors": ["theorchard", "sme"], "name": "iTunes", "update_threshold_days": 2, }, 14: { "storename": "YouTube", "storeid": 453, "distributors": ["theorchard"], "name": "YouTube Claim", "update_threshold_days": 3, }, 15: { "storename": "TDC Play", "storeid": 11, "distributors": ["theorchard"], "name": "24/7 (Juke)", "update_threshold_days": 3, }, # 16 storename Amazon Music Feature flagged 16: { "storename": "Amazon Music", "storeid": 187, "distributors": ["theorchard", "sme"], "name": "Amazon Downloads", "update_threshold_days": 2, }, 17: { "storename": "Amazon Prime Video", "storeid": 376, "distributors": ["theorchard"], "name": "Amazon (Prime) Instant Video", "update_threshold_days": 2, }, 18: { "storename": "Google Play", "storeid": 496, "distributors": ["theorchard"], "name": "Google Play Movies & TV", "update_threshold_days": 3, }, 19: { "storename": "VUDU", "storeid": 489, "distributors": ["theorchard"], "name": "VUDU", "update_threshold_days": 3, }, 20: { "storename": "Microsoft Xbox", "storeid": 487, "distributors": ["theorchard"], "name": "Xbox", "update_threshold_days": 3, }, 21: { "storename": "FandangoNOW", "storeid": 624, "distributors": ["theorchard"], "name": "FandangoNOW", "update_threshold_days": 3, }, 22: { "storename": "PlayStation Video", "storeid": 478, "distributors": ["theorchard"], "name": "PlayStation Video", "update_threshold_days": 3, }, 23: { "storename": "", "storeid": None, "distributors": ["theorchard"], "name": "Shazam", "update_threshold_days": 3, }, 24: { "storename": "", "storeid": None, "distributors": ["theorchard"], "name": "GFK", "update_threshold_days": 3, }, 25: { "storename": "eMusic", "storeid": 7, "distributors": ["theorchard"], "name": "eMusic", "update_threshold_days": 3, }, 26: { "storename": "MixRadio", "storeid": 27, "distributors": ["theorchard"], "name": "MixRadio", "update_threshold_days": 3, }, 27: { "storename": "Verizon", "storeid": 36, "distributors": ["theorchard"], "name": "Verizon", "update_threshold_days": 3, }, 28: { "storename": "MusicLoad", "storeid": 153, "distributors": ["theorchard"], "name": "MusicLoad", "update_threshold_days": 3, }, 30: { "storename": "Myxer", "storeid": 355, "distributors": ["theorchard"], "name": "Myxer", "update_threshold_days": 3, }, 31: { "storename": "Hulu", "storeid": 392, "distributors": ["theorchard"], "name": "Hulu", "update_threshold_days": 3, }, 32: { "storename": "Rdio", "storeid": 424, "distributors": ["theorchard"], "name": "Rdio", "update_threshold_days": 3, }, 33: { "storename": "Simfy GmbH", "storeid": 440, "distributors": ["theorchard"], "name": "Simfy", "update_threshold_days": 3, }, 34: { "storename": "Cricket Communications, Inc.", "storeid": 497, "distributors": ["theorchard"], "name": "Cricket Communications, Inc. (Muve)", "update_threshold_days": 3, }, # 35 - 37 Amazon Music Feature flagged 35: { "storename": "Amazon Music", "storeid": 187, "distributors": ["theorchard", "sme"], "name": "Amazon Prime", "update_threshold_days": 2, }, 36: { "storename": "Amazon Music", "storeid": 187, "distributors": ["theorchard", "sme"], "name": "Amazon AdSupported", "update_threshold_days": 2, }, 37: { "storename": "Amazon Music", "storeid": 187, "distributors": ["theorchard", "sme"], "name": "Amazon Unlimited", "update_threshold_days": 2, }, 38: { "storename": "YouTube", "storeid": 453, "distributors": ["theorchard", "sme"], "name": "YouTube Bulk Reports", "update_threshold_days": 3, }, # 39: {'storename': 'VKontakte', 'storeid': 1405, # 'distributors': ['theorchard'], # 'name': 'VKontakte'}, # 40: {'storename': 'Odnoklassniki', 'storeid': 1406, # 'distributors': ['theorchard'], # 'name': 'Odnoklassniki'}, 42: { "storename": "LINE Japan", "storeid": 1503, "distributors": ["theorchard"], "name": "LINE Japan", "update_threshold_days": 1, }, 43: { "storename": "SoundCloud", "storeid": 1505, "distributors": ["theorchard"], "name": "SoundCloud", "update_threshold_days": 1, }, 44: { "storename": "AWA", "storeid": 1600, "distributors": ["theorchard"], "name": "AWA", "update_threshold_days": 1, }, 45: { "storename": "TikTok", "storeid": 1202, "distributors": ["theorchard"], "name": "TikTok", "update_threshold_days": 2, }, 48: { "storename": "Beatport", "storeid": 213, "distributors": ["theorchard"], "name": "Beatport", "update_threshold_days": 3, }, 49: { "storename": "Kuwo", "storeid": 1601, "distributors": ["theorchard", "sme"], "name": "Kuwo", "update_threshold_days": 2, }, 50: { "storename": "Kugou", "storeid": 1602, "distributors": ["theorchard", "sme"], "name": "Kugou", "update_threshold_days": 2, }, 51: { "storename": "QQ", "storeid": 1603, "distributors": ["theorchard", "sme"], "name": "QQ", "update_threshold_days": 2, }, } FEED_IDS = list(FEEDS.keys()) SLOW_DYNAMO_TIME = float(os.environ.get("SLOW_DYNAMO_TIME", 0.5))