"""Main Handler.""" from datetime import datetime import os from botocore.exceptions import ClientError from bulk_metadata_ingester_common.constants.file import ( CSV_DELIMITER, CSV_QUOTECHAR, INPUT_FILE_EXT_LIST) from bulk_metadata_ingester_common.utils.data import pivot_bulk_file from bulk_metadata_ingester_common.utils.jitter import jitter from bulk_metadata_ingester_common.utils.logging import get_current_logger import config from config import graphql_gateway from exceptions import FileTypeException from lambdacommon.aws import s3 import pandas as pd import simplejson def handler(event, context): """Lambda Entrypoint.""" key = event.get('key') bucket = event.get('bucket') correlation_id = event.get('correlation_id') logger = get_current_logger( config.ENVIRONMENT, config.LAMBDA_NAME, logging_level=config.LOGGING_LEVEL, correlation_id=correlation_id) # Jitter calls - Sleep for randomness jitter(logger) graphql_gateway.set_headers( { 'Orchard-User-Id': config.OA_USER, 'Correlation-Id': correlation_id } ) # Log lambda begins message logger.info(f'convert_csv_to_json received: {event}') try: # Check object head = s3.head_object(bucket, key) # Grab mimetype content_type = head['ContentType'] except ClientError as e: if e.response['Error']['Code'] == 'NoSuchKey': logger.error(f'S3 object at {bucket}/{key} not found: {str(e)}') return else: raise ClientError() from e # Log filename and mime to console logger.info(f'Found file {bucket}/{key}. ContentType: {content_type}.') # Confirm file is expected type if os.path.splitext(key)[1] in INPUT_FILE_EXT_LIST: # Convert CSV or XLSX file to OrderedDict conversion_response = convert_file_to_dict(bucket, key, logger) else: raise FileTypeException( f'S3 object {bucket}/{key} is not a valid file type.') # Dump OrderedDict to JSON # We use simplejson here for the ignore_nan flag, for more details check: # https://stackoverflow.com/questions/28639953/python-json-encoder-convert-nans-to-null-instead/28642022#28642022 # noqa: E501 ingestion_json = simplejson.dumps(conversion_response, ignore_nan=True) # Get "today" at runtime # "2023-05-09T10:15:16.136767" to "2023-05-09T10-15-16" today = datetime.now().isoformat().replace(':', '-').split('.')[0] # Make JSON key with the same name as the CSV key split_key = key.split('/') file_name = split_key[-1].split('.')[0] full_file_key = 'generated_json/' + file_name + '_' + today + '.json' # Write JSON in same dir as source key as list s3.resource.Object(bucket, full_file_key).put(Body=ingestion_json) # Construct the list of keys in the JSON file items = list(dict(conversion_response).keys()) # Return result result = { **event, 'key': full_file_key, 'orig_key': key, 'items': items, } return result def convert_file_to_dict(bucket: str, key: str, logger: object) -> dict: """Convert a CSV or XLSX file to a dict.""" # Log lambda begins message logger.info(f'Processing CSV at {bucket}/{key}.') # Parse file to pandas Dataframe df = read_s3_file_to_dataframe(bucket, key) # Parse Dataframe to dict, pivoted on `project_code_____product_code` data_by_release = pivot_bulk_file(df, logger) # Return parsed, pivoted dict return data_by_release def read_s3_file_to_dataframe(bucket: str, key: str) -> object: """Read a CSV or XLSX file from S3 into a pandas dataframe.""" # Grab CSV from S3 obj = s3.client.get_object(Bucket=bucket, Key=key) if key and key.endswith('.xlsx'): # Load Excel as panda DataFrame # obj['Body'] is of type StreamingBody, read() returns binary data # S3 StreamingBody does not support seek so you can't pass it to pandas # https://github.com/boto/boto3/issues/564#issuecomment-201357974 df = pd.read_excel(obj['Body'].read(), na_filter=False) else: # Load CSV as panda DataFrame df = pd.read_csv( obj['Body'], # sep=CSV_VALUE_SEPARATOR, delimiter=CSV_DELIMITER, quotechar=CSV_QUOTECHAR, na_filter=False) return df