Tickets working
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
apiserver/data/
|
||||||
|
*__pycache__/
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
fastapi dev ./src/main.py
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
fastapi[standard]
|
||||||
@@ -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
|
||||||
@@ -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.")
|
||||||
@@ -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 = ""
|
||||||
@@ -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
|
||||||
@@ -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 = ""
|
||||||
@@ -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."}
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user