From fe25701659d3979cc5e75de711c890347af6c983 Mon Sep 17 00:00:00 2001 From: Dom Date: Mon, 15 Jul 2024 07:57:02 +0100 Subject: [PATCH] Breaking functions into separate files, moving Discord notifications to embeds, adding shipment IDs & destinations to notifications --- ShipmentNotifier.py | 169 +++++++++------------------------------- amazonAPI.py | 78 +++++++++++++++++++ discordNotifications.py | 36 +++++++++ log.py | 26 +++++++ sentNotifications.py | 19 +++++ timeOperations.py | 16 ++++ 6 files changed, 210 insertions(+), 134 deletions(-) create mode 100644 amazonAPI.py create mode 100644 discordNotifications.py create mode 100644 log.py create mode 100644 sentNotifications.py create mode 100644 timeOperations.py diff --git a/ShipmentNotifier.py b/ShipmentNotifier.py index 82b8790..f8f9490 100644 --- a/ShipmentNotifier.py +++ b/ShipmentNotifier.py @@ -1,144 +1,45 @@ -import requests -import json -import yaml -import logging -from datetime import datetime, timedelta +from log import log +from timeOperations import isInboundShipmentPlanWithinSpecifiedDelta +from amazonAPI import getAccessToken, getInboundShipmentData, getInboundShipmentPlans, getInboundShipmentPlan, getProductName +from sentNotifications import isInboundShipmentPlanIDInSentNotifications, updateSentNotifications +from discordNotifications import sendDiscordNotification -SETTINGS = yaml.safe_load(open('settings.yaml')) +def parseInboundShipmentPlans(): + log('\U0001F504 Getting inbound shipment plans...', 'info') + inboundShipmentPlans = getInboundShipmentPlans() + inboundShipmentPlanIDs = [] + inboundShipmentPlanData = {} -def log(log_message, level): - logger = logging.getLogger('sn-logger') + for plan in inboundShipmentPlans: + if isInboundShipmentPlanWithinSpecifiedDelta(plan['createdAt']): + log('Adding inbound shipment plan to list: {}'.format(plan['inboundPlanId']), 'info') + inboundShipmentPlanIDs.append(plan['inboundPlanId']) + inboundShipmentPlanData.update({plan['inboundPlanId']: {'creationDate': plan['createdAt']}}) - log_message_types = { - 'debug': logger.debug, - 'info': logger.info, - 'warning': logger.warning, - 'error': logger.error, - 'critical': logger.critical - } - - if not logger.handlers: - logger.setLevel(logging.DEBUG) - - formatter = logging.Formatter('[%(levelname)s] %(message)s') - - file_handler = logging.FileHandler('logs/' + 'log_' + datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + '.log') - file_handler.setLevel(logging.DEBUG) - file_handler.setFormatter(formatter) - - logger.addHandler(file_handler) - - log_message_types[level](log_message) - -def updateSentNotifications(inboundPlanId): - with open('SentNotifications.json') as NotificationsSentJson: - NotificationsSent = json.load(NotificationsSentJson) - - NotificationsSent['NotificationsSent'].append(inboundPlanId) - - with open('SentNotifications.json', mode='w') as outputNotificationsSent: - outputNotificationsSent.write(json.dumps(NotificationsSent, indent=4)) - -def isIDInSentNotifications(inboundPlanId): - with open('SentNotifications.json') as NotificationsSentJson: - NotificationsSent = json.load(NotificationsSentJson) - - if inboundPlanId in NotificationsSent['NotificationsSent']: - return True - else: - return False - -def isShipmentWithinSpecifiedDelta(shipmentCreationTime, delta=360): - currentTime = datetime.now() - shipmentTime = datetime.strptime(shipmentCreationTime, '%Y-%m-%dT%H:%M:%SZ') - timeDelta = currentTime - shipmentTime - - log(f'Current time: {currentTime}', 'info') - log(f'Shipment creation time: {shipmentTime}', 'info') - log(f'Time delta: {timeDelta}', 'info') - - if timeDelta < timedelta(minutes=delta): - return True - else: - return False - -def getAccessToken(settings=SETTINGS): - AccessToken = requests.post( - 'https://api.amazon.com/auth/o2/token', - { - 'grant_type': 'refresh_token', - 'refresh_token': settings['REFRESH_TOKEN'], - 'client_id': settings['CLIENT_ID'], - 'client_secret': settings['CLIENT_SECRET'], - } - ) - - return AccessToken.json()['access_token'] - -def getProductName(SKU, settings=SETTINGS): - product = requests.get( - settings['SPAPI_ENDPOINT'] + '/listings/2021-08-01/items/' + settings['SELLER_ID'] + f'/{SKU}', - headers = { - 'x-amz-access-token': getAccessToken(), - }, - params = { - 'marketplaceIds': settings['MARKETPLACE_ID'] - }, - ) - - return product.json()['summaries'][0]['itemName'] - -def sendDiscordNotification(settings=SETTINGS, content=None): - notification = {"content": content} - requests.post(settings['DISCORD_WEBHOOK'], json=notification) - -def getInboundShipments(settings=SETTINGS): - InboundShipments = requests.get( - settings['SPAPI_ENDPOINT'] + '/inbound/fba/2024-03-20/inboundPlans?pageSize=10&sortBy=CREATION_TIME&sortOrder=DESC&status=SHIPPED', - headers = { - 'x-amz-access-token': getAccessToken(), - } - ) - - return InboundShipments.json()['inboundPlans'] - -def parseInboundShipments(settings=SETTINGS): - log('\U0001F504 Getting shipments...', 'info') - InboundShipments = getInboundShipments() - inboundPlanIDs = [] - shipmentData = {} - - for shipment in InboundShipments: - if isShipmentWithinSpecifiedDelta(shipment['createdAt']): - log('Adding inbound plan to list: {}'.format(shipment['inboundPlanId']), 'info') - inboundPlanIDs.append(shipment['inboundPlanId']) - - if inboundPlanIDs: - log('\U0001F440 Checking shipments...', 'info') - log(f'Shipments to check: {len(inboundPlanIDs)}', 'info') + if inboundShipmentPlanIDs: + log('\U0001F440 Checking inbound shipment plans...', 'info') + log(f'Shipments to check: {len(inboundShipmentPlanIDs)}', 'info') - for ID in inboundPlanIDs: - if isIDInSentNotifications(ID): - log(f'Ignoring {ID}, notification has already been sent', 'info') - elif not isIDInSentNotifications(ID): - getShipment = requests.get( - settings['SPAPI_ENDPOINT'] + f'/inbound/fba/2024-03-20/inboundPlans/{ID}/items', - headers = { - 'x-amz-access-token': getAccessToken(), - } - ) - if getShipment.json()['items']: + for inboundShipmentPlanID in inboundShipmentPlanIDs: + if isInboundShipmentPlanIDInSentNotifications(inboundShipmentPlanID): + log(f'Ignoring inbound shipment plan {inboundShipmentPlanID}, notification has already been sent', 'info') + elif not isInboundShipmentPlanIDInSentNotifications(inboundShipmentPlanID): + inboundShipmentPlan = getInboundShipmentPlan(inboundShipmentPlanID) + if inboundShipmentPlan['items']: itemDict = {} totalItemCount = 0 - for item in getShipment.json()['items']: + for item in inboundShipmentPlan['items']: productName = getProductName(item['msku']) itemDict.update({productName: item.get('quantity')}) totalItemCount += item['quantity'] - itemDict.update({'Total item count': totalItemCount}) - shipmentData.update({ID: itemDict}) - log(f'\U0001F514 Sending Discord notification for {ID}...', 'info') - newline = '\n' - sendDiscordNotification(content=f':package: New shipment detected :package:\nShipment contents:\n{newline.join(f"- {MSKU}: {Count}" for MSKU, Count in shipmentData[ID].items())}') - updateSentNotifications(ID) + itemDict.update({'Total unit count': totalItemCount}) + inboundShipmentPlanData[inboundShipmentPlanID]['contents'] = itemDict + inboundShipmentData = getInboundShipmentData(inboundShipmentPlanID) + inboundShipmentPlanData[inboundShipmentPlanID]['destinations'] = inboundShipmentData[inboundShipmentPlanID]['destinations'] + inboundShipmentPlanData[inboundShipmentPlanID]['shipmentIDs'] = inboundShipmentData[inboundShipmentPlanID]['shipmentIDs'] + log(f'\U0001F514 Sending Discord notification for {inboundShipmentPlanID}...', 'info') + sendDiscordNotification(inboundShipmentPlanID, inboundShipmentPlanData[inboundShipmentPlanID]) + updateSentNotifications(inboundShipmentPlanID) -parseInboundShipments() \ No newline at end of file +if __name__ == '__main__': + parseInboundShipmentPlans() \ No newline at end of file diff --git a/amazonAPI.py b/amazonAPI.py new file mode 100644 index 0000000..7908005 --- /dev/null +++ b/amazonAPI.py @@ -0,0 +1,78 @@ +import requests +import json +import yaml + +from log import log +from discordNotifications import sendDiscordNotification +from sentNotifications import isInboundShipmentPlanIDInSentNotifications, updateSentNotifications +from timeOperations import isInboundShipmentPlanWithinSpecifiedDelta + +SETTINGS = yaml.safe_load(open('settings.yaml')) + +def getAccessToken(settings=SETTINGS): + accessToken = requests.post( + 'https://api.amazon.com/auth/o2/token', + { + 'grant_type': 'refresh_token', + 'refresh_token': settings['REFRESH_TOKEN'], + 'client_id': settings['CLIENT_ID'], + 'client_secret': settings['CLIENT_SECRET'], + } + ) + + return accessToken.json()['access_token'] + +def getProductName(MSKU, settings=SETTINGS): + product = requests.get( + settings['SPAPI_ENDPOINT'] + '/listings/2021-08-01/items/' + settings['SELLER_ID'] + f'/{MSKU}', + headers = { + 'x-amz-access-token': getAccessToken(), + }, + params = { + 'marketplaceIds': settings['MARKETPLACE_ID'] + }, + ) + + return product.json()['summaries'][0]['itemName'] + +def getInboundShipmentPlans(settings=SETTINGS): + inboundShipmentPlans = requests.get( + settings['SPAPI_ENDPOINT'] + '/inbound/fba/2024-03-20/inboundPlans?pageSize=10&sortBy=CREATION_TIME&sortOrder=DESC&status=SHIPPED', + headers = { + 'x-amz-access-token': getAccessToken(), + } + ) + + return inboundShipmentPlans.json()['inboundPlans'] + +def getInboundShipmentPlan(inboundShipmentPlanID, settings=SETTINGS): + inboundShipmentPlan = requests.get( + settings['SPAPI_ENDPOINT'] + f'/inbound/fba/2024-03-20/inboundPlans/{inboundShipmentPlanID}/items', + headers = { + 'x-amz-access-token': getAccessToken(), + } + ) + + return inboundShipmentPlan.json() + +def getInboundShipmentData(inboundPlanId, settings=SETTINGS): + inboundShipmentData = {inboundPlanId: {'destinations': [], 'shipmentIDs': []}} + + inboundPlanShipmentData = requests.get( + settings['SPAPI_ENDPOINT'] + f'/inbound/fba/2024-03-20/inboundPlans/{inboundPlanId}/placementOptions', + headers = { + 'x-amz-access-token': getAccessToken(), + } + ) + + for shipmentID in inboundPlanShipmentData.json()['placementOptions'][0]['shipmentIds']: + individualShipmentData = requests.get( + settings['SPAPI_ENDPOINT'] + f'/inbound/fba/2024-03-20/inboundPlans/{inboundPlanId}/shipments/{shipmentID}', + headers = { + 'x-amz-access-token': getAccessToken(), + } + ) + inboundShipmentData[inboundPlanId]['shipmentIDs'].append(individualShipmentData.json()['shipmentConfirmationId']) + inboundShipmentData[inboundPlanId]['destinations'].append(individualShipmentData.json()['destination']['warehouseId']) + + return inboundShipmentData diff --git a/discordNotifications.py b/discordNotifications.py new file mode 100644 index 0000000..9e1b83b --- /dev/null +++ b/discordNotifications.py @@ -0,0 +1,36 @@ +import yaml +import requests + +SETTINGS = yaml.safe_load(open('settings.yaml')) + +def sendDiscordNotification(*args, settings=SETTINGS): + newLine = '\n' + shipmentNotification = requests.post( + SETTINGS['DISCORD_WEBHOOK'], + json = { + "embeds": [ + { + "title": ":package: New Inbound Shipment Detected :package:", + "url": f"https://sellercentral.amazon.co.uk/fba/sendtoamazon?wf={args[0]}", + "color": 4886754, + "fields": [ + { + "name": "Shipment Information", + "value": f''' + > Inbound Shipment Plan ID: {args[0]} + > Creation Date: {args[1]['creationDate']} + > Destination Fulfilment Centres: {', '.join(destination for destination in args[1]['destinations'])} + > Shipments: {', '.join(f'[{shipmentID}](https://sellercentral.amazon.co.uk/fba/inbound-shipment/summary/{shipmentID}/contents)' for shipmentID in args[1]['shipmentIDs'])} + ''' + }, + { + "name": "Shipment Contents", + "value": f''' + {newLine.join(f"> {MSKU}: {Count}" for MSKU, Count in args[1]['contents'].items())} + ''' + } + ] + } + ] + }, + ) \ No newline at end of file diff --git a/log.py b/log.py new file mode 100644 index 0000000..e080780 --- /dev/null +++ b/log.py @@ -0,0 +1,26 @@ +import logging +from datetime import datetime + +def log(log_message, level): + logger = logging.getLogger('sn-logger') + + log_message_types = { + 'debug': logger.debug, + 'info': logger.info, + 'warning': logger.warning, + 'error': logger.error, + 'critical': logger.critical + } + + if not logger.handlers: + logger.setLevel(logging.DEBUG) + + formatter = logging.Formatter('[%(levelname)s] %(message)s') + + file_handler = logging.FileHandler('logs/' + 'log_' + datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + '.log') + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(formatter) + + logger.addHandler(file_handler) + + log_message_types[level](log_message) \ No newline at end of file diff --git a/sentNotifications.py b/sentNotifications.py new file mode 100644 index 0000000..03622ad --- /dev/null +++ b/sentNotifications.py @@ -0,0 +1,19 @@ +import json + +def updateSentNotifications(inboundPlanId): + with open('SentNotifications.json') as sentNotificationsJSON: + sentNotifications = json.load(sentNotificationsJSON) + + sentNotifications['sentNotifications'].append(inboundPlanId) + + with open('SentNotifications.json', mode='w') as outputSentNotifications: + outputSentNotifications.write(json.dumps(sentNotifications, indent=4)) + +def isInboundShipmentPlanIDInSentNotifications(inboundShipmentPlanId): + with open('SentNotifications.json') as sentNotificationsJSON: + sentNotifications = json.load(sentNotificationsJSON) + + if inboundShipmentPlanId in sentNotifications['sentNotifications']: + return True + else: + return False \ No newline at end of file diff --git a/timeOperations.py b/timeOperations.py new file mode 100644 index 0000000..be88ded --- /dev/null +++ b/timeOperations.py @@ -0,0 +1,16 @@ +from datetime import datetime, timedelta +from log import log + +def isInboundShipmentPlanWithinSpecifiedDelta(inboundShipmentPlanCreationTime, delta=360): + currentTime = datetime.now() + inboundShipmentPlanTime = datetime.strptime(inboundShipmentPlanCreationTime, '%Y-%m-%dT%H:%M:%SZ') + timeDelta = currentTime - inboundShipmentPlanTime + + log(f'Current time: {currentTime}', 'info') + log(f'Inbound shipment plan creation time: {inboundShipmentPlanTime}', 'info') + log(f'Time delta: {timeDelta}', 'info') + + if timeDelta < timedelta(minutes=delta): + return True + else: + return False \ No newline at end of file