"""AWA Data Ingestion Workflow.""" import csv from datetime import datetime import logging import os import tempfile import boto3 from botocore.exceptions import ClientError from garcon import task from garcon_contrib.dynamo_feed_status import garcon_feed_status from snowflake.connector.errors import ProgrammingError from snowflake_connector.etl_connector import SQLLoader from feed_ingestion.flows import registered_executors from feed_ingestion.flows.awa import config from feed_ingestion.flows.helpers import get_sf_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, reload=None): """Bootstrap workflow by getting the correct configurations. Args: activity (ActivityWorker): The activity worker. date (str): Reporting date (YYYY-MM-DD). Returns: dict: Context. """ date_obj = (datetime.strptime( date, '%Y-%m-%d') if date else datetime.today()) date = date_obj.strftime('%Y-%m-%d') assert licensor in config.licensors, f'Invalid licensor: {licensor}' licensor_config = config.licensors[licensor] feed_name = '_'.join([config.feed_name, licensor]) if licensor == 'smej': licensor_bootstrap = dict( stop_after_staging_raw=True, ) expected_completion_status = ( garcon_feed_status.STATUS_POPULATED_RAW_TABLE) elif licensor == 'theorchard': licensor_bootstrap = dict( stop_after_staging_raw=False, staging_raw_table=( licensor_config['reports']['play_summary'] ['staging_raw_table']), fact_table_report='play_summary' ) expected_completion_status = garcon_feed_status.STATUS_INGESTED else: raise ValueError(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 == expected_completion_status: activity.logger.info(f'Already ingested {feed_name} for {date}') return {'stop': True, 'reason': 'Already ingested'} activity.logger.info('Bootstrap flow: {}'.format(date_obj)) drop_path = licensor_config['s3_drop_path'].format(date=date_obj) s3_archive_path = licensor_config['s3_archive_path'].format( date=date_obj) source_file_name = licensor_config['source_file_name'].format( date=date_obj) source_bucket_name = licensor_config['source_bucket_name'] s3_clean_path = f'{s3_archive_path}clean/' return dict( feed_name=feed_name, date=date_obj.strftime('%Y-%m-%d'), drop_path=drop_path, licensor=licensor, archive_path=s3_archive_path, clean_path=s3_clean_path, source_file_name=source_file_name, source_bucket_name=source_bucket_name, secrets_path=config.secrets_path, **licensor_bootstrap ) @task.decorate(timeout=3600) @check_status() def grab_drop_files( activity, feed_name, date, source_bucket_name, source_file_name, 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_bucket_name (str): The name of src s3 bucket. source_file_name (str): The name of the raw file. drop_path (str): s3 path to raw files. archive_path (str): s3 path to drop raw files. """ source_key_name = '{}{}'.format(drop_path, source_file_name) archive_key_name = '{}{}'.format(archive_path, source_file_name) result = s3_tasks.copy_file( activity=activity, source_bucket_name=source_bucket_name, 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_file_name): 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_file_name]) return {'stop': True} garcon_feed_status.set_overall_status( feed_name, date, garcon_feed_status.STATUS_DOWNLOADED) return result def convert_reporting_txt_to_csv( source_path, target_path, filter_by_record_type): """Convert a reporting file from txt to csv. Args: source_path (str): The path to the source file. target_path (str): The path to the target file. filter_by_record_type (str): The record type to filter by. """ with open(target_path, 'w') as target, \ open(source_path, 'r') as source: writer = csv.writer(target, delimiter='\t', lineterminator='\n', escapechar='\\', quoting=csv.QUOTE_NONE ) for line in source: values = line.strip().split('#*#') if not values: continue if not values[0] == filter_by_record_type: continue writer.writerow(values) @task.decorate(timeout=1200) @check_status() def grab_drop_files_smej( activity, feed_name, date, source_file_name, source_bucket_name, drop_path, archive_path, clean_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_bucket_name (str): The name of s3 bucket. source_file_name (str): The name of the raw file. drop_path (str): s3 path to raw files. archive_path (str): s3 path to drop raw files. clean_path (str): s3 path for preprocessed files. """ source_key_name = '{}{}'.format(drop_path, source_file_name) archive_key_name = '{}{}'.format(archive_path, source_file_name) with tempfile.TemporaryDirectory() as tmp_dir: downloaded_file_path = os.path.join(tmp_dir, source_file_name) s3_client = boto3.client('s3') activity.logger.info(f'downloading={source_key_name}') try: s3_client.download_file( source_bucket_name, source_key_name, downloaded_file_path ) except ClientError as e: 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_file_name]) if e.response['Error']['Code'] == '404': logging.error(f'File not found: {source_key_name}') return {'stop': True} else: raise s3_client.upload_file( downloaded_file_path, config.data_bucket, archive_key_name ) activity.logger.info('download completed') for report_config in config.licensors['smej']['reports'].values(): report_file_name = report_config['target_filename'] record_type = report_config['record_type'] report_file_path = os.path.join(tmp_dir, report_file_name) convert_reporting_txt_to_csv( source_path=downloaded_file_path, target_path=report_file_path, filter_by_record_type=record_type, ) clean_key = '{}{}'.format(clean_path, report_file_name) activity.logger.info(f'Uploading to {clean_key}') s3_client.upload_file( report_file_path, config.data_bucket, clean_key ) garcon_feed_status.set_overall_status( feed_name, date, garcon_feed_status.STATUS_DOWNLOADED) return {} @task.decorate(timeout=3600) def load_temp_staging_raw_table( activity, feed_name, date, s3_dir_path, source_files, temp_staging_raw_table): """Load data into temp staging raw table. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. date (str): Reporting date (YYYY-MM-DD). s3_dir_path (str): Path of files in S3. source_files (list): List of source files. temp_staging_raw_table (str): The name of the temp table. """ if task_status.is_completed_task( feed_name, date, 'load_temp_staging_raw_table'): return Executor = registered_executors.get(feed_name) with Executor(get_sf_config(config.secrets_path)) as sf_executor: try: sf_executor.load_temp_staging_raw_table( temp_staging_raw_table, None, key_dir=s3_dir_path, files=source_files, ) except ProgrammingError: status = garcon_feed_status.STATUS_NOT_INGESTED garcon_feed_status.set_overall_status(feed_name, date, status) raise activity.logger.info( 'Loaded temp staging raw table for: {}'.format( temp_staging_raw_table)) @task.decorate(timeout=3600) def create_temp_staging_fact_table(activity, feed_name, date, report): """Create temp staging raw table. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. date (str): Reporting date (YYYY-MM-DD). """ if task_status.is_completed_task( feed_name, date, 'load_fact_data'): return Executor = registered_executors.get(feed_name) with Executor(get_sf_config(config.secrets_path)) as sf_executor: sf_executor.create_temp_staging_fact_table( report, date ) activity.logger.info('Created temp staging fact unpivot table for AWA') @task.decorate(timeout=3600) def load_temp_staging_fact_table(activity, feed_name, date, report): """Create temp staging raw table. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. date (str): Reporting date (YYYY-MM-DD). """ if task_status.is_completed_task( feed_name, date, 'load_fact_data'): return Executor = registered_executors.get(feed_name) with Executor(get_sf_config(config.secrets_path)) as sf_executor: sf_executor.load_temp_staging_fact_table( date, report) activity.logger.info('Loaded temp staging fact unpivot table for AWA')