332 lines
No EOL
13 KiB
Python
332 lines
No EOL
13 KiB
Python
from os import environ
|
|
from io import BytesIO
|
|
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
|
|
import filetype
|
|
|
|
load_dotenv()
|
|
app = Flask(__name__)
|
|
CORS(app)
|
|
deta = Deta()
|
|
|
|
### connect to dbs and drive
|
|
itemsDB = deta.Base('items')
|
|
storesDB = deta.Base('stores')
|
|
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):
|
|
if key is not None:
|
|
data = storesDB.get(key)
|
|
return data
|
|
else:
|
|
northing = float(request.args['northing'])
|
|
easting = float(request.args['easting'])
|
|
zone = int(request.args['zone'])
|
|
# fetch store from deta Base
|
|
fetch = storesDB.fetch({
|
|
'easting?r': [easting - 1, easting + 1],
|
|
'northing?r': [northing - 1, northing + 1],
|
|
'zone': zone,
|
|
})
|
|
data = fetch.items
|
|
# if we didn't find the record on the first go around, try again
|
|
if not data:
|
|
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
|
|
})
|
|
# 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
|
|
def fetchItem(store):
|
|
# fetch item from deta Base
|
|
fetch = itemsDB.fetch({
|
|
'lowername': request.args['itemName'].lower(),
|
|
'store': store['key'],
|
|
})
|
|
data = fetch.items
|
|
# if we didn't find the record on the first go around, try again
|
|
if not data:
|
|
while (fetch.last is not None):
|
|
fetch = itemsDB.fetch({
|
|
'lowername': request.args['itemName'].lower(),
|
|
'store': store['key'],
|
|
last: fetch.last
|
|
})
|
|
# check with each loop iteration if we've found it
|
|
if fetch.items:
|
|
data.append(fetch.items)
|
|
# if so, break loop
|
|
break
|
|
return data
|
|
|
|
# fetches all items from item database
|
|
def fetchAllItems(store):
|
|
# fetch item from deta Base
|
|
fetch = itemsDB.fetch({
|
|
'store': store['key'],
|
|
})
|
|
data = fetch.items
|
|
while (fetch.last is not None):
|
|
fetch = itemsDB.fetch({
|
|
'store': store['key'],
|
|
last: fetch.last
|
|
})
|
|
data = data + fetch.items
|
|
return data
|
|
|
|
# compares this item to other items from its store to determine if it is the cheapest then updates store accordingly
|
|
def updateCheapest(item, store):
|
|
cheapest = True
|
|
storeItems = fetchAllItems(store)
|
|
itemPerFloz = item['perFloz']
|
|
for storeItem in storeItems:
|
|
storeItemPerFloz = storeItem['perFloz']
|
|
if storeItemPerFloz < itemPerFloz: cheapest = False
|
|
if cheapest:
|
|
store['cheapestItem'] = item['name']
|
|
store['cheapestFloz'] = itemPerFloz
|
|
storesDB.put(store)
|
|
|
|
### routes
|
|
|
|
@app.route('/', methods=['GET'])
|
|
def get():
|
|
# if there are no arguments on the request
|
|
if not request.args:
|
|
# get all stores
|
|
fetch = storesDB.fetch({})
|
|
data = fetch.items
|
|
while (fetch.last is not None):
|
|
fetch = storesDB.fetch({
|
|
last: fetch.last
|
|
})
|
|
data = data + fetch.items
|
|
else:
|
|
# get store location details from URL arguments
|
|
if 'easting' in request.args and 'northing' in request.args and 'zone' in request.args:
|
|
stores = fetchStore()
|
|
if not stores: abort(404)
|
|
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'])
|
|
def post():
|
|
# checks for args
|
|
# easier to work with args here because it allows for reuse of functions above
|
|
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'])
|
|
# checks if this is posting a new store
|
|
# test for presence of stores in close vicinity with similar name
|
|
fetch = storesDB.fetch({
|
|
'easting?r': [easting - 10, easting + 10],
|
|
'northing?r': [northing - 10, northing + 10],
|
|
'zone': zone,
|
|
'lowername?contains': request.args['name'].lower(),
|
|
})
|
|
stores = fetch.items
|
|
if not stores:
|
|
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
|
|
})
|
|
# check with each loop iteration if we've found it
|
|
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'])
|
|
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'),
|
|
}
|
|
# upload store to DB
|
|
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'),
|
|
})
|
|
# upload item to DB
|
|
item['store'] = store['key']
|
|
itemsDB.put(item)
|
|
# if there is an image upload
|
|
if request.files:
|
|
if 'file' in request.files:
|
|
file = request.files['file']
|
|
drive.put(store['key'], data=request.files['file'].read())
|
|
# finish successfully
|
|
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:
|
|
store = fetchStore(request.args['storeKey'])
|
|
if not store: abort(404)
|
|
# check for existence of duplicate item
|
|
found = fetchItem(store)
|
|
if (found): abort(409)
|
|
# let's create the record
|
|
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)
|
|
# beam it up
|
|
itemsDB.put(item)
|
|
# finish successfully
|
|
return make_response("Success", 200)
|
|
abort(400)
|
|
|
|
@app.route('/', methods=['PUT'])
|
|
def put():
|
|
# checks for arguments
|
|
if request.args:
|
|
if 'storeKey' in request.args:
|
|
# find store
|
|
store = fetchStore(request.args['storeKey'])
|
|
if not store: abort(404)
|
|
# updating item price
|
|
if 'itemName' in request.args and 'itemPrice' in request.args:
|
|
# find item
|
|
items = fetchItem(store)
|
|
if not items: abort (404)
|
|
item = items[0]
|
|
# change price
|
|
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)
|
|
# beam it up
|
|
itemsDB.put(item)
|
|
# finish successfully
|
|
return make_response("Success", 200)
|
|
# updating store image
|
|
# if there is an image upload
|
|
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())
|
|
# finish successfully
|
|
return make_response("Success", 200)
|
|
abort(400)
|
|
|
|
@app.route('/img', methods=['GET'])
|
|
def getImage():
|
|
if not request.args: abort(400)
|
|
else:
|
|
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)
|
|
|
|
@app.route('/search', methods=['GET'])
|
|
def search():
|
|
if not request.args: abort(400)
|
|
else:
|
|
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'])
|
|
easting = float(request.args['easting'])
|
|
# fetch store from deta Base
|
|
fetch = storesDB.fetch({
|
|
'easting?r': [easting - 80000, easting + 80000],
|
|
'northing?r': [northing - 80000, northing + 80000]
|
|
})
|
|
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):
|
|
fetch = storesDB.fetch({
|
|
'easting?r': [easting + 80000, easting + 80000],
|
|
'northing?r': [northing - 80000, northing + 80000],
|
|
last: fetch.last
|
|
})
|
|
stores = stores + fetch.items
|
|
if not stores: abort(404)
|
|
# for item matching query
|
|
items = []
|
|
for store in stores:
|
|
# fetch item from deta Base
|
|
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
|
|
# append distance information
|
|
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)
|
|
|
|
# no deletion planned (for now) |