"""SoundCloud Data Ingestion Workflow.""" from datetime import datetime import boto3 from garcon import task from garcon_contrib.dynamo_feed_status import garcon_feed_status from snowflake_connector.etl_connector import SQLLoader from feed_ingestion.conf.config import merge_configs from feed_ingestion.flows import registered_executors from feed_ingestion.flows.helpers import get_sf_config from feed_ingestion.flows.soundcloud import config from feed_ingestion.tasks import check_status from feed_ingestion.tasks import s3_tasks from feed_ingestion.util import task_status # Load SQL templates sql_loader = SQLLoader(__file__) @task.decorate(timeout=1000) def bootstrap(activity, date, licensor: str, dw_config=None, reload=False): """Bootstrap workflow by getting the correct configurations. Args: activity (ActivityWorker): The activity worker. date (str): Reporting date (YYYY-MM-DD). licensor (str): The licensor to ingest. Returns: dict: Context. """ date_obj = (datetime.strptime( date, '%Y-%m-%d') if date else datetime.today()) activity.logger.info('Bootstrap flow: {}'.format(date_obj)) activity.logger.info('licensor: {}'.format(licensor)) activity.logger.info('dw_config: {}'.format(dw_config)) activity.logger.info('reload: {}'.format(reload)) # ensure is not None if licensor is None: raise Exception('Licensor is required') # ensure valid lisensor assert licensor in config.licensors.keys(), f'Invalid licensor: {licensor}' # feedname feed_name = '_'.join([config.feed_name, licensor]) if reload == 'True': activity.logger.info('Delete status for feed: {} {} '.format( feed_name, date)) garcon_feed_status.delete_status(feed_name, date) else: overall_status = garcon_feed_status.get_overall_status( feed_name, date) if overall_status == garcon_feed_status.STATUS_INGESTED: message = 'Already ingested for {}'.format(date) activity.logger.info(message) return { 'message': message, 'stop': True } # boostrapping feed activity.logger.info( 'Bootstrapping {feed_name}...'.format(feed_name=feed_name)) # s3 paths drop_path = config.s3['drop'][licensor].format(date=date_obj) archive_path = config.s3['archive'][licensor].format(date=date_obj) source_files_dict = { report: config.filename[licensor].format(date=date_obj, type=report) for report in config.reports} activity.logger.info( 'Bootstrapping {feed_name}...'.format(feed_name=feed_name)) s3_archive_path = f's3://{config.data_bucket}/{archive_path}' return dict( feed_name=feed_name, date=date_obj.strftime('%Y-%m-%d'), licensor=licensor, drop_path=drop_path, archive_path=archive_path, source_files_dict=source_files_dict, s3_archive_path=s3_archive_path, reports=config.reports, secrets_path=config.secrets_path ) @task.decorate(timeout=3600) @check_status() def grab_drop_files( activity, feed_name, date, source_files_dict, drop_path, archive_path): """Copy a feed files from the drop location into the archive folder. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. date (str): Reporting date (YYYY-MM-DD). source_files_dict (dict): Dict with source files names. drop_path (str): s3 path to raw files. archive_path (str): s3 path to drop raw files. licensor (str): The licensor to ingest. """ activity.logger.info('Grab Drop files: {}'.format(drop_path)) for report in source_files_dict: source_key_name = '{}{}'.format( drop_path, source_files_dict[report]) archive_key_name = '{}{}'.format( archive_path, source_files_dict[report]) result = s3_tasks.copy_file( activity=activity, source_bucket_name=config.drop_bucket, source_key_name=source_key_name, destination_bucket_name=config.data_bucket, destination_key_name=archive_key_name, replace=True) if not result.get(source_files_dict[report]): garcon_feed_status.set_overall_status( feed_name, date, garcon_feed_status.STATUS_NOT_AVAILABLE) garcon_feed_status.set_missing_files( feed_name, date, [source_files_dict[report]]) return {'stop': True} garcon_feed_status.set_overall_status( feed_name, date, garcon_feed_status.STATUS_DOWNLOADED) @task.decorate(timeout=3000) @check_status() def grab_drop_files_sme( activity, feed_name, date, source_files_dict, drop_path, archive_path): """Copy drop file from sme S3 bucket to theorchard s3.""" activity.logger.info('Grab Drop files SME: {}'.format(drop_path)) for report in source_files_dict: source_key_name = '{}{}'.format( drop_path, source_files_dict[report]) archive_key_name = '{}{}'.format( archive_path, source_files_dict[report]) result = s3_tasks.copy_file_from_sme_s3_to_theocrhard( activity, config.secrets_path, source_bucket_name=config.sme_drop_bucket, source_key_name=source_key_name, destination_bucket_name=config.data_bucket, destination_key_name=archive_key_name, replace=True) if not result.get(source_files_dict[report]): garcon_feed_status.set_overall_status( feed_name, date, garcon_feed_status.STATUS_NOT_AVAILABLE) garcon_feed_status.set_missing_files( feed_name, date, [source_files_dict[report]]) return {'stop': True} garcon_feed_status.set_overall_status( feed_name, date, garcon_feed_status.STATUS_DOWNLOADED) @task.decorate(timeout=14000) def load_aggregated_skips_and_saves(activity, date, feed_name, sfdb_params, secrets_path): """Load skips and saves into aggregated_skips_and_saves table. Args: activity (ActivityWorker): The activity worker. date (str): Date of the data being process (YYYY-MM-DD). feed_name (str): Name of the feed to get executor class. sfdb_params (dict): Dict with params to optionally override default ones (Snowflake db and schema name). """ task_id = 'load_aggregated_skips_and_saves' if task_status.is_completed_task(feed_name, date, task_id): activity.logger.info( 'Task {task_id} of {feed_name} for {date} already complete, and ' '"reload" flag was not passed, skipping...'.format( task_id=task_id, feed_name=feed_name, date=date)) return sf_config = get_sf_config(secrets_path) sf_config_custom = merge_configs(sf_config, sfdb_params) ExecutorFA = registered_executors.get(feed_name) with ExecutorFA(sf_config_custom) as sf_executor: sf_executor.delete_from_aggregated_skips_and_saves(date) activity.logger.info( 'Data was deleted from aggregated_skips_and_saves table') sf_executor.load_into_aggregated_skips_and_saves(date) activity.logger.info( 'Table aggregated_skips_and_saves was loaded ') task_status.mark_completed_task(feed_name, date, task_id) activity.logger.info( 'Task {task_id} of {feed_name} for {date} completed'.format( task_id=task_id, feed_name=feed_name, date=date)) @task.decorate(timeout=60) def sns_publish_message(activity, feed_name, date, topic, message, subject): """Send SNS messages to specific topic w/subject & message. Assumes AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY are set as env vars Args: activity (ActivityWorker): The swf activity worker. feed_name (str): Feed name of workflow execution for status updates. date (str): Reporting date (YYYY-MM-DD). topic (str): Topic ARN (ex.arn:aws:sns:us-east-1:103233932089:dev_test) message (str): The message you want to send to the topic. Messages must be UTF-8 encoded strings and be at most 4KB in size. subject (str): Optional parameter to be used as the "Subject" line of the email notifications. """ activity.logger.info('Sending SNS message to topic: {}'.format(topic)) activity.logger.info('Feed Name: {}'.format(feed_name)) activity.logger.info('Message: {}'.format(message)) activity.logger.info('Subject: {}'.format(subject)) task_id = 'sns_publish_message' if task_status.is_completed_task(feed_name, date, task_id): activity.logger.info( 'Task {task_id} of {feed_name} for {date} already complete, and ' '"reload" flag was not passed, skipping...'.format( task_id=task_id, feed_name=feed_name, date=date)) return if subject: client = boto3.client('sns', region_name='us-east-1') client.publish(TopicArn=topic, Message=message, Subject=subject) activity.logger.info('SNS report about update_dim_tables sent') task_status.mark_completed_task(feed_name, date, task_id) activity.logger.info( 'Task {task_id} of {feed_name} for {date} completed'.format( task_id=task_id, feed_name=feed_name, date=date))