"""Tasks of the Chartmetric Socials Ingestion Workflow.""" from datetime import datetime from datetime import timedelta from garcon import task from feed_ingestion.flows.chartmetric_socials import config from feed_ingestion.flows.chartmetric_socials.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_limit=None, platform_names=None, reload=None): """Bootstrap workflow by getting the correct configurations. Args: activity (ActivityWorker): The activity worker. date (str): The date to ingest from (YYYY-MM-DD). date_limit (str): The date limit to ingest to (YYYY-MM-DD). platform_names (str): A comma-separated list of platforms (e.g., "instagram,twitter"). reload (str): A flag which indicates if we have to clear out the log table, and perform a force refresh. 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) # date_limit is the date_limit passed in or a date very far in the future if date_limit: date_limit_obj = datetime.strptime(date_limit, '%Y-%m-%d') else: date_limit_obj = datetime.strptime('3000-01-01', '%Y-%m-%d') activity.logger.info('Bootstrap flow from {} to {}'.format( date_obj, date_limit_obj)) return dict( feed_name=config.feed_name, date=date_obj.strftime('%Y-%m-%d'), date_limit=date_limit_obj.strftime('%Y-%m-%d'), platform_names=platform_names.split( ',') if platform_names else config.all_platform_names, reload=True if reload == 'True' else False, ingestion_started_at=datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') ) @task.decorate(timeout=3600) def clear_log_table(activity, feed_name, platform_name, reload): """Create the table that will hold the spotify data to ingest. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. platform_name (str): The name of the platform (e.g. spotify). reload (str): A flag which indicates if we have to clear out the log table, and perform a force refresh. Returns: dict: Context. """ activity.logger.info( 'Starting clear_log_table for {}'.format(platform_name)) if reload: with SnowflakeExecutor( get_sf_config(config.secrets_path)) as sf_executor: params = { 'platform_name': platform_name, 'log_table_name': config.log_table_name } sf_executor.execute_query('clear_log_table', **params) activity.logger.info( 'clear_log_table for {} finished'.format(platform_name)) else: activity.logger.info( 'clear_log_table for {} not required, skip'.format(platform_name)) @task.decorate(timeout=3600) def create_accounts_table( activity, feed_name, platform_name): """Create the table that will hold the data to ingest. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. platform_name (str): The name of the platform (e.g. instagram). Returns: dict: Context. """ activity.logger.info('Starting create_accounts_table for {}'.format( platform_name)) sf_config = get_sf_config(feed_name) with SnowflakeExecutor(sf_config) as sf_executor: query_name = 'create_temp_accounts_{}_table'.format(platform_name) params = { 'temp_table_name': 'temp_accounts_{}_{}'.format( feed_name, platform_name) } sf_executor.execute_query(query_name, **params) params = { 'table_name': 'accounts_{}_{}'.format(feed_name, platform_name), 'temp_table_name': 'temp_accounts_{}_{}'.format( feed_name, platform_name), 'log_table_name': config.log_table_name, 'platform_name': platform_name, 'main_accounts_export_limit': config.main_accounts_export_limit } sf_executor.execute_query('create_main_accounts_table', **params) activity.logger.info('Finished create_accounts_tables for {}'.format( platform_name)) @task.decorate(timeout=7200 * 5) def ingest_social_accounts(activity, feed_name, platform_name): """Ingest raw social accounts data from the Snowflake table into Neo4j. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. platform_name (str): The name of the platform (e.g. instagram). Returns: dict: Context. """ activity.logger.info( 'Starting ingest_accounts for {}'.format(platform_name)) with Neo4jExecutor(feed_name, get_neo4j_config(feed_name)) \ as neo4j_executor: offset = 0 limit = config.row_limit_per_batch while True: with SnowflakeExecutor(get_sf_config(feed_name)) as sf_executor: sf_params = { 'table_name': 'accounts_{}_{}'.format( feed_name, platform_name), 'offset': offset, 'limit': limit, } rows = sf_executor.fetchall_dict_query( 'get_social_accounts', **sf_params) offset += limit if not rows: # no more data in the table break neo_params = { 'rows': rows, 'platform_name': platform_name, 'created_by': ( 'swf-feed-ingestion/chartmetric-socials/' 'ingest-{platform_name}-social-accounts'.format( platform_name=platform_name)), } neo4j_query_name = 'ingest_accounts' activity.logger.info( f'Neo4j Starting {neo4j_query_name} ' f'with {len(rows)} to execution...') neo4j_executor.execute_write_query(neo4j_query_name, neo_params) activity.logger.info(f'Neo4j Done {neo4j_query_name}') activity.logger.info( 'Finished ingest_accounts for {}'.format(platform_name)) @task.decorate(timeout=7200 * 5) def delete_pending_social_accounts(activity, feed_name, query_name): """Delete pending social account nodes matching social account in Neo4j. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. Returns: dict: Context. """ activity.logger.info( ('Starting pending social account deletion for {}' .format(query_name))) with Neo4jExecutor(feed_name, get_neo4j_config(feed_name)) \ as neo4j_executor: neo4j_query_name = query_name activity.logger.info( f'Neo4j Starting {neo4j_query_name} ') neo4j_executor.execute_write_query(neo4j_query_name) activity.logger.info(f'Neo4j Done {neo4j_query_name}') activity.logger.info( ('Finished pending social account deletion for {}' .format(query_name))) @task.decorate(timeout=900) def create_temp_aggregates_table(activity, platform_name): """Create the table that will hold the aggregated social data. Args: activity (ActivityWorker): The activity worker. platform_name (str): The name of the platform (e.g. spotify). Returns: dict: Context. """ activity.logger.info( 'Starting create_temp_aggregates_table_for {platform_name}'.format( platform_name=platform_name)) with SnowflakeExecutor(get_sf_config(config.secrets_path)) as sf_executor: query_name = 'create_temp_aggregates_{platform_name}_table'.format( platform_name=platform_name) params = { 'temp_table_name': 'temp_aggregates_{platform_name}'.format( platform_name=platform_name), } sf_executor.execute_query(query_name, **params) activity.logger.info( 'Finished create_temp_aggregates_table for {platform_name}'.format( platform_name=platform_name)) @task.decorate(timeout=600) def create_aggregate_by_account_table(activity): """Create the table that will hold the aggregated by account social data. Args: activity (ActivityWorker): The activity worker. Returns: dict: Context. """ activity.logger.info('Starting create_aggregate_by_account_table') with SnowflakeExecutor(get_sf_config(config.secrets_path)) as sf_executor: query_name = 'create_aggregate_by_account_table' params = { 'table_name': 'aggregate_by_account', } sf_executor.execute_query(query_name, **params) # this is useful for investigating issues in production sf_executor.grant_select_to_facts_db_prod_schema_read( params['table_name']) activity.logger.info('Finished create_aggregate_by_account_table') @task.decorate(timeout=600) def create_temp_aggregate_by_participant_table(activity): """Create the table that will hold the temp aggregated by participant data. Args: activity (ActivityWorker): The activity worker. Returns: dict: Context. """ activity.logger.info('Starting create_temp_aggregate_by_participant_table') with SnowflakeExecutor(get_sf_config(config.secrets_path)) as sf_executor: query_name = 'create_temp_aggregate_by_participant_table' params = { 'table_name': 'temp_aggregate_by_participant', } sf_executor.execute_query(query_name, **params) activity.logger.info('Finished create_temp_aggregate_by_participant_table') @task.decorate(timeout=3600) def populate_temp_aggregate_by_participant_table(activity, platform_name): """Populate the temp aggregate_by_participant table. Args: activity (ActivityWorker): The activity worker. platform_name (str): The name of the platform (e.g. spotify). Returns: dict: Context. """ activity.logger.info( 'Starting populate_temp_aggregate_by_participant_table for {}'.format( platform_name)) with SnowflakeExecutor(get_sf_config(config.secrets_path)) as sf_executor: query_name = 'insert_into_temp_aggregate_by_participant_table' params = { 'table_name': 'temp_aggregate_by_participant', 'temp_table_name': 'temp_aggregates_{platform_name}'.format( platform_name=platform_name) } sf_executor.execute_query(query_name, **params) activity.logger.info( 'Finished populate populate_temp_aggregate_by_participant_table for {}' .format(platform_name)) @task.decorate(timeout=600) def create_main_aggregate_by_participant_table(activity): """Create the table that will hold the aggregated by participant data. Args: activity (ActivityWorker): The activity worker. Returns: dict: Context. """ activity.logger.info('Starting create_main_aggregate_by_participant_table') with SnowflakeExecutor(get_sf_config(config.secrets_path)) as sf_executor: query_name = 'create_aggregated_aggregate_by_participant_table' params = { 'table_name': 'aggregated_aggregate_by_participant', 'temp_aggregate_by_participant_table_name': 'temp_aggregate_by_participant' } sf_executor.execute_query(query_name, **params) query_name = 'create_main_aggregate_by_participant_table' params = { 'table_name': 'aggregate_by_participant', 'log_table_name': config.log_table_name, 'aggregate_accounts_export_limit': config.aggregate_accounts_export_limit, # noqa 'aggregated_aggregate_by_participant_table_name': 'aggregated_aggregate_by_participant' } sf_executor.execute_query(query_name, **params) # this is useful for investigating issues in production sf_executor.grant_select_to_facts_db_prod_schema_read( params['table_name']) activity.logger.info('Finished create_main_aggregate_by_participant_table') @task.decorate(timeout=3600) def populate_aggregate_by_account_table(activity, platform_name): """Populate the aggregate_by_account table. Args: activity (ActivityWorker): The activity worker. platform_name (str): The name of the platform (e.g. spotify). Returns: dict: Context. """ activity.logger.info('Starting populate_aggregate_by_account_table for {}' .format(platform_name)) with SnowflakeExecutor(get_sf_config(config.secrets_path)) as sf_executor: query_name = 'insert_into_aggregate_by_account_table' params = { 'table_name': 'aggregate_by_account', 'temp_table_name': 'temp_aggregates_{platform_name}'.format( platform_name=platform_name), 'log_table_name': config.log_table_name, 'aggregate_accounts_export_limit': config.aggregate_accounts_export_limit, # noqa } sf_executor.execute_query(query_name, **params) activity.logger.info('Finished populate aggregate_by_account_table for {}' .format(platform_name)) @task.decorate(timeout=7200 * 5) def ingest_aggregate_social_data(activity, feed_name, query_name): """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 Returns: dict: Context. """ activity.logger.info('Starting aggregation ingest for {}' .format(query_name)) if query_name == 'ingest_aggregate_socials_by_account': table_name = 'aggregate_by_account' sf_query_name = 'get_aggregate_socials_by_account' elif query_name == 'ingest_aggregate_socials_by_participant': table_name = 'aggregate_by_participant' sf_query_name = 'get_aggregate_socials_by_participant' else: raise f'Wrong query_name {query_name}' with Neo4jExecutor(feed_name, get_neo4j_config(feed_name)) \ as neo4j_executor: offset = 0 limit = config.row_limit_per_batch while True: with SnowflakeExecutor(get_sf_config(feed_name)) as sf_executor: sf_params = { 'table_name': table_name, 'offset': offset, 'limit': limit, } rows = sf_executor.fetchall_dict_query( sf_query_name, **sf_params) offset += limit if not rows: # no more data in the table break neo_params = { 'rows': rows, } neo4j_query_name = query_name activity.logger.info( f'Neo4j Starting {neo4j_query_name} ' f'with {len(rows)} to execution...') neo4j_executor.execute_write_query(neo4j_query_name, neo_params) activity.logger.info(f'Neo4j Done {neo4j_query_name}') activity.logger.info('Finished aggregation ingest for {}' .format(query_name)) @task.decorate(timeout=7200) def insert_into_log_table( activity, feed_name, ingestion_started_at, platform_name): """Insert unique keys of ingested rows to the log table. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. ingestion_started_at (str): Timestamp '%Y-%m-%d %H:%M:%S' when ETL has started. platform_name (str): The name of the platform (e.g. spotify). """ activity.logger.info( 'Insert into log table for {}...'.format(platform_name)) with SnowflakeExecutor(get_sf_config(config.secrets_path)) as sf_executor: query_name = 'insert_into_log_table' if platform_name == 'aggregate_by_participant': table_name = 'aggregate_by_participant' elif platform_name == 'aggregate_by_account': table_name = 'aggregate_by_account' else: table_name = 'accounts_{}_{}'.format(feed_name, platform_name) params = { 'table_name': table_name, 'platform_name': platform_name, 'log_table_name': config.log_table_name, 'ingestion_started_at': ingestion_started_at } sf_executor.execute_query(query_name, **params) @task.decorate(timeout=7200) def insert_fact_socials( activity, feed_name, date, date_limit, ingestion_started_at, platform_name, reload_artist_with_no_stat): """Insert unique keys of ingested rows to the log table. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. date (str): The date to ingest from (YYYY-MM-DD). date_limit (str): The date limit to ingest to (YYYY-MM-DD). ingestion_started_at (str): Timestamp '%Y-%m-%d %H:%M:%S' when ETL has started. platform_name (str): The name of the platform (e.g. spotify). """ if not reload_artist_with_no_stat: reload_artist_with_no_stat = 'False' else: reload_artist_with_no_stat = 'True' activity.logger.info( 'Insert into fact_socials table for {}...'.format(platform_name)) with SnowflakeExecutor(get_sf_config(config.secrets_path)) as sf_executor: query_name = f'insert_fact_socials_{platform_name}' params = { 'platform_name': platform_name, 'fact_socials_table': config.fact_socials_table, 'ingest_date': date, 'ingest_date_limit': date_limit, 'ingestion_started_at': ingestion_started_at, 'reload_artist_with_no_stat': reload_artist_with_no_stat } sf_executor.execute_query(query_name, **params) @task.decorate(timeout=1200) def create_fact_social_latest(activity): """Recreate fact_social_latest with the latest data. Args: activity (ActivityWorker): The activity worker. """ activity.logger.info( 'Create table fact_social_latest') with SnowflakeExecutor(get_sf_config(config.secrets_path)) as sf_executor: query_name = 'create_fact_social_latest' sf_executor.execute_query(query_name) sf_executor.grant_select_to_facts_db_prod_schema_read( table_name='fact_socials_latest') environment = config.environment.upper() if environment in ['QA', 'PROD']: role = f'{environment}_OWS_ANALYTICS_READ' sf_executor.grant_permission_on_table( permission='SELECT', table_name='FACT_SOCIALS_LATEST', to_role=role, )