"""Tasks of the GFK Ingestion Workflow.""" from datetime import datetime import os from botocore.exceptions import ClientError from garcon import task from garcon_contrib.dynamo_feed_status import garcon_feed_status import paramiko from feed_ingestion.flows.gfk import config from feed_ingestion.tasks import bootstrap as reload from feed_ingestion.util import dates_util from feed_ingestion.util.aws import s3 @task.decorate(timeout=1000) @reload.reset_dynamodb_status_on_reload(config.feed_name) def bootstrap(activity, date, dw_config=None): """Bootstrap workflow by getting the correct configurations. Args: activity (ActivityWorker): The activity worker. date (str): Reporting date (YYYY-MM-DD). Returns: dict: Context. """ # date is the date passed in or yesterday's date date_obj = datetime.strptime( date, '%Y-%m-%d').date() if date else datetime.today().date() activity.logger.info('Bootstrap flow: {}'.format(date_obj)) # Short circuit flow if overall status is already INGESTED if garcon_feed_status.get_overall_status( config.feed_name, date_obj.strftime( '%Y-%m-%d')) == garcon_feed_status.STATUS_INGESTED: activity.logger.info('Feed already ingested for {}'.format(date)) return { 'stop': True, 'message': '{feed_name} is already ingested for {date:%Y-%m-%d}'.format( feed_name=config.feed_name, date=date_obj)} year_and_week_number_dict = ( dates_util.calculate_release_week_number_from_date(date_obj)) filename = config.source_filename_template.format( year=year_and_week_number_dict['year'], week_number=year_and_week_number_dict['week_number']) source_files_dict = {'files': [{'file_name': filename}]} s3_archive_path = config.s3_archive_path_template.format(date=date_obj) return dict( feed_name=config.feed_name, secrets_path=config.secrets_path, staging_raw_table=config.snowflake_table_names['staging_raw'], s3_archive_path=s3_archive_path, source_files_dict=source_files_dict, date=date_obj.strftime('%Y-%m-%d'), ) def _get_sftp_connection(ftp_creds): """Prepare sftp connection.""" port = 22 # sets paramiko chunk size paramiko.sftp_file.SFTPFile.MAX_REQUEST_SIZE = 4194304 transport = paramiko.Transport((ftp_creds['host'], port)) transport.connect( username=ftp_creds['username'], password=ftp_creds['password'] ) return paramiko.SFTPClient.from_transport(transport) def _get_latest_file(sftp_files, date): """Get the latest filename and size.""" filename = '' size = '' for item in sftp_files: if (date in item.filename): filename = item.filename size = item.st_size return {'filename': filename, 'size': size} @task.decorate(timeout=7200) def fetch_from_drop_location( activity, feed_name, date, s3_archive_path, ftp_creds): """Copy source file from FTP to S3. Args: activity (Activity): Activity instance. date (str): Date being processed. s3_archive_path (str): S3 directory to write file to. ftp_creds (dict): FTP credentials. Returns: dict: A dict with the the metadata about the downloaded file. """ activity.logger.info('Downloading file from FTP...') sftp = _get_sftp_connection(ftp_creds) path = '/{}'.format(ftp_creds['path'].rstrip('/')) sftp_files = sftp.listdir_attr(path) latest_file = _get_latest_file(sftp_files, date) latest_filename = latest_file['filename'] file_size = latest_file['size'] if len(latest_filename) > 0: sftp.get('{}/{}'.format(path, latest_filename), latest_filename) sftp.close() activity.logger.info('Uploading file to S3...') remote_s3_dir_path = '/'.join(s3_archive_path.split('/')[3:]) bucket_name = config.s3_bucket object_key = '{bucket_path}{file_name}'.format( bucket_path=remote_s3_dir_path, file_name=latest_filename) try: s3.upload_to_s3( file_path=latest_filename, bucket_name=bucket_name, object_key=object_key, ) except ClientError: raise finally: os.remove(latest_filename) # result in the format which is acceptable by downstream tasks result = [{ 'file_name': latest_filename, 'file_size': file_size, 'found': True }] return dict(source_files_dict={'files': result}) activity.logger.info('Failed to fetch files from FTP') garcon_feed_status.set_overall_status( feed_name, date, garcon_feed_status.STATUS_NOT_AVAILABLE) return {'stop': True, 'message': 'Failed to fetch files from FTP'}