"""Tasks of the Chartmetric Tracks Ingestion Workflow.""" from datetime import datetime from datetime import timedelta import time from garcon import task from feed_ingestion.flows.chartmetric_tracks import config from feed_ingestion.flows.chartmetric_tracks.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, full_refresh=None, date_limit=None, platform_names=None): """Bootstrap workflow by getting the correct configurations. Args: activity (ActivityWorker): The activity worker. date (str): The date to ingest from (YYYY-MM-DD). full_refresh (str or None): If 'True' reingest all data. date_limit (str): The date limit to ingest to (YYYY-MM-DD). platform_names (str): A comma-separated list of platforms (e.g., "amazon,apple"). 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)) full_refresh = full_refresh == 'True' 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, full_refresh=full_refresh ) @task.decorate(timeout=72000 * 2) def create_table( activity, feed_name, date, date_limit, full_refresh, platform_name): """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. date (str): The date to ingest from (YYYY-MM-DD). date_limit (str): The date limit to ingest to (YYYY-MM-DD). full_refresh (bool): Define wether we need to reingest all data. platform_name (str): The name of the platform (e.g. spotify). Returns: dict: Context. """ activity.logger.info('Starting create_table for {}'.format(platform_name)) with SnowflakeExecutor(get_sf_config(config.secrets_path)) as sf_executor: query_name = 'create_{}_table'.format(platform_name) params = { 'table_name': '{}_{}'.format(feed_name, platform_name), 'ingest_date': date, 'ingest_date_limit': date_limit, 'platform_name': platform_name, 'full_refresh': full_refresh } 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_table for {}'.format(platform_name)) @task.decorate(timeout=72000 * 2) def ingest_data(activity, feed_name, platform_names): """Ingest data from the Snowflake table into Neo4j. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. platform_names (List[str]): Platform names to ingest. """ for platform_name in platform_names: _ingest_data(activity, feed_name, platform_name) def _ingest_data(activity, feed_name, platform_name): """Ingest 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. spotify). Returns: dict: Context. """ activity.logger.info('Starting ingest_data for {}'.format(platform_name)) sf_config = get_sf_config(config.secrets_path) neo4j_config = get_neo4j_config(feed_name) last_write_at = None with Neo4jExecutor(feed_name, neo4j_config) as neo4j_executor: offset = 0 limit = config.row_limit_per_batch while True: with SnowflakeExecutor(sf_config) as sf_executor: sf_params = { 'table_name': '{}_{}'.format(feed_name, platform_name), 'offset': offset, 'limit': limit } sf_query_name = 'get_{}_raw_data'.format(platform_name) rows = sf_executor.fetchall_dict_query( sf_query_name, **sf_params) offset += limit if not rows: break neo4j_params = {'rows': rows} neo4j_query_name = 'ingest_{}_raw_data'.format( platform_name) activity.logger.info( f'Neo4j Starting {neo4j_query_name} ' f'with {len(rows)} to execution...') _write_delay(activity, last_write_at) neo4j_executor.execute_write_query(neo4j_query_name, neo4j_params) last_write_at = datetime.now() activity.logger.info(f'Neo4j Done {neo4j_query_name}') activity.logger.info('Finished ingest_data for {}'.format(platform_name)) def _write_delay(activity, last_write_at): """Make a pause between write transactions if needed.""" if last_write_at: time_to_wait = config.WRITE_TRANSACTION_INTERVAL - \ (datetime.now() - last_write_at).total_seconds() if time_to_wait > 0: activity.logger.info(f'Waiting for {time_to_wait} seconds...') time.sleep(time_to_wait) @task.decorate(timeout=3600 * 5) def delete_defunct_participations(activity, feed_name, date): """Delete defunct relations between public Participant and SoundRecording. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. date (str): The date to ingest from (YYYY-MM-DD). """ activity.logger.info( 'Start deleting defunct relations between ' 'PublicSoundRecording and PublicParticipant.' ) sf_config = get_sf_config(config.secrets_path) neo4j_config = get_neo4j_config(feed_name) neo4j_executor_cm = Neo4jExecutor(feed_name, neo4j_config) with neo4j_executor_cm as neo4j_executor: offset = 0 limit = config.row_limit_per_batch neo4j_query_name = sf_query_name = 'delete_defunct_participations' while True: with SnowflakeExecutor(sf_config) as sf_executor: sf_params = { 'offset': offset, 'limit': limit } rows = sf_executor.fetchall_dict_query( sf_query_name, **sf_params) offset += limit if not rows: break activity.logger.info( f'Neo4j Starting {neo4j_query_name} ' f'with {len(rows)} to execution...') neo4j_params = {'rows': rows} neo4j_executor.execute_write_query(neo4j_query_name, neo4j_params) activity.logger.info(f'Neo4j Done {neo4j_query_name}') activity.logger.info( 'defunct relations between PublicSoundRecording and ' 'PublicParticipant were deleted.' )