"""Theatrical ETL Tasks.""" from datetime import datetime from garcon import task from garcon_contrib.dynamo_feed_status import garcon_feed_status from flows import datastore from flows import util from flows.theatrical import config from flows.theatrical import download_to_db_utils as dtd_utils from flows.theatrical import log from flows.theatrical import queries from flows.theatrical import s3 from flows.theatrical import status from flows.theatrical import utils @task.decorate(timeout=100) def bootstrap( activity, correlation_id, date_start, date_end, with_archive, unload_bucket, unload_prefix, archive_bucket, archive_prefix): """Bootstrap task: compose list of s3 keys for the following ingestion. Args: activity (Activity): SWF Activity. correlation_id (str): correlation id. date_start (str): start bound of date range. date_end (str): end excluding bound of date range. with_archive (bool): if True - look up. unload_bucket (str): source bucket name. unload_prefix (str): s3 source folder path. archive_bucket (srt): archive bucket name. archive_prefix (str): s3 archive folder path. Returns: dict: Dict with update to SWF context. """ date_start = datetime.strptime(date_start, '%Y-%m-%d').date() date_end = datetime.strptime(date_end, '%Y-%m-%d').date() source_files = s3.find_files( unload_bucket, unload_prefix, date_start, date_end) for file in source_files: file['new'] = True if with_archive: archive_files = s3.find_files( archive_bucket, archive_prefix, date_start, date_end) # Add only unique (by filename) paths from archive folder: source_file_names = [p.get('filename') for p in source_files] for archive_file in archive_files: archive_filename = archive_file.get('filename') if archive_filename not in source_file_names: source_files.append(archive_file) if not source_files: log.update_status(correlation_id, status.TERMINATED_NO_FILES) return {'stop': True} log.update_status(correlation_id, status.BOOTSTRAPPED) return {'source_files': source_files} @task.decorate(timeout=200) def clean_dynamo_status(activity, correlation_id, source_files): """Clean DynamoDB statuses for files. Args: activity (Activity): SWF Activity. correlation_id (str): correlation id. source_files (list(dict)): list of file descriptions. """ for file in source_files: garcon_feed_status.delete_status( config.SWF_WORKFLOW_NAME, file['batch_date']) log.update_status(correlation_id, status.DYNAMO_STATUS_CLEANED) @task.decorate(timeout=500) def download_to_db(activity, correlation_id, source_files): """Download to db task. Args: activity (Activity): SWF Activity. correlation_id (str): correlation id. source_files (list(dict)): list of file descriptions. """ with datastore.context() as (cursor, connection): cursor.execute('START TRANSACTION;') for source_file in source_files: date_start = utils.deserialize_date(source_file['date_start']) date_end = utils.deserialize_date(source_file['date_end']) header, rows = s3.download_csv( source_file['bucket_name'], source_file['s3_path']) new_header, new_rows, new_upcs = dtd_utils.filter_and_transform( header, rows, date_start, date_end) try: delete_sqls = queries.clear_existing_data( config.THEATRICAL_REVENUE_RAW, new_upcs) for delete_sql in delete_sqls: cursor.execute( delete_sql, {'date_start': date_start, 'date_end': date_end}) insert_raw_sql = queries.insert_raw_data(new_header) cursor.executemany(insert_raw_sql, new_rows) except Exception as ex: log.update_status( correlation_id, status.TERMINATED_BAD_SOURCE_FILES) return { 'stop': True, 'error_file': source_file, 'correlation_id': correlation_id, 'error_details': ex} log.update_status(correlation_id, status.UNLOADED) @task.decorate(timeout=100) def create_theatrical_revenue_temp_table(activity, correlation_id): """Create temp revenue table. Args: activity (Activity): SWF Activity. correlation_id (str): correlation id. Returns: dict: Dict with temp table name for SWF context. """ table_name = '{table_name}_{correlation_id}'.format( table_name=config.THEATRICAL_REVENUE, correlation_id=util.correlation_id_hex(correlation_id)) create_table_sql = queries.CREATE_TEMP_TABLE.format( temp_table=table_name, revenue_table=config.THEATRICAL_REVENUE) datastore.execute(create_table_sql) log.update_status(correlation_id, status.TEMP_TABLE_CREATED) return {'temp_table_name': table_name} @task.decorate(timeout=100) def insert_to_theatrical_revenue_temp_table( activity, correlation_id, temp_table_name, source_files, upcs): """Create temp revenue table. Args: activity (Activity): SWF Activity. correlation_id (str): correlation id. temp_table_name (str): name for temp table created for particular flow execution. source_files (list(dict)): list of file descriptions. upcs (list): list of UPCs to ingest. """ insert_sqls = list(queries.insert_to_temp_from_raw(temp_table_name, upcs)) with datastore.context() as (cursor, connection): cursor.execute('START TRANSACTION;') for source_file in source_files: date_start = utils.deserialize_date(source_file['date_start']) date_end = utils.deserialize_date(source_file['date_end']) for insert_sql in insert_sqls: cursor.execute( insert_sql, {'date_start': date_start, 'date_end': date_end}) log.update_status(correlation_id, status.TEMP_TABLE_INSERTED) @task.decorate(timeout=200) def insert_new_data( activity, correlation_id, temp_table_name, source_files, upcs): """Copy ingested data from temp to prod table. Args: activity (Activity): SWF Activity. correlation_id (str): correlation id. temp_table_name (str): name of temp table. source_files (list(dict)): list of file descriptors. upcs (list): list of UPCs to ingest. """ delete_sqls = list( queries.clear_existing_data(config.THEATRICAL_REVENUE, upcs)) with datastore.context() as (cursor, connection): cursor.execute('START TRANSACTION;') for source_file in source_files: date_start = utils.deserialize_date(source_file['date_start']) date_end = utils.deserialize_date(source_file['date_end']) for delete_sql in delete_sqls: cursor.execute( delete_sql, {'date_start': date_start, 'date_end': date_end}) copy_sql = queries.COPY_TO_PROD.format( revenue_table=config.THEATRICAL_REVENUE, temp_table=temp_table_name) drop_sql = queries.DROP_TABLE.format(table_name=temp_table_name) cursor.execute(copy_sql) cursor.execute(drop_sql) log.update_status(correlation_id, status.TEMP_TABLE_MADE_LIVE) @task.decorate(timeout=200) def move_source_files_to_archive(activity, correlation_id, source_files): """Move files to the appropriate locations. Args: activity (Activity): SWF Activity. correlation_id (str): correlation id. source_files (list(dict)): list of file descriptors. """ files_for_move = [] for file in source_files: if file.get('new', False): files_for_move.append( {'old_bucket': file['bucket_name'], 'old_key_path': file['s3_path'], 'new_bucket': config.ARCHIVE_BUCKET, 'new_dir': config.ARCHIVE_PREFIX, 'file_name': file['filename']}) if files_for_move: s3.move_files(files_for_move) log.update_status(correlation_id, status.SOURCE_FILES_ARCHIVED) @task.decorate(timeout=200) def send_notification(activity, correlation_id, upcs): """Send notice that this ETL has finished. Args: activity (Activity): SWF Activity. correlation_id (str): correlation id. upcs (str): context key used in DatabaseParam object. """ utils.send_success_notification(correlation_id, upcs) @task.decorate(timeout=200) def set_dynamo_status(activity, correlation_id, source_files): """Set DynamoDB statuses for files. Args: activity (Activity): SWF Activity. correlation_id (str): correlation id. source_files (list(dict)): list of file descriptions. """ for file in source_files: garcon_feed_status.set_overall_status( config.SWF_WORKFLOW_NAME, file['batch_date'], garcon_feed_status.STATUS_INGESTED) log.update_status(correlation_id, status.DYNAMO_STATUS_UPDATED) @task.decorate(timeout=200) def set_final_status(activity, correlation_id): """Set status INGESTED to the db log. Args: activity (Activity): SWF Activity. correlation_id (str): correlation id. """ log.update_status(correlation_id, status.INGESTED)