"""YouTube Bulk Reports Workflow.""" import csv from datetime import date as date_module from datetime import datetime from io import StringIO from itertools import zip_longest from pathlib import Path from garcon import task from garcon_contrib.dynamo_feed_status import garcon_feed_status from feed_ingestion.flows import registered_executors from feed_ingestion.flows.helpers import get_sf_config from feed_ingestion.flows.youtube_channel_names import config from feed_ingestion.tasks import check_status from feed_ingestion.tasks.youtube_tasks import \ save_youtube_access_token_from_secrets from feed_ingestion.util import youtube_util from feed_ingestion.util.aws import s3 as s3utils STOP_RESPONSE = {'stop': True} @task.decorate(timeout=1000) def bootstrap(activity, date, licensor=None, reload=None, dw_config=None): """Bootstrap workflow by injecting initial context from config. Args: activity (ActivityWorker): The Garcon activity worker. date (str): Reporting date (YYYY-MM-DD). dw_config (dict): Dictionary of data warehouse config options. Returns: dict: Initial context for the workflow. """ if not licensor: licensor = 'theorchard' assert licensor in config.licensors, f'unsupported licensor "{licensor}"' date = date or date_module.today().strftime('%Y-%m-%d') parsed_date = datetime.strptime(date, '%Y-%m-%d') feed_name = '_'.join([config.feed_name, licensor]) activity.logger.info('Bootstrap flow: {}'.format(parsed_date)) if reload == 'True': activity.logger.info( 'Delete status for feed: {} {} '.format(feed_name, date)) garcon_feed_status.delete_status(feed_name, date) else: overall_status = garcon_feed_status.get_overall_status( feed_name, date) if overall_status == garcon_feed_status.STATUS_INGESTED: return STOP_RESPONSE s3_preprocessed_path = config.s3['preprocessed'].format( data_bucket=config.data_bucket, feed_name=config.s3_feed_name, licensor=licensor) processed_filename = config.s3['preprocessed_filename'].format( date=parsed_date) credentials_path = config.credentials_paths.get(licensor) return { 'feed_name': feed_name, 'licensor': licensor, 'date': date, 'processed_filename': processed_filename, 'temp_staging_raw_table': config.snowflake['temp_staging_raw_table'].format( date=parsed_date, licensor=licensor), 'temp_staging_table_kwargs': {'file_pattern': processed_filename}, 's3_preprocessed_path': s3_preprocessed_path, 'credentials_path': credentials_path} @task.decorate(timeout=36000) @check_status() def get_missing_channels( activity, date, feed_name, processed_filename, s3_preprocessed_path, credentials_path): """Get missing channel names and upload to S3. Args: activity (ActivityWorker): The Garcon activity worker. date (str): Reporting date (YYYY-MM-DD). feed_name (str): Feed name of workflow execution for status updates. processed_filename (str): Filename of processed file. s3_preprocessed_path (str): S3 path to the preprocessed files location. """ sf_config = get_sf_config(config.secrets_path) ExecutorFA = registered_executors.get(feed_name) with ExecutorFA(sf_config) as executor: channel_ids = executor.get_missing_channels(date) save_youtube_access_token_from_secrets( path=Path(credentials_path), secrets_path=config.secrets_path ) api = youtube_util.get_authenticated_services( credentials_path, config.youtube_reporting_api_service_name, config.youtube_reporting_api_version) names = _get_channel_names(api, channel_ids) csv_obj = StringIO() writer = csv.DictWriter( csv_obj, fieldnames=[ 'channel_id', 'channel_display_name', 'description', 'published_at', 'thumbnail_url', 'subscriber_count', 'updated_at', 'view_count', 'video_count', 'is_in_mcn', 'channel_owner'], delimiter='\t') writer.writeheader() writer.writerows(names) file_path = '{path}{file_name}'.format( path=s3_preprocessed_path, file_name=processed_filename) s3utils.upload_processed_to_s3( csv_obj, file_path, expected_bucket_owner=config.expected_bucket_owner ) @task.decorate(timeout=36000) @check_status() def update_dim_table( activity, date, feed_name, processed_filename, s3_preprocessed_path, temp_staging_raw_table): """Update channel names mapping table. Args: activity (ActivityWorker): The Garcon activity worker. date (str): Reporting date (YYYY-MM-DD). feed_name (str): Feed name of workflow execution for status updates. processed_filename (str): Filename of processed file. s3_preprocessed_path (str): S3 path to the preprocessed files location. temp_staging_raw_table (str): A table name in Snowflake. """ sf_config = get_sf_config(config.secrets_path) ExecutorFA = registered_executors.get(feed_name) with ExecutorFA(sf_config) as executor: executor.update_dim_table( s3_preprocessed_path, processed_filename, temp_staging_raw_table) def _get_channel_names(api, channel_ids): """Get channel names via Data API. Args: api (Resource): YouTube authenticated API instance. channel_ids (list): list of missing channel ids. Returns: list: List of channel names. """ def grouper(iterable, n, fillvalue=None): """Iterate in chunks.""" args = [iter(iterable)] * n return zip_longest(*args, fillvalue=fillvalue) names = [] for ch in [channel_ids[i:i + 50] for i in range(0, len(channel_ids), 50)]: ids = ','.join(map(lambda x: x[0], ch)) request = api.channels().list(part='snippet,statistics', id=ids) response = request.execute() for i in response['items']: names.append( {'channel_id': i['id'], 'channel_display_name': i['snippet']['title'], 'description': i['snippet']['description'], 'published_at': i['snippet']['publishedAt'], 'thumbnail_url': i['snippet']['thumbnails']['high']['url'], # Subscriber count could be hidden. 'subscriber_count': i['statistics'].get('subscriberCount'), 'updated_at': datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ'), 'view_count': i['statistics']['viewCount'], 'video_count': i['statistics']['videoCount'], 'is_in_mcn': False, 'channel_owner': False}) return names