"""Tasks for the Proper New Releases Tracks workflow.""" from datetime import datetime from botocore.exceptions import ClientError from garcon import task from feed_sender.flows import helpers from feed_sender.flows.proper_new_releases_tracks.conf import settings from feed_sender.flows.proper_new_releases_tracks.proper import Proper from feed_sender.flows.proper_new_releases_tracks.util import bootstrap_util from feed_sender.garcon_contrib.ftp import garcon_ftp from feed_sender.util import job_status from feed_sender.util import mysql from feed_sender.util import proper_common @task.decorate(timeout=100) def bootstrap(activity, cutoff=None, reload=False, s3_only=False): """Bootstrap of Proper outgoing feed. Args: activity (ActivityWorker): The activity worker. cutoff (Optional[str]): Latest registered time (of the context date) of release to be processed. reload (Optional[bool]): True will cause the workflow to re-generate and re-deliver the feed files. s3_only (Optional[bool]): True will skip the delivery of the files to sftp. Returns: dict: Dictionary contains full S3 path to final releases and tracklistings files. Example: s3://dev-feed-sender/proper/outgoing/2016-01-04/ """ feed_name = settings.FEED_NAME date_format = '%Y-%m-%d' datetime_format = '%Y-%m-%d %H:%M:%S' now = datetime.now() if not cutoff: cutoff = now.strftime(datetime_format) if not datetime.strptime(cutoff, datetime_format) <= now: return { 'stop': True, 'message': ( 'Cutoff {} is in the future. Now: {}.').format(cutoff, now) } context_date = datetime.strptime( cutoff, datetime_format).strftime(date_format) if reload: activity.logger.info('Delete status for feed: {} {} '.format( feed_name, context_date)) helpers.delete_status(feed_name, context_date) if bootstrap_util.feed_already_sent(feed_name, context_date, s3_only): return { 'stop': True, 'message': ( 'Feed for {} has already been sent.').format(context_date) } destination_s3_path = settings.NEW_RELEASES_TRACKLISTS_S3_PATH.format( date=context_date) destination_sftp_path = settings.SFTP_REMOTE_PATH.format( date=context_date) new_releases_filename = settings.NEW_RELEASES_FILENAME.format( date=context_date.replace('-', '_')) new_tracklistings_filename = settings.NEW_TRACKLISTINGS_FILENAME.format( date=context_date.replace('-', '_')) release_ids_filename = settings.RELEASE_IDS_FILENAME.format( date=context_date) job_ids_filename = settings.JOB_IDS_FILENAME.format( date=context_date) return { 'context_date': context_date, 'feed_name': feed_name, 'cutoff': cutoff, 'new_releases_tracklistings_s3_path': destination_s3_path, 'new_releases_filename': new_releases_filename, 'new_tracklistings_filename': new_tracklistings_filename, 'new_releases_tracklistings_sftp_path': destination_sftp_path, 'release_ids_filename': release_ids_filename, 'filenames_for_sftp_activity': [ new_releases_filename, new_tracklistings_filename ], 'job_ids_filename': job_ids_filename } @task.decorate(timeout=36000) def generate_feed_for_releases( activity, context_date, cutoff, s3path, filename, release_ids_filename, proper_in_vector, job_ids_filename, ows_pricing_in_proper=False): """Get metadata for releases to be included in the feed. Args: activity (ActivityWorker): The activity worker. context_date (str): Reporting date. cutoff (str): Processing cutoff. s3path (str): S3 file path. filename (str): Filename for new releases. release_ids_filename (str): Filename for list of release_ids. proper_in_vector (bool): Flag for proper in vector. job_ids_filename (str): Filename for list of job_ids. ows_pricing_in_proper (bool): Flag for using ows-pricing Returns: dict: Status and return message. """ proper = Proper(cutoff) try: proper.connect_to_sql() # TODO: add a method to save the original filtered release ids # to be processed by the flow. if proper_in_vector: proper.fetch_products(s3path, job_ids_filename) else: proper.filter_products_query() if proper.total_releases > proper.releases_threshold: activity.logger.warning( '{num_releases} is greater than release threshold ' '{threshold}'.format( num_releases=proper.total_releases, threshold=proper.releases_threshold)) activity.logger.info( 'Total Releases are: {num_releases}'.format( num_releases=proper.total_releases)) proper.convert_releases_to_csv(s3path, filename, ows_pricing_in_proper) proper.save_release_ids_to_s3(s3path, release_ids_filename) finally: proper.close_sql() return {'success': True} @task.decorate(timeout=7200) def generate_feed_for_tracks( activity, cutoff, s3path, tracks_file, release_ids_file, ows_pricing_in_proper=False): """Update history database. Update with the release_ids of products that have been successfully sent via SFTP. Args: activity (ActivityWorker): The activity worker. cutoff (str): Processing cutoff. s3path (str): S3 file path. tracks_file (str): Output tracks file. release_ids_file (str): Filename for list of release_ids. ows_pricing_in_proper (bool): Flag for using ows-pricing Returns: dict: Status and return message. """ proper = Proper(cutoff) proper.connect_to_sql() try: proper.convert_tracks_to_csv( s3path, tracks_file, release_ids_file, ows_pricing_in_proper) finally: proper.close_sql() return {'success': True} @task.decorate(timeout=7200) def update_delivery_history(activity, cutoff, s3path, release_ids_filename): """Update history database. Update with the release_ids of products that have been successfully sent via SFTP. Args: activity (ActivityWorker): The activity worker. cutoff (str): Processing cutoff. s3path (str): S3 file path. release_ids_filename (str): Filename for list of release_ids. Returns: dict: Status and return message. """ proper = Proper(cutoff) if not proper.update_delivery_history(s3path, release_ids_filename): return { 'stop': True, 'message': ( 'Error in writing to {db}'.format( db=proper.physical_delivery_db)) } return {'success': True} @task.decorate(timeout=7200) def update_job_status( activity, s3path, job_ids_filename, status, error_log=None): """Update job status. Args: activity (ActivityWorker): The activity worker. s3path (str): S3 file path. job_ids_filename (str): Filename for list of job_ids. status (str): Job status. error_log (str): Error log. Returns: bool: success status. """ parsed_ids = proper_common.get_ids_from_s3(s3path, job_ids_filename) for job_id in parsed_ids.get('job_ids'): data = {'status': status, 'eqd_id': job_id, 'error_log': error_log} job_status.update(data) return {'success': True} @task.decorate(timeout=7200) def obtain_jobs(activity, s3path, job_ids_filename): """Obtain jobs from SQS queue. Args: activity (ActivityWorker): The activity worker. s3path (str): S3 file path. job_ids_filename (str): Filename for list of job_ids. """ try: dd_db_conn = mysql.get_dd_db_connection_pymysql() results = proper_common.get_products_from_queue(dd_db_conn) dd_db_conn.close() except ClientError as err: return {'error': '{}'.format(err)} if not results: return {'error': 'No jobs available from queue.'} proper_common.save_job_ids_to_s3( s3path, job_ids_filename, results.get('job_ids')) @task.decorate(timeout=7200) def upload_to_sftp( activity, sftp_creds, file_names_list, remote_s3_dir_path, remote_ftp_dir_path, pkey=None): """Upload file from s3 to sftp. The exception hanlding will try to catch all possible exceptions from paramiko.SFTPClient. See ref at: https://github.com/paramiko/paramiko/blob/master/paramiko/sftp_client.py Args: activity (ActivityWorker): The swf activity worker. sftp_creds (dict): sftp connection credentials dictionary file_names_list (list): list of file names remote_s3_dir_path (str): s3 path to the folder which contains the source file remote_ftp_dir_path (str): path to the destination sftp location pkey (Optional[str]): path to private key. If present, use key, otherwise use username and password """ try: garcon_ftp.copy_from_s3_to_ftp( activity, sftp_creds, file_names_list, remote_s3_dir_path, remote_ftp_dir_path, pkey) except Exception as e: return {'error': str(e)} return {'success': True}