Files
nimbusflow/backend/main.py

293 lines
13 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import datetime as dt
import sqlite3
import random
from pathlib import Path
from typing import List, Dict, Set, Tuple
# ----------------------------------------------------------------------
# 1⃣ Helper that creates the database (runs the schema file)
# ----------------------------------------------------------------------
def init_db(db_path: Path) -> None:
"""
If the DB file does not exist, create it and run the schema.sql script.
The schema file lives in backend/database/schema.sql.
"""
if db_path.exists():
print(f"✅ Database already exists at {db_path}")
return
print(f"🗂️ Creating new SQLite DB at {db_path}")
conn = sqlite3.connect(db_path)
cur = conn.cursor()
schema_path = Path(__file__).parent / "database" / "schema.sql"
if not schema_path.is_file():
raise FileNotFoundError(f"Schema file not found: {schema_path}")
with open(schema_path, "r", encoding="utf8") as f:
sql = f.read()
cur.executescript(sql)
conn.commit()
conn.close()
print("✅ Schema executed database is ready.")
def next_n_sundays(n: int) -> list[dt.date]:
"""Return a list with the next `n` Sundays after today."""
today = dt.date.today()
# weekday(): Mon=0 … Sun=6 → we want the offset to the *next* Sunday
days_until_sunday = (6 - today.weekday()) % 7
# If today is Sunday, days_until_sunday == 0 → we still want the *next* one
days_until_sunday = days_until_sunday or 7
first_sunday = today + dt.timedelta(days=days_until_sunday)
# Build the list of n Sundays
return [first_sunday + dt.timedelta(weeks=i) for i in range(n)]
def seed_db(repo) -> None:
"""
Populate a tiny dataset, run the roundrobin queue, accept one
schedule, decline another and print audit tables.
"""
print("\n=== 📦 Seeding reference data ===")
# ----- classifications -------------------------------------------------
baritone_cls = repo.create_classification("Baritone")
tenor_cls = repo.create_classification("Tenor")
alto_cls = repo.create_classification("Alto / Mezzo")
soprano_cls = repo.create_classification("Soprano")
print(f"""Created classifications →
{baritone_cls.ClassificationId}=Baritone,
{tenor_cls.ClassificationId}=Tenor,
{alto_cls.ClassificationId}=Alto,
{soprano_cls.ClassificationId}=Soprano\n""")
# ----- members --------------------------------------------------------
members = [
# 15 (Tenor)
repo.create_member("John", "Doe", "john.doe@example.com", "+155512340001", tenor_cls.ClassificationId),
repo.create_member("Mary", "Smith", "mary.smith@example.com", "+155512340002", tenor_cls.ClassificationId),
repo.create_member("David", "Lee", "david.lee@example.com", "+155512340003", tenor_cls.ClassificationId),
repo.create_member("Emma", "Clark", "emma.clark@example.com", "+155512340004", tenor_cls.ClassificationId),
repo.create_member("Jack", "Taylor", "jack.taylor@example.com", "+155512340005", tenor_cls.ClassificationId),
# 610 (Alto / Mezzo)
repo.create_member("Alice", "Brown", "alice.brown@example.com", "+155512340006", alto_cls.ClassificationId),
repo.create_member("Frank", "Davis", "frank.davis@example.com", "+155512340007", alto_cls.ClassificationId),
repo.create_member("Grace", "Miller", "grace.miller@example.com", "+155512340008", alto_cls.ClassificationId),
repo.create_member("Henry", "Wilson", "henry.wilson@example.com", "+155512340009", alto_cls.ClassificationId),
repo.create_member("Isla", "Anderson", "isla.anderson@example.com", "+155512340010", alto_cls.ClassificationId),
# 1115 (Soprano)
repo.create_member("Bob", "Johnson", "bob.johnson@example.com", "+155512340011", soprano_cls.ClassificationId),
repo.create_member("Kara", "Thomas", "kara.thomas@example.com", "+155512340012", soprano_cls.ClassificationId),
repo.create_member("Liam", "Jackson", "liam.jackson@example.com", "+155512340013", soprano_cls.ClassificationId),
repo.create_member("Mia", "White", "mia.white@example.com", "+155512340014", soprano_cls.ClassificationId),
repo.create_member("Noah", "Harris", "noah.harris@example.com", "+155512340015", soprano_cls.ClassificationId),
# 1620 (Baritone)
repo.create_member("Olivia", "Martin", "olivia.martin@example.com", "+155512340016", baritone_cls.ClassificationId),
repo.create_member("Paul", "Doe", "paul.doe@example.com", "+155512340017", baritone_cls.ClassificationId),
repo.create_member("Quinn", "Smith", "quinn.smith@example.com", "+155512340018", baritone_cls.ClassificationId),
repo.create_member("Ruth", "Brown", "ruth.brown@example.com", "+155512340019", baritone_cls.ClassificationId),
repo.create_member("Sam", "Lee", "sam.lee@example.com", "+155512340020", baritone_cls.ClassificationId),
# 2125 (Tenor again)
repo.create_member("Tina", "Clark", "tina.clark@example.com", "+155512340021", tenor_cls.ClassificationId),
repo.create_member("Umar", "Davis", "umar.davis@example.com", "+155512340022", tenor_cls.ClassificationId),
repo.create_member("Vera", "Miller", "vera.miller@example.com", "+155512340023", tenor_cls.ClassificationId),
repo.create_member("Walt", "Wilson", "walt.wilson@example.com", "+155512340024", tenor_cls.ClassificationId),
repo.create_member("Xena", "Anderson", "xena.anderson@example.com", "+155512340025", tenor_cls.ClassificationId),
# 2630 (Alto / Mezzo again)
repo.create_member("Yara", "Thomas", "yara.thomas@example.com", "+155512340026", alto_cls.ClassificationId),
repo.create_member("Zane", "Jackson", "zane.jackson@example.com", "+155512340027", alto_cls.ClassificationId),
repo.create_member("Anna", "White", "anna.white@example.com", "+155512340028", alto_cls.ClassificationId),
repo.create_member("Ben", "Harris", "ben.harris@example.com", "+155512340029", alto_cls.ClassificationId),
repo.create_member("Cara", "Martin", "cara.martin@example.com", "+155512340030", alto_cls.ClassificationId),
]
for m in members:
print(f" Member {m.MemberId}: {m.FirstName} {m.LastName} ({m.ClassificationId})")
print("\n")
print("=== 📦 Seeding Service Types & Availability ===")
# -----------------------------------------------------------------
# 1⃣ Service Types (keep the IDs for later use)
# -----------------------------------------------------------------
st_9am = repo.create_service_type("9AM")
st_11am = repo.create_service_type("11AM")
st_6pm = repo.create_service_type("6PM")
service_type_ids: Dict[str, int] = {
"9AM": st_9am.ServiceTypeId,
"11AM": st_11am.ServiceTypeId,
"6PM": st_6pm.ServiceTypeId,
}
print(
f"Created service types → "
f"{st_9am.ServiceTypeId}=9AM, "
f"{st_11am.ServiceTypeId}=11AM, "
f"{st_6pm.ServiceTypeId}=6PM"
)
# -----------------------------------------------------------------
# 2⃣ Build a baseline availability map (member_id → set of ServiceTypeIds)
# -----------------------------------------------------------------
def base_availability() -> Set[int]:
"""Return a set of ServiceTypeIds the member can take."""
roll = random.random()
if roll < 0.30: # ~30% get *all* three slots
return set(service_type_ids.values())
elif roll < 0.70: # ~40% get exactly two slots
return set(random.sample(list(service_type_ids.values()), 2))
else: # ~30% get a single slot
return {random.choice(list(service_type_ids.values()))}
# Populate the map for every member you created earlier
availability_map: Dict[int, Set[int]] = {}
for m in members: # `members` is the list you seeded above
availability_map[m.MemberId] = base_availability()
# -----------------------------------------------------------------
# 3⃣ Handcrafted overrides (edgecases you want to guarantee)
# -----------------------------------------------------------------
# Tenor block (IDs 15 & 2125) → only 9AM & 11AM
tenor_ids = [1, 2, 3, 4, 5, 21, 22, 23, 24, 25]
for mid in tenor_ids:
availability_map[mid] = {
service_type_ids["9AM"],
service_type_ids["11AM"],
}
# Baritone block (IDs 1620) → only 6PM
baritone_ids = [16, 17, 18, 19, 20]
for mid in baritone_ids:
availability_map[mid] = {service_type_ids["6PM"]}
# Ensure at least one member can do each slot (explicit adds)
availability_map[1].add(service_type_ids["9AM"]) # John Tenor → 9AM
availability_map[6].add(service_type_ids["11AM"]) # Alice Alto → 11AM
availability_map[11].add(service_type_ids["6PM"]) # Bob Soprano → 6PM
# -----------------------------------------------------------------
# 4⃣ Bulkinsert into ServiceAvailability
# -----------------------------------------------------------------
rows: List[Tuple[int, int]] = []
for member_id, type_set in availability_map.items():
for st_id in type_set:
rows.append((member_id, st_id))
for row in rows:
repo.db.execute(
"""
INSERT INTO ServiceAvailability (MemberId, ServiceTypeId)
VALUES (?, ?)
""",
row,
)
print(
f"Inserted {len(rows)} ServiceAvailability rows "
f"(≈ {len(members)} members × avg. {len(rows)//len(members)} slots each)."
)
# ----- service (the day we are scheduling) ---------------------------
service_dates = next_n_sundays(3)
services = []
for service_date in service_dates:
service = repo.create_service(st_6pm.ServiceTypeId, service_date)
print(f"Created Service → ServiceId={service.ServiceId}, Date={service.ServiceDate}")
services.append(service)
print("\n")
# --------------------------------------------------------------------
# 1⃣ Get the first Tenor and ACCEPT it
# --------------------------------------------------------------------
print("=== 🎯 FIRST SCHEDULE (should be John) ===")
scheduled_member = repo.schedule_next_member(
classification_id=soprano_cls.ClassificationId,
service_id=services[0].ServiceId,
only_active=True,
)
print(scheduled_member)
scheduled_member = repo.schedule_next_member(
classification_id=tenor_cls.ClassificationId,
service_id=services[0].ServiceId,
only_active=True,
)
print(scheduled_member)
scheduled_member = repo.schedule_next_member(
classification_id=tenor_cls.ClassificationId,
service_id=services[0].ServiceId,
only_active=True,
)
print(scheduled_member)
scheduled_member = repo.schedule_next_member(
classification_id=tenor_cls.ClassificationId,
service_id=services[0].ServiceId,
only_active=True,
)
print(scheduled_member)
scheduled_member = repo.schedule_next_member(
classification_id=tenor_cls.ClassificationId,
service_id=services[0].ServiceId,
only_active=True,
)
print(scheduled_member)
scheduled_member = repo.schedule_next_member(
classification_id=tenor_cls.ClassificationId,
service_id=services[2].ServiceId,
only_active=True,
)
print(scheduled_member)
# ----------------------------------------------------------------------
# 2⃣ Demo that exercises the full repository API
# ----------------------------------------------------------------------
def demo(repo) -> None:
return
# ----------------------------------------------------------------------
# 5⃣ Entrypoint
# ----------------------------------------------------------------------
if __name__ == "__main__":
# --------------------------------------------------------------
# Path to the SQLite file (feel free to change)
# --------------------------------------------------------------
DB_PATH = Path(__file__).parent / "database_demo.db"
# --------------------------------------------------------------
# Initialise DB if necessary
# --------------------------------------------------------------
init_db(DB_PATH)
exit()
# --------------------------------------------------------------
# Build the connection / repository objects
# --------------------------------------------------------------
from backend.database.connection import DatabaseConnection
from backend.database.repository import Repository
db = DatabaseConnection(str(DB_PATH))
repo = Repository(db)
try:
demo(repo)
finally:
# Always close the connection SQLite locks the file while open
db.close()
print("\n✅ Demo finished connection closed.")