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'], }) item = fetch.items[0] if not item: 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] break 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)