"""iTunes Hides tasks.""" import csv from datetime import datetime from io import BytesIO from io import StringIO import zipfile from boto3.exceptions import S3UploadFailedError from garcon import task from garcon_contrib.aws.utils import garcon_s3 from garcon_contrib.dynamo_feed_status import garcon_feed_status 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.itunes_hides import config from feed_ingestion.tasks import bootstrap from feed_ingestion.tasks import check_ingested_status from feed_ingestion.tasks import check_status from feed_ingestion.util.aws import s3 STOP_RESPONSE = {'stop': True} @task.decorate(timeout=1000) @bootstrap.reset_dynamodb_status_on_reload(config.feed_name) @check_ingested_status(config.feed_name) def bootstrap(activity, date, dw_config=None): """Bootstrap workflow by injecting initial context from config. Args: activity (ActivityWorker): The Garcon activity worker. date (str): Reporting date (YYYY-MM-DD). dw_config (dict): Dictionary of data warehouse config options. Returns: dict: Initial context for the workflow. """ date_obj = (datetime.strptime( date, '%Y-%m-%d') if date else datetime.today()) date = date_obj.strftime('%Y-%m-%d') return { 'feed_name': config.feed_name, 'secrets_path': config.secrets_path, 'date': date, 'file_pattern': config.file_pattern, 'archive_path': config.s3['archive_path'].format(date=date_obj), 'download_path': config.s3['download_path'], 'temp_staging_raw_table': config.snowflake_table_names['temp_staging_raw'].format( date=date_obj), 'staging_raw_table': config.snowflake_table_names['staging_raw'], 'preprocessed_path': config.s3['preprocessed_path'].format( date=date_obj), 'snapshot_table': config.snowflake_table_names['full_snapshot_path'], 'snapshots_amount': config.snapshots_amount_to_keep } @task.decorate(timeout=36000) def load_snapshot_table( activity, date, sfdb_params, feed_name, secrets_path, kwargs, temp_staging_raw_table, snapshot_table, snapshots_amount_to_keep): """Load snapshot table from temp staging raw. Args: activity (ActivityWorker): The Garcon activity worker. date (str): Reporting date (YYYY-MM-DD). sfdb_params (dict): Dict with params to optionally override default ones (Snowflake db and schema name). feed_name (str): Name of the feed to get executor class. secrets_path (str): Secrets manager path of the flow. kwargs (dict): Custom activity params. temp_staging_raw_table (str): Name of the temp staging_raw table. snapshot_table (str): Name of the snapshot table. snapshots_amount_to_keep (int): Number of snapshots being keep. """ sf_config = get_sf_config(secrets_path) sf_config_custom = merge_configs(sf_config, sfdb_params) ExecutorSR = registered_executors.get(feed_name) with ExecutorSR(sf_config_custom) as sf_executor: sf_executor.clean_snapshot_table( snapshot_table, date) activity.logger.info( '{table} was cleaned from ' 'the rows with the date (or date range) ' 'of the current workflow run'.format(table=snapshot_table)) sf_executor.load_snapshot_table( temp_staging_raw_table, snapshot_table) activity.logger.info( '{table} was loaded from ' 'temp staging {temp_stg}'.format( table=snapshot_table, temp_stg=temp_staging_raw_table)) sf_executor.remove_previous_snapshots( snapshot_table, snapshots_amount_to_keep) activity.logger.info( 'snapshots were removed from {table} ' 'except last {snapshots_amount}'.format( table=snapshot_table, snapshots_amount=snapshots_amount_to_keep)) @task.decorate(timeout=36000) @check_status(feed_name=config.feed_name) def process_drop_files( activity, feed_name, date, s3_archive_path, s3_preprocessed_path, source_files_dict): """Process drop files. Args: activity (ActivityWorker): The Garcon activity worker. feed_name (str): Feed name of workflow execution for status updates. date (str): Reporting date (YYYY-MM-DD). s3_archive_path (str): Archive path on S3. s3_preprocessed_path (str): S3 path to preprocessed files. source_files_dict (dict): Dict containing metadata of files. """ hides = [] for _, content in s3.get_source_files_content( s3_archive_path, source_files_dict, encoding=None): with zipfile.ZipFile(BytesIO(content), 'r') as z: for file in z.namelist(): hides.extend(_get_hides(z.read(file).decode('utf-8'), date)) hides_csv = _create_csv(hides, config.fields, '\t') _upload_preprocessed_file( activity, feed_name, date, hides_csv, s3_preprocessed_path) activity.logger.info('Successfully processed drop files for {}'.format( date)) @task.decorate(timeout=600) @check_status(feed_name=config.feed_name) def remove_files_from_path(activity, date, download_path, source_files_dict): """Remove files from drop location. Args: date (str): Reporting date (YYYY-MM-DD). activity (ActivityWorker): The Garcon activity worker. download_path (str): Download path on S3. source_files_dict (dict): Dict containing metadata of files. """ for file in source_files_dict['files']: full_path = f'{download_path}{file["file_name"]}' bucket_name, key_prefix = garcon_s3.extract_bucket_path(full_path) s3.delete_s3_obj(bucket_name, key_prefix) activity.logger.info(f'Successfully remove file {file["file_name"]}') def _upload_preprocessed_file(activity, feed_name, date, csv_obj, path): """Upload preprocessed files on S3. Args: activity (ActivityWorker): The activity worker. feed_name (str): Feed name of workflow execution for status updates. date (str): Reporting date (YYYY-MM-DD). csv_obj (StringIO): CSV object to upload. path (str): Upload path. """ try: s3.upload_processed_to_s3( csv_obj, path, expected_bucket_owner=config.expected_bucket_owner ) except S3UploadFailedError as e: garcon_feed_status.set_overall_status( feed_name, date, garcon_feed_status.STATUS_NOT_AVAILABLE) activity.logger.error( 'Cannot upload notes file to {path}. {exception_body}'.format( path=path, exception_body=e)) raise e def _create_csv(items, fieldnames, delimiter): """Create a CSV object from items list. Args: items (list): list of items. fieldnames (list): CSV fieldnames. delimiter (str): CSV delimiter. Return: StringIO: csv object. """ csv_obj = StringIO() writer = csv.DictWriter( csv_obj, fieldnames=fieldnames, delimiter=delimiter) writer.writeheader() writer.writerows(items) return csv_obj def _get_hides(file, date): """Parse hides file and preprocess hide entries. Args: file (str): Source file. date (str): Ingestion date. Returns: list: List of preprocessed entries. """ def parse_date(date_str): if date_str: return datetime.strptime(date_str, '%d-%b-%Y') else: return date_str new_rows = [] reader = csv.DictReader( file.split('\n')[1:], fieldnames=config.fields, delimiter='\t') for row in reader: row['is_complete'] = config.bool_mapping[row['is_complete']] row['audio_must_be_redelivered_for_itunes_plus'] = config.bool_mapping[ row['audio_must_be_redelivered_for_itunes_plus']] row['import_date'] = parse_date(row['import_date']) row['hide_date'] = parse_date(row['hide_date']) row['ingest_date'] = date if row['hide_date'] and row['import_date']: row['days_until_hidden'] = ( row['hide_date'] - row['import_date']).days else: row['days_until_hidden'] = None row['original_reason'] = row['reason'] row['reason'] = row['reason'].replace( 'Hidden: ', '').strip('()').upper() row['content_owner'] = config.cms_map.get( row['content_owner'], row['content_owner']) row['appleid_reason'] = '_'.join([row['apple_id'], row['reason']]) new_rows.append(row) return new_rows