Compare commits
7
Commits
6706abfc97
...
v1.0rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aecc0731bc | ||
|
|
751af50e8a | ||
|
|
005ac8634f | ||
|
|
055dd3b43b | ||
|
|
4a4eda09d1 | ||
|
|
e647bc7811 | ||
|
|
bf7c969629 |
@@ -13,5 +13,7 @@ wheels/
|
|||||||
*.sqlite
|
*.sqlite
|
||||||
*.duckdb
|
*.duckdb
|
||||||
*.db
|
*.db
|
||||||
|
*.log
|
||||||
|
*.gz
|
||||||
|
|
||||||
.vscode/*
|
.vscode/*
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to the Eurovision-25 backend will be documented in this file.
|
||||||
|
|
||||||
|
## [1.0rc1] - 2025-05-10
|
||||||
|
|
||||||
|
### First Release Candidate
|
||||||
|
|
||||||
|
This is the first release candidate for Eurovision-25 backend, featuring all core functionality:
|
||||||
|
|
||||||
|
- Authentication and user management
|
||||||
|
- Song management and reviews
|
||||||
|
- Results calculation and display
|
||||||
|
- Admin functionality
|
||||||
|
- Static file serving for avatars and country flags
|
||||||
|
|
||||||
|
The application is now feature complete, but may still contain bugs that will be addressed before the final 1.0.0 release.
|
||||||
@@ -10,3 +10,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- ADMIN_USERNAME=admin
|
||||||
|
- ADMIN_PASSWORD=password
|
||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "Eurovision-25-backend"
|
name = "Eurovision-25-backend"
|
||||||
version = "0.1.0"
|
version = "1.0rc1"
|
||||||
description = "Backend for Eurovision 25"
|
description = "Backend for Eurovision 25"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
@@ -8,6 +8,7 @@ dependencies = [
|
|||||||
"aiofiles>=24.1.0",
|
"aiofiles>=24.1.0",
|
||||||
"duckdb>=1.2.2",
|
"duckdb>=1.2.2",
|
||||||
"fastapi>=0.115.12",
|
"fastapi>=0.115.12",
|
||||||
|
"loguru>=0.7.3",
|
||||||
"pandas>=2.2.3",
|
"pandas>=2.2.3",
|
||||||
"passlib[bcrypt]>=1.7.4",
|
"passlib[bcrypt]>=1.7.4",
|
||||||
"python-dotenv>=1.1.0",
|
"python-dotenv>=1.1.0",
|
||||||
|
|||||||
+4
-1
@@ -5,13 +5,14 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
# Import routers
|
# Import routers
|
||||||
from routes import auth, admin, users, songs, reviews, results
|
from routes import auth, admin, users, songs, reviews, results
|
||||||
|
|
||||||
|
from lib.logger import logger
|
||||||
from lib.db import init_db
|
from lib.db import init_db
|
||||||
|
|
||||||
# Initialize FastAPI app
|
# Initialize FastAPI app
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Eurovision 25 Homereview API",
|
title="Eurovision 25 Homereview API",
|
||||||
description="Backend API for Eurovision 25 Homereview application",
|
description="Backend API for Eurovision 25 Homereview application",
|
||||||
version="1.0.0",
|
version="1.0rc1",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add CORS middleware
|
# Add CORS middleware
|
||||||
@@ -32,12 +33,14 @@ app.mount("/static/flags", StaticFiles(directory="static/flags"), name="flags")
|
|||||||
# Startup event to create database tables
|
# Startup event to create database tables
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
def on_startup():
|
def on_startup():
|
||||||
|
logger.info("Starting up...")
|
||||||
init_db()
|
init_db()
|
||||||
|
|
||||||
|
|
||||||
# Health check endpoint
|
# Health check endpoint
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
|
logger.debug("Root endpoint accessed")
|
||||||
return {"status": "ok", "message": "Eurovision 25 Homereview API"}
|
return {"status": "ok", "message": "Eurovision 25 Homereview API"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+44
-3
@@ -91,7 +91,7 @@ CREATE TABLE Review (
|
|||||||
song_id INTEGER NOT NULL, -- Foreign Key -> Song.id
|
song_id INTEGER NOT NULL, -- Foreign Key -> Song.id
|
||||||
score_song INTEGER NOT NULL, -- Score (1-100) for song quality
|
score_song INTEGER NOT NULL, -- Score (1-100) for song quality
|
||||||
score_show INTEGER NOT NULL, -- Score (1-100) for the stage show
|
score_show INTEGER NOT NULL, -- Score (1-100) for the stage show
|
||||||
score_wardrobe INTEGER NOT NULL, -- Score (1-100) for wardrobe/costumes
|
score_costume INTEGER NOT NULL, -- Score (1-100) for wardrobe/costumes
|
||||||
text_review VARCHAR, -- Optional textual comments (nullable)
|
text_review VARCHAR, -- Optional textual comments (nullable)
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the review was created
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the review was created
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the review was last modified
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the review was last modified
|
||||||
@@ -102,7 +102,7 @@ CREATE TABLE Review (
|
|||||||
-- Ensure scores are within the valid range (1-100)
|
-- Ensure scores are within the valid range (1-100)
|
||||||
CHECK (score_song >= 1 AND score_song <= 100),
|
CHECK (score_song >= 1 AND score_song <= 100),
|
||||||
CHECK (score_show >= 1 AND score_show <= 100),
|
CHECK (score_show >= 1 AND score_show <= 100),
|
||||||
CHECK (score_wardrobe >= 1 AND score_wardrobe <= 100),
|
CHECK (score_costume >= 1 AND score_costume <= 100),
|
||||||
|
|
||||||
-- Ensure each user can only submit one review per song
|
-- Ensure each user can only submit one review per song
|
||||||
UNIQUE (user_id, song_id)
|
UNIQUE (user_id, song_id)
|
||||||
@@ -111,8 +111,49 @@ CREATE TABLE Review (
|
|||||||
CREATE TABLE CountryCodes (
|
CREATE TABLE CountryCodes (
|
||||||
code VARCHAR PRIMARY KEY,
|
code VARCHAR PRIMARY KEY,
|
||||||
name_en VARCHAR NOT NULL,
|
name_en VARCHAR NOT NULL,
|
||||||
name_fi VARCHAR NOT NULL
|
name_fi VARCHAR NOT NULL,
|
||||||
|
name_sv VARCHAR NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Insert country codes data
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ALB', 'Albania', 'Albania', 'Albanien');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ARM', 'Armenia', 'Armenia', 'Armenien');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('AUS', 'Australia', 'Australia', 'Australien');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('AUT', 'Austria', 'Itävalta', 'Österrike');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('AZE', 'Azerbaijan', 'Azerbaidžan', 'Azerbajdzjan');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('BEL', 'Belgium', 'Belgia', 'Belgien');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('HRV', 'Croatia', 'Kroatia', 'Kroatien');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('CYP', 'Cyprus', 'Kypros', 'Cypern');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('CZE', 'Czechia', 'Tšekki', 'Tjeckien');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('DNK', 'Denmark', 'Tanska', 'Danmark');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('EST', 'Estonia', 'Viro', 'Estland');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('FIN', 'Finland', 'Suomi', 'Finland');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('FRA', 'France', 'Ranska', 'Frankrike');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('GEO', 'Georgia', 'Georgia', 'Georgien');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('DEU', 'Germany', 'Saksa', 'Tyskland');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('GRC', 'Greece', 'Kreikka', 'Grekland');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ISL', 'Iceland', 'Islanti', 'Island');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('IRL', 'Ireland', 'Irlanti', 'Irland');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ISR', 'Israel', 'Israel', 'Israel');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ITA', 'Italy', 'Italia', 'Italien');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('LVA', 'Latvia', 'Latvia', 'Lettland');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('LTU', 'Lithuania', 'Liettua', 'Litauen');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('LUX', 'Luxembourg', 'Luxemburg', 'Luxemburg');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('MLT', 'Malta', 'Malta', 'Malta');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('MNE', 'Montenegro', 'Montenegro', 'Montenegro');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('NLD', 'Netherlands', 'Alankomaat', 'Nederländerna');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('NOR', 'Norway', 'Norja', 'Norge');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('POL', 'Poland', 'Puola', 'Polen');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('PRT', 'Portugal', 'Portugali', 'Portugal');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('SMR', 'San Marino', 'San Marino', 'San Marino');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('SRB', 'Serbia', 'Serbia', 'Serbien');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('SVN', 'Slovenia', 'Slovenia', 'Slovenien');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('ESP', 'Spain', 'Espanja', 'Spanien');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('SWE', 'Sweden', 'Ruotsi', 'Sverige');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('CHE', 'Switzerland', 'Sveitsi', 'Schweiz');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('UKR', 'Ukraine', 'Ukraina', 'Ukraina');
|
||||||
|
INSERT INTO CountryCodes (code, name_en, name_fi, name_sv) VALUES ('GBR', 'United Kingdom', 'Englanti', 'Storbritannien');
|
||||||
|
|
||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
-- Views (Optional - Not Created Here Based on Documentation Decision)
|
-- Views (Optional - Not Created Here Based on Documentation Decision)
|
||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
|
|||||||
+4
-23
@@ -12,7 +12,6 @@ load_dotenv()
|
|||||||
DB_PATH = Path("./data/data.duckdb").absolute()
|
DB_PATH = Path("./data/data.duckdb").absolute()
|
||||||
SQL_PATH = Path("./lib/database.sql").absolute()
|
SQL_PATH = Path("./lib/database.sql").absolute()
|
||||||
SEED_JSON_PATH = Path("./lib/seed_data.json").absolute()
|
SEED_JSON_PATH = Path("./lib/seed_data.json").absolute()
|
||||||
COUNTRYCODES_JSON_PATH = Path("./lib/countrycodes.json").absolute()
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
@@ -53,23 +52,7 @@ def seed_db() -> None:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Seed CountryCodes
|
# CountryCodes are now directly inserted in the SQL file
|
||||||
with open(COUNTRYCODES_JSON_PATH, "r") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
if not data:
|
|
||||||
return
|
|
||||||
|
|
||||||
for item in data:
|
|
||||||
with get_connection() as conn:
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO CountryCodes (code, name_en, name_fi) VALUES (?, ?, ?)",
|
|
||||||
(
|
|
||||||
item["code"],
|
|
||||||
item["name_en"],
|
|
||||||
item["name_fi"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Seed Teams
|
# Seed Teams
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
@@ -77,7 +60,7 @@ def seed_db() -> None:
|
|||||||
|
|
||||||
# Seed Admin users
|
# Seed Admin users
|
||||||
admin_username = os.getenv("ADMIN_USERNAME")
|
admin_username = os.getenv("ADMIN_USERNAME")
|
||||||
hashed_admin_password = hash_password(os.getenv("ADMIN_PASSWORD"))
|
hashed_admin_password = hash_password(os.getenv("ADMIN_PASSWORD", "password"))
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
'INSERT INTO "User" (username, hashed_password, team_id, is_active, is_admin) VALUES (?, ?, ?, ?, ?)',
|
'INSERT INTO "User" (username, hashed_password, team_id, is_active, is_admin) VALUES (?, ?, ?, ?, ?)',
|
||||||
@@ -92,12 +75,10 @@ def seed_db() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
|
if Path(DB_PATH).exists():
|
||||||
|
return
|
||||||
with open(SQL_PATH, "r") as f:
|
with open(SQL_PATH, "r") as f:
|
||||||
sql = f.read()
|
sql = f.read()
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
conn.execute(sql)
|
conn.execute(sql)
|
||||||
seed_db()
|
seed_db()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
init_db()
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from loguru import logger
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
LOG_PATH = Path("./data/logs/logs.log").absolute()
|
||||||
|
ERROR_PATH = Path("./data/logs/errors.log").absolute()
|
||||||
|
|
||||||
|
logger.remove()
|
||||||
|
logger.add(LOG_PATH, rotation="10 MB", compression="gz", level="INFO")
|
||||||
|
logger.add(sys.stdout, level="INFO")
|
||||||
|
|
||||||
|
logger.add(ERROR_PATH, rotation="10 MB", compression="gz", level="ERROR")
|
||||||
|
logger.add(sys.stderr, level="ERROR")
|
||||||
|
logger.add(sys.stdout, level="DEBUG")
|
||||||
|
|
||||||
|
logger = logger
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewBase(BaseModel):
|
||||||
|
song_id: int = Field(description="ID of the song", examples=[1])
|
||||||
|
user_id: int = Field(description="ID of the user", examples=[1])
|
||||||
|
score_song: int = Field(
|
||||||
|
gt=0, le=100, description="Score given by the user", examples=[1]
|
||||||
|
)
|
||||||
|
score_show: int = Field(
|
||||||
|
gt=0, le=100, description="Score given by the user", examples=[1]
|
||||||
|
)
|
||||||
|
score_costume: int = Field(
|
||||||
|
gt=0, le=100, description="Score given by the user", examples=[1]
|
||||||
|
)
|
||||||
|
text_review: str | None = Field(
|
||||||
|
default=None, description="Comment given by the user", examples=["comment"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewIn(ReviewBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewSearch(BaseModel):
|
||||||
|
song_id: int = Field(description="ID of the song", examples=[1])
|
||||||
|
user_id: int = Field(description="ID of the user", examples=[1])
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewOut(ReviewBase):
|
||||||
|
created_at: datetime = Field(
|
||||||
|
default_factory=datetime.now,
|
||||||
|
description="Creation timestamp",
|
||||||
|
examples=["2025-01-01T00:00:00.000Z"],
|
||||||
|
)
|
||||||
|
updated_at: datetime = Field(
|
||||||
|
default_factory=datetime.now,
|
||||||
|
description="Last update timestamp",
|
||||||
|
examples=["2025-01-01T00:00:00.000Z"],
|
||||||
|
)
|
||||||
+66
-2
@@ -1,3 +1,67 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
router = APIRouter(prefix="/songs", tags=["songs"])
|
from models.review import ReviewOut, ReviewSearch, ReviewIn
|
||||||
|
from lib.db import get_connection
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/reviews", tags=["reviews"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/all", response_model=list[ReviewOut])
|
||||||
|
async def list_reviews():
|
||||||
|
with get_connection() as conn:
|
||||||
|
reviews = conn.execute("SELECT * FROM Review").fetchdf()
|
||||||
|
if reviews.empty:
|
||||||
|
raise HTTPException(status_code=404, detail="Reviews not found")
|
||||||
|
return reviews.to_dict(orient="records")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=ReviewOut)
|
||||||
|
async def get_review(review_id: ReviewSearch):
|
||||||
|
with get_connection() as conn:
|
||||||
|
review = conn.execute(
|
||||||
|
"SELECT * FROM Review WHERE song_id = ? AND user_id = ?",
|
||||||
|
(review_id.song_id, review_id.user_id),
|
||||||
|
).fetchdf()
|
||||||
|
if review.empty:
|
||||||
|
raise HTTPException(status_code=404, detail="Review not found")
|
||||||
|
return review.to_dict(orient="records")[0]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=ReviewOut)
|
||||||
|
async def create_review(review: ReviewIn):
|
||||||
|
with get_connection() as conn:
|
||||||
|
review = conn.execute(
|
||||||
|
"INSERT INTO Review (user_id, song_id, score_song, score_show, score_costume, text_review) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
(
|
||||||
|
review.user_id,
|
||||||
|
review.song_id,
|
||||||
|
review.score_song,
|
||||||
|
review.score_show,
|
||||||
|
review.score_costume,
|
||||||
|
review.text_review,
|
||||||
|
),
|
||||||
|
).fetchdf()
|
||||||
|
if review.empty:
|
||||||
|
raise HTTPException(status_code=404, detail="Review not found")
|
||||||
|
return review.to_dict(orient="records")[0]
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/", response_model=ReviewOut)
|
||||||
|
async def update_review(review: ReviewIn):
|
||||||
|
with get_connection() as conn:
|
||||||
|
review = conn.execute(
|
||||||
|
"UPDATE Review SET user_id = ?, song_id = ?, score_song = ?, score_show = ?, score_costume = ?, text_review = ? WHERE song_id = ? AND user_id = ?",
|
||||||
|
(
|
||||||
|
review.user_id,
|
||||||
|
review.song_id,
|
||||||
|
review.score_song,
|
||||||
|
review.score_show,
|
||||||
|
review.score_costume,
|
||||||
|
review.text_review,
|
||||||
|
review.song_id,
|
||||||
|
review.user_id,
|
||||||
|
),
|
||||||
|
).fetchdf()
|
||||||
|
if review.empty:
|
||||||
|
raise HTTPException(status_code=404, detail="Review not found")
|
||||||
|
return review.to_dict(orient="records")[0]
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ dependencies = [
|
|||||||
{ name = "aiofiles" },
|
{ name = "aiofiles" },
|
||||||
{ name = "duckdb" },
|
{ name = "duckdb" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
|
{ name = "loguru" },
|
||||||
{ name = "pandas" },
|
{ name = "pandas" },
|
||||||
{ name = "passlib", extra = ["bcrypt"] },
|
{ name = "passlib", extra = ["bcrypt"] },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
@@ -154,6 +155,7 @@ requires-dist = [
|
|||||||
{ name = "aiofiles", specifier = ">=24.1.0" },
|
{ name = "aiofiles", specifier = ">=24.1.0" },
|
||||||
{ name = "duckdb", specifier = ">=1.2.2" },
|
{ name = "duckdb", specifier = ">=1.2.2" },
|
||||||
{ name = "fastapi", specifier = ">=0.115.12" },
|
{ name = "fastapi", specifier = ">=0.115.12" },
|
||||||
|
{ name = "loguru", specifier = ">=0.7.3" },
|
||||||
{ name = "pandas", specifier = ">=2.2.3" },
|
{ name = "pandas", specifier = ">=2.2.3" },
|
||||||
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
|
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.1.0" },
|
{ name = "python-dotenv", specifier = ">=1.1.0" },
|
||||||
@@ -218,6 +220,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "loguru"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markdown-it-py"
|
name = "markdown-it-py"
|
||||||
version = "3.0.0"
|
version = "3.0.0"
|
||||||
@@ -639,3 +654,12 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "win32-setctime"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user