Compare commits
19
Commits
f8609ac317
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2beef62041 | ||
|
|
9c2a3ac42e | ||
|
|
b5e693ab9f | ||
|
|
cecd9a4458 | ||
|
|
ab7372cc9c | ||
|
|
6d7eae5ea4 | ||
|
|
10303accd2 | ||
|
|
7f6dcefb91 | ||
|
|
960856cdd8 | ||
|
|
6e8b62ffe1 | ||
|
|
8ecd30db0e | ||
|
|
cc0fd8ec64 | ||
|
|
6beba3945e | ||
|
|
9a3b4d9699 | ||
|
|
90b9a3a717 | ||
|
|
90dbed634d | ||
|
|
eb3b4478fc | ||
|
|
da41fb5990 | ||
|
|
895ac1e276 |
+2
-1
@@ -9,7 +9,8 @@ wheels/
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
||||
.env
|
||||
.env*
|
||||
.ruff_cache
|
||||
|
||||
*.sqlite
|
||||
*.duckdb
|
||||
+4
-3
@@ -4,11 +4,12 @@ WORKDIR /app
|
||||
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
|
||||
ADD ./pyproject.toml .
|
||||
ADD ./uv.lock .
|
||||
ADD ./src .
|
||||
ADD pyproject.toml uv.lock .
|
||||
RUN mkdir -p /app/data
|
||||
RUN uv sync --frozen --no-dev
|
||||
|
||||
ADD /src .
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
ENTRYPOINT []
|
||||
|
||||
+17
-2
@@ -1,7 +1,22 @@
|
||||
services:
|
||||
web:
|
||||
pjl-backend:
|
||||
# Build the image from the Dockerfile in the current directory
|
||||
build: .
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: pjl-backend:1.0rc1
|
||||
|
||||
env_file:
|
||||
- .env.prod
|
||||
|
||||
Environment:
|
||||
DB_URL: /app/data/pjl.duckdb
|
||||
DEFAULT_AVATAR_URL: https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=Kingston
|
||||
OMDB_API_KEY: ${OMDB_API_KEY}
|
||||
STAGE: prod
|
||||
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
|
||||
# Host the FastAPI application on port 8000
|
||||
ports:
|
||||
|
||||
+13
-3
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pjl"
|
||||
version = "0.1.0"
|
||||
name = "pjl-backend"
|
||||
version = "1.0rc1"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
@@ -11,8 +11,18 @@ dependencies = [
|
||||
"loguru>=0.7.2",
|
||||
"pydantic-settings>=2.6.1",
|
||||
"pydantic>=2.9.2",
|
||||
"requests>=2.32.3",
|
||||
"rich>=13.9.4",
|
||||
"uvicorn>=0.32.0",
|
||||
"pandas>=2.2.3",
|
||||
"requests>=2.32.3",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff>=0.8.0",
|
||||
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
lint.extend-select = ["C4", "SIM", "TCH", "ERA", "FAST", "DOC"]
|
||||
target-version = "py312"
|
||||
@@ -15,6 +15,7 @@ app = FastAPI(
|
||||
title="Paska joululeffa 2024",
|
||||
name="Paska joululeffa 2024",
|
||||
description="API for Paska joululeffa 2024",
|
||||
version="0.9",
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
|
||||
+55
-18
@@ -1,10 +1,14 @@
|
||||
from datetime import datetime
|
||||
import bcrypt
|
||||
import requests
|
||||
|
||||
from lib.database.database import db_run
|
||||
from models import Message
|
||||
from models.user import UserCredentials
|
||||
from models.movie import Movie
|
||||
from models.movie import MovieIn, Movie
|
||||
from models.card import Card
|
||||
|
||||
import bcrypt
|
||||
from lib.settings import settings
|
||||
from lib.logger import logger
|
||||
|
||||
|
||||
def add_user(user: UserCredentials):
|
||||
@@ -14,40 +18,71 @@ def add_user(user: UserCredentials):
|
||||
sql = f"INSERT INTO users (username, password) VALUES ('{user.username}', '{hashed_password}')"
|
||||
try:
|
||||
db_run(sql)
|
||||
except:
|
||||
return Message(message="User already exists", message_type="error")
|
||||
except Exception as e:
|
||||
return Message(message=f"User already exists. Error: {e}", message_type="error")
|
||||
|
||||
return Message(message="User added successfully", message_type="success")
|
||||
|
||||
|
||||
def remove_user(user_id: str):
|
||||
logger.debug(f"Removing user {user_id}")
|
||||
sql = f"DELETE FROM users WHERE id = '{user_id}'"
|
||||
|
||||
try:
|
||||
db_run(sql)
|
||||
except:
|
||||
return Message(message="User not found", message_type="error")
|
||||
except Exception as e:
|
||||
return Message(message=f"User not found. Error: {e}", message_type="error")
|
||||
|
||||
return Message(message="User removed successfully", message_type="success")
|
||||
|
||||
|
||||
def add_movie(movie: Movie):
|
||||
sql = "INSERT INTO movies (name, imdb_id, actors, release_date, plot, showtime) VALUES (?, ?, ?, ?, ?, ?)"
|
||||
def add_movie(movie: MovieIn):
|
||||
logger.info(f"Adding movie {movie.imdb_id}")
|
||||
logger.debug(f"Requesting movie data from OMDB: {movie.imdb_id}")
|
||||
request_url = "https://omdbapi.com/"
|
||||
request_params = {
|
||||
"apikey": settings.OMDB_API_KEY,
|
||||
"i": movie.imdb_id,
|
||||
}
|
||||
|
||||
response = requests.get(request_url, params=request_params)
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Failed to add movie {movie.imdb_id}")
|
||||
return Message(message="Failed to add movie", message_type="error")
|
||||
movie_data = response.json()
|
||||
logger.debug(f"Movie data received from OMDB: {movie_data}")
|
||||
if "Released" in movie_data:
|
||||
movie_data["Released"] = datetime.strptime(movie_data["Released"], "%d %b %Y")
|
||||
|
||||
new_movie = Movie(
|
||||
name=movie_data["Title"],
|
||||
imdb_id=movie_data["imdbID"],
|
||||
actors=[actor.strip() for actor in movie_data["Actors"].split(",")],
|
||||
release_date=movie_data["Released"],
|
||||
plot=movie.plot,
|
||||
poster_url=movie_data["Poster"],
|
||||
showtime=movie.showtime,
|
||||
)
|
||||
logger.debug(f"Movie data: {new_movie.model_dump()}")
|
||||
sql = "INSERT INTO movies (name, imdb_id, actors, release_date, plot, showtime, poster_url) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||
try:
|
||||
db_run(
|
||||
sql,
|
||||
(
|
||||
movie.name,
|
||||
movie.imdb_id,
|
||||
movie.actors,
|
||||
movie.release_date,
|
||||
movie.plot,
|
||||
movie.showtime,
|
||||
new_movie.name,
|
||||
new_movie.imdb_id,
|
||||
new_movie.actors,
|
||||
new_movie.release_date,
|
||||
new_movie.plot,
|
||||
new_movie.showtime,
|
||||
new_movie.poster_url,
|
||||
),
|
||||
)
|
||||
except:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add movie {movie.imdb_id}. Error: {e}")
|
||||
return Message(message="Failed to add movie", message_type="error")
|
||||
|
||||
logger.info(f"Movie {movie.imdb_id} added successfully")
|
||||
return Message(message="Movie added successfully", message_type="success")
|
||||
|
||||
|
||||
@@ -55,8 +90,10 @@ def set_movie_watched(movie_id: str):
|
||||
sql = f"UPDATE movies SET is_watched = TRUE WHERE id = '{movie_id}'"
|
||||
try:
|
||||
db_run(sql)
|
||||
except:
|
||||
return Message(message="Failed to set movie as watched", message_type="error")
|
||||
except Exception as e:
|
||||
return Message(
|
||||
message=f"Failed to set movie as watched. Error: {e}", message_type="error"
|
||||
)
|
||||
|
||||
return Message(message="Movie set as watched successfully", message_type="success")
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
from contextlib import contextmanager
|
||||
import duckdb
|
||||
import bcrypt
|
||||
from rich import print
|
||||
|
||||
from lib import settings
|
||||
from lib.logger import logger
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_db():
|
||||
try:
|
||||
conn = duckdb.connect(database=settings.db_url)
|
||||
logger.debug(f"Connecting to database {settings.db_url.absolute()}")
|
||||
conn = duckdb.connect(database=settings.db_url.absolute())
|
||||
yield conn
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -23,11 +24,11 @@ def db_run(sql, values: tuple = None):
|
||||
|
||||
def init_db():
|
||||
if settings.db_url.exists():
|
||||
pass
|
||||
# TODO: Development feature. Remove this in production
|
||||
from os import unlink
|
||||
# from os import unlink
|
||||
|
||||
unlink(settings.db_url)
|
||||
# return
|
||||
# unlink(settings.db_url)
|
||||
sql = ""
|
||||
with open("lib/database/database.sql") as f:
|
||||
sql = f.read()
|
||||
@@ -38,8 +39,5 @@ def init_db():
|
||||
|
||||
hashed_password = bcrypt.hashpw("password".encode("utf-8"), bcrypt.gensalt())
|
||||
|
||||
sql = f"INSERT INTO users (username, password, is_admin) VALUES ('admin', '{hashed_password.decode('utf-8')}', true)"
|
||||
db_run(sql)
|
||||
|
||||
sql = f"INSERT INTO users (username, password, is_admin) VALUES ('test', '{hashed_password.decode('utf-8')}', false)"
|
||||
sql = f"INSERT INTO users (username, password, is_admin) VALUES ('admin', '{hashed_password.decode('utf-8')}', true), ('kessinen', '{hashed_password.decode('utf-8')}', false), ('jensku', '{hashed_password.decode('utf-8')}', false), ('emppu', '{hashed_password.decode('utf-8')}', false)"
|
||||
db_run(sql)
|
||||
|
||||
@@ -53,48 +53,55 @@ CREATE TABLE log (
|
||||
);
|
||||
|
||||
-- Add placeholder data
|
||||
|
||||
INSERT INTO movies (name, imdb_id, actors, release_date, plot, poster_url, showtime)
|
||||
VALUES ('A Maple Valley Christmas',
|
||||
'tt21841642',
|
||||
['Peyton List', 'Andrew W. Walker', 'Frances Flanagan'],
|
||||
VALUES ('Christmas Sail',
|
||||
'tt15758364',
|
||||
['Katee Sackhoff', 'Patrick Sabongui', 'Terry O''Quinn'],
|
||||
'2022-11-05',
|
||||
'Erica on karjatilallinen, joka on viettänyt koko elämänsä työskennellen perhetilalla äitinsä ja sisarensa kanssa.
|
||||
Kun Aaron saapuu ja häiritsee hänen suunnitelmiaan, hän alkaa kyseenalaistaa, mitä hän oikeastaan haluaa.',
|
||||
'https://m.media-amazon.com/images/M/MV5BZjliMTRlMGItOGQzZC00NTI2LWJkODctZWZmMDIzOGRmYTQ3XkEyXkFqcGc@._V1_.jpg',
|
||||
'2024-12-01');
|
||||
'Lizin etääntynyt isä pyytää apua ja Liz palaa kotikaupunkiinsa. Hän päättää luoda täydellisen joulun tyttärelleen ja saada ihmissuhteensa kuntoon.',
|
||||
'https://m.media-amazon.com/images/M/MV5BZTA0MjA2MmMtNWZhNy00N2UyLWI5YjItZTA4ZDVkZmNmMWVjXkEyXkFqcGc@._V1_SX300.jpg',
|
||||
'2024-12-02'),
|
||||
('The Holiday Stocking',
|
||||
'tt22308008',
|
||||
['Nadine Ellis', 'Tamala Jones', 'B.J. Britt'],
|
||||
'2022-12-03','Enkeli palaa maan päälle vieraana ja saa mahdollisuuden auttaa sisariaan sovintoon - asia mitä hän ei onnistunut tekemään elinaikanaan.',
|
||||
'https://m.media-amazon.com/images/M/MV5BZTI5OTc5OTYtOWVlMC00YmRhLWFhMmQtMGIwYjNmZTI1Y2Y5XkEyXkFqcGc@._V1_SX300.jpg',
|
||||
'2024-12-03'),;
|
||||
|
||||
INSERT INTO cards (title, description, point_value)
|
||||
VALUES
|
||||
('Punainen villakangastakki', 'Päähenkilöllä on aina kirkkaanpunainen, täydellisesti istuva talvitakki.', 1),
|
||||
('Keskeytetty suudelma', 'Juuri, kun romanttinen hetki on käsillä, jokin keskeyttää ensisuudelman.', 1),
|
||||
('Miespääosa yh', 'Miespäähenkilö on yksinhuoltaja, joka tasapainoilee uran ja lapsenhoidon välillä.', 2),
|
||||
('Leskimies', 'Päähenkilö on menettänyt puolisonsa ja yrittää nyt toipua elämässään.', 2),
|
||||
('Jouluoksennus', 'Koti, työpaikka tai jokin muu koristeltu liioitellusti jouluteemalla, ilman mitään hillittyä tyylitajua.', 1),
|
||||
('Blondi tuli taloon', 'Naispääosalla pitkät blondit hiukset.', 1),
|
||||
('Ensilumi', 'Juuri kun kaikki toivo on menetetty, ensilumi alkaa sataa.', 3),
|
||||
('Evergreen', 'Tarinan kylä tai kaupunki on idyllinen paikka, nimeltään jotain jouluista, kuten ''Evergreen'' tai ''Snowville''.', 3),
|
||||
('Yritys pulassa', 'Päähenkilön yritys on konkurssin partaalla tai vaarassa tulla ostetuksi.', 2),
|
||||
('Naispääosa pitkä blondi', 'Naispääosalla pitkät blondit hiukset.', 1),
|
||||
('Hengailua kirjastossa', 'Pieni kirjakauppa tai kirjasto on keskeinen kohtauspaikka ja keskustelujen paikka.', 3),
|
||||
('Himo jouluttaja', 'Yksi hahmoista on ylitsepursuava jouluihminen, jolla on täydellisesti koristeltu koti.', 1),
|
||||
('Jouluenkeli', 'Jouluenkeli – joko koristeena tai todellisena ilmestyksenä – ohjaa hahmoja oikealle tielle.', 3),
|
||||
('Joulukuusten Invaasio', 'Elokuvassa on koti jossa on vähintään 5 joulukuusta. Kuuset voivat olla eri kokoisia.', 2),
|
||||
('Joulumörkö', 'Hahmo, joka vihaa joulua, mutta päätyy lopulta rakastamaan sitä.', 1),
|
||||
('Pääosa jumissa', 'Päähenkilö jää jumiin esimerkiksi kaukaiseen kylään tai pikkukaupunkiin.', 1),
|
||||
('Jouluoksennus', 'Koti, työpaikka tai jokin muu koristeltu liioitellusti jouluteemalla, ilman mitään hillittyä tyylitajua.', 1),
|
||||
('Kantakahvila', 'Pieni, kodikas kahvila toimii kaikkien tapaamispaikkana ja keskustelujen keskuksena.', 2),
|
||||
('Keskeytetty suudelma', 'Juuri, kun romanttinen hetki on käsillä, jokin keskeyttää ensisuudelman.', 1),
|
||||
('Kuuma kaakao', 'Kuumaa kaakaota juodaan lukemattomia kertoja, aina kermavaahdon kera.', 2),
|
||||
('Kuusen valinta', 'Kohtaus, jossa kuusen valinnasta tehdään koko tarinan käännekohta.', 2),
|
||||
('Kylän joulutapahtuma', 'Joulukulkue, jouluvalotapahtuma tai kuusen valaiseminen, jossa kaikki kokoontuvat yhteen.', 1),
|
||||
('Lapsen joulutoive', 'Pieni lapsi toivoo joululta jotain suurta, yleensä perheeseen liittyvää.', 3),
|
||||
('Leskimies', 'Päähenkilö on menettänyt puolisonsa ja yrittää nyt toipua elämässään.', 2),
|
||||
('Lumipyry', 'Suuri lumipyry eristää päähenkilöt mökkiin tai kylään.', 3),
|
||||
('Lumiukko', 'Lumiukon rakentaminen on täydellisen joulupäivän kohokohta.', 3),
|
||||
('Majatalo', 'Tarinaan kuuluu aina viehättävä majatalo, jossa on perinteinen joulukoristelu.', 2),
|
||||
('Miespääosa eräjorma', 'Miespäähenkilö on karskin komea eränkävijä, jolla on piilotettu herkkä puoli.', 2),
|
||||
('Työkriisi', 'Päähenkilön työelämässä on kriisi, joka vaatii kaiken huomion joulun alla.', 1),
|
||||
('Kylän joulutapahtuma', 'Joulukulkue, jouluvalotapahtuma tai kuusen valaiseminen, jossa kaikki kokoontuvat yhteen.', 1),
|
||||
('Naispääosa kaupungista', 'Uraohjautunut nainen suurkaupungista, jolla on asenneongelma pientä kylää kohtaan.', 1),
|
||||
('Naispääosan tuore ero', 'Päähenkilö on juuri eronnut ja toipuu kipeästä ihmissuhteesta.', 2),
|
||||
('Suuri salaisuus', 'Joku hahmo kantaa suurta salaisuutta, joka paljastuu juuri oikealla hetkellä.', 2),
|
||||
('Himo jouluttaja', 'Yksi hahmoista on ylitsepursuava jouluihminen, jolla on täydellisesti koristeltu koti.', 1),
|
||||
('Kuusen valinta', 'Kohtaus, jossa kuusen valinnasta tehdään koko tarinan käännekohta.', 2),
|
||||
('Täysikuu', 'Jouluyön täysikuu valaisee maiseman ja tuo taianomaisen tunnelman.', 3),
|
||||
('Kuuma kaakao', 'Kuumaa kaakaota juodaan lukemattomia kertoja, aina kermavaahdon kera.', 2),
|
||||
('Punainen lava-auto', 'Joku hahmo ajaa punaisella lava-autolla, usein ilman mitään käytännön syytä.', 1),
|
||||
('Lumiukko', 'Lumiukon rakentaminen on täydellisen joulupäivän kohokohta.', 3),
|
||||
('Hengailua kirjastossa', 'Pieni kirjakauppa tai kirjasto on keskeinen kohtauspaikka ja keskustelujen paikka.', 3),
|
||||
('Jouluenkeli', 'Jouluenkeli – joko koristeena tai todellisena ilmestyksenä – ohjaa hahmoja oikealle tielle.', 3),
|
||||
('Lumipyry', 'Suuri lumipyry eristää päähenkilöt mökkiin tai kylään.', 3),
|
||||
('Yllättävä vieras', 'Joulun alla joku saapuu yllättäen perhejuhliin tai kylään.', 3),
|
||||
('Vanha kirje', 'Päähenkilö löytää vanhan kirjeen, joka muuttaa hänen elämänsä.', 3),
|
||||
('Pääosa jumissa', 'Päähenkilö jää jumiin esimerkiksi kaukaiseen kylään tai pikkukaupunkiin.', 1),
|
||||
('Pakollinen luistelukohtaus', 'Pari luisteluhetkellä, jonka aikana tunteet alkavat syttyä.', 2),
|
||||
('Punainen lava-auto', 'Joku hahmo ajaa punaisella lava-autolla, usein ilman mitään käytännön syytä.', 1),
|
||||
('Punainen villakangastakki', 'Päähenkilöllä on aina kirkkaanpunainen, täydellisesti istuva talvitakki.', 1),
|
||||
('Suuri salaisuus', 'Joku hahmo kantaa suurta salaisuutta, joka paljastuu juuri oikealla hetkellä.', 2),
|
||||
('Täysikuu', 'Jouluyön täysikuu valaisee maiseman ja tuo taianomaisen tunnelman.', 3),
|
||||
('The Huoltaja', 'Miespäähenkilö on yksinhuoltaja, joka tasapainoilee uran ja lapsenhoidon välillä.', 2),
|
||||
('Työkriisi', 'Päähenkilön työelämässä on kriisi, joka vaatii kaiken huomion joulun alla.', 1),
|
||||
('Vanha kirje', 'Päähenkilö löytää vanhan kirjeen, joka muuttaa hänen elämänsä.', 3),
|
||||
('Vanhan parin rakkausneuvo', 'Iäkäs pariskunta antaa päähenkilölle neuvoja rakkaudesta.', 3),
|
||||
('Ensilumi', 'Juuri kun kaikki toivo on menetetty, ensilumi alkaa sataa.', 3),
|
||||
('Lapsen joulutoive', 'Pieni lapsi toivoo joululta jotain suurta, yleensä perheeseen liittyvää.', 3);
|
||||
('Yllättävä vieras', 'Joulun alla joku saapuu yllättäen perhejuhliin tai kylään.', 3),
|
||||
('Yritys pulassa', 'Päähenkilön yritys on konkurssin partaalla tai vaarassa tulla ostetuksi.', 2);
|
||||
|
||||
+12
-15
@@ -1,25 +1,22 @@
|
||||
from models.movie import Movie
|
||||
from lib.database.database import get_db
|
||||
from lib.logger import logger
|
||||
|
||||
|
||||
@logger.catch
|
||||
def get_all_movies() -> list[Movie]:
|
||||
logger.info("Getting all movies")
|
||||
sql = "SELECT * FROM movies"
|
||||
with get_db() as conn:
|
||||
result = conn.execute(sql).fetchall()
|
||||
result = conn.execute(sql).df()
|
||||
result_dict = result.to_dict(orient="records")
|
||||
movies = []
|
||||
for row in result:
|
||||
movie = Movie(
|
||||
id=str(row[0]),
|
||||
name=row[1],
|
||||
imdb_id=row[2],
|
||||
actors=row[3],
|
||||
release_date=row[4],
|
||||
plot=row[5],
|
||||
showtime=row[6],
|
||||
is_watched=row[7],
|
||||
created_at=row[8],
|
||||
updated_at=row[9],
|
||||
)
|
||||
for row in result_dict:
|
||||
row["id"] = str(row["id"])
|
||||
logger.debug(f"Found movie: {row}")
|
||||
print("Type of name:", type(row["name"]))
|
||||
movie = Movie(**row)
|
||||
|
||||
movies.append(movie)
|
||||
|
||||
return movies
|
||||
@@ -28,7 +25,7 @@ def get_all_movies() -> list[Movie]:
|
||||
|
||||
|
||||
def get_next_movie() -> Movie | None:
|
||||
sql = "SELECT * FROM movies WHERE is_watched = FALSE AND showtime > CURRENT_TIMESTAMP ORDER BY showtime ASC"
|
||||
sql = "SELECT * FROM movies WHERE is_watched = FALSE AND showtime = CURRENT_DATE ORDER BY showtime ASC"
|
||||
with get_db() as conn:
|
||||
result = conn.execute(sql).fetchdf()
|
||||
if result.empty:
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from models.score import ScoreBase
|
||||
from lib.database.database import get_db
|
||||
from models.general import Message
|
||||
|
||||
|
||||
def set_score(score: ScoreBase) -> Message:
|
||||
sql = "INSERT INTO scores (movie_id, user_id, score) VALUES (?, ?, ?)"
|
||||
with get_db() as conn:
|
||||
conn.execute(sql, (score.movie_id, score.user_id, score.score))
|
||||
|
||||
return Message(message="Score added successfully", message_type="success")
|
||||
+18
-13
@@ -1,24 +1,29 @@
|
||||
from datetime import datetime
|
||||
|
||||
from models.user import UserIn, UserOut
|
||||
from lib.database.database import get_db
|
||||
from lib.logger import logger
|
||||
|
||||
|
||||
def set_last_login(user_id: str) -> datetime:
|
||||
logger.debug(f"Setting last login for user {user_id}")
|
||||
cur_time = datetime.now()
|
||||
sql = f"UPDATE users SET last_login = '{cur_time}' WHERE id = '{user_id}'"
|
||||
with get_db() as conn:
|
||||
conn.execute(sql)
|
||||
|
||||
return cur_time
|
||||
|
||||
|
||||
def get_user_by_username(username) -> UserIn | None:
|
||||
sql = f"SELECT * FROM users WHERE username = '{username}'"
|
||||
with get_db() as conn:
|
||||
result = conn.execute(sql).fetchone()
|
||||
result = conn.execute(sql).df().to_dict(orient="records")[0]
|
||||
if result is not None:
|
||||
return UserIn(
|
||||
id=str(result[0]),
|
||||
username=result[1],
|
||||
password=result[2],
|
||||
email=result[3],
|
||||
avatar_url=result[4],
|
||||
is_active=result[5],
|
||||
is_admin=result[6],
|
||||
last_login=result[7],
|
||||
created_at=result[8],
|
||||
updated_at=result[9],
|
||||
)
|
||||
last_login = set_last_login(result["id"])
|
||||
result["id"] = str(result["id"])
|
||||
result["last_login"] = last_login
|
||||
return UserIn(**result)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
+2
-4
@@ -4,14 +4,12 @@ from pathlib import Path
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
db_url: Path = Field(Path("pjl.duckdb"), env="DB_URL")
|
||||
db_url: Path = Field(Path("pjl_default.duckdb"), env="DB_URL")
|
||||
default_avatar_url: str = Field(
|
||||
"https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=Kingston",
|
||||
env="DEFAULT_AVATAR_URL",
|
||||
)
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
OMDB_API_KEY: str = Field(env="OMDB_API_KEY")
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# from .movie import Movie
|
||||
# from .score import Score
|
||||
# from .card import Card
|
||||
from .general import Message
|
||||
|
||||
__all__ = [
|
||||
# "Movie",
|
||||
# "Score",
|
||||
# "Card",
|
||||
"Message",
|
||||
]
|
||||
|
||||
+24
-28
@@ -1,44 +1,40 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
from datetime import datetime, date
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Movie(BaseModel):
|
||||
id: Optional[str] = Field(
|
||||
default=None, examples=["123e4567-e89b-12d3-a456-426655440000"]
|
||||
)
|
||||
name: str = Field(
|
||||
description="Name of the movie", examples=["A Maple Valley Christmas"]
|
||||
class MovieBase(BaseModel):
|
||||
name: Optional[str] | None = Field(
|
||||
None, description="Name of the movie", examples=["A Maple Valley Christmas"]
|
||||
)
|
||||
imdb_id: str = Field(description="IMDB ID of the movie", examples=["tt1234567"])
|
||||
actors: list[str] = Field(
|
||||
description="Actors of the movie",
|
||||
examples=[["Peyton List", "Andrew W. Walker"]],
|
||||
)
|
||||
showtime: date = Field(description="Showtime of the movie", examples=["2024-12-01"])
|
||||
plot: str = Field(
|
||||
description="Plot of the movie", examples=["Erica is a rancher..."]
|
||||
)
|
||||
release_date: datetime = Field(
|
||||
description="Release date of the movie", examples=["2022-11-05"]
|
||||
|
||||
|
||||
class MovieIn(MovieBase):
|
||||
actors: Optional[list[str]] | None = Field(
|
||||
None,
|
||||
description="Actors of the movie",
|
||||
examples=[["Peyton List", "Andrew W. Walker"]],
|
||||
)
|
||||
poster_url: str = Field(
|
||||
description="Poster URL of the movie", examples=["https://..."]
|
||||
release_date: datetime | None = Field(
|
||||
None, description="Release date of the movie", examples=["2022-11-05"]
|
||||
)
|
||||
showtime: datetime = Field(
|
||||
description="Showtime of the movie", examples=["2024-12-01"]
|
||||
poster_url: str | None = Field(
|
||||
None, description="Poster URL of the movie", examples=["https://..."]
|
||||
)
|
||||
is_watched: bool = Field(
|
||||
default=False,
|
||||
description="Whether the movie is watched or not",
|
||||
examples=[True],
|
||||
|
||||
|
||||
class Movie(MovieIn):
|
||||
id: Optional[str] = Field(
|
||||
default=None, examples=["123e4567-e89b-12d3-a456-426655440000"]
|
||||
)
|
||||
created_at: datetime = Field(
|
||||
default_factory=datetime.utcnow,
|
||||
description="Creation date of the movie",
|
||||
examples=[datetime.utcnow()],
|
||||
default_factory=datetime.utcnow, description="Creation date of the movie"
|
||||
)
|
||||
updated_at: datetime = Field(
|
||||
default_factory=datetime.utcnow,
|
||||
description="Modification date of the movie",
|
||||
examples=[datetime.utcnow()],
|
||||
modified_at: datetime = Field(
|
||||
default_factory=datetime.utcnow, description="Modification date of the movie"
|
||||
)
|
||||
|
||||
+17
-8
@@ -1,12 +1,21 @@
|
||||
from sqlmodel import Field, SQLModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class Score(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
user_id: Optional[int] = Field(default=None, foreign_key="user.id")
|
||||
movie_id: Optional[int] = Field(default=None, foreign_key="movie.id")
|
||||
score: int
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
class ScoreBase(BaseModel):
|
||||
user_id: str = Field(
|
||||
description="UUID representation of the user",
|
||||
examples=["123e4567-e89b-12d3-a456-426655440000"],
|
||||
)
|
||||
movie_id: str = Field(
|
||||
description="UUID representation of the movie",
|
||||
examples=["123e4567-e89b-12d3-a456-426655440000"],
|
||||
)
|
||||
score: int = Field(description="Score of the movie", examples=[5], ge=-18, le=33)
|
||||
created_at: Optional[datetime] = Field(default_factory=datetime.utcnow)
|
||||
updated_at: Optional[datetime] = Field(default_factory=datetime.utcnow)
|
||||
|
||||
|
||||
class ScoreDB(ScoreBase):
|
||||
id: Optional[str]
|
||||
|
||||
+50
-8
@@ -2,7 +2,7 @@ from fastapi import APIRouter
|
||||
|
||||
from models import Message
|
||||
from models.user import UserCredentials
|
||||
from models.movie import Movie
|
||||
from models.movie import MovieBase
|
||||
from models.card import Card
|
||||
from lib.database.admin import (
|
||||
add_user,
|
||||
@@ -22,35 +22,77 @@ router = APIRouter(
|
||||
|
||||
@router.post("/user", response_model=Message)
|
||||
async def add_user_endpoint(user: UserCredentials):
|
||||
"""Add a user to the database"""
|
||||
"""Add a user to the database
|
||||
|
||||
Args:
|
||||
user (UserCredentials): The user to add
|
||||
|
||||
Returns:
|
||||
Message: A message indicating the result of the operation
|
||||
"""
|
||||
return add_user(user)
|
||||
|
||||
|
||||
@router.delete("/user", response_model=Message)
|
||||
async def remove_user_endpoint(user_id: str):
|
||||
"""Remove a user from the database"""
|
||||
"""Remove a user from the database
|
||||
|
||||
Args:
|
||||
user_id (str): The id of the user to remove
|
||||
|
||||
Returns:
|
||||
Message: A message indicating the result of the operation
|
||||
"""
|
||||
return remove_user(user_id)
|
||||
|
||||
|
||||
@router.post("/movie", response_model=Message)
|
||||
async def add_movie_endpoint(movie: Movie):
|
||||
"""Add a movie to the database"""
|
||||
async def add_movie_endpoint(movie: MovieBase):
|
||||
"""Add a movie to the database
|
||||
|
||||
Args:
|
||||
movie (MovieBase): The movie to add
|
||||
|
||||
Returns:
|
||||
Message: A message indicating the result of the operation
|
||||
"""
|
||||
return add_movie(movie)
|
||||
|
||||
|
||||
@router.patch("/movie", response_model=Message)
|
||||
async def set_movie_watched_endpoint(movie_id: str):
|
||||
"""Set movie as watched"""
|
||||
"""Set movie as watched
|
||||
|
||||
Args:
|
||||
movie_id (str): The id of the movie to set as watched
|
||||
|
||||
Returns:
|
||||
Message: A message indicating the result of the operation
|
||||
"""
|
||||
return set_movie_watched(movie_id)
|
||||
|
||||
|
||||
@router.post("/card", response_model=Message)
|
||||
async def add_card_endpoint(card: Card):
|
||||
"""Add a card to the database"""
|
||||
"""Add a card to the database
|
||||
|
||||
Args:
|
||||
card (Card): The card to add
|
||||
|
||||
Returns:
|
||||
Message: A message indicating the result of the operation
|
||||
"""
|
||||
return add_card(card)
|
||||
|
||||
|
||||
@router.delete("/card", response_model=Message)
|
||||
async def remove_card_endpoint(card_id: str):
|
||||
"""Remove a card from the database"""
|
||||
"""Remove a card from the database
|
||||
|
||||
Args:
|
||||
card_id (str): The id of the card to remove
|
||||
|
||||
Returns:
|
||||
Message: A message indicating the result of the operation
|
||||
"""
|
||||
return remove_card(card_id)
|
||||
|
||||
+6
-2
@@ -10,7 +10,11 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", response_model=list[Card])
|
||||
@router.get("/", responses={200: {"model": list[Card]}})
|
||||
async def get_cards() -> list[Card]:
|
||||
"""Get all cards"""
|
||||
"""Get all cards
|
||||
|
||||
Returns:
|
||||
list[Card]: List of cards
|
||||
"""
|
||||
return get_all_cards()
|
||||
|
||||
+5
-1
@@ -18,7 +18,11 @@ async def get_movies():
|
||||
|
||||
@router.get("/next", response_model=Movie)
|
||||
async def get_next_movie_endpoint():
|
||||
"""Get the next movie to watch"""
|
||||
"""Get the next movie to watch
|
||||
|
||||
Returns:
|
||||
Movie: The next movie to watch
|
||||
"""
|
||||
next_movie = get_next_movie()
|
||||
if next_movie is None:
|
||||
return Response(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from models.score import ScoreBase
|
||||
from lib.database.score import set_score
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/score",
|
||||
tags=["score"],
|
||||
@@ -10,3 +13,8 @@ router = APIRouter(
|
||||
@router.get("/leaderboard")
|
||||
async def get_leaderboard():
|
||||
return {"leaderboard": ["user1", "user2", "user3"]}
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def add_score(score: ScoreBase):
|
||||
return set_score(score)
|
||||
|
||||
+21
-6
@@ -14,11 +14,14 @@ router = APIRouter(
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=list[UserOut],
|
||||
responses={200: {"model": UserOut}, 404: {"model": Message}},
|
||||
responses={200: {"model": list[UserOut]}, 404: {"model": Message}},
|
||||
)
|
||||
async def get_users() -> list[UserOut]:
|
||||
"""Get all users"""
|
||||
"""Get all users
|
||||
|
||||
Returns:
|
||||
list[UserOut]: List of users
|
||||
"""
|
||||
return get_all_users()
|
||||
|
||||
|
||||
@@ -29,11 +32,20 @@ async def get_users() -> list[UserOut]:
|
||||
400: {"model": Message},
|
||||
404: {"model": Message},
|
||||
},
|
||||
summary="Login a user",
|
||||
description="Login a user by providing their username and password",
|
||||
)
|
||||
async def login(user: UserCredentials):
|
||||
"""Login a user"""
|
||||
user_from_db: UserIn = get_user_by_username(user.username)
|
||||
"""Login a user by providing their username and password
|
||||
|
||||
Returns:
|
||||
UserOut: User information
|
||||
Response: Error message
|
||||
"""
|
||||
# Get the user from the database
|
||||
user_from_db: UserIn | None = get_user_by_username(user.username)
|
||||
if user_from_db is None:
|
||||
# User not found
|
||||
return Response(
|
||||
status_code=404,
|
||||
media_type="application/json",
|
||||
@@ -41,11 +53,14 @@ async def login(user: UserCredentials):
|
||||
message="User not found", message_type="error"
|
||||
).model_dump_json(),
|
||||
)
|
||||
password = user.password.encode("utf-8")
|
||||
|
||||
# Check the password
|
||||
password = user.password.encode("utf-8")
|
||||
if bcrypt.checkpw(password, user_from_db.password.encode("utf-8")):
|
||||
# Login successful
|
||||
return UserOut(**user_from_db.model_dump())
|
||||
|
||||
# Invalid password
|
||||
return Response(
|
||||
status_code=400,
|
||||
media_type="application/json",
|
||||
|
||||
@@ -289,8 +289,8 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pjl"
|
||||
version = "0.1.0"
|
||||
name = "pjl-backend"
|
||||
version = "1.0rc1"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "bcrypt" },
|
||||
@@ -305,6 +305,11 @@ dependencies = [
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "bcrypt", specifier = ">=4.2.0" },
|
||||
@@ -319,6 +324,9 @@ requires-dist = [
|
||||
{ name = "uvicorn", specifier = ">=0.32.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "ruff", specifier = ">=0.8.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.9.2"
|
||||
@@ -448,6 +456,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/d6/a2373f3ba7180ddb44420d2a9d1f1510e1a4d162b3d27282bedcb09c8da9/ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44", size = 3276537 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/77/e889ee3ce7fd8baa3ed1b77a03b9fb8ec1be68be1418261522fd6a5405e0/ruff-0.8.0-py3-none-linux_armv6l.whl", hash = "sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea", size = 10518283 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/c8/0a47de01edf19fb22f5f9b7964f46a68d0bdff20144d134556ffd1ba9154/ruff-0.8.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:295bb4c02d58ff2ef4378a1870c20af30723013f441c9d1637a008baaf928c8b", size = 10317691 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/17/9885e4a0eeae07abd2a4ebabc3246f556719f24efa477ba2739146c4635a/ruff-0.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b1f1c76b47c18fa92ee78b60d2d20d7e866c55ee603e7d19c1e991fad933a9a", size = 9940999 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/cd/46b6f7043597eb318b5f5482c8ae8f5491cccce771e85f59d23106f2d179/ruff-0.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb0d4f250a7711b67ad513fde67e8870109e5ce590a801c3722580fe98c33a99", size = 10772437 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/87/afc95aeb8bc78b1d8a3461717a4419c05aa8aa943d4c9cbd441630f85584/ruff-0.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e55cce9aa93c5d0d4e3937e47b169035c7e91c8655b0974e61bb79cf398d49c", size = 10299156 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/fa/04c647bb809c4d65e8eae1ed1c654d9481b21dd942e743cd33511687b9f9/ruff-0.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f4cd64916d8e732ce6b87f3f5296a8942d285bbbc161acee7fe561134af64f9", size = 11325819 },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/26/7dad6e7d833d391a8a1afe4ee70ca6f36c4a297d3cca83ef10e83e9aacf3/ruff-0.8.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c5c1466be2a2ebdf7c5450dd5d980cc87c8ba6976fb82582fea18823da6fa362", size = 12023927 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/a0/be5296dda6428ba8a13bda8d09fbc0e14c810b485478733886e61597ae2b/ruff-0.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2dabfd05b96b7b8f2da00d53c514eea842bff83e41e1cceb08ae1966254a51df", size = 11589702 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/3f/7602eb11d2886db545834182a9dbe500b8211fcbc9b4064bf9d358bbbbb4/ruff-0.8.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:facebdfe5a5af6b1588a1d26d170635ead6892d0e314477e80256ef4a8470cf3", size = 12782936 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/5d/083181bdec4ec92a431c1291d3fff65eef3ded630a4b55eb735000ef5f3b/ruff-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c", size = 11138488 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/23/c12cdef58413cee2436d6a177aa06f7a366ebbca916cf10820706f632459/ruff-0.8.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85e654f0ded7befe2d61eeaf3d3b1e4ef3894469cd664ffa85006c7720f1e4a2", size = 10744474 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/61/a12f3b81520083cd7c5caa24ba61bb99fd1060256482eff0ef04cc5ccd1b/ruff-0.8.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:83a55679c4cb449fa527b8497cadf54f076603cc36779b2170b24f704171ce70", size = 10369029 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/2a/c013f4f3e4a54596c369cee74c24870ed1d534f31a35504908b1fc97017a/ruff-0.8.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:812e2052121634cf13cd6fddf0c1871d0ead1aad40a1a258753c04c18bb71bbd", size = 10867481 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/f7/685b1e1d42a3e94ceb25eab23c70bdd8c0ab66a43121ef83fe6db5a58756/ruff-0.8.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:780d5d8523c04202184405e60c98d7595bdb498c3c6abba3b6d4cdf2ca2af426", size = 11237117 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/20/401132c0908e8837625e3b7e32df9962e7cd681a4df1e16a10e2a5b4ecda/ruff-0.8.0-py3-none-win32.whl", hash = "sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468", size = 8783511 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/5c/4d800fca7854f62ad77f2c0d99b4b585f03e2d87a6ec1ecea85543a14a3c/ruff-0.8.0-py3-none-win_amd64.whl", hash = "sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f", size = 9559876 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/bc/cc8a6a5ca4960b226dc15dd8fb511dd11f2014ff89d325c0b9b9faa9871f/ruff-0.8.0-py3-none-win_arm64.whl", hash = "sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6", size = 8939733 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
|
||||
Reference in New Issue
Block a user