"""Tasks of the Chartmetric Socials Backfill Workflow.""" from datetime import datetime from datetime import timedelta from garcon import task from feed_ingestion.flows.chartmetric_socials_backfill import config from feed_ingestion.flows.chartmetric_socials_backfill.snowflake_executor \ import SnowflakeExecutor from feed_ingestion.flows.helpers import get_neo4j_config from feed_ingestion.flows.helpers import get_sf_config from feed_ingestion.util.neo4j.neo4j_executor import \ Neo4jExecutor @task.decorate(timeout=600) def bootstrap(activity, date, date_from, platform_names=None, reload=False): """Bootstrap workflow by getting the correct configurations. Args: activity (ActivityWorker): The activity worker. date (str): The date to ingest from (YYYY-MM-DD). date_from (str): The date to ingest starting from (YYYY-MM-DD). platform_names (str): A comma-separated list of platforms (e.g., "facebook,twitter"). reload (str): A flag which indicates if we need to ignore log table. Returns: dict: Context. """ # date is the date passed in or yesterday's date if date: date_obj = datetime.strptime(date, '%Y-%m-%d') else: date_obj = datetime.utcnow().date() - timedelta(days=1) if date_from: date_from_obj = datetime.strptime(date_from, '%Y-%m-%d') else: date_from_obj = date_obj - timedelta(days=1) activity.logger.info('Bootstrap flow backfill for {}'.format( date_obj)) return dict( feed_name=config.feed_name, date=date_obj.strftime('%Y-%m-%d'), date_from=date_from_obj.strftime('%Y-%m-%d'), platform_names=platform_names.split( ',') if platform_names else config.all_platform_names, reload=reload == 'True', ingestion_started_at=datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') ) @task.decorate(timeout=7200 * 5) def delete_removed_accounts(activity, feed_name, platform_name): """Delete accounts from Neo4j and SF that were deleted in Chartmetric. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. platform_name (str): The name of the platform (e.g. facebook). Returns: dict: Context. """ activity.logger.info('Deleting removed accounts for {}'.format( platform_name)) neo4j_config = get_neo4j_config(feed_name) sf_config = get_sf_config(config.secrets_path) with Neo4jExecutor(feed_name, neo4j_config) as neo4j_executor, \ SnowflakeExecutor(sf_config) as sf_executor: sf_params = { 'platform_ID': config.platforms_mapping.get(platform_name), 'platform_name': platform_name, } result = sf_executor.fetchmany_dict_query( query_name='get_deleted_accounts', size=config.row_limit_per_batch, **sf_params) for rows in result: deleted_accounts = [i['ACCOUNT_URL'] for i in rows] activity.logger.info( f'Going to delete {len(deleted_accounts)} ' f'social accounts for {platform_name}') neo4j_query_name = 'delete_incorrect_accounts' activity.logger.info( f'Neo4j Starting {neo4j_query_name} ' f'with {len(deleted_accounts)} rows to execute...') neo4j_params = { 'deleted_accounts': list(filter(lambda i: i != 'null_url', deleted_accounts)), 'platform_name': platform_name, } neo4j_executor.execute_write_query( neo4j_query_name, neo4j_params) activity.logger.info(f'Neo4j completed {neo4j_query_name}') sf_params = { 'deleted_accounts': deleted_accounts, 'platform_name': platform_name, } activity.logger.info('Deleting from snowflake... ') sf_executor.execute_query( 'delete_incorrect_accounts_fact_socials', **sf_params) activity.logger.info('Deleting from snowflake... Done') activity.logger.info('Deleting removed accounts for {} - DONE'.format( platform_name)) return {} @task.decorate(timeout=600 * 2) def get_modified_accounts(activity, feed_name, date, date_from, reload): """Ingest raw social data from the Snowflake table into Neo4j. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. date (str): The date from which to ingest data (YYYY-MM-DD). date_from (str): The date from which to start ingest data (YYYY-MM-DD). reload (bool): A flag which indicates if we need to ignore log table. Returns: dict: Dict with target_ids table name """ table_name = 'chartmetric_socials_backfill_target_ids_{}'.format( date.replace('-', '_')) params = { 'date': date, 'date_from': date_from, 'table_name': table_name, 'reload': reload, 'cm_url_backfill_table': config.cm_url_backfill_table} sf_config = get_sf_config(config.secrets_path) with SnowflakeExecutor(sf_config) as sf_executor: sf_executor.fetchall_query( 'get_modified_social_accounts', **params) return dict(target_ids_table=table_name) @task.decorate(timeout=600) def create_table(activity, feed_name): """Create the table that will hold the aggregated social data to ingest. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. Returns: dict: Context. """ activity.logger.info('Starting create_table') with SnowflakeExecutor(get_sf_config(config.secrets_path)) as sf_executor: query_name = 'create_snowflake_table' params = { 'table_name': 'aggregate_socials_backfill', } sf_executor.execute_query(query_name, **params) activity.logger.info('Finished create_table') @task.decorate(timeout=3600) def populate_table(activity, feed_name, target_ids_table, platform_name): """Populate the aggregation table. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. target_ids_table (str): Name of table with target_ids. platform_name (str): The name of the platform (e.g. facebook). Returns: dict: Context. """ activity.logger.info('Starting populate_table for {}' .format(platform_name)) with SnowflakeExecutor(get_sf_config(config.secrets_path)) as sf_executor: query_name = 'populate_{}_socials'.format(platform_name) params = { 'table_name': 'aggregate_socials_backfill', 'target_ids_table': target_ids_table } sf_executor.execute_query(query_name, **params) activity.logger.info('Finished populate_table for {}' .format(platform_name)) @task.decorate(timeout=3600 * 5) def ingest_aggregate_social_data(activity, feed_name, query_name, date): """Ingest aggregated data from snowflake into Neo4j. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. query_name (str): The name of the cypher file date (str): The date from which to ingest data (YYYY-MM-DD) Returns: dict: Context. """ activity.logger.info('Starting aggregation ingest for {}' .format(query_name)) sf_config = get_sf_config(config.secrets_path) limit = config.row_limit_per_batch offset = 0 with Neo4jExecutor(feed_name, get_neo4j_config(feed_name)) \ as neo4j_executor: while True: with SnowflakeExecutor(sf_config) as sf_executor: sf_params = { 'table_name': 'aggregate_socials_backfill', 'ingest_date': '2020-01-01', 'ingest_date_limit': date, 'limit': limit, 'offset': offset, } sf_query_name = f'get_for_{query_name}' activity.logger.info( f'Query {sf_query_name} ' f'with limit {limit} offset {offset}') rows = sf_executor.fetchall_dict_query( sf_query_name, **sf_params) offset += limit activity.logger.info( f'Query {sf_query_name} ' f'returned {len(rows)} rows') if not rows: break neo4j_params = { 'rows': rows } activity.logger.info(f'Neo4j Query {query_name}') neo4j_executor.execute_write_query(query_name, neo4j_params) activity.logger.info(f'Neo4j Query {query_name} - DONE') activity.logger.info('Finished aggregation ingest for {}' .format(query_name)) @task.decorate(timeout=2400) def update_fact_socials( activity, feed_name, date, target_ids_table, platform_name): """Remove social accounts with incorrect url and insert the new ones. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. date (str): The date from which to ingest data (YYYY-MM-DD). target_ids_table (str): Name of table with target_ids. platform_name (str): The name of the platform (e.g. facebook). """ activity.logger.info( f'Deleting removed accounts for {platform_name} ' f'from {config.fact_socials_table}') sf_config = get_sf_config(config.secrets_path) with SnowflakeExecutor(sf_config) as sf_executor: params = { 'fact_socials_table': config.fact_socials_table, 'platform_ID': config.platforms_mapping[platform_name], 'platform_name': platform_name, 'table_name': target_ids_table, 'since_date': config.since_date, 'ingest_date_limit': date, } activity.logger.info( f'Insert socials for {platform_name} ' f'from {config.fact_socials_table}') sf_executor.execute_query( f'insert_fact_socials_{platform_name}', **params) activity.logger.info(f'update_fact_socials finished for {platform_name}') @task.decorate(timeout=1200) def save_new_accounts(activity, target_ids_table, ingestion_started_at): """Insert modified accounts into log table. Args: activity (ActivityWorker): The activity worker. target_ids_table (str): Name of table with target_ids. ingestion_started_at (str): Timestamp '%Y-%m-%d %H:%M:%S' when ETL has started. Returns: dict: Context. """ activity.logger.info('Saving new accounts to log table') sf_config = get_sf_config(config.secrets_path) with SnowflakeExecutor(sf_config) as sf_executor: params = { 'cm_url_backfill_table': config.cm_url_backfill_table, 'target_ids_table': target_ids_table, 'ingestion_started_at': ingestion_started_at } sf_executor.execute_query('save_modified_accounts', **params) @task.decorate(timeout=600) def drop_temp_table(activity, temp_table_name): """Drop temp table. Args: activity (ActivityWorker): The Garcon activity worker. temp_table_name (str): Table name to drop. """ sf_config = get_sf_config(config.secrets_path) with SnowflakeExecutor(sf_config) as sf_executor: sf_executor.drop_table(temp_table_name) activity.logger.info('{} was dropped'.format(temp_table_name))