refactored backend code

This commit is contained in:
ak 2023-12-26 20:59:32 -08:00
parent 486562f12c
commit 94adb2a820

View file

@ -1,43 +1,39 @@
from os import environ from os import environ
from io import BytesIO from io import BytesIO
import filetype
from dotenv import load_dotenv from dotenv import load_dotenv
from flask import Flask, request, abort, make_response, send_file from flask import Flask, request, abort, make_response, send_file
from flask_cors import CORS from flask_cors import CORS
from datetime import datetime from datetime import datetime
from deta import Deta from deta import Deta
import filetype
load_dotenv() load_dotenv()
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
deta = Deta() deta = Deta()
### connect to dbs and drive
itemsDB = deta.Base('items') itemsDB = deta.Base('items')
storesDB = deta.Base('stores') storesDB = deta.Base('stores')
drive = deta.Drive('beerbuddy') drive = deta.Drive('beerbuddy')
### reusable functions
# fetch requested store from store database based on request arguments
# arguments are assumed checked beforehand
def fetchStore(key = None): def fetchStore(key = None):
if key is not None: if key is not None: return storesDB.get(key)
data = storesDB.get(key)
return data
else: else:
northing = float(request.args['northing']) northing = float(request.args['northing'])
easting = float(request.args['easting']) easting = float(request.args['easting'])
zone = int(request.args['zone']) zone = int(request.args['zone'])
# fetch store from deta Base
fetch = storesDB.fetch({ fetch = storesDB.fetch({
'easting?r': [easting - 1, easting + 1], 'easting?r': [easting - 1, easting + 1],
'northing?r': [northing - 1, northing + 1], 'northing?r': [northing - 1, northing + 1],
'zone': zone, 'zone': zone,
}) })
data = fetch.items store = fetch.items[0]
# if we didn't find the record on the first go around, try again
if not data: if not store:
while (fetch.last is not None): while (fetch.last is not None):
fetch = storesDB.fetch({ fetch = storesDB.fetch({
'easting?r': [easting - 1, easting + 1], 'easting?r': [easting - 1, easting + 1],
@ -45,120 +41,106 @@ def fetchStore(key = None):
'zone': zone, 'zone': zone,
last: fetch.last last: fetch.last
}) })
# check with each loop iteration if we've found it
if fetch.items:
data = fetch.items
# if so, break loop
break
return data
# fetch requested item from item database based on request arguments and passed store if fetch.items:
store = fetch.items[0]
break
return store
def fetchItem(store): def fetchItem(store):
# fetch item from deta Base
fetch = itemsDB.fetch({ fetch = itemsDB.fetch({
'lowername': request.args['itemName'].lower(), 'lowername': request.args['itemName'].lower(),
'store': store['key'], 'store': store['key'],
}) })
data = fetch.items item = fetch.items[0]
# if we didn't find the record on the first go around, try again
if not data: if not item:
while (fetch.last is not None): while (fetch.last is not None):
fetch = itemsDB.fetch({ fetch = itemsDB.fetch({
'lowername': request.args['itemName'].lower(), 'lowername': request.args['itemName'].lower(),
'store': store['key'], 'store': store['key'],
last: fetch.last last: fetch.last
}) })
# check with each loop iteration if we've found it
if fetch.items: if fetch.items:
data.append(fetch.items) item = fetch.items[0]
# if so, break loop
break break
return data
# fetches all items from item database return item
def fetchAllItems(store):
# fetch item from deta Base def fetchItems(store):
fetch = itemsDB.fetch({ fetch = itemsDB.fetch({
'store': store['key'], 'store': store['key'],
}) })
data = fetch.items items = fetch.items
while (fetch.last is not None): while (fetch.last is not None):
fetch = itemsDB.fetch({ fetch = itemsDB.fetch({
'store': store['key'], 'store': store['key'],
last: fetch.last last: fetch.last
}) })
data = data + fetch.items items = items + fetch.items
return data
return items
# compares this item to other items from its store to determine if it is the cheapest then updates store accordingly
def updateCheapest(item, store): def updateCheapest(item, store):
cheapest = True cheapest = True
storeItems = fetchAllItems(store) storeItems = fetchItems(store)
itemPerFloz = item['perFloz'] itemPerFloz = item['perFloz']
for storeItem in storeItems: for storeItem in storeItems:
storeItemPerFloz = storeItem['perFloz'] if storeItem['perFloz'] < itemPerFloz: cheapest = False
if storeItemPerFloz < itemPerFloz: cheapest = False
if cheapest: if cheapest:
store['cheapestItem'] = item['name'] store['cheapestItem'] = item['name']
store['cheapestFloz'] = itemPerFloz store['cheapestFloz'] = itemPerFloz
storesDB.put(store) storesDB.put(store)
### routes
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
def get(): def get():
# if there are no arguments on the request if request.args:
if not request.args: if 'storeKey' in request.args: return fetchStore(request.args['storeKey'])
# get all stores
if 'easting' not in request.args and 'northing' not in request.args and 'zone' not in request.args: abort(400)
store = fetchStore()
if not store: abort(404)
store['items'] = fetchItems(store)
return store
fetch = storesDB.fetch({}) fetch = storesDB.fetch({})
data = fetch.items stores = fetch.items
while (fetch.last is not None): while (fetch.last is not None):
fetch = storesDB.fetch({ fetch = storesDB.fetch({
last: fetch.last last: fetch.last
}) })
data = data + fetch.items stores = stores + fetch.items
else:
# get store location details from URL arguments for store in stores:
if 'easting' in request.args and 'northing' in request.args and 'zone' in request.args: store['items'] = fetchItems(store)
stores = fetchStore()
if not stores: abort(404) return stores
store = stores[0]
items = fetchAllItems(store)
store['items'] = items
# if an item GET request
if 'itemName' in request.args:
# passing through the store at position [0] in the fetched stores array
items = fetchItem(store)
if not items: abort(404)
data = items[0]
# otherwise is a store GET request
else: data = store
# else store key passed, get by key
elif 'storeKey' in request.args: data = fetchStore(request.args['storeKey'])
# otherwise is a malformed request
else: abort(400)
# otherwise return data
return data
@app.route('/', methods=['POST']) @app.route('/', methods=['POST'])
def post(): def post():
# checks for args
# easier to work with args here because it allows for reuse of functions above
if request.args: if request.args:
if 'easting' in request.args and 'northing' in request.args and 'zone' in request.args and 'zoneLetter' and 'name' in request.args: if 'easting' in request.args and 'northing' in request.args and 'zone' in request.args and 'zoneLetter' and 'name' in request.args:
northing = float(request.args['northing']) northing = float(request.args['northing'])
easting = float(request.args['easting']) easting = float(request.args['easting'])
zone = int(request.args['zone']) zone = int(request.args['zone'])
# checks if this is posting a new store
# test for presence of stores in close vicinity with similar name # +/- 10 meters
fetch = storesDB.fetch({ fetch = storesDB.fetch({
'easting?r': [easting - 10, easting + 10], 'easting?r': [easting - 10, easting + 10],
'northing?r': [northing - 10, northing + 10], 'northing?r': [northing - 10, northing + 10],
'zone': zone, 'zone': zone,
'lowername?contains': request.args['name'].lower(), 'lowername?contains': request.args['name'].lower(),
}) })
stores = fetch.items if fetch.items: abort(409)
if not stores:
while (fetch.last is not None): while (fetch.last is not None):
fetch = storesDB.fetch({ fetch = storesDB.fetch({
'easting?r': [easting - 10, easting + 10], 'easting?r': [easting - 10, easting + 10],
@ -167,16 +149,8 @@ def post():
'lowername?contains': request.args['name'].lower(), 'lowername?contains': request.args['name'].lower(),
last: fetch.last last: fetch.last
}) })
# check with each loop iteration if we've found it if fetch.items: abort(409)
if fetch.items:
stores = stores + fetch.items
# if so, break loop
break
# if found a store within +/-10 meters named similarly, abort
if stores: abort(409)
# by this point, app would have aborted if there were a problem
# let's create the records
# first the item dict
price = float(request.args['itemPrice']) price = float(request.args['itemPrice'])
volume = int(request.args['itemVolume']) volume = int(request.args['itemVolume'])
item = { item = {
@ -187,7 +161,7 @@ def post():
'perFloz': round(price / float(volume), 2), 'perFloz': round(price / float(volume), 2),
'lastUpdated': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'), 'lastUpdated': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'),
} }
# upload store to DB
store = storesDB.put({ store = storesDB.put({
'easting': easting, 'easting': easting,
'northing': northing, 'northing': northing,
@ -199,24 +173,22 @@ def post():
'cheapestFloz': item['perFloz'], 'cheapestFloz': item['perFloz'],
'lastUpdated': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'), 'lastUpdated': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'),
}) })
# upload item to DB
item['store'] = store['key'] item['store'] = store['key']
itemsDB.put(item) itemsDB.put(item)
# if there is an image upload
if request.files: if request.files:
if 'file' in request.files: if 'file' in request.files: drive.put(store['key'], data=request.files['file'].read())
file = request.files['file']
drive.put(store['key'], data=request.files['file'].read())
# finish successfully
return make_response("Success", 200) return make_response("Success", 200)
# or if it's posting a new item in the store
elif 'storeKey' in request.args and 'itemName' in request.args and 'itemPrice' in request.args and 'itemVolume' in request.args: if 'storeKey' in request.args and 'itemName' in request.args and 'itemPrice' in request.args and 'itemVolume' in request.args:
store = fetchStore(request.args['storeKey']) store = fetchStore(request.args['storeKey'])
if not store: abort(404) if not store: abort(404)
# check for existence of duplicate item
found = fetchItem(store) found = fetchItem(store)
if (found): abort(409) if (found): abort(409)
# let's create the record
price = float(request.args['itemPrice']) price = float(request.args['itemPrice'])
volume = int(request.args['itemVolume']) volume = int(request.args['itemVolume'])
item = { item = {
@ -228,74 +200,72 @@ def post():
'lastUpdated': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'), 'lastUpdated': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'),
'store': store['key'] 'store': store['key']
} }
updateCheapest(item, store) updateCheapest(item, store)
# beam it up
itemsDB.put(item) itemsDB.put(item)
# finish successfully
return make_response("Success", 200) return make_response("Success", 200)
abort(400) abort(400)
@app.route('/', methods=['PUT']) @app.route('/', methods=['PUT'])
def put(): def put():
# checks for arguments
if request.args: if request.args:
if 'storeKey' in request.args: if 'storeKey' in request.args:
# find store
store = fetchStore(request.args['storeKey']) store = fetchStore(request.args['storeKey'])
if not store: abort(404) if not store: abort(404)
# updating item price
if 'itemName' in request.args and 'itemPrice' in request.args: if 'itemName' in request.args and 'itemPrice' in request.args:
# find item item = fetchItem(store)
items = fetchItem(store) if not item: abort (404)
if not items: abort (404)
item = items[0]
# change price
price = float(request.args['itemPrice']) price = float(request.args['itemPrice'])
volume = int(item['volumeFloz']) volume = int(item['volumeFloz'])
item['price'] = price item['price'] = price
item['perFloz'] = round(price / float(volume), 2) item['perFloz'] = round(price / float(volume), 2)
item['lastUpdated'] = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') item['lastUpdated'] = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
updateCheapest(item, store) updateCheapest(item, store)
# beam it up
itemsDB.put(item) itemsDB.put(item)
# finish successfully
return make_response("Success", 200) return make_response("Success", 200)
# updating store image
# if there is an image upload
if request.files: if request.files:
if 'file' in request.files: if 'file' in request.files:
store['lastUpdated'] = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') store['lastUpdated'] = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
storesDB.put(store) storesDB.put(store)
file = request.files['file'] file = request.files['file']
drive.put(store['key'], data=request.files['file'].read()) drive.put(store['key'], data=request.files['file'].read())
# finish successfully
return make_response("Success", 200) return make_response("Success", 200)
abort(400) abort(400)
@app.route('/img', methods=['GET']) @app.route('/img', methods=['GET'])
def getImage(): def getImage():
if not request.args: abort(400) if request.args:
else:
if 'imageKey' in request.args: if 'imageKey' in request.args:
mimetype = filetype.guess(drive.get(request.args['imageKey']).read()).mime mimetype = filetype.guess(drive.get(request.args['imageKey']).read()).mime
return send_file(drive.get(request.args['imageKey']), download_name = request.args['imageKey'] + ".png", mimetype = mimetype) return send_file(drive.get(request.args['imageKey']), download_name = request.args['imageKey'] + ".png", mimetype = mimetype)
abort(400)
@app.route('/search', methods=['GET']) @app.route('/search', methods=['GET'])
def search(): def search():
if not request.args: abort(400) if request.args:
else:
if 'easting' in request.args and 'northing' and 'query' in request.args: if 'easting' in request.args and 'northing' and 'query' in request.args:
# search all stores within 50 mile radius (80 km)
northing = float(request.args['northing']) northing = float(request.args['northing'])
easting = float(request.args['easting']) easting = float(request.args['easting'])
# fetch store from deta Base
fetch = storesDB.fetch({ fetch = storesDB.fetch({
'easting?r': [easting - 80000, easting + 80000], 'easting?r': [easting - 80000, easting + 80000],
'northing?r': [northing - 80000, northing + 80000] 'northing?r': [northing - 80000, northing + 80000]
}) })
stores = fetch.items stores = fetch.items
# if we didn't find records on the first go around, try again
if not stores:
while (fetch.last is not None): while (fetch.last is not None):
fetch = storesDB.fetch({ fetch = storesDB.fetch({
'easting?r': [easting + 80000, easting + 80000], 'easting?r': [easting + 80000, easting + 80000],
@ -303,18 +273,21 @@ def search():
last: fetch.last last: fetch.last
}) })
stores = stores + fetch.items stores = stores + fetch.items
if not stores: abort(404) if not stores: abort(404)
# for item matching query
items = [] items = []
for store in stores: for store in stores:
# fetch item from deta Base
querySubstrings = request.args['query'].split() querySubstrings = request.args['query'].split()
for querySubstring in querySubstrings: for querySubstring in querySubstrings:
fetch = itemsDB.fetch({ fetch = itemsDB.fetch({
'store': store['key'], 'store': store['key'],
'lowername?contains': querySubstring.lower(), 'lowername?contains': querySubstring.lower(),
}) })
items = items + fetch.items items = items + fetch.items
while (fetch.last is not None): while (fetch.last is not None):
fetch = itemsDB.fetch({ fetch = itemsDB.fetch({
'store': store['key'], 'store': store['key'],
@ -322,11 +295,12 @@ def search():
last: fetch.last last: fetch.last
}) })
items = items + fetch.items items = items + fetch.items
# append distance information
for item in items: for item in items:
if item['store'] == store['key']: item['distance'] = max(abs(store['easting'] - easting), abs(store['northing'] - northing)) if item['store'] == store['key']: item['distance'] = max(abs(store['easting'] - easting), abs(store['northing'] - northing))
if not items: abort(404)
return items
abort(400)
# no deletion planned (for now) if not items: abort(404)
return items
abort(400)