"""Spotify Charts Data Ingestion Workflow.""" from datetime import datetime from datetime import timedelta from tempfile import NamedTemporaryFile from garcon import task from garcon_contrib.dynamo_feed_status import garcon_feed_status import jenkins from requests import HTTPError from snowflake_connector.etl_connector import SQLLoader from feed_ingestion.flows.helpers import get_secret from feed_ingestion.flows.helpers import get_sf_config from feed_ingestion.flows.spotify_charts import config from feed_ingestion.flows.spotify_charts.snowflake_executor import \ SpotifyCharts from feed_ingestion.flows.spotify_charts.spotify_api import SpotifyChartsAPI from feed_ingestion.util import task_status from feed_ingestion.util.aws.s3 import upload_on_s3 sql_loader = SQLLoader(__file__) STOP_RESPONSE = {'stop': True} @task.decorate(timeout=1000) def bootstrap(activity, date, reload, chart): """Bootstrap workflow by getting the correct configurations. Args: activity (ActivityWorker): The activity worker. date (str): Reporting date (YYYY-MM-DD). reload (str or None): If 'True' delete all feed statuses in DynamoDB. chart (str): The chart name. Returns: dict: Context. """ date_obj = (datetime.strptime( date, '%Y-%m-%d') if date else datetime.today()) # if date is not Thursday, get last Thursday as a date if (config.charts[chart]['frequency'] == 'weekly' and date_obj.weekday() != 3): date_obj = date_obj - timedelta((date_obj.weekday() - 3) % 7) feed_name = f'{config.feed_name}_{chart}' activity.logger.info( 'Bootstrap flow: {} feed {}'.format(date_obj, feed_name)) if reload == 'True': activity.logger.info('Delete status for feed: {} {} '.format( feed_name, date_obj.strftime('%Y-%m-%d'))) garcon_feed_status.delete_status(feed_name, date_obj.strftime('%Y-%m-%d')) else: overall_status = garcon_feed_status.get_overall_status( feed_name, date_obj.strftime('%Y-%m-%d')) if overall_status == garcon_feed_status.STATUS_INGESTED: return STOP_RESPONSE archive_path = config.s3['archive'].format( chart=chart, date=date_obj) return dict( feed_name=feed_name, date=date_obj.strftime('%Y-%m-%d'), chart=chart, staging_raw_table=config.staging_raw_table, secrets_path=config.secrets_path, archive_path=archive_path, s3_archive_path=f's3://{config.data_bucket}/{archive_path}', file_pattern=config.file_pattern.format( chart_type=config.charts[chart]['chart_type'], date=date_obj, frequency=config.charts[chart]['frequency']), temp_staging_raw_table=config.temp_staging_raw_table.format( chart=chart, date=date_obj), common_kwargs=dict(config.charts[chart])) @task.decorate(timeout=3600) def grab_drop_files( activity, feed_name, date, chart, file_pattern, archive_path): """Copy a feed file from the drop location into the archive folder. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. date (str): Reporting date (YYYY-MM-DD). chart (str): The chart name. file_pattern (str): The file pattern. archive_path (str): Name of zipped archive. Returns: dict: With list of available countries. """ spotify_api = SpotifyChartsAPI( client_id=get_secret( config.secrets_path, 'SPOTIFY_API_CLIENT_ID'), client_secret=get_secret( config.secrets_path, 'SPOTIFY_API_CLIENT_SECRET'), licensor_name='theorchard', version='v1') ingested_countries = task_status.get_values( feed_name, date, 'ingested_countries_status') missing_files = [] new_countries = [] for country in set(config.expected_countries) - set(ingested_countries): filename = file_pattern.format(country=country) try: with NamedTemporaryFile('wb') as fd: spotify_api.get_charts_to_file( fd, date, country=country.lower(), frequency=config.charts[chart]['frequency'], chart_type=config.charts[chart]['chart_type']) upload_on_s3( config.data_bucket, archive_path, filename, fd, config.expected_bucket_owner ) new_countries.append(country) except HTTPError: missing_files.append(filename) garcon_feed_status.set_missing_files(feed_name, date, missing_files) # no charts are available if len(new_countries) == 0: activity.logger.info( f'For {chart} {date} are {len(missing_files)} missing files') garcon_feed_status.set_overall_status( feed_name, date, garcon_feed_status.STATUS_NOT_AVAILABLE) return STOP_RESPONSE # there are some new charts if set(new_countries) == set(ingested_countries): activity.logger.info( f'For {chart} {date} there are no new files.') return STOP_RESPONSE # there are some new charts activity.logger.info( f'For {chart} {date} {len(new_countries)} countries were uploaded.') garcon_feed_status.set_overall_status( feed_name, date, garcon_feed_status.STATUS_DOWNLOADED) return dict(countries=new_countries) @task.decorate(timeout=2000) def load_staging_charts_data(activity, feed_name, date, chart, countries): """Copy a feed file from the drop location into the archive folder. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. date (str): Reporting date (YYYY-MM-DD). chart (str): The chart name. countries (list): List of countries which it is needed to load. """ sf_config = get_sf_config(config.secrets_path) with SpotifyCharts(sf_config) as executor: activity.logger.info(f'{config.chart_table} table was cleared.') executor.clean_staging_raw_table( staging_raw_table=config.chart_table, date=date, chart_type=config.charts[chart]['chart_type'], frequency=config.charts[chart]['frequency'], countries=countries) for table in config.spotify_metadata_tables: executor.load_spotify_tables( date=date, chart_type=config.charts[chart]['chart_type'], frequency=config.charts[chart]['frequency'], table_name=table, countries=countries) activity.logger.info(f'{table} table was ingested.') @task.decorate(timeout=2000) def set_status_to_ingested(activity, feed_name, date, countries, chart): """Copy the temp_staging_raw data to the feed's staging_raw tables. Args: activity (ActivityWorker): The activity worker. feed_name (str): The name of the feed. date (str): Reporting date of the data file. countries (list): List of ingested countries. chart (str): The chart name. """ # update ingested_countries with new list of ingested countries ingested_countries = task_status.get_values( feed_name, date, 'ingested_countries_status') ingested_countries.extend(countries) task_status.set_values( feed_name, date, 'ingested_countries', ingested_countries) if len(ingested_countries) >= config.charts[chart]['number_of_countries']: activity.logger.info( 'Setting status for feed: {feed_name} with date: {date} ' 'to: {status}, via a task'.format( feed_name=feed_name, date=date, status=garcon_feed_status.STATUS_INGESTED)) garcon_feed_status.set_overall_status( feed_name, date, garcon_feed_status.STATUS_INGESTED) else: activity.logger.info( 'Setting status for feed: {feed_name} with date: {date} ' 'to: {status}, via a task'.format( feed_name=feed_name, date=date, status=garcon_feed_status.STATUS_NOT_AVAILABLE)) garcon_feed_status.set_overall_status( feed_name, date, garcon_feed_status.STATUS_NOT_AVAILABLE) @task.decorate(timeout=600) def build_jenkins_charts(activity, feed_name, date, chart, build_charts): """Build Chartmetric Charts job if data is already ingested. Args: activity (ActivityWorker): The Garcon activity worker. feed_name (str): Name of the feed to set status in DynamoDB. date (str): Reporting date (YYYY-MM-DD). chart (str): The chart name. build_charts (str, None): If 'True' run jenkins dbt job. """ if build_charts == 'True': server = jenkins.Jenkins( config.jenkins_url, username=config.jenkins_username, password=get_secret( config.jenkins_secrets_path, 'JENKINS_API_TOKEN') ) job_info = server.get_job_info(config.jenkins_job) # if there is no queue if not job_info['inQueue']: next_build_number = job_info['nextBuildNumber'] server.build_job(config.jenkins_job, config.jenkins_job_params) activity.logger.info( f'feed_name: {feed_name} date: {date} {config.jenkins_job} ' f'#{next_build_number} was built') return {'build': True} return {'build': False}