from fastapi import FastAPI, File, UploadFile, Depends from fastapi.responses import JSONResponse from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.middleware.cors import CORSMiddleware # CORS from secrets import compare_digest from datetime import datetime from uuid import uuid4 import sqlite3 # use database residing here DB_LOCATION = ( "../testbox/photovoter.dblite" # Q: any allowances for this being not OUR database? ) app = FastAPI() security = HTTPBasic() con = sqlite3.connect(DB_LOCATION) con.row_factory = sqlite3.Row cur = con.cursor() # NB! single is enough for now, we might require multiple later origins = [ # CORS "*", ] app.add_middleware( # CORS CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/new_session", responses={503: {"description": "Unable to initiate session"}}) async def new_session(): """Start a new session""" # add session to the database time = datetime.utcnow().replace(microsecond=0).isoformat() tries = 3 # something is very wrong with our random, if we miss 3 times for i in range(tries): try: # generate a cookie cookie = uuid4().hex cur.execute( """INSERT INTO sessions(cookie, time) VALUES(:cookie, :time) """, {"cookie": cookie, "time": time}, ) con.commit() except sqlite3.IntegrityError as e: if i < tries - 1 and str(e) == "UNIQUE constraint failed: sessions.cookie": continue elif str(e) == "UNIQUE constraint failed: sessions.cookie": return JSONResponse(status_code=503) else: raise break # return new session cookie return {"cookie": cookie} @app.get( "/next_picture/{cookie}", responses={ 204: {"description": "All available images have been appraised"}, 409: {"description": "Uninitiated session"}, }, ) async def next_picture(cookie: str): """Request new picture to rate.""" # check if the cookie is valid cur.execute( """SELECT sessionid FROM sessions WHERE cookie = :cookie LIMIT 1""", {"cookie": cookie}, ) sessionid = cur.fetchone() if sessionid is None: return JSONResponse(status_code=409) # take not rated picture from the database # do not insert anything in the database yet # return this picture # SELECT all images EXCEPT images with marks from the current session -> # -> SELECT paths for these images # FIXME[0]: can this be done better? cur.execute( """SELECT imgid, resizedpath FROM images WHERE imgid IN (SELECT imgid FROM images EXCEPT SELECT imgid FROM marks WHERE sessionid = :sessionid) LIMIT 1 """, {"sessionid": sessionid["sessionid"]}, ) r = cur.fetchone() if r is not None: return {"picture_id": r["imgid"], "picture_uri": r["resizedpath"]} else: # All available pics have been voted for by this sessionid return JSONResponse(status_code=204) @app.get( "/rate_picture/{cookie}/{picture_id}/{mark}", responses={ 406: {"description": "Already appraised"}, 409: {"description": "Uninitiated session"}, }, ) async def rate_picture(cookie: str, picture_id: int, mark: int): """Submit a rating for the picture""" # check if the cookie is valid cur.execute( """SELECT sessionid FROM sessions WHERE cookie = :cookie LIMIT 1""", {"cookie": cookie}, ) sessionid = cur.fetchone() if sessionid is None: return JSONResponse(status_code=409) # add new mark to the session table try: cur.execute( """INSERT INTO marks(imgid, sessionid, mark) VALUES(:imgid,:sessionid,:mark) """, {"imgid": picture_id, "sessionid": sessionid["sessionid"], "mark": mark}, ) con.commit() except sqlite3.IntegrityError as e: if str(e) == "UNIQUE constraint failed: marks.imgid, marks.sessionid": return JSONResponse(status_code=406) return {"status": "success"} @app.get("/photo_points") async def photo_points(): """Get points with the url of a photo and the rate""" # assume we always have at least some photos # fetch them all cur.execute( """SELECT images.imgid, resizedpath, GPSLatitude, GPSLongitude, 100*SUM(marks.mark)/COUNT(marks.mark)/MAX(marks.mark) FROM images LEFT JOIN marks ON images.imgid = marks.imgid GROUP BY images.imgid; """, # 100 * SUM(marks.mark)/COUNT(marks.mark)/MAX(marks.mark) # is an ad-hoc percentage of likes without know how front end defined like/dislike # returns None with no marks (sqlite handles division by 0 gracefully) ) points = cur.fetchall() return [ { "id": point["imgid"], "url": point["resizedpath"], "lon": point["GPSLongitude"], "lat": point["GPSLatitude"], "rate": point["100*SUM(marks.mark)/COUNT(marks.mark)/MAX(marks.mark)"], } for point in points ] @app.post( "/upload_pictures/", responses={ 401: {"description": "Authentication is required to access this resource"}, 415: {"description": "Cannot process uploaded archive"}, }, ) async def upload_pictures( credentials: HTTPBasicCredentials = Depends(security), file: UploadFile = File(...) ): """Интерфейс для загрузки фотографий""" """Условно кладём в браузер zip с фотографиями и он их потихоньку ест. Доступ к этому интерфейсу, наверное, лучше ограничить паролем или как-нибудь ещё. Пока исходим из предположения, что только я буду загружать фотографии.""" # check authenticity correct_username = compare_digest(credentials.username, "1") correct_password = compare_digest(credentials.password, "1") if not (correct_username and correct_password): return JSONResponse(status_code=401) # slurp the zip # *detach from the interface, if possible # unpack zip # feed the pictures to util/import_photos.py return {"filename": file.filename}