Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logging, Exception Management Assignment #30

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions fast_api_als/database/db_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,9 @@ def verify_response(response_code):

session = get_boto3_session()
db_helper_session = DBHelper(session)


def log_res_data(obj : DBHelper):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is this function called?

res = obj.table.some_operation()
http_code = res['ResponseMetadata']['HTTPStatusCode']
logging.info("Response status from table operation:"+str(http_code))
6 changes: 4 additions & 2 deletions fast_api_als/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import time

import logging
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fast_api_als.routers import users, submit_lead, lead_conversion, reinforcement, oem, three_pl, quicksight
Expand All @@ -25,6 +25,8 @@
allow_headers=["*"],
)

# to ensure logs don't print to stdout and data is not lost after clearing output
logging.basicConfig(filename = "fast_api_als.log", level = logging.INFO)

@app.get("/")
def root():
Expand All @@ -35,4 +37,4 @@ def root():
def ping():
start = time.process_time()
time_taken = (time.process_time() - start) * 1000
return {f"Pong with response time {time_taken} ms"}
return {f"Ping with response time {time_taken} ms"}
21 changes: 16 additions & 5 deletions fast_api_als/routers/lead_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
import time

from fastapi import Request
from fastapi import Request, HTTPException
from starlette import status

from fast_api_als.database.db_helper import db_helper_session
Expand All @@ -26,6 +26,15 @@ def get_quicksight_data(lead_uuid, item):
Returns:
S3 data
"""
try:
assert(lead_uuid != None)
except AssertionError:
logging.error("lead_uuid should not be null")
raise AssertionError

if "make" not in item.keys() or "model" not in item.keys():
logging.error("model and make fields missing in item")
raise KeyError
data = {
"lead_hash": lead_uuid,
"epoch_timestamp": int(time.time()),
Expand All @@ -47,15 +56,17 @@ async def submit(file: Request, token: str = Depends(get_token)):

if 'lead_uuid' not in body or 'converted' not in body:
# throw proper HTTPException
pass
logging.error("lead_uuid and converted both keys not present in response body")
raise HTTPException(status_code = 406, detail = "lead_uuid and converted both keys not present in response")

lead_uuid = body['lead_uuid']
converted = body['converted']

oem, role = get_user_role(token)
if role != "OEM":
# throw proper HTTPException
pass
logging.error("Role not OEM")
raise HTTPException(status_code = 406, detail = "Role required to be OEM")

is_updated, item = db_helper_session.update_lead_conversion(lead_uuid, oem, converted)
if is_updated:
Expand All @@ -66,5 +77,5 @@ async def submit(file: Request, token: str = Depends(get_token)):
"message": "Lead Conversion Status Update"
}
else:
# throw proper HTTPException
pass
logging.error("Lead conversion not updated")
raise HTTPException(status_code = 406, detail = "Lead conversion not updated")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*already updated, status_code should be 400

19 changes: 16 additions & 3 deletions fast_api_als/routers/submit_lead.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from datetime import datetime
from fastapi import APIRouter
from fastapi import Request, Depends
from fastapi import Request, Depends, HTTPException
from fastapi.security.api_key import APIKey
from concurrent.futures import ThreadPoolExecutor, as_completed

Expand Down Expand Up @@ -38,7 +38,8 @@ async def submit(file: Request, apikey: APIKey = Depends(get_api_key)):

if not db_helper_session.verify_api_key(apikey):
# throw proper fastpi.HTTPException
pass
logging.error("Invalid API key")
raise HTTPException(status_code = 403, detail = "API key invalid")

body = await file.body()
body = str(body, 'utf-8')
Expand All @@ -47,6 +48,7 @@ async def submit(file: Request, apikey: APIKey = Depends(get_api_key)):

# check if xml was not parsable, if not return
if not obj:
logging.error("XML could not be parsed")
provider = db_helper_session.get_api_key_author(apikey)
obj = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try except for line 59

'provider': {
Expand All @@ -60,14 +62,16 @@ async def submit(file: Request, apikey: APIKey = Depends(get_api_key)):
"code": "1_INVALID_XML",
"message": "Error occured while parsing XML"
}

logging.info("Calculating lead hash")
lead_hash = calculate_lead_hash(obj)
logging.info("Lead has calculated:"+str(lead_hash))

# check if adf xml is valid
validation_check, validation_code, validation_message = check_validation(obj)

#if not valid return
if not validation_check:
logging.error("Validation failed")
item, path = create_quicksight_data(obj['adf']['prospect'], lead_hash, 'REJECTED', validation_code, {})
s3_helper_client.put_file(item, path)
return {
Expand Down Expand Up @@ -95,11 +99,13 @@ async def submit(file: Request, apikey: APIKey = Depends(get_api_key)):
for future in as_completed(futures):
result = future.result()
if result.get('Duplicate_Api_Call', {}).get('status', False):
logging.warning("Duplicate API call")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log info for the time taken for request completion

return {
"status": f"Already {result['Duplicate_Api_Call']['response']}",
"message": "Duplicate Api Call"
}
if result.get('Duplicate_Lead', False):
logging.error("Duplicate lead")
return {
"status": "REJECTED",
"code": "12_DUPLICATE",
Expand All @@ -108,12 +114,14 @@ async def submit(file: Request, apikey: APIKey = Depends(get_api_key)):
if "fetch_oem_data" in result:
fetched_oem_data = result['fetch_oem_data']
if fetched_oem_data == {}:
logging.error("Empty response")
return {
"status": "REJECTED",
"code": "20_OEM_DATA_NOT_FOUND",
"message": "OEM data not found"
}
if 'threshold' not in fetched_oem_data:
logging.error("Threshold field not present")
return {
"status": "REJECTED",
"code": "20_OEM_DATA_NOT_FOUND",
Expand All @@ -123,6 +131,7 @@ async def submit(file: Request, apikey: APIKey = Depends(get_api_key)):

# if dealer is not available then find nearest dealer
if not dealer_available:
logging.info("Current dealer not available, finding nearest dealer")
lat, lon = get_customer_coordinate(obj['adf']['prospect']['customer']['contact']['address']['postalcode'])
nearest_vendor = db_helper_session.fetch_nearest_dealer(oem=make,
lat=lat,
Expand All @@ -145,13 +154,15 @@ async def submit(file: Request, apikey: APIKey = Depends(get_api_key)):
response_body["status"] = "ACCEPTED"
response_body["code"] = "0_ACCEPTED"
else:
logging.info("Low score")
response_body["status"] = "REJECTED"
response_body["code"] = "16_LOW_SCORE"

# verify the customer
if response_body['status'] == 'ACCEPTED':
contact_verified = await new_verify_phone_and_email(email, phone)
if not contact_verified:
logging.info("Contact not verified")
response_body['status'] = 'REJECTED'
response_body['code'] = '17_FAILED_CONTACT_VALIDATION'

Expand All @@ -161,6 +172,7 @@ async def submit(file: Request, apikey: APIKey = Depends(get_api_key)):
# insert the lead into ddb with oem & customer details
# delegate inserts to sqs queue
if response_body['status'] == 'ACCEPTED':
logging.info("Accepted current response")
make_model_filter = db_helper_session.get_make_model_filter_status(make)
message = {
'put_file': {
Expand Down Expand Up @@ -199,6 +211,7 @@ async def submit(file: Request, apikey: APIKey = Depends(get_api_key)):
res = sqs_helper_session.send_message(message)

else:
logging.info("Rejected current response")
message = {
'put_file': {
'item': item,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line 226 try except block

Expand Down
18 changes: 15 additions & 3 deletions fast_api_als/routers/three_pl.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@ async def reset_authkey(request: Request, token: str = Depends(get_token)):
body = json.loads(body)
provider, role = get_user_role(token)
if role != "ADMIN" and (role != "3PL"):
logging.info("Role is neither Admin nor 3PL")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raise http exception with proper status code, add info logs where necessary

pass
if role == "ADMIN":
logging.info("Role is Admin")
provider = body['3pl']
apikey = db_helper_session.set_auth_key(username=provider)
try:
apikey = db_helper_session.set_auth_key(username=provider)
except Exception as e:
logging.error("API key not set:"+str(e.message))
raise e
return {
"status_code": HTTP_200_OK,
"x-api-key": apikey
Expand All @@ -33,11 +39,17 @@ async def view_authkey(request: Request, token: str = Depends(get_token)):
body = json.loads(body)
provider, role = get_user_role(token)

if role != "ADMIN" and role != "3PL":
if role != "ADMIN" and (role != "3PL"):
logging.info("Role is neither Admin nor 3PL")
pass
if role == "ADMIN":
logging.info("Role is Admin")
provider = body['3pl']
apikey = db_helper_session.get_auth_key(username=provider)
try:
apikey = db_helper_session.get_auth_key(username=provider)
except Exception as e:
logging.error("API key not found:"+str(e.message))
raise e
return {
"status_code": HTTP_200_OK,
"x-api-key": apikey
Expand Down
2 changes: 2 additions & 0 deletions fast_api_als/services/enrich_lead.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@


def get_enriched_lead_json(adf_json: dict) -> dict:
if adf_json == {}:
logging.warning("Empty JSON")
pass

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raise KeyError for missing 'adf' key and check if adj_json is valid to raise ValueError

1 change: 1 addition & 0 deletions fast_api_als/services/verify_phone_and_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

info logs missing throughout the file

async def call_validation_service(url: str, topic: str, value: str, data: dict) -> None: # 2
if value == '':
logging.info("Found empty value")
return
async with httpx.AsyncClient() as client: # 3
response = await client.get(url)
Expand Down
30 changes: 22 additions & 8 deletions fast_api_als/utils/adf.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ def process_before_validating(input_json):
def validate_iso8601(requestdate):
try:
if match_iso8601(requestdate) is not None:
logging.info(f"Match found for {requestdate}")
return True
except:
pass
return False
else:
logging.error(f"No match found for {requestdate}")
return False
except Exception as e:
logging.error("ISO validation failed:"+str(e.message))
return False


def is_nan(x):
Expand All @@ -39,8 +43,13 @@ def is_nan(x):

def parse_xml(adf_xml):
# use exception handling
obj = xmltodict.parse(adf_xml)
return obj
try:
obj = xmltodict.parse(adf_xml)
logging.info("XML parsed successfully")
return obj
except Exception as e:
logging.error("XML not parsed successfully:"+str(e.message))
return None


def validate_adf_values(input_json):
Expand All @@ -60,14 +69,17 @@ def validate_adf_values(input_json):

if not first_name or not last_name:
return {"status": "REJECTED", "code": "6_MISSING_FIELD", "message": "name is incomplete"}
logging.info("First Name and Last Name field present")

if not email and not phone:
return {"status": "REJECTED", "code": "6_MISSING_FIELD", "message": "either phone or email is required"}
logging.info("Email and Phone field present")

# zipcode validation
res = zipcode_search.by_zipcode(zipcode)
if not res:
return {"status": "REJECTED", "code": "4_INVALID_ZIP", "message": "Invalid Postal Code"}
logging.info("Zip code validated")

# check for TCPA Consent
tcpa_consent = False
Expand All @@ -76,11 +88,11 @@ def validate_adf_values(input_json):
tcpa_consent = True
if not email and not tcpa_consent:
return {"status": "REJECTED", "code": "7_NO_CONSENT", "message": "Contact Method missing TCPA consent"}

logging.info("TCPA Consent present")

# request date in ISO8601 format
if not validate_iso8601(input_json['requestdate']):
return {"status": "REJECTED", "code": "3_INVALID_FIELD", "message": "Invalid DateTime"}

return {"status": "OK"}


Expand All @@ -94,8 +106,10 @@ def check_validation(input_json):
)
response = validate_adf_values(input_json)
if response['status'] == "REJECTED":
logging.error("Response status REJECTED")
return False, response['code'], response['message']
logging.info("Response status OK")
return True, "input validated", "validation_ok"
except Exception as e:
logger.error(f"Validation failed: {e.message}")
logging.error(f"Validation failed: {e.message}")
return False, "6_MISSING_FIELD", e.message