perf: load seed data from Parquet instead of generating on every query
Browse files- app.py +16 -2
- data/attendance.parquet +0 -0
- data/enrollment.parquet +0 -0
- data/export_parquet.py +65 -0
- data_engine.py +40 -9
app.py
CHANGED
|
@@ -14,10 +14,24 @@ from model_inference import load_model, generate_sql
|
|
| 14 |
from data_engine import create_session, execute_safe, QueryTimeoutError
|
| 15 |
|
| 16 |
# ── Startup ───────────────────────────────────────────────────────────
|
| 17 |
-
# Model loads into RAM at startup; Zero GPU Spaces run CPU-only so the
|
| 18 |
-
# model stays resident between @spaces.GPU calls.
|
| 19 |
|
| 20 |
print("🚀 Starting Kasualdad LFED...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
llm = load_model()
|
| 22 |
print("✅ Ready.")
|
| 23 |
|
|
|
|
| 14 |
from data_engine import create_session, execute_safe, QueryTimeoutError
|
| 15 |
|
| 16 |
# ── Startup ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
| 17 |
|
| 18 |
print("🚀 Starting Kasualdad LFED...")
|
| 19 |
+
|
| 20 |
+
# Ensure Parquet seed files exist (generate on first boot, persist in /data/)
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
_parquet_dirs = [Path("/data"), Path(__file__).parent / "data"]
|
| 23 |
+
_pq_files = ["enrollment.parquet", "attendance.parquet"]
|
| 24 |
+
_pq_found = any(
|
| 25 |
+
all((base / f).exists() for f in _pq_files)
|
| 26 |
+
for base in _parquet_dirs
|
| 27 |
+
)
|
| 28 |
+
if not _pq_found:
|
| 29 |
+
print("📦 Generating seed Parquet files (first boot)...")
|
| 30 |
+
from data.export_parquet import export_parquet
|
| 31 |
+
_pq_out = _parquet_dirs[0] if _parquet_dirs[0].exists() else _parquet_dirs[1]
|
| 32 |
+
export_parquet(_pq_out)
|
| 33 |
+
|
| 34 |
+
print("🦙 Loading model...")
|
| 35 |
llm = load_model()
|
| 36 |
print("✅ Ready.")
|
| 37 |
|
data/attendance.parquet
ADDED
|
Binary file (31.5 kB). View file
|
|
|
data/enrollment.parquet
ADDED
|
Binary file (1.53 kB). View file
|
|
|
data/export_parquet.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
export_parquet.py — Generate seed Parquet files for Kasualdad LFED.
|
| 3 |
+
|
| 4 |
+
Run once to produce enrollment.parquet and attendance.parquet.
|
| 5 |
+
Place them in /data/ (HF Space persistent storage) for fast loading.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python data/export_parquet.py # → data/
|
| 9 |
+
python data/export_parquet.py --out /data # → /data/ (HF Space)
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import sys
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
# Allow running from repo root
|
| 17 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 18 |
+
|
| 19 |
+
import duckdb
|
| 20 |
+
from data.generate_seed import generate_seed_data
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def export_parquet(out_dir: Path) -> None:
|
| 24 |
+
"""Generate seed data and export both tables as Parquet."""
|
| 25 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 26 |
+
|
| 27 |
+
conn = duckdb.connect(":memory:")
|
| 28 |
+
conn.execute("SET enable_progress_bar = false;")
|
| 29 |
+
generate_seed_data(conn)
|
| 30 |
+
|
| 31 |
+
enrollment_path = out_dir / "enrollment.parquet"
|
| 32 |
+
attendance_path = out_dir / "attendance.parquet"
|
| 33 |
+
|
| 34 |
+
conn.execute(f"COPY enrollment TO '{enrollment_path}' (FORMAT PARQUET)")
|
| 35 |
+
conn.execute(f"COPY attendance TO '{attendance_path}' (FORMAT PARQUET)")
|
| 36 |
+
|
| 37 |
+
size_enroll = enrollment_path.stat().st_size
|
| 38 |
+
size_attend = attendance_path.stat().st_size
|
| 39 |
+
|
| 40 |
+
print(f"✅ Exported:")
|
| 41 |
+
print(f" {enrollment_path} ({size_enroll:,} bytes)")
|
| 42 |
+
print(f" {attendance_path} ({size_attend:,} bytes)")
|
| 43 |
+
print(f" Total: {(size_enroll + size_attend):,} bytes")
|
| 44 |
+
|
| 45 |
+
# Verify round-trip
|
| 46 |
+
verify = duckdb.connect(":memory:")
|
| 47 |
+
verify.execute(f"CREATE TABLE enroll AS SELECT * FROM read_parquet('{enrollment_path}')")
|
| 48 |
+
verify.execute(f"CREATE TABLE attend AS SELECT * FROM read_parquet('{attendance_path}')")
|
| 49 |
+
n_enroll = verify.execute("SELECT COUNT(*) FROM enroll").fetchone()[0]
|
| 50 |
+
n_attend = verify.execute("SELECT COUNT(*) FROM attend").fetchone()[0]
|
| 51 |
+
print(f" ✓ Round-trip OK: {n_enroll} enrollment rows, {n_attend} attendance rows")
|
| 52 |
+
verify.close()
|
| 53 |
+
conn.close()
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
if __name__ == "__main__":
|
| 57 |
+
parser = argparse.ArgumentParser(description="Export seed data to Parquet")
|
| 58 |
+
parser.add_argument(
|
| 59 |
+
"--out",
|
| 60 |
+
type=Path,
|
| 61 |
+
default=Path(__file__).parent,
|
| 62 |
+
help="Output directory (default: data/)",
|
| 63 |
+
)
|
| 64 |
+
args = parser.parse_args()
|
| 65 |
+
export_parquet(args.out)
|
data_engine.py
CHANGED
|
@@ -34,6 +34,15 @@ QUERY_TIMEOUT_SEC = 10
|
|
| 34 |
DATA_DIR = Path(__file__).parent / "data"
|
| 35 |
SEED_SQL_PATH = DATA_DIR / "seed.sql"
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
# ── Connection factory ─────────────────────────────────────────────────
|
| 39 |
|
|
@@ -52,28 +61,50 @@ def get_connection(read_only: bool = False) -> duckdb.DuckDBPyConnection:
|
|
| 52 |
|
| 53 |
# ── Database seeding ───────────────────────────────────────────────────
|
| 54 |
|
| 55 |
-
def seed_database(
|
|
|
|
|
|
|
|
|
|
| 56 |
"""
|
| 57 |
-
Create tables and load seed data
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
| 61 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
if seed_sql_path is None:
|
| 63 |
seed_sql_path = SEED_SQL_PATH
|
| 64 |
|
| 65 |
if seed_sql_path.exists():
|
| 66 |
-
# User-provided seed.sql — execute it
|
| 67 |
with open(seed_sql_path) as f:
|
| 68 |
sql = f.read()
|
| 69 |
for statement in sql.split(";"):
|
| 70 |
statement = statement.strip()
|
| 71 |
if statement and not statement.startswith("--"):
|
| 72 |
conn.execute(statement)
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
| 77 |
|
| 78 |
|
| 79 |
# ── Schema introspection ───────────────────────────────────────────────
|
|
|
|
| 34 |
DATA_DIR = Path(__file__).parent / "data"
|
| 35 |
SEED_SQL_PATH = DATA_DIR / "seed.sql"
|
| 36 |
|
| 37 |
+
# Parquet files (pre-generated once via data/export_parquet.py).
|
| 38 |
+
# On HF Spaces, place them in the persistent /data/ directory.
|
| 39 |
+
_PARQUET_DIRS = [
|
| 40 |
+
Path("/data"), # HF Space persistent storage
|
| 41 |
+
DATA_DIR, # local dev (data/)
|
| 42 |
+
]
|
| 43 |
+
_ENROLLMENT_PQ = "enrollment.parquet"
|
| 44 |
+
_ATTENDANCE_PQ = "attendance.parquet"
|
| 45 |
+
|
| 46 |
|
| 47 |
# ── Connection factory ─────────────────────────────────────────────────
|
| 48 |
|
|
|
|
| 61 |
|
| 62 |
# ── Database seeding ───────────────────────────────────────────────────
|
| 63 |
|
| 64 |
+
def seed_database(
|
| 65 |
+
conn: duckdb.DuckDBPyConnection,
|
| 66 |
+
seed_sql_path: Path | None = None,
|
| 67 |
+
) -> None:
|
| 68 |
"""
|
| 69 |
+
Create tables and load seed data. Tries, in order:
|
| 70 |
|
| 71 |
+
1. Parquet files (fastest — pre-generated, ~33 KB total)
|
| 72 |
+
→ /data/enrollment.parquet + /data/attendance.parquet (Space)
|
| 73 |
+
→ data/enrollment.parquet + data/attendance.parquet (local)
|
| 74 |
+
2. data/seed.sql (custom overrides)
|
| 75 |
+
3. Python generator (slow fallback)
|
| 76 |
"""
|
| 77 |
+
# ── 1. Parquet files ─────────────────────────────────────────
|
| 78 |
+
for base in _PARQUET_DIRS:
|
| 79 |
+
enroll_pq = base / _ENROLLMENT_PQ
|
| 80 |
+
attend_pq = base / _ATTENDANCE_PQ
|
| 81 |
+
if enroll_pq.exists() and attend_pq.exists():
|
| 82 |
+
conn.execute(
|
| 83 |
+
"CREATE TABLE enrollment AS "
|
| 84 |
+
f"SELECT * FROM read_parquet('{enroll_pq}')"
|
| 85 |
+
)
|
| 86 |
+
conn.execute(
|
| 87 |
+
"CREATE TABLE attendance AS "
|
| 88 |
+
f"SELECT * FROM read_parquet('{attend_pq}')"
|
| 89 |
+
)
|
| 90 |
+
return
|
| 91 |
+
|
| 92 |
+
# ── 2. seed.sql ──────────────────────────────────────────────
|
| 93 |
if seed_sql_path is None:
|
| 94 |
seed_sql_path = SEED_SQL_PATH
|
| 95 |
|
| 96 |
if seed_sql_path.exists():
|
|
|
|
| 97 |
with open(seed_sql_path) as f:
|
| 98 |
sql = f.read()
|
| 99 |
for statement in sql.split(";"):
|
| 100 |
statement = statement.strip()
|
| 101 |
if statement and not statement.startswith("--"):
|
| 102 |
conn.execute(statement)
|
| 103 |
+
return
|
| 104 |
+
|
| 105 |
+
# ── 3. Python generator (slow) ───────────────────────────────
|
| 106 |
+
from data.generate_seed import generate_seed_data
|
| 107 |
+
generate_seed_data(conn)
|
| 108 |
|
| 109 |
|
| 110 |
# ── Schema introspection ───────────────────────────────────────────────
|