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)