You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

202 lines
6.7 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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}