""" Update status of feed in dynamo """ from collections import deque import datetime import boto3 from boto3.dynamodb.types import TypeSerializer from garcon_contrib.dynamo_feed_status import config STATUS_NOT_AVAILABLE = 'NOT_AVAILABLE' STATUS_DOWNLOADED = 'DOWNLOADED' STATUS_POPULATED_RAW_TABLE = 'POPULATED_RAW_TABLE' STATUS_INGESTED = 'INGESTED' STATUS_NOT_INGESTED = 'NOT_INGESTED' STATUS_INGESTED_TO_MYSQL = 'INGESTED_TO_MYSQL' STATUS_DEFAULT = None dynamodb = boto3.client('dynamodb', region_name=config.aws_region) serializer = TypeSerializer() def set_missing_files(feed_name, date, missing_files): """Set missing_files attribute for a feed for a date. This is a helper function for the unified update_s3_file_status task. Args: feed_name (str): name of the feed date (str): reporting date of the ingestion job missing_files (list): a list of missing files """ # key(s) of the item to be updated key = { 'feed_name': {'S': feed_name}, 'date': {'S': date} } missing_files_as_comma_separated_str = ','.join( file_name for file_name in missing_files) dynamodb.update_item( TableName=config.feed_ingestion_table, Key=key, UpdateExpression='SET missing_files = :missing_files', ExpressionAttributeValues={ ':missing_files': {'S': missing_files_as_comma_separated_str}}) def get_attribute_name_for_file_status(file_name, date): """Get attribute name for file status Args: file_name (str): name of the file being extracted date (str): reporting date Returns: str: attribute name """ return '{}_status'.format( file_name.split('.')[0].replace('_{}'.format(date), '')) def set_status(feed_name, date, file_name, status=STATUS_NOT_AVAILABLE): """Set status of the ingestion job Args: feed_name (str): name of the feed date (str): reporting date of the ingestion job file_name (str): name of the file to be ingested status (str): status code constant """ attribute_name = get_attribute_name_for_file_status(file_name, date) # key(s) of the item to be updated key = { 'feed_name': {'S': feed_name}, 'date': {'S': date} } # we use a placeholder to avoid key validation error as the new key # being added is a filename and may have some forbidden chars in it placeholder = 'file_name_status' update_expression = 'SET #{} = :{}'.format(placeholder, placeholder) # defines what the placeholder should be replaced with using expression expression_attribute_names = { '#{}'.format(placeholder): '{}'.format(attribute_name) } # defines the value for that key expression_attribute_values = { ':{}'.format(placeholder): {'S': status} } dynamodb.update_item( TableName=config.feed_ingestion_table, Key=key, UpdateExpression=update_expression, ExpressionAttributeNames=expression_attribute_names, ExpressionAttributeValues=expression_attribute_values, ReturnValues='NONE') def set_overall_status(feed_name, date, overall_status=None, attributes=None): """Set overall status Set overall status by looking at all the status for all files. If status is provided, set overall status with that. Args: feed_name (str): name of the feed date (str): reporting date of the ingestion job overall_status (str): overall_status code constant attributes (dict): additional attributes to set (optional) """ if not overall_status: overall_status = _determine_overall_status(feed_name, date) key = { 'feed_name': {'S': feed_name}, 'date': {'S': date} } attributes = attributes or {} attributes['status'] = overall_status expression_attribute_names = { f'#{k}': k for k in attributes.keys() } expression_attribute_values = { f':{k}': serializer.serialize(v) for k, v in attributes.items() } update_expression = ', '.join( f'#{k} = :{k}' for k in attributes.keys() ) update_expression = f'SET {update_expression}' dynamodb.update_item( TableName=config.feed_ingestion_table, Key=key, UpdateExpression=update_expression, ExpressionAttributeNames=expression_attribute_names, ExpressionAttributeValues=expression_attribute_values) def get_overall_status(feed_name, date): """Get status of the ingestion job Args: feed_name (str): Feed name date (str): Reporting date of the ingestion job Returns: mixed (False | status code): False if item not found. Status code if item is found. """ item = _get_item(feed_name, date) if item: return item.get('status') return False def get_status(feed_name, date, file_name): """Get status of extracting the file Args: feed_name (str): Feed name date (str): Reporting date of the ingestion job file_name (str): name of the file that is being extracted Returns: str: status (NOT_AVAILABLE | DOWNLOADED) """ item = _get_item(feed_name, date) if item: attribute_name = get_attribute_name_for_file_status(file_name, date) return item.get(attribute_name) def _determine_overall_status(feed_name, date): """Set overall status of the feed Args: feed_name (str): name of the feed date (str): reporting date of the ingestion job Returns: str: overall status """ overall_status = STATUS_NOT_INGESTED item = _get_item(feed_name, date) assert item, 'No status found for feed {feed_name} and date {date}'. \ format(feed_name=feed_name, date=date) available_statuses = [STATUS_NOT_AVAILABLE, STATUS_DOWNLOADED] non_file_keys = [ 'feed_name', 'date', 'status', 'completed_tasks_status'] statuses = [item[k] for k in item if k not in non_file_keys] num_of_files = len(statuses) for status in available_statuses: if num_of_files == statuses.count(status): overall_status = status return overall_status def _get_item(feed_name, date): """Get Dynamodb item Args: feed_name (str): Feed name date (str): Reporting date of the ingestion job Returns: mixed (False | status code): False if item not found. item if found. """ assert feed_name, 'feed_name is required.' assert date, 'date is required.' response = dynamodb.get_item( TableName=config.feed_ingestion_table, Key={ 'feed_name': {'S': feed_name}, 'date': {'S': date} } ) if 'Item' in response: item = response['Item'] # keys of the item are attribute names # values of the item are dicts in form {'S': 'actual_value'} # convert each item value to actual_value: return {k: next(iter(v.values())) for k, v in item.items()} return def delete_status(feed_name, date): """Delete item from dynamodb Args: feed_name (str): name fo the feed date (str): ingestion date """ dynamodb.delete_item( TableName=config.feed_ingestion_table, Key={ 'feed_name': {'S': feed_name}, 'date': {'S': date} } ) def get_missing_dates(feed_name, day_range=14, date_format='%Y-%m-%d'): """Find previous dates on which data was not ingested yet Args: feed_name (str): data feed name. day_range (int[optional]): number of days back from which search is being done. date_format (str[optional]): format of a date. Returns: deque: deque of dates string """ date_to_be_checked = datetime.date.today() - datetime.timedelta( days=day_range) dates = deque() while date_to_be_checked <= datetime.date.today(): status = get_overall_status( feed_name, date_to_be_checked.strftime(date_format)) if status != STATUS_INGESTED: dates.append(str(date_to_be_checked)) date_to_be_checked = date_to_be_checked + datetime.timedelta(days=1) return dates def find_non_processed_date(feed_name, available_dates): """Find files that have not been processed Args: feed_name (str): name of the feed available_dates (list): list of dates in str """ for available_date in available_dates: status = get_overall_status(feed_name, available_date) if status != STATUS_INGESTED: return available_date