"""TikTok and Douyin Data Ingestion Workflow.""" from datetime import datetime import os from garcon import task from garcon_contrib.dynamo_feed_status import garcon_feed_status from feed_ingestion.conf.config import merge_configs from feed_ingestion.flows.helpers import get_sf_config from feed_ingestion.flows.tiktok import config from feed_ingestion.flows.tiktok.snowflake_executor import TikTok from feed_ingestion.tasks import check_status from feed_ingestion.tasks import s3_tasks from feed_ingestion.tasks.overall_status_tasks \ import set_overall_status_enhanced from feed_ingestion.util import task_status from feed_ingestion.util.aws.s3 import convert_zip_to_gzip_on_s3 from feed_ingestion.util.context_util import get_context_values STOP_RESPONSE = {'stop': True} @task.decorate(timeout=1000) def check_feed_status(activity, date, reload, licensor): """Check and reset feed status if it is needed. Args: activity (ActivityWorker): The Garcon activity worker. date (str): Reporting date (YYYY-MM-DD). reload (str or None): If 'True' delete all feed statuses in DynamoDB. licensor (str): The licensor. Returns: dict: dict or STOP_RESPONSE. """ overall_feed_name = '_'.join([config.feed_name, licensor]) if reload == 'True': activity.logger.info('Delete status for feed: {} {} '.format( overall_feed_name, date)) garcon_feed_status.delete_status(overall_feed_name, date) else: overall_status = garcon_feed_status.get_overall_status( overall_feed_name, date) if overall_status == garcon_feed_status.STATUS_INGESTED: return STOP_RESPONSE return {'feed_name': overall_feed_name} @task.decorate(timeout=1000) def bootstrap( activity, date, feed_name, reload, licensor, reports, use_sme_s3): """Bootstrap workflow by getting the correct configurations. Args: activity (ActivityWorker): The activity worker. feed_name (str): The overall feed name. date (str): Reporting date (YYYY-MM-DD). reload (str or None): If 'True' delete all feed statuses in DynamoDB. licensor (str): The licensor name. reports (str or None): List of the reports to ingest (optional). use_sme_s3 (str or None): Option to use sme shared s3 bucket. Returns: dict: Context. """ date_obj = (datetime.strptime( date, '%Y-%m-%d') if date else datetime.today()) date = date_obj.strftime('%Y-%m-%d') activity.logger.info('Bootstrap flow: {}'.format(date_obj)) licensor_config = config.licensors[licensor] reports = get_context_values(reports, list(licensor_config['reports'])) s3_config = licensor_config['s3'] if licensor == 'altafonte' and date_obj >= datetime.strptime( config.altafonte_source_bucket_switch_date, '%Y-%m-%d'): s3_config = licensor_config['s3_new'] reports = licensor_config['reports_new'] if use_sme_s3 == 'True' and licensor == 'theorchard': s3_config = licensor_config['s3_sme'] drop_bucket = s3_config['drop_bucket'] drop_path = s3_config['drop'].format(date=date_obj) archive_path = s3_config['archive'].format(date=date_obj) processed_path = s3_config['processed'].format(date=date_obj) reports_info = dict() for report in reports: report_feed_name = get_feed_name(licensor, report) if reload == 'True': activity.logger.info(f'Delete status for feed: {report_feed_name}') garcon_feed_status.delete_status(report_feed_name, date) filename = s3_config['filename'].format( report=report, sme_report=config.sme_report_mapping[report], date=date_obj) reports_info[report] = dict( drop_bucket=drop_bucket, drop_path=f'{drop_path}{filename}', archive_path=f'{archive_path}{filename}', processed_path=f'{processed_path}{filename}'.replace('zip', 'gz'), feed_name=report_feed_name, temp_staging_raw_table=config.temp_staging_raw_table.format( report=report, date=date_obj, licensor=licensor) ) return dict( feed_name=feed_name, date=date, reports_info=reports_info) @task.decorate(timeout=3800) @check_status() def grab_drop_files( activity, feed_name, licensor, date, drop_bucket, drop_path, archive_path, processed_path): """Download 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. licensor (str): The licensor name. date (str): Reporting date (YYYY-MM-DD). drop_bucket (str): Source S3 bucket. drop_path (str): Source S3 path to the archive location. archive_path (str): Destination S3 path to the archive location. processed_path (str): Destination S3 path for unzipped file. """ if drop_bucket != config.sme_drop_bucket: # copy zip archive result = s3_tasks.copy_file( activity, source_bucket_name=drop_bucket, source_key_name=drop_path, destination_bucket_name=config.data_bucket, destination_key_name=archive_path, replace=True) else: result = s3_tasks.copy_file_from_sme_s3_to_theocrhard( activity, secrets_path=config.sme_secrets_path, source_bucket_name=drop_bucket, source_key_name=drop_path, destination_bucket_name=config.data_bucket, destination_key_name=archive_path, replace=True) # file is not downloaded if not result.get(os.path.basename(drop_path)): set_overall_status_enhanced( feed_name, date, garcon_feed_status.STATUS_NOT_AVAILABLE, activity) return {'stop': True} elif licensor == 'theorchard' or licensor == 'altafonte': convert_zip_to_gzip_on_s3( activity, f's3://{config.data_bucket}/{archive_path}', f's3://{config.data_bucket}/{processed_path}', local_temp_dir='./') @task.decorate(timeout=600) def check_available_reports(activity, date, reports_info, feed_name): """Return new reports which are available. Args: activity (ActivityWorker): The activity worker. date (str): Reporting date (YYYY-MM-DD). reports_info (dict): List of reports, which should be processed. feed_name (str): The overall feed name. Returns: dict: of reports and their statuses, which files were uploaded on S3 statuses of possibility loading staging_raw and facts tables. """ downloaded_reports = {} missing_reports = {} for report, info in reports_info.items(): if task_status.is_completed_task( info['feed_name'], date, 'grab_drop_files'): downloaded_reports[report] = dict(info) else: missing_reports[report] = info['drop_path'] if missing_reports: return {'stop': True, 'missing_reports': missing_reports} set_overall_status_enhanced( feed_name, date, garcon_feed_status.STATUS_DOWNLOADED, activity) return dict(downloaded_reports=downloaded_reports) @task.decorate(timeout=2000) def set_status_to_ingested(activity, date, licensor, feed_name, reports): """Copy the temp_staging_raw data to the feed's staging_raw tables. Args: activity (ActivityWorker): The activity worker. date (str): Reporting date of the data file. licensor (str): The licensor. feed_name (str): The overall feed name. reports (str or None): List of the reports to ingest (optional). """ reports = get_context_values(reports, list(config.reports)) for report in reports: report_feed_name = get_feed_name(licensor, report) status = garcon_feed_status.get_overall_status( report_feed_name, date) # for now it should be POPULATED_RAW_TABLE status. if status != garcon_feed_status.STATUS_POPULATED_RAW_TABLE: return set_overall_status_enhanced( feed_name=feed_name, date=date, status=garcon_feed_status.STATUS_INGESTED, activity=activity) @task.decorate(timeout=3600) def remap_fingerprint_isrc_product_code( activity, date, feed_name, sfdb_params, licensor): """Update isrc and product_code using three-tier lookup strategy. Args: activity (ActivityWorker): The activity worker. date (str): Reporting date (YYYY-MM-DD). feed_name (str): The feed name. sfdb_params (dict): Dict with Snowflake db and schema name. licensor (str): The licensor. """ if licensor != 'theorchard': activity.logger.info( 'Skipping remap_fingerprint_isrc_product_code task' ' as it is not required.') return sf_config = merge_configs(get_sf_config(config.secrets_path), sfdb_params) with TikTok(sf_config) as executor: executor.update_staging_raw_table( staging_raw_table=config.staging_raw_table, date=date) activity.logger.info( 'Updated isrc and product_code in staging_raw table.') def get_feed_name(licensor: str, report_name: str) -> str: """Generate feed_name for specified licensor. Args: licensor (str): Nme of licensor. report_name (str): Name of report. Returns: str: feed_name. """ return '_'.join([config.feed_name, licensor, report_name])