""" Cable Calculation ETL Tasks. All SWF tasks here to transform raw data from csv's provided by rentrak to final data in cable_revenue table. """ from datetime import datetime from datetime import timedelta from garcon import task from garcon_contrib.dynamo_feed_status import garcon_feed_status from flows import datastore from flows import log as etl_logger from flows import util from flows.cable_calculation import log from flows.cable_calculation import queries from flows.cable_calculation import status from flows.cable_calculation import util as cable_util @task.decorate(timeout=120) def create_temp_table(activity, correlation_id, create, temp_table_name): """Create the temporary table in the datastore. Args: activity (ActivityWorker): activity worker. correlation_id (str): etl correlation ID. create (str): create temp table query. temp_table_name (str): temp table name format. Returns: dict: temp table name. """ table_name = temp_table_name.format( correlation_hex=util.correlation_id_hex(correlation_id)) sql = create.format(table_name=table_name) datastore.execute(sql) log.update_status(correlation_id, status.TEMP_TABLE_CREATED) return {'table_name': table_name} @task.decorate(timeout=3600) def unload_calculate_load_temp_table( activity, correlation_id, upcs, date_start, date_end, select_est_dates, select_dbo, select, insert, temp_table_name): """Unload, calculate and load back. Unload data from raw table, calculate split, and load data to temp table. Those steps are all grouped together in a single task and activity because of I/O constraint with Garcon. The calc has to be done in Python because of its complexity (see split rules). Args: activity (ActivityWorker): activity worker. correlation_id (str): etl correlation ID. upcs (list): list of upcs. date_start (str): YYYY-MM-DD inclusive date start range. date_end (str): YYYY-MM-DD exclusive date end range. select_est_dates (str): query to select est dates. select_dbo (str): query to select dbo amounts. select (str): query to select from raw cable revenue table. insert (str): query to insert to temp cable revenue table. temp_table_name (str): temp table name. Returns: dict: stop flag if there are no calculated rows. """ # pull transactions est dates est_dates_batches = cable_util.load_est_date_for_releases( upcs, select_est_dates) est_dates = {} for est_dates_batch in est_dates_batches: est_dates.update(est_dates_batch) # pull dbo revenue dbo_batches = cable_util.load_dbo_from_theatrical_revenue(upcs, select_dbo) dbo = {} for dbo_batch in dbo_batches: dbo.update(dbo_batch) # pull transactions from raw table raw_transactions_batches = cable_util.unload_from_raw_table( upcs, date_start, date_end, select) raw_transactions = [] for raw_transactions_batch in raw_transactions_batches: raw_transactions.extend(raw_transactions_batch) log.update_status(correlation_id, status.RAW_UNLOADED) if not raw_transactions: log.update_status(correlation_id, status.CALCULATION_NOT_NEEDED) return {'stop': True} # Calculate split for each transaction calculated_transactions = cable_util.calculate_split( est_dates, dbo, raw_transactions) log.update_status(correlation_id, status.CALCULATION_COMPLETED) # load into temp table cable_util.load_temp_table( insert, temp_table_name, calculated_transactions) log.update_status(correlation_id, status.TEMP_TABLE_INSERTED) @task.decorate(timeout=3600) def move_temp_table_to_cable_revenue( activity, correlation_id, date_end, date_start, temp_table_name, upcs): """Insert all rows from the temp table to digital revenue. Args: activity (ActivityWorker): activity worker. correlation_id (str): etl correlation ID. date_end (str): YYYY-MM-DD exclusive date end range for unload select. date_start (str): YYYY-MM-DD inclusive date start range for unload select. temp_table_name (str): temp table name. upcs (list): UPCs of existing data to delete. """ drop = queries.DROP_TEMP_TABLE.format(table_name=temp_table_name) insert = queries.INSERT_FROM_TEMP_TABLE.format(table_name=temp_table_name) bad_rows = queries.SELECT_BAD_ROWS_FROM_TEMP.format( table_name=temp_table_name) delete_sqls = queries.get_delete_from_cable_revenue_sql(upcs) with datastore.context() as (cursor, connection): # log bad data that wont make it to cable_revenue cursor.execute(bad_rows) num_rows = cursor.fetchone()[0] etl_logger.get_logger().error( 'Skipped #{num_rows} invalid rows for correlation={correlation_id}' .format(num_rows=num_rows, correlation_id=correlation_id)) # clear old data - add new data - delete temp table - update status cursor.execute('START TRANSACTION') for delete_sql in delete_sqls: cursor.execute( delete_sql, {'date_end': date_end, 'date_start': date_start}) cursor.execute(insert) cursor.execute(drop) log.update_status(correlation_id, status.TEMP_TABLE_MADE_LIVE) @task.decorate(timeout=120) def send_sns(activity, correlation_id, date_end, date_start, upcs): """Send all listeners a message that the ETL has completed. Args: activity (ActivityWorker): activity worker. correlation_id (str): etl correlation ID. date_end (str): YYYY-MM-DD exclusive date end range for unload select. date_start (str): YYYY-MM-DD inclusive date start range for unload select. upcs (str): DatabaseParam style context key for UPCs. """ sns_correlation_id = correlation_id + '.1' cable_util.send_success_notification( sns_correlation_id, date_end, date_start, upcs) @task.decorate(timeout=120) def queue_build_cache( activity, correlation_id, date_end, date_start, upcs): """Enqueue a cache cleaning job message. Args: activity (ActivityWorker): activity worker. correlation_id (str): etl correlation ID. date_end (str): YYYY-MM-DD exclusive date end range for unload select. date_start (str): YYYY-MM-DD inclusive date start range for unload select. upcs (str): DatabaseParam style context key for UPCs. """ cable_util.queue_build_cache(correlation_id, date_end, date_start, upcs) @task.decorate(timeout=120) def set_final_status(activity, correlation_id): """Set status COMPLETED to the db log. Args: activity (ActivityWorker): activity worker. correlation_id (str): etl correlation ID. """ log.update_status(correlation_id, status.COMPLETED, True) @task.decorate(timeout=120) def update_dashboard_status(activity, wflow_name, end_date, status): """Update the Orchard global level overall status for the dashboard. Since end_date is default tomorrow's date (range exclusive), one day is subtracted from it to make better sense for the dashboard. Args: activity (ActivityWorker): activity worker. wflow_name (str): Etl wflow name. end_date (str): Date when the etl is executed. status (str): status attribute to update to. """ time_format = '%Y-%m-%d' status_date = datetime.strptime(end_date, time_format) - timedelta(days=1) status_date = status_date.strftime(time_format) garcon_feed_status.set_overall_status(wflow_name, status_date, status)