"""FTP- and SFTP-related tasks for Amazon Prime and Unlimited Market Share.""" from ftplib import FTP import os from garcon import task from garcon_contrib.dynamo_feed_status import \ garcon_feed_status from garcon_contrib.ftp import garcon_ftp import paramiko from feed_ingestion.util import task_status def _get_files_from_ftp(ftp_creds, ftp_path_template, file_pattern): """Search files on FTP and return paths to them. Args: ftp_creds (dict): FTP credentials. ftp_path_template (str): The ftp path template should be like '/root_path/{country_code}/rest_of_path'. file_pattern (str): File pattern for searching. Returns: files (dict): Dict of full path to file and new file name for it (add country code at the beginning of the file name). """ files = {} ftp = FTP(ftp_creds['host']) ftp.login(ftp_creds['username'], ftp_creds['password']) country_dir = ftp_path_template[:ftp_path_template.find('{country_code}')] for country in ftp.nlst(country_dir): country_code = country.split('/')[-1] for file in ftp.nlst( ftp_path_template.format(country_code=country_code)): file_name = file.split('/')[-1] if not file_pattern or file_pattern in file_name: # map old file name on new file name files[file] = '{country_code} {file_name}'.format( country_code=country_code, file_name=file_name) ftp.close() return files def _get_files_from_sftp(sftp_creds, sftp_path_template, file_pattern): """Search files on SFTP and return paths to them. Args: sftp_creds (dict): SFTP credentials. sftp_path_template (str): The sftp path template should be like '/root_path/{country_code}/rest_of_path'. file_pattern (str): File pattern for searching. Returns: files (dict): Dict of full path to file and new file name for it (add country code at the beginning of the file name). """ files = {} if 'pkey' in sftp_creds: pkey = paramiko.RSAKey.from_private_key_file(sftp_creds['pkey']) else: pkey = None with paramiko.Transport( (sftp_creds['host'])) as transport: transport.connect( username=sftp_creds['username'], password=sftp_creds['password'], pkey=pkey) with paramiko.SFTPClient.from_transport(transport) as sftp: country_dir = \ sftp_path_template[:sftp_path_template.find('{country_code}')] for country in sftp.listdir_attr(country_dir): country_code = str(country).split(' ')[-1] for file in sftp.listdir_attr(sftp_path_template.format( country_code=country_code)): file_name = str(file).split(' ')[-1] if 'test' in file_name: continue file_ = sftp_path_template.format( country_code=country_code) + '/' + file_name if not file_pattern or file_pattern in str(file_name): # map old file name on new file name files[file_] = '{country_code} {file_name}'.format( country_code=country_code, file_name=file_name) return files @task.decorate(timeout=28800) def check_for_new_files( activity, feed_name, date, ftp_creds, ftp_path_template, file_pattern): """Check if there are some new files on SFTP. Search files on sftp, construct new file name for each file (add country code at the beginning of the file name) and return dict with paths to files and their new names, which are grouped by sftp account name. Args: activity (ActivityWorker): The activity worker. feed_name (str): Feed name of workflow execution for status updates. date (str): Reporting date (YYYY-MM-DD). ftp_creds (dict): dictionary contains SFTP credentials. Can work with different ftp accounts: { 'account_name1': { 'host': 'host1', 'username': 'username1', 'password': 'password1', 'port': 21, }, 'account_name2': { 'host': 'host2', 'username': 'username2', 'password': 'password2', 'port': 21, }, } ftp_path_template (str): The ftp path template should be like '/root_path/{country_code}/rest_of_path'. file_pattern (str): File pattern for searching. Returns: files_on_ftp (dict): Dict with paths to files, which are grouped by ftp or sftp account name: { 'account_name1': { '/root_path/AT/rest_of_path/filename1': 'AT filename1', '/root_path/AT/rest_of_path/filename2': 'AT filename2' }, 'account_name2': { '/root_path/AT/rest_of_path/filename3': 'AT filename3', '/root_path/AT/rest_of_path/filename4': 'AT filename4' }, }. """ files_on_ftp = {} new_filenames = set() for ftp_acc in ftp_creds: files_on_ftp[ftp_acc] = _get_files_from_sftp( ftp_creds[ftp_acc], ftp_path_template, file_pattern) new_filenames.update(files_on_ftp[ftp_acc].values()) ingested_files = task_status.get_values( feed_name, date, 'ingested_files_status') # if there are new files if set(new_filenames) - set(ingested_files): activity.logger.info( 'New files were found for {} {}'.format(feed_name, date)) garcon_feed_status.delete_status(feed_name, date) return {'files_on_ftp': files_on_ftp} activity.logger.info( 'For {} {} there are no any new files'.format(feed_name, date)) return {'stop': True} @task.decorate(timeout=28800) def fetch_from_drop_location( activity, date, files_on_ftp, s3_archive_path, ftp_creds): """Copy the source files from FTP or SFTP to S3. Filter if necessary. Deduplicate the list of source file names, sort by the length of the names in a reverse order (for faster processing), skip the older versions of the revised files. Args: activity (Activity): Activity instance. date (str): date being processed. files_on_ftp (dict): Dict with paths to files with new file names, which are grouped by ftp or sftp account name s3_archive_path (str): S3 directory to write files to. ftp_creds (dict): FTP or SFTP credentials. Returns: dict: Context patch. """ def copy_file(ftp_creds, full_path, new_file_name): """Copy one file from FTP or SFTP to S3.""" ftp_path = os.path.dirname(full_path) file_name = os.path.basename(full_path) copy_response = garcon_ftp.copy_file_from_ftp_to_s3( activity, ftp_creds, ftp_path, file_name, s3_archive_path, new_file_name) found = copy_response['status'] if found: activity.logger.info('File downloaded: %s', copy_response) else: activity.logger.info('File not found: %s', copy_response) return { 'file_name': new_file_name, 'file_size': copy_response.get('file_size', -1), 'found': found } activity.logger.info('Fetching source files: %s', date) file_list = [] for ftp_acc, files_on_ftp in files_on_ftp.items(): for full_path, new_file_name in files_on_ftp.items(): file_list.append(copy_file( ftp_creds[ftp_acc], full_path, new_file_name)) file_list = [dict(t) for t in {tuple(d.items()) for d in file_list}] file_list.sort(key=lambda i: i['file_name'], reverse=True) result = [] # skip file if there is a newer _Rev or _revised version for s in file_list: if not any( [s['file_name'].replace('.txt', '') in o['file_name'] for o in result if '_Rev' in o['file_name'] or '_revised' in o['file_name'].lower()]): result.append(s) return {'source_files_dict': {'files': result}}