import ast import decimal import json import logging import operator import os from copy import deepcopy from io import BytesIO import boto3 import pandas as pd from jinjasql import JinjaSql from sqlalchemy import create_engine logger = logging.getLogger('logger') logger.setLevel(logging.INFO) DATABASE_NAME = os.getenv('DATABASE_NAME') # TODO! in prod, remove this if os.getenv('PROFILE') == 'local': boto3.setup_default_session(profile_name='fansifter') # fansifter #boto3.setup_default_session(profile_name='fansifter') # TODO! remove def get_sql_from_template(query, bind_params): if not bind_params: return query params = deepcopy(bind_params) for key, val in params.items(): params[key] = val return query % params def get_rendered_sql_template(params, template_sql): """ Apply a JinjaSql template (string) substituting parameters (dict) and return the final SQL. """ current_path = os.path.dirname(__file__) template_file = open(f'{current_path}/../sql/{template_sql}', 'r').read() j = JinjaSql(param_style='pyformat') query, bind_params = j.prepare_query(template_file, params) return get_sql_from_template(query, bind_params) def get_secret(secret_name): """ This needs AWS default config (or profile named "fansifter") """ region_name = "eu-west-1" # Create a Secrets Manager client session = boto3.session.Session() # profile_name='fansifter' client = session.client( service_name='secretsmanager', region_name=region_name ) get_secret_value_response = client.get_secret_value(SecretId=secret_name)['SecretString'] # In this sample we only handle the specific exceptions for the 'GetSecretValue' API. # See https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html # We rethrow the exception by default. """try: get_secret_value_response = client.get_secret_value( SecretId=secret_name ) except ClientError as e: if e.response['Error']['Code'] == 'DecryptionFailureException': # Secrets Manager can't decrypt the protected secret text using the provided KMS key. # Deal with the exception here, and/or rethrow at your discretion. raise e elif e.response['Error']['Code'] == 'InternalServiceErrorException': # An error occurred on the server side. # Deal with the exception here, and/or rethrow at your discretion. raise e elif e.response['Error']['Code'] == 'InvalidParameterException': # You provided an invalid value for a parameter. # Deal with the exception here, and/or rethrow at your discretion. raise e elif e.response['Error']['Code'] == 'InvalidRequestException': # You provided a parameter value that is not valid for the current state of the resource. # Deal with the exception here, and/or rethrow at your discretion. raise e elif e.response['Error']['Code'] == 'ResourceNotFoundException': # We can't find the resource that you asked for. # Deal with the exception here, and/or rethrow at your discretion. raise e else: # Decrypts secret using the associated KMS CMK. # Depending on whether the secret is a string or binary, one of these fields will be populated. if 'SecretString' in get_secret_value_response: secret = get_secret_value_response['SecretString'] else: decoded_binary_secret = base64.b64decode(get_secret_value_response['SecretBinary']) """ return get_secret_value_response def get_rds_engine(DATABASE_NAME): """ Get RDS connection for a given db instance """ # TODO! unify naming in fargate templates and secret names so you can use get_secret(DATABASE_NAME) once if DATABASE_NAME == 'fansifter': secret = get_secret("fansifter-rds") elif DATABASE_NAME == 'fansifter-dev': # For backwards compatibility, temporarily duplicates the above (until we've renamed the actual RDS). secret = get_secret("fansifter-rds") elif DATABASE_NAME == 'fansifter-live': secret = get_secret("fansifter-rds-live") elif DATABASE_NAME == 'fansifter-test': secret = get_secret("fansifter-rds-test") params = { 'host': ast.literal_eval(secret)['host'], 'port': ast.literal_eval(secret)['port'], 'dbname': ast.literal_eval(secret)['dbname'], 'user': ast.literal_eval(secret)['username'], 'password': ast.literal_eval(secret)['password'] } engine = create_engine("postgresql+psycopg2://{user}:{password}@{host}/{dbname}".format(**params), use_batch_mode=True, pool_size=20, max_overflow=100, pool_recycle=3600) # Add pool_pre_ping=True if SSL thing continues return engine def s3_to_pandas(bucket, key, header=None, file_format='csv'): """ Downloads S3 file and returns it in a pandas DataFrame. If you want to get header from file, pass header="infer". """ # logger.info(f'Attempting to read {key} file from {bucket} bucket') client = boto3.client('s3') temp_object = client.get_object(Bucket=bucket, Key=key)['Body'].read(100) obj = client.get_object(Bucket=bucket, Key=key)['Body'] """ Find a deliminter. Although csv-sniffer in C and Python should do that automatically, they can fail sometimes, as was evident with "devel---useruploadoriginals/2c1a9dd1e9f518e1d4fee2f2a14917722b80da4b029e084fa64ceaeab322beb3/testcase.csv" We see the occuranes of ; , \t and decide what should be delimiter. We do this on temp_object, else we already start reading the obj stream. """ chars = str(temp_object) char_stats = {';': chars.count(";"), ",": chars.count(","), "\t": chars.count("\t"), } delimiter = max(char_stats.items(), key=operator.itemgetter(1))[0] # TODO! handle encoding guessing # import chardet # result = chardet.detect(temp_object) # charenc = result['encoding'] # component_logger.info(f' ENCODING = {charenc}') if not delimiter: raise Exception("We could not determine supported delimiters within the first 100 chars from file") if file_format == 'csv': return pd.read_csv(obj, sep=delimiter, header=header, dtype=str) elif file_format == 'xlsx' or file_format == 'xls': return pd.read_excel(BytesIO(obj.read())) # TODO! Add Error Handling for unsupported file type return False if __name__ == '__main__': """ # S3 test bucket = 'fansifter-model-data' key = 'data.csv' header = 'infer' df = s3_to_pandas(bucket, key, header) print(df.head())""" pass