beerbuddy/flask/backend.py
2024-02-10 13:52:47 -08:00

317 lines
11 KiB
Python

from os import environ
from io import BytesIO
import filetype
from dotenv import load_dotenv
from flask import Flask, request, abort, make_response, send_file
from flask_cors import CORS
from datetime import datetime
from deta import Deta
load_dotenv()
app = Flask(__name__)
CORS(app)
deta = Deta()
itemsDB = deta.Base('items')
storesDB = deta.Base('stores')
drive = deta.Drive('beerbuddy')
RADIUS = 80000
def fetchStore(key = None):
""" Fetch a store with the given key. If no key is provided, fetch store based on location data in request arguments """
if key is not None: return storesDB.get(key)
else:
northing = float(request.args['northing'])
easting = float(request.args['easting'])
zone = int(request.args['zone'])
fetch = storesDB.fetch({
'easting?r': [easting - 1, easting + 1],
'northing?r': [northing - 1, northing + 1],
'zone': zone,
})
store = fetch.items[0]
if not store:
while (fetch.last is not None):
fetch = storesDB.fetch({
'easting?r': [easting - 1, easting + 1],
'northing?r': [northing - 1, northing + 1],
'zone': zone,
last: fetch.last
})
if fetch.items:
store = fetch.items[0]
break
return store
def fetchItem(store):
""" Fetch an item from given store that matches request arguments """
fetch = itemsDB.fetch({
'lowername': request.args['itemName'].lower(),
'store': store['key'],
})
if fetch.items:
item = fetch.items[0]
return item
else:
while (fetch.last is not None):
fetch = itemsDB.fetch({
'lowername': request.args['itemName'].lower(),
'store': store['key'],
last: fetch.last
})
if fetch.items:
item = fetch.items[0]
return item
def fetchItems(store):
""" Fetch all items from a given store """
fetch = itemsDB.fetch({
'store': store['key'],
})
items = fetch.items
while (fetch.last is not None):
fetch = itemsDB.fetch({
'store': store['key'],
last: fetch.last
})
items = items + fetch.items
return items
def updateCheapest(item, store):
""" Compare price of current item to other items in the given store and update store's cheapest item if this item is cheaper than the others """
cheapest = True
storeItems = fetchItems(store)
itemPerFloz = item['perFloz']
for storeItem in storeItems:
if storeItem['perFloz'] < itemPerFloz: cheapest = False
if cheapest:
store['cheapestItem'] = item['name']
store['cheapestFloz'] = itemPerFloz
storesDB.put(store)
@app.route('/', methods=['GET'])
def get():
""" Returns store based on request arguments. If no arguments are provided, returns all stores in database """
if request.args:
if 'storeKey' in request.args: return fetchStore(request.args['storeKey'])
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({})
stores = fetch.items
while (fetch.last is not None):
fetch = storesDB.fetch({
last: fetch.last
})
stores = stores + fetch.items
for store in stores:
store['items'] = fetchItems(store)
return stores
@app.route('/', methods=['POST'])
def post():
""" Adds a store or item to the database depending on request arguments """
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:
northing = float(request.args['northing'])
easting = float(request.args['easting'])
zone = int(request.args['zone'])
# +/- 10 meters
fetch = storesDB.fetch({
'easting?r': [easting - 10, easting + 10],
'northing?r': [northing - 10, northing + 10],
'zone': zone,
'lowername?contains': request.args['name'].lower(),
})
if fetch.items: abort(409)
while (fetch.last is not None):
fetch = storesDB.fetch({
'easting?r': [easting - 10, easting + 10],
'northing?r': [northing - 10, northing + 10],
'zone': zone,
'lowername?contains': request.args['name'].lower(),
last: fetch.last
})
if fetch.items: abort(409)
price = float(request.args['itemPrice'])
volume = int(request.args['itemVolume'])
item = {
'name': request.args['itemName'],
'lowername': request.args['itemName'].lower(),
'volumeFloz': volume,
'price': price,
'perFloz': round(price / float(volume), 2),
'lastUpdated': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'),
}
store = storesDB.put({
'easting': easting,
'northing': northing,
'zone': zone,
'zoneLetter': request.args['zoneLetter'],
'name': request.args['name'],
'lowername': request.args['name'].lower(),
'cheapestItem': item['name'],
'cheapestFloz': item['perFloz'],
'lastUpdated': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'),
})
item['store'] = store['key']
itemsDB.put(item)
if request.files:
if 'file' in request.files: drive.put(store['key'], data=request.files['file'].read())
return make_response("Success", 200)
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'])
if not store: abort(404)
found = fetchItem(store)
if (found): abort(409)
price = float(request.args['itemPrice'])
volume = int(request.args['itemVolume'])
item = {
'name': request.args['itemName'],
'lowername': request.args['itemName'].lower(),
'volumeFloz': volume,
'price': price,
'perFloz': round(price / float(volume), 2),
'lastUpdated': datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'),
'store': store['key']
}
updateCheapest(item, store)
itemsDB.put(item)
return make_response("Success", 200)
abort(400)
@app.route('/', methods=['PUT'])
def put():
""" Updates item or store picture depending on request arguments """
if request.args:
if 'storeKey' in request.args:
store = fetchStore(request.args['storeKey'])
if not store: abort(404)
if 'itemName' in request.args and 'itemPrice' in request.args:
item = fetchItem(store)
if not item: abort (404)
price = float(request.args['itemPrice'])
volume = int(item['volumeFloz'])
item['price'] = price
item['perFloz'] = round(price / float(volume), 2)
item['lastUpdated'] = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
updateCheapest(item, store)
itemsDB.put(item)
return make_response("Success", 200)
if request.files:
if 'file' in request.files:
store['lastUpdated'] = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
storesDB.put(store)
file = request.files['file']
drive.put(store['key'], data=request.files['file'].read())
return make_response("Success", 200)
abort(400)
@app.route('/img', methods=['GET'])
def getImage():
""" Returns store image based on image key provided in request arguments """
if request.args:
if 'imageKey' in request.args:
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)
abort(400)
@app.route('/search', methods=['GET'])
def search():
""" Returns items matching query string in searchable radius (currently 50mi/80km) """
if request.args:
if 'easting' in request.args and 'northing' and 'query' in request.args:
northing = float(request.args['northing'])
easting = float(request.args['easting'])
fetch = storesDB.fetch({
'easting?r': [easting - RADIUS, easting + RADIUS],
'northing?r': [northing - RADIUS, northing + RADIUS]
})
stores = fetch.items
while (fetch.last is not None):
fetch = storesDB.fetch({
'easting?r': [easting + RADIUS, easting + RADIUS],
'northing?r': [northing - RADIUS, northing + RADIUS],
last: fetch.last
})
stores = stores + fetch.items
if not stores: abort(404)
items = []
for store in stores:
querySubstrings = request.args['query'].split()
for querySubstring in querySubstrings:
fetch = itemsDB.fetch({
'store': store['key'],
'lowername?contains': querySubstring.lower(),
})
items = items + fetch.items
while (fetch.last is not None):
fetch = itemsDB.fetch({
'store': store['key'],
'lowername?contains': querySubstring.lower(),
last: fetch.last
})
items = items + fetch.items
for item in items:
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)