Tickets working

This commit is contained in:
2026-05-01 17:43:48 -04:00
parent c9d6a87499
commit 5e135c786b
13 changed files with 442 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
fastapi dev ./src/main.py
+1
View File
@@ -0,0 +1 @@
fastapi[standard]
View File
+95
View File
@@ -0,0 +1,95 @@
from typing import Optional
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
from .models import AuthKey, AuthReq, RepoTemplate
from .settings import get_config
invalid_pw = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid password"
)
class AuthRepo(RepoTemplate):
def create_key(self, description: str):
while True:
key = AuthKey(description=description)
self.cur.execute("SELECT * FROM auth WHERE auth_key = ?", (key.auth_key,))
result = self.cur.fetchall()
if len(result) == 0:
self.cur.execute(
"INSERT INTO auth VALUES (?, ?)", (key.auth_key, key.description)
)
self.conn.commit()
break
return key
def check_key(self, key: str | None):
self.cur.execute("SELECT * FROM auth WHERE auth_key = ?", (key,))
result = self.cur.fetchall()
if len(result) > 0:
return True
else:
return False
def verify_key(self, key: str | None):
self.cur.execute("SELECT * FROM auth WHERE auth_key = ?", (key,))
result = self.cur.fetchall()
if len(result) == 0:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Auth Key"
)
def list_keys(self):
self.cur.execute("SELECT * FROM auth")
results = self.cur.fetchall()
return [AuthKey(*r) for r in results]
def del_key(self, key: str):
self.cur.execute("DELETE FROM auth WHERE auth_key = ?", (key,))
self.conn.commit()
return {"detail": "Key deleted if exists."}
auth_router = APIRouter(prefix="/api/auth")
@auth_router.get("")
def list_auth_keys(
request: Request,
config: dict = Depends(get_config),
tam_auth_pw: Optional[str] = Header(None),
):
auth_pw = tam_auth_pw
if auth_pw == config["api_pw"]:
return AuthRepo().list_keys()
else:
raise invalid_pw
@auth_router.post("")
def req_key(
key_req: AuthReq,
config: dict = Depends(get_config),
tam_auth_pw: Optional[str] = Header(None),
):
auth_pw = tam_auth_pw
if auth_pw == config["api_pw"]:
key = AuthRepo().create_key(key_req.description)
return key
else:
raise invalid_pw
@auth_router.delete("")
def del_key(
request: Request,
config: dict = Depends(get_config),
del_key: str = "",
tam_auth_pw: Optional[str] = Header(None),
):
auth_pw = tam_auth_pw
if auth_pw == config["api_pw"]:
return AuthRepo().del_key(del_key)
else:
raise invalid_pw
+60
View File
@@ -0,0 +1,60 @@
import sqlite3
from ..core.settings import get_data_path
def create_session():
path = get_data_path() / "./tam.db"
conn = sqlite3.connect(path)
cur = conn.cursor()
return conn, cur
def init_db():
conn, cur = create_session()
cur.execute("""CREATE TABLE IF NOT EXISTS auth (auth_key TEXT PRIMARY KEY, description TEXT)""")
cur.execute("""CREATE TABLE IF NOT EXISTS prefixes (
prefix TEXT PRIMARY KEY,
color TEXT,
weight INT
)""")
cur.execute("""CREATE TABLE IF NOT EXISTS tickets (
prefix TEXT,
ticket_id INT,
first_name TEXT,
last_name TEXT,
phone_number TEXT,
pref TEXT,
PRIMARY KEY (prefix, ticket_id)
)""")
cur.execute("""CREATE TABLE IF NOT EXISTS baskets (
prefix TEXT,
basket_id INT,
description TEXT,
winning_ticket INT,
PRIMARY KEY (prefix, basket_id)
)""")
cur.execute("""CREATE TABLE IF NOT EXISTS donors (
donor_id INTEGER PRIMARY KEY AUTOINCREMENT,
donor_name TEXT,
donor_business TEXT
)""")
cur.execute("""CREATE TABLE IF NOT EXISTS r_basket_donor (
b_prefix TEXT REFERENCES baskets(prefix),
b_id INT REFERENCES baskets(basket_id),
d_id INT REFERENCES donors(donor_id),
PRIMARY KEY (b_prefix, b_id, d_id)
)""")
cur.execute("""CREATE VIEW IF NOT EXISTS winners_by_basket AS
SELECT b.prefix, b.basket_id, b.description, b.winning_ticket, t.last_name, t.first_name, t.phone_number, t.pref
FROM baskets b LEFT JOIN tickets t ON b.prefix = t.prefix AND b.winning_ticket = t.ticket_id
ORDER BY b.prefix, b.basket_id""")
cur.execute("""CREATE VIEW IF NOT EXISTS winners_by_name AS
SELECT b.prefix, t.last_name, t.first_name, t.phone_number, b.basket_id, b.description, t.pref
FROM baskets b LEFT JOIN tickets t ON b.prefix = t.prefix AND b.winning_ticket = t.ticket_id
ORDER BY b.prefix, t.last_name, t.first_name, t.phone_number, b.basket_id""")
cur.execute("""CREATE VIEW IF NOT EXISTS counts AS
SELECT prefix, COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))) AS unique_buyers, COUNT(*) AS total_buys
GROUP BY prefix
UNION ALL
SELECT 'Total', COUNT(DISTINCT(CONCAT(first_name, last_name, phone_number))), COUNT(*)""")
conn.commit()
print("DB initiated.")
+20
View File
@@ -0,0 +1,20 @@
from dataclasses import dataclass
import string
import random as r
from .db import create_session
class RepoTemplate:
def __init__(self):
self.conn, self.cur = create_session()
choose_from = string.ascii_uppercase + string.digits
@dataclass
class AuthKey:
auth_key: str = "".join(r.choice(choose_from) for _ in range(24))
description: str = ""
@dataclass
class AuthReq:
description: str = ""
+29
View File
@@ -0,0 +1,29 @@
import json
import os
from pathlib import Path
def get_data_path():
data_path = Path(os.getenv("TAM_DATA_PATH", "./data"))
data_path = data_path.expanduser()
if not data_path.is_dir():
data_path.mkdir(parents=True)
return data_path
def get_settings_path():
data_path = get_data_path()
settings_path = data_path / "./settings.json"
return settings_path
def get_config():
path = get_settings_path()
if path.is_file():
with open(path, "r") as f:
return json.load(f)
else:
default_settings = {
"mode": os.getenv("TAM_MODE", "prod"),
"api_pw": os.getenv("TAM_API_PW", "tam")
}
with open(path, "w") as f:
json.dump(default_settings, f)
return default_settings
+56
View File
@@ -0,0 +1,56 @@
from dataclasses import dataclass
@dataclass
class Prefix:
prefix: str = ""
color: str = ""
weight: int = 0
@dataclass
class Ticket:
prefix: str = ""
ticket_id: int = 0
first_name: str = ""
last_name: str = ""
phone_number: str = ""
pref: str = ""
@dataclass
class Basket:
prefix: str = ""
basket_id: int = 0
description: str = ""
winning_ticket: int = 0
@dataclass
class Donor:
donor_id: int = 0
donor_name: str = ""
donor_business: str = ""
@dataclass
class BasketDonorRel:
b_prefix: str = ""
b_id: int = 0
d_id: int = 0
@dataclass
class WinnerByBasket:
prefix: str = ""
basket_id: int = 0
description: str = ""
winning_ticket: int = 0
last_name: str = ""
first_name: str = ""
phone_number: str = ""
pref: str = ""
@dataclass
class WinnerByName:
prefix: str = ""
last_name: str = ""
first_name: str = ""
phone_number: str = ""
basket_id: int = 0
description: str = ""
pref: str = ""
+75
View File
@@ -0,0 +1,75 @@
from ..core.models import RepoTemplate
from .models import Prefix, Ticket
class PrefixRepo(RepoTemplate):
"""Repo that controls prefixes."""
def get_prefixes(self):
"""Returns all prefixes."""
self.cur.execute("SELECT * FROM prefixes ORDER BY weight, prefix")
results = self.cur.fetchall()
return [Prefix(*r) for r in results]
def get_one_prefix(self, prefix: str):
"""Returns one prefix."""
self.cur.execute("SELECT * FROM prefixes WHERE prefix = ?", (prefix,))
result = self.cur.fetchone()
if result:
return Prefix(*result)
else:
return Prefix(prefix)
def post_prefix(self, p: Prefix):
"""Posts a prefix in the database."""
self.cur.execute(
"INSERT INTO prefixes VALUES (?, ?, ?) ON CONFLICT (prefix) DO UPDATE SET color = EXCLUDED.color, weight = EXCLUDED.weight",
(p.prefix, p.color, p.weight),
)
self.conn.commit()
return {"detail": "Prefix posted successfully."}
def del_prefix(self, prefix: str):
"""Deletes a prefix from the database."""
self.cur.execute("DELETE FROM prefixes WHERE prefix = ?", (prefix,))
self.conn.commit()
return {"detail": "Prefix deleted successfully."}
class TicketRepo(RepoTemplate):
"""Repo that controls ticket operations."""
def get_all_tickets(self):
"""Gets all tickets and returns them."""
self.cur.execute("SELECT * FROM tickets ORDER BY prefix, ticket_id")
results = self.cur.fetchall()
return [Ticket(*r) for r in results]
def get_prefix_tickets(self, prefix: str):
"""Gets all tickets of a certain prefix."""
self.cur.execute("SELECT * FROM tickets WHERE prefix = ? ORDER BY prefix, ticket_id", (prefix,))
results = self.cur.fetchall()
return [Ticket(*r) for r in results]
def get_one_ticket(self, prefix: str, t_id: int):
"""Gets one particular ticket."""
self.cur.execute("SELECT * FROM tickets WHERE prefix = ? AND ticket_id = ?", (prefix, t_id))
result = self.cur.fetchone()
if result:
return Ticket(*result)
else:
return Ticket(prefix, t_id)
def get_range_tickets(self, prefix: str, r_start: int, r_end: int):
"""Gets a range of tickets."""
self.cur.execute("SELECT * FROM tickets WHERE prefix = ? AND ticket_id BETWEEN ? AND ? ORDER BY prefix, ticket_id", (prefix, r_start, r_end))
results = self.cur.fetchall()
return [Ticket(*r) for r in results]
def post_tickets(self, ts: list[Ticket]):
"""Posts a range of tickets."""
for t in ts:
self.cur.execute("""INSERT INTO tickets VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (prefix, ticket_id)
DO UPDATE SET first_name = EXCLUDED.first_name, last_name = EXCLUDED.last_name, phone_number = EXCLUDED.phone_number,
pref = EXCLUDED.pref""", (t.prefix, t.ticket_id, t.first_name, t.last_name, t.phone_number, t.pref))
self.conn.commit()
return {"detail": "Tickets posted successfully."}
+63
View File
@@ -0,0 +1,63 @@
from fastapi import APIRouter, Header, status, HTTPException
from .repos import PrefixRepo, TicketRepo
from .models import Prefix, Ticket
from ..core.auth import AuthRepo
prefix_router = APIRouter(prefix="/api/prefix")
inv_numbers_ex = HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="After last / has to be either an integer or range of integers (such as 1-20).")
@prefix_router.get("")
def get_all_prefixes(tam_auth_key: str = Header("")):
AuthRepo().verify_key(tam_auth_key)
return PrefixRepo().get_prefixes()
@prefix_router.get("/{prefix}")
def get_one_prefix(prefix: str, tam_auth_key: str = Header("")):
AuthRepo().verify_key(tam_auth_key)
return PrefixRepo().get_one_prefix(prefix)
@prefix_router.post("")
def post_one_prefix(prefix: Prefix, tam_auth_key: str = Header("")):
AuthRepo().verify_key(tam_auth_key)
return PrefixRepo().post_prefix(prefix)
@prefix_router.delete("")
def del_one_prefix(prefix: str = "", tam_auth_key: str = Header("")):
AuthRepo().verify_key(tam_auth_key)
return PrefixRepo().del_prefix(prefix)
ticket_router = APIRouter(prefix="/api/tickets")
@ticket_router.get("")
def get_all_tickets(tam_auth_key: str = Header("")):
AuthRepo().verify_key(tam_auth_key)
return TicketRepo().get_all_tickets()
@ticket_router.get("/{prefix}")
def get_prefix_tickets(prefix: str, tam_auth_key: str = Header("")):
AuthRepo().verify_key(tam_auth_key)
return TicketRepo().get_prefix_tickets(prefix)
@ticket_router.get("/{prefix}/{s_id}")
def get_ticket_scope(prefix: str, s_id: str, tam_auth_key: str = Header("")) -> Ticket | list[Ticket]:
AuthRepo().verify_key(tam_auth_key)
if "-" in s_id:
l_id = s_id.split("-", maxsplit=2)
try:
r_start, r_end = int(l_id[0]), int(l_id[1])
except ValueError:
raise inv_numbers_ex
return TicketRepo().get_range_tickets(prefix, r_start, r_end)
else:
try:
i_id = int(s_id)
except ValueError:
raise inv_numbers_ex
return TicketRepo().get_one_ticket(prefix, i_id)
@ticket_router.post("")
def post_tickets(ts: list[Ticket], tam_auth_key: str = Header("")):
AuthRepo().verify_key(tam_auth_key)
return TicketRepo().post_tickets(ts)
+29
View File
@@ -0,0 +1,29 @@
from fastapi import FastAPI, Header
from .core.auth import AuthRepo
from .core.db import init_db
from .core.settings import get_config
from .routers import append_routers
config = get_config()
init_db()
app = FastAPI(
title="TAM Server",
docs_url=None if config["mode"] == "prod" else "/docs",
redoc_url=None if config["mode"] == "prod" else "/redoc",
openapi_url=None if config["mode"] == "prod" else "/openapi.json",
)
@app.get("/api")
def main_path(tam_auth_key: str = Header("")):
return {
"whoami": "TAM Server",
"authenticated": AuthRepo().check_key(tam_auth_key),
"status": "healthy",
}
append_routers(app)
+9
View File
@@ -0,0 +1,9 @@
from fastapi import FastAPI
from .core.auth import auth_router
from .data import routers
def append_routers(app: FastAPI):
app.include_router(auth_router)
app.include_router(routers.prefix_router)
app.include_router(routers.ticket_router)