Compare commits
17
Commits
da41fb5990
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2beef62041 | ||
|
|
9c2a3ac42e | ||
|
|
b5e693ab9f | ||
|
|
cecd9a4458 | ||
|
|
ab7372cc9c | ||
|
|
6d7eae5ea4 | ||
|
|
10303accd2 | ||
|
|
7f6dcefb91 | ||
|
|
960856cdd8 | ||
|
|
6e8b62ffe1 | ||
|
|
8ecd30db0e | ||
|
|
cc0fd8ec64 | ||
|
|
6beba3945e | ||
|
|
9a3b4d9699 | ||
|
|
90b9a3a717 | ||
|
|
90dbed634d | ||
|
|
eb3b4478fc |
+2
-1
@@ -9,7 +9,8 @@ wheels/
|
|||||||
# Virtual environments
|
# Virtual environments
|
||||||
.venv
|
.venv
|
||||||
|
|
||||||
.env
|
.env*
|
||||||
|
.ruff_cache
|
||||||
|
|
||||||
*.sqlite
|
*.sqlite
|
||||||
*.duckdb
|
*.duckdb
|
||||||
@@ -5,6 +5,7 @@ WORKDIR /app
|
|||||||
ENV UV_COMPILE_BYTECODE=1
|
ENV UV_COMPILE_BYTECODE=1
|
||||||
|
|
||||||
ADD pyproject.toml uv.lock .
|
ADD pyproject.toml uv.lock .
|
||||||
|
RUN mkdir -p /app/data
|
||||||
RUN uv sync --frozen --no-dev
|
RUN uv sync --frozen --no-dev
|
||||||
|
|
||||||
ADD /src .
|
ADD /src .
|
||||||
|
|||||||
+14
-2
@@ -1,10 +1,22 @@
|
|||||||
services:
|
services:
|
||||||
pjl:
|
pjl-backend:
|
||||||
# Build the image from the Dockerfile in the current directory
|
# Build the image from the Dockerfile in the current directory
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: pjl:0.9
|
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
|
# Host the FastAPI application on port 8000
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
+13
-3
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "pjl"
|
name = "pjl-backend"
|
||||||
version = "0.9"
|
version = "1.0rc1"
|
||||||
description = "Add your description here"
|
description = "Add your description here"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
@@ -11,8 +11,18 @@ dependencies = [
|
|||||||
"loguru>=0.7.2",
|
"loguru>=0.7.2",
|
||||||
"pydantic-settings>=2.6.1",
|
"pydantic-settings>=2.6.1",
|
||||||
"pydantic>=2.9.2",
|
"pydantic>=2.9.2",
|
||||||
"requests>=2.32.3",
|
|
||||||
"rich>=13.9.4",
|
"rich>=13.9.4",
|
||||||
"uvicorn>=0.32.0",
|
"uvicorn>=0.32.0",
|
||||||
"pandas>=2.2.3",
|
"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"
|
||||||
+55
-18
@@ -1,10 +1,14 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import bcrypt
|
||||||
|
import requests
|
||||||
|
|
||||||
from lib.database.database import db_run
|
from lib.database.database import db_run
|
||||||
from models import Message
|
from models import Message
|
||||||
from models.user import UserCredentials
|
from models.user import UserCredentials
|
||||||
from models.movie import Movie
|
from models.movie import MovieIn, Movie
|
||||||
from models.card import Card
|
from models.card import Card
|
||||||
|
from lib.settings import settings
|
||||||
import bcrypt
|
from lib.logger import logger
|
||||||
|
|
||||||
|
|
||||||
def add_user(user: UserCredentials):
|
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}')"
|
sql = f"INSERT INTO users (username, password) VALUES ('{user.username}', '{hashed_password}')"
|
||||||
try:
|
try:
|
||||||
db_run(sql)
|
db_run(sql)
|
||||||
except:
|
except Exception as e:
|
||||||
return Message(message="User already exists", message_type="error")
|
return Message(message=f"User already exists. Error: {e}", message_type="error")
|
||||||
|
|
||||||
return Message(message="User added successfully", message_type="success")
|
return Message(message="User added successfully", message_type="success")
|
||||||
|
|
||||||
|
|
||||||
def remove_user(user_id: str):
|
def remove_user(user_id: str):
|
||||||
|
logger.debug(f"Removing user {user_id}")
|
||||||
sql = f"DELETE FROM users WHERE id = '{user_id}'"
|
sql = f"DELETE FROM users WHERE id = '{user_id}'"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db_run(sql)
|
db_run(sql)
|
||||||
except:
|
except Exception as e:
|
||||||
return Message(message="User not found", message_type="error")
|
return Message(message=f"User not found. Error: {e}", message_type="error")
|
||||||
|
|
||||||
return Message(message="User removed successfully", message_type="success")
|
return Message(message="User removed successfully", message_type="success")
|
||||||
|
|
||||||
|
|
||||||
def add_movie(movie: Movie):
|
def add_movie(movie: MovieIn):
|
||||||
sql = "INSERT INTO movies (name, imdb_id, actors, release_date, plot, showtime) VALUES (?, ?, ?, ?, ?, ?)"
|
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:
|
try:
|
||||||
db_run(
|
db_run(
|
||||||
sql,
|
sql,
|
||||||
(
|
(
|
||||||
movie.name,
|
new_movie.name,
|
||||||
movie.imdb_id,
|
new_movie.imdb_id,
|
||||||
movie.actors,
|
new_movie.actors,
|
||||||
movie.release_date,
|
new_movie.release_date,
|
||||||
movie.plot,
|
new_movie.plot,
|
||||||
movie.showtime,
|
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")
|
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")
|
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}'"
|
sql = f"UPDATE movies SET is_watched = TRUE WHERE id = '{movie_id}'"
|
||||||
try:
|
try:
|
||||||
db_run(sql)
|
db_run(sql)
|
||||||
except:
|
except Exception as e:
|
||||||
return Message(message="Failed to set movie as watched", message_type="error")
|
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")
|
return Message(message="Movie set as watched successfully", message_type="success")
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
import duckdb
|
import duckdb
|
||||||
import bcrypt
|
import bcrypt
|
||||||
from rich import print
|
|
||||||
|
|
||||||
from lib import settings
|
from lib import settings
|
||||||
|
from lib.logger import logger
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def get_db():
|
def get_db():
|
||||||
try:
|
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
|
yield conn
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -23,11 +24,11 @@ def db_run(sql, values: tuple = None):
|
|||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
if settings.db_url.exists():
|
if settings.db_url.exists():
|
||||||
|
pass
|
||||||
# TODO: Development feature. Remove this in production
|
# TODO: Development feature. Remove this in production
|
||||||
from os import unlink
|
# from os import unlink
|
||||||
|
|
||||||
unlink(settings.db_url)
|
# unlink(settings.db_url)
|
||||||
# return
|
|
||||||
sql = ""
|
sql = ""
|
||||||
with open("lib/database/database.sql") as f:
|
with open("lib/database/database.sql") as f:
|
||||||
sql = f.read()
|
sql = f.read()
|
||||||
@@ -38,8 +39,5 @@ def init_db():
|
|||||||
|
|
||||||
hashed_password = bcrypt.hashpw("password".encode("utf-8"), bcrypt.gensalt())
|
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)"
|
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)
|
|
||||||
|
|
||||||
sql = f"INSERT INTO users (username, password, is_admin) VALUES ('test', '{hashed_password.decode('utf-8')}', false)"
|
|
||||||
db_run(sql)
|
db_run(sql)
|
||||||
|
|||||||
@@ -53,48 +53,55 @@ CREATE TABLE log (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- Add placeholder data
|
-- Add placeholder data
|
||||||
|
|
||||||
INSERT INTO movies (name, imdb_id, actors, release_date, plot, poster_url, showtime)
|
INSERT INTO movies (name, imdb_id, actors, release_date, plot, poster_url, showtime)
|
||||||
VALUES ('A Maple Valley Christmas',
|
VALUES ('Christmas Sail',
|
||||||
'tt21841642',
|
'tt15758364',
|
||||||
['Peyton List', 'Andrew W. Walker', 'Frances Flanagan'],
|
['Katee Sackhoff', 'Patrick Sabongui', 'Terry O''Quinn'],
|
||||||
'2022-11-05',
|
'2022-11-05',
|
||||||
'Erica on karjatilallinen, joka on viettänyt koko elämänsä työskennellen perhetilalla äitinsä ja sisarensa kanssa.
|
'Lizin etääntynyt isä pyytää apua ja Liz palaa kotikaupunkiinsa. Hän päättää luoda täydellisen joulun tyttärelleen ja saada ihmissuhteensa kuntoon.',
|
||||||
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/MV5BZTA0MjA2MmMtNWZhNy00N2UyLWI5YjItZTA4ZDVkZmNmMWVjXkEyXkFqcGc@._V1_SX300.jpg',
|
||||||
'https://m.media-amazon.com/images/M/MV5BZjliMTRlMGItOGQzZC00NTI2LWJkODctZWZmMDIzOGRmYTQ3XkEyXkFqcGc@._V1_.jpg',
|
'2024-12-02'),
|
||||||
'2024-12-01');
|
('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)
|
INSERT INTO cards (title, description, point_value)
|
||||||
VALUES
|
VALUES
|
||||||
('Punainen villakangastakki', 'Päähenkilöllä on aina kirkkaanpunainen, täydellisesti istuva talvitakki.', 1),
|
('Blondi tuli taloon', 'Naispääosalla pitkät blondit hiukset.', 1),
|
||||||
('Keskeytetty suudelma', 'Juuri, kun romanttinen hetki on käsillä, jokin keskeyttää ensisuudelman.', 1),
|
('Ensilumi', 'Juuri kun kaikki toivo on menetetty, ensilumi alkaa sataa.', 3),
|
||||||
('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),
|
|
||||||
('Evergreen', 'Tarinan kylä tai kaupunki on idyllinen paikka, nimeltään jotain jouluista, kuten ''Evergreen'' tai ''Snowville''.', 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),
|
('Hengailua kirjastossa', 'Pieni kirjakauppa tai kirjasto on keskeinen kohtauspaikka ja keskustelujen paikka.', 3),
|
||||||
('Naispääosa pitkä blondi', 'Naispääosalla pitkät blondit hiukset.', 1),
|
('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),
|
('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),
|
('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),
|
('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),
|
('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ää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),
|
('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),
|
('Pääosa jumissa', 'Päähenkilö jää jumiin esimerkiksi kaukaiseen kylään tai pikkukaupunkiin.', 1),
|
||||||
('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),
|
|
||||||
('Pakollinen luistelukohtaus', 'Pari luisteluhetkellä, jonka aikana tunteet alkavat syttyä.', 2),
|
('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),
|
('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),
|
('Yllättävä vieras', 'Joulun alla joku saapuu yllättäen perhejuhliin tai kylään.', 3),
|
||||||
('Lapsen joulutoive', 'Pieni lapsi toivoo joululta jotain suurta, yleensä perheeseen liittyvää.', 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 models.movie import Movie
|
||||||
from lib.database.database import get_db
|
from lib.database.database import get_db
|
||||||
|
from lib.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
@logger.catch
|
||||||
def get_all_movies() -> list[Movie]:
|
def get_all_movies() -> list[Movie]:
|
||||||
|
logger.info("Getting all movies")
|
||||||
sql = "SELECT * FROM movies"
|
sql = "SELECT * FROM movies"
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
result = conn.execute(sql).fetchall()
|
result = conn.execute(sql).df()
|
||||||
|
result_dict = result.to_dict(orient="records")
|
||||||
movies = []
|
movies = []
|
||||||
for row in result:
|
for row in result_dict:
|
||||||
movie = Movie(
|
row["id"] = str(row["id"])
|
||||||
id=str(row[0]),
|
logger.debug(f"Found movie: {row}")
|
||||||
name=row[1],
|
print("Type of name:", type(row["name"]))
|
||||||
imdb_id=row[2],
|
movie = Movie(**row)
|
||||||
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],
|
|
||||||
)
|
|
||||||
movies.append(movie)
|
movies.append(movie)
|
||||||
|
|
||||||
return movies
|
return movies
|
||||||
@@ -28,7 +25,7 @@ def get_all_movies() -> list[Movie]:
|
|||||||
|
|
||||||
|
|
||||||
def get_next_movie() -> Movie | None:
|
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:
|
with get_db() as conn:
|
||||||
result = conn.execute(sql).fetchdf()
|
result = conn.execute(sql).fetchdf()
|
||||||
if result.empty:
|
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 models.user import UserIn, UserOut
|
||||||
from lib.database.database import get_db
|
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:
|
def get_user_by_username(username) -> UserIn | None:
|
||||||
sql = f"SELECT * FROM users WHERE username = '{username}'"
|
sql = f"SELECT * FROM users WHERE username = '{username}'"
|
||||||
with get_db() as conn:
|
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:
|
if result is not None:
|
||||||
return UserIn(
|
last_login = set_last_login(result["id"])
|
||||||
id=str(result[0]),
|
result["id"] = str(result["id"])
|
||||||
username=result[1],
|
result["last_login"] = last_login
|
||||||
password=result[2],
|
return UserIn(**result)
|
||||||
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],
|
|
||||||
)
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
+2
-4
@@ -4,14 +4,12 @@ from pathlib import Path
|
|||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
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(
|
default_avatar_url: str = Field(
|
||||||
"https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=Kingston",
|
"https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=Kingston",
|
||||||
env="DEFAULT_AVATAR_URL",
|
env="DEFAULT_AVATAR_URL",
|
||||||
)
|
)
|
||||||
|
OMDB_API_KEY: str = Field(env="OMDB_API_KEY")
|
||||||
class Config:
|
|
||||||
env_file = ".env"
|
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
# from .movie import Movie
|
|
||||||
# from .score import Score
|
|
||||||
# from .card import Card
|
|
||||||
from .general import Message
|
from .general import Message
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# "Movie",
|
|
||||||
# "Score",
|
|
||||||
# "Card",
|
|
||||||
"Message",
|
"Message",
|
||||||
]
|
]
|
||||||
|
|||||||
+24
-28
@@ -1,44 +1,40 @@
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from datetime import datetime
|
from datetime import datetime, date
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class Movie(BaseModel):
|
class MovieBase(BaseModel):
|
||||||
id: Optional[str] = Field(
|
name: Optional[str] | None = Field(
|
||||||
default=None, examples=["123e4567-e89b-12d3-a456-426655440000"]
|
None, description="Name of the movie", examples=["A Maple Valley Christmas"]
|
||||||
)
|
|
||||||
name: str = Field(
|
|
||||||
description="Name of the movie", examples=["A Maple Valley Christmas"]
|
|
||||||
)
|
)
|
||||||
imdb_id: str = Field(description="IMDB ID of the movie", examples=["tt1234567"])
|
imdb_id: str = Field(description="IMDB ID of the movie", examples=["tt1234567"])
|
||||||
actors: list[str] = Field(
|
showtime: date = Field(description="Showtime of the movie", examples=["2024-12-01"])
|
||||||
description="Actors of the movie",
|
|
||||||
examples=[["Peyton List", "Andrew W. Walker"]],
|
|
||||||
)
|
|
||||||
plot: str = Field(
|
plot: str = Field(
|
||||||
description="Plot of the movie", examples=["Erica is a rancher..."]
|
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(
|
release_date: datetime | None = Field(
|
||||||
description="Poster URL of the movie", examples=["https://..."]
|
None, description="Release date of the movie", examples=["2022-11-05"]
|
||||||
)
|
)
|
||||||
showtime: datetime = Field(
|
poster_url: str | None = Field(
|
||||||
description="Showtime of the movie", examples=["2024-12-01"]
|
None, description="Poster URL of the movie", examples=["https://..."]
|
||||||
)
|
)
|
||||||
is_watched: bool = Field(
|
|
||||||
default=False,
|
|
||||||
description="Whether the movie is watched or not",
|
class Movie(MovieIn):
|
||||||
examples=[True],
|
id: Optional[str] = Field(
|
||||||
|
default=None, examples=["123e4567-e89b-12d3-a456-426655440000"]
|
||||||
)
|
)
|
||||||
created_at: datetime = Field(
|
created_at: datetime = Field(
|
||||||
default_factory=datetime.utcnow,
|
default_factory=datetime.utcnow, description="Creation date of the movie"
|
||||||
description="Creation date of the movie",
|
|
||||||
examples=[datetime.utcnow()],
|
|
||||||
)
|
)
|
||||||
updated_at: datetime = Field(
|
modified_at: datetime = Field(
|
||||||
default_factory=datetime.utcnow,
|
default_factory=datetime.utcnow, description="Modification date of the movie"
|
||||||
description="Modification date of the movie",
|
|
||||||
examples=[datetime.utcnow()],
|
|
||||||
)
|
)
|
||||||
|
|||||||
+17
-8
@@ -1,12 +1,21 @@
|
|||||||
from sqlmodel import Field, SQLModel
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class Score(SQLModel, table=True):
|
class ScoreBase(BaseModel):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
user_id: str = Field(
|
||||||
user_id: Optional[int] = Field(default=None, foreign_key="user.id")
|
description="UUID representation of the user",
|
||||||
movie_id: Optional[int] = Field(default=None, foreign_key="movie.id")
|
examples=["123e4567-e89b-12d3-a456-426655440000"],
|
||||||
score: int
|
)
|
||||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
movie_id: str = Field(
|
||||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
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 import Message
|
||||||
from models.user import UserCredentials
|
from models.user import UserCredentials
|
||||||
from models.movie import Movie
|
from models.movie import MovieBase
|
||||||
from models.card import Card
|
from models.card import Card
|
||||||
from lib.database.admin import (
|
from lib.database.admin import (
|
||||||
add_user,
|
add_user,
|
||||||
@@ -22,35 +22,77 @@ router = APIRouter(
|
|||||||
|
|
||||||
@router.post("/user", response_model=Message)
|
@router.post("/user", response_model=Message)
|
||||||
async def add_user_endpoint(user: UserCredentials):
|
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)
|
return add_user(user)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/user", response_model=Message)
|
@router.delete("/user", response_model=Message)
|
||||||
async def remove_user_endpoint(user_id: str):
|
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)
|
return remove_user(user_id)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/movie", response_model=Message)
|
@router.post("/movie", response_model=Message)
|
||||||
async def add_movie_endpoint(movie: Movie):
|
async def add_movie_endpoint(movie: MovieBase):
|
||||||
"""Add a movie to the database"""
|
"""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)
|
return add_movie(movie)
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/movie", response_model=Message)
|
@router.patch("/movie", response_model=Message)
|
||||||
async def set_movie_watched_endpoint(movie_id: str):
|
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)
|
return set_movie_watched(movie_id)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/card", response_model=Message)
|
@router.post("/card", response_model=Message)
|
||||||
async def add_card_endpoint(card: Card):
|
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)
|
return add_card(card)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/card", response_model=Message)
|
@router.delete("/card", response_model=Message)
|
||||||
async def remove_card_endpoint(card_id: str):
|
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)
|
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]:
|
async def get_cards() -> list[Card]:
|
||||||
"""Get all cards"""
|
"""Get all cards
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Card]: List of cards
|
||||||
|
"""
|
||||||
return get_all_cards()
|
return get_all_cards()
|
||||||
|
|||||||
+5
-1
@@ -18,7 +18,11 @@ async def get_movies():
|
|||||||
|
|
||||||
@router.get("/next", response_model=Movie)
|
@router.get("/next", response_model=Movie)
|
||||||
async def get_next_movie_endpoint():
|
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()
|
next_movie = get_next_movie()
|
||||||
if next_movie is None:
|
if next_movie is None:
|
||||||
return Response(
|
return Response(
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from models.score import ScoreBase
|
||||||
|
from lib.database.score import set_score
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="/score",
|
prefix="/score",
|
||||||
tags=["score"],
|
tags=["score"],
|
||||||
@@ -10,3 +13,8 @@ router = APIRouter(
|
|||||||
@router.get("/leaderboard")
|
@router.get("/leaderboard")
|
||||||
async def get_leaderboard():
|
async def get_leaderboard():
|
||||||
return {"leaderboard": ["user1", "user2", "user3"]}
|
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(
|
@router.get(
|
||||||
"/",
|
"/",
|
||||||
response_model=list[UserOut],
|
responses={200: {"model": list[UserOut]}, 404: {"model": Message}},
|
||||||
responses={200: {"model": UserOut}, 404: {"model": Message}},
|
|
||||||
)
|
)
|
||||||
async def get_users() -> list[UserOut]:
|
async def get_users() -> list[UserOut]:
|
||||||
"""Get all users"""
|
"""Get all users
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[UserOut]: List of users
|
||||||
|
"""
|
||||||
return get_all_users()
|
return get_all_users()
|
||||||
|
|
||||||
|
|
||||||
@@ -29,11 +32,20 @@ async def get_users() -> list[UserOut]:
|
|||||||
400: {"model": Message},
|
400: {"model": Message},
|
||||||
404: {"model": Message},
|
404: {"model": Message},
|
||||||
},
|
},
|
||||||
|
summary="Login a user",
|
||||||
|
description="Login a user by providing their username and password",
|
||||||
)
|
)
|
||||||
async def login(user: UserCredentials):
|
async def login(user: UserCredentials):
|
||||||
"""Login a user"""
|
"""Login a user by providing their username and password
|
||||||
user_from_db: UserIn = get_user_by_username(user.username)
|
|
||||||
|
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:
|
if user_from_db is None:
|
||||||
|
# User not found
|
||||||
return Response(
|
return Response(
|
||||||
status_code=404,
|
status_code=404,
|
||||||
media_type="application/json",
|
media_type="application/json",
|
||||||
@@ -41,11 +53,14 @@ async def login(user: UserCredentials):
|
|||||||
message="User not found", message_type="error"
|
message="User not found", message_type="error"
|
||||||
).model_dump_json(),
|
).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")):
|
if bcrypt.checkpw(password, user_from_db.password.encode("utf-8")):
|
||||||
|
# Login successful
|
||||||
return UserOut(**user_from_db.model_dump())
|
return UserOut(**user_from_db.model_dump())
|
||||||
|
|
||||||
|
# Invalid password
|
||||||
return Response(
|
return Response(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
media_type="application/json",
|
media_type="application/json",
|
||||||
|
|||||||
@@ -289,8 +289,8 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pjl"
|
name = "pjl-backend"
|
||||||
version = "0.9"
|
version = "1.0rc1"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "bcrypt" },
|
{ name = "bcrypt" },
|
||||||
@@ -305,6 +305,11 @@ dependencies = [
|
|||||||
{ name = "uvicorn" },
|
{ name = "uvicorn" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "bcrypt", specifier = ">=4.2.0" },
|
{ name = "bcrypt", specifier = ">=4.2.0" },
|
||||||
@@ -319,6 +324,9 @@ requires-dist = [
|
|||||||
{ name = "uvicorn", specifier = ">=0.32.0" },
|
{ name = "uvicorn", specifier = ">=0.32.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [{ name = "ruff", specifier = ">=0.8.0" }]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.9.2"
|
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 },
|
{ 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]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user