forked from dxdc/babynames
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.py
More file actions
165 lines (122 loc) · 4.41 KB
/
Copy pathserver.py
File metadata and controls
165 lines (122 loc) · 4.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"""Baby Names — FastAPI backend for Glicko-2 state persistence."""
import json
import os
import sqlite3
from contextlib import contextmanager
from pathlib import Path
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
DATA_DIR = Path(os.environ.get("DATA_DIR", "/data"))
DB_PATH = DATA_DIR / "babynames.db"
app = FastAPI(title="Baby Names")
# ---------------------------------------------------------------
# Database
# ---------------------------------------------------------------
def get_db() -> sqlite3.Connection:
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
return conn
@contextmanager
def db_session():
conn = get_db()
try:
yield conn
conn.commit()
finally:
conn.close()
def init_db():
DATA_DIR.mkdir(parents=True, exist_ok=True)
with db_session() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS user_state (
user TEXT NOT NULL,
gender TEXT NOT NULL,
state TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user, gender)
)
""")
# ---------------------------------------------------------------
# Models
# ---------------------------------------------------------------
class SaveStateRequest(BaseModel):
user: str
gender: str
ratings: dict
vetoes: dict
history: list
totalComparisons: int
scopeLimit: int = 250
activeOrigins: list = []
# ---------------------------------------------------------------
# API Routes
# ---------------------------------------------------------------
@app.post("/api/state")
def save_state(req: SaveStateRequest):
if not req.user or not req.user.strip():
raise HTTPException(400, "User name required")
state = req.model_dump(exclude={"user", "gender"})
with db_session() as conn:
conn.execute(
"""INSERT INTO user_state (user, gender, state, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(user, gender) DO UPDATE SET
state = excluded.state, updated_at = CURRENT_TIMESTAMP""",
(req.user.lower(), req.gender, json.dumps(state)),
)
return {"ok": True}
@app.get("/api/state")
def load_state(user: str = Query(...), gender: str = Query("M")):
with db_session() as conn:
row = conn.execute(
"SELECT state FROM user_state WHERE user = ? AND gender = ?",
(user.lower(), gender),
).fetchone()
if not row:
raise HTTPException(404, "No saved state")
return JSONResponse(json.loads(row["state"]))
@app.get("/api/compare")
def compare(a: str = Query(...), b: str = Query(...), gender: str = Query("M")):
with db_session() as conn:
row_a = conn.execute(
"SELECT state FROM user_state WHERE user = ? AND gender = ?",
(a.lower(), gender),
).fetchone()
row_b = conn.execute(
"SELECT state FROM user_state WHERE user = ? AND gender = ?",
(b.lower(), gender),
).fetchone()
if not row_a:
raise HTTPException(404, f"No saved state for {a}")
if not row_b:
raise HTTPException(404, f"No saved state for {b}")
state_a = json.loads(row_a["state"])
state_b = json.loads(row_b["state"])
def top_n(ratings_dict, n=30):
entries = [
{"rank": int(k), **v}
for k, v in ratings_dict.get("ratings", {}).items()
if v.get("comparisons", 0) > 0
]
entries.sort(key=lambda e: e.get("mu", 0), reverse=True)
return entries[:n]
top_a = top_n(state_a)
top_b = top_n(state_b)
return {"a": top_a, "b": top_b}
# ---------------------------------------------------------------
# Static files — serve the frontend
# ---------------------------------------------------------------
@app.get("/")
def index():
return FileResponse("index.html")
# Mount static files last so API routes take priority
app.mount("/", StaticFiles(directory="."), name="static")
# ---------------------------------------------------------------
# Startup
# ---------------------------------------------------------------
@app.on_event("startup")
def on_startup():
init_db()