Deploy URAAS — African Research Archival & Analytics System
Browse files- scripts/init_db.py +52 -1
- scripts/push_to_hf.py +18 -2
- uraas/database.py +46 -0
scripts/init_db.py
CHANGED
|
@@ -9,7 +9,7 @@ import sys
|
|
| 9 |
# Add project root to path (parent of scripts/)
|
| 10 |
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 11 |
|
| 12 |
-
from uraas.database import Collection, Community, SessionLocal, init_db
|
| 13 |
|
| 14 |
# Import citation tracker models so SQLAlchemy registers their tables with
|
| 15 |
# Base.metadata before create_all() runs — otherwise citations,
|
|
@@ -18,6 +18,53 @@ import uraas.services.citation_tracker # noqa: F401
|
|
| 18 |
|
| 19 |
from uraas.utils.unilag_classifier import UNILAG_STRUCTURE
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
def seed_communities_and_collections():
|
| 23 |
"""Seed the database with UNILAG faculty and department structure."""
|
|
@@ -72,6 +119,10 @@ def main():
|
|
| 72 |
print("[OK] Tables created successfully!")
|
| 73 |
print()
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
# Seed communities and collections
|
| 76 |
seed_communities_and_collections()
|
| 77 |
print()
|
|
|
|
| 9 |
# Add project root to path (parent of scripts/)
|
| 10 |
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 11 |
|
| 12 |
+
from uraas.database import Collection, Community, SessionLocal, init_db, sync_schema_columns
|
| 13 |
|
| 14 |
# Import citation tracker models so SQLAlchemy registers their tables with
|
| 15 |
# Base.metadata before create_all() runs — otherwise citations,
|
|
|
|
| 18 |
|
| 19 |
from uraas.utils.unilag_classifier import UNILAG_STRUCTURE
|
| 20 |
|
| 21 |
+
# Base.metadata.create_all() (called by init_db() below) only creates tables
|
| 22 |
+
# that don't exist yet — it never alters an EXISTING table to add a column a
|
| 23 |
+
# newer version of the ORM model expects. On a fresh DB that's a no-op (every
|
| 24 |
+
# table is new, so every column is already there); on a database that
|
| 25 |
+
# already existed before some column was added to the model (e.g. an HF
|
| 26 |
+
# Space's persistent /data/uraas.db surviving across deploys), the table is
|
| 27 |
+
# silently left on its old schema and the very next query touching that
|
| 28 |
+
# column crashes — confirmed live 2026-07-19: the Space failed to start
|
| 29 |
+
# entirely ("no such column: communities.unit_type") because its persistent
|
| 30 |
+
# DB predated that column and nothing ever ran the matching migration
|
| 31 |
+
# against it. Running every idempotent (column_exists()-guarded, ALTER-TABLE-
|
| 32 |
+
# only) migration here means any existing database — this one included —
|
| 33 |
+
# self-heals to the current schema on every startup, not just fresh ones.
|
| 34 |
+
# migrate_add_ror.py is deliberately excluded: unlike the others it also
|
| 35 |
+
# *writes* a default ROR value to existing NULL rows, and that default is
|
| 36 |
+
# the deprecated pre-2026 UNILAG ROR (03qcnxw14, since corrected to
|
| 37 |
+
# 05rk03822 by migrate_unilag_ror.py) — safe to run once by hand, not safe
|
| 38 |
+
# to run unconditionally on every boot.
|
| 39 |
+
_SCHEMA_MIGRATIONS = [
|
| 40 |
+
"migrate_add_pid_source",
|
| 41 |
+
"migrate_add_community_ace_columns",
|
| 42 |
+
"migrate_add_author_isni",
|
| 43 |
+
"migrate_add_item_funders",
|
| 44 |
+
"migrate_2026_upgrade",
|
| 45 |
+
"migrate_add_sc_columns",
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def run_schema_migrations():
|
| 50 |
+
import importlib
|
| 51 |
+
|
| 52 |
+
print("Applying schema migrations (idempotent — safe to re-run)...")
|
| 53 |
+
print(" Generic column sync:")
|
| 54 |
+
sync_schema_columns()
|
| 55 |
+
for mod_name in _SCHEMA_MIGRATIONS:
|
| 56 |
+
try:
|
| 57 |
+
mod = importlib.import_module(mod_name)
|
| 58 |
+
mod.main()
|
| 59 |
+
except Exception as e:
|
| 60 |
+
# A migration failing shouldn't take down the whole app if the
|
| 61 |
+
# underlying tables/columns it depends on genuinely aren't there
|
| 62 |
+
# yet for some other reason — log and continue rather than abort
|
| 63 |
+
# startup entirely (seeding below will surface the real error if
|
| 64 |
+
# it still matters).
|
| 65 |
+
print(f" [WARN] {mod_name} failed (continuing): {e}")
|
| 66 |
+
print()
|
| 67 |
+
|
| 68 |
|
| 69 |
def seed_communities_and_collections():
|
| 70 |
"""Seed the database with UNILAG faculty and department structure."""
|
|
|
|
| 119 |
print("[OK] Tables created successfully!")
|
| 120 |
print()
|
| 121 |
|
| 122 |
+
# Heal any existing database (e.g. a persistent volume surviving across
|
| 123 |
+
# deploys) whose tables predate a column the current models expect.
|
| 124 |
+
run_schema_migrations()
|
| 125 |
+
|
| 126 |
# Seed communities and collections
|
| 127 |
seed_communities_and_collections()
|
| 128 |
print()
|
scripts/push_to_hf.py
CHANGED
|
@@ -39,7 +39,18 @@ IGNORE_FILES = {
|
|
| 39 |
"docker-compose.replica.yml",
|
| 40 |
"docker-compose.demo.yml",
|
| 41 |
}
|
| 42 |
-
IGNORE_EXTS = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
|
| 45 |
def should_skip(rel_path: str, is_dir: bool) -> bool:
|
|
@@ -47,9 +58,14 @@ def should_skip(rel_path: str, is_dir: bool) -> bool:
|
|
| 47 |
name = parts[-1]
|
| 48 |
if is_dir:
|
| 49 |
return name in IGNORE_DIRS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
return (
|
| 51 |
name in IGNORE_FILES
|
| 52 |
-
or os.path.splitext(name)[1] in IGNORE_EXTS
|
| 53 |
)
|
| 54 |
|
| 55 |
|
|
|
|
| 39 |
"docker-compose.replica.yml",
|
| 40 |
"docker-compose.demo.yml",
|
| 41 |
}
|
| 42 |
+
IGNORE_EXTS = {
|
| 43 |
+
".pyc", ".pyo", ".pyd",
|
| 44 |
+
# Database files/backups — belt-and-suspenders beyond the exact
|
| 45 |
+
# "uraas.db" name check below and the startswith("uraas.db") check,
|
| 46 |
+
# since a missed pattern here means real crawled data (author names,
|
| 47 |
+
# DOIs, institutional affiliations, emails) goes to a PUBLIC repo.
|
| 48 |
+
# Confirmed this actually happened (2026-07-19): uraas.db.bak, a
|
| 49 |
+
# 1819-item/14MB snapshot, was uploaded because only "uraas.db" itself
|
| 50 |
+
# was excluded, not the ".bak" variant — deleted from the live Space
|
| 51 |
+
# after the fact, but should never have gone up in the first place.
|
| 52 |
+
".bak", ".backup", ".old", ".orig", ".db", ".sqlite", ".sqlite3",
|
| 53 |
+
}
|
| 54 |
|
| 55 |
|
| 56 |
def should_skip(rel_path: str, is_dir: bool) -> bool:
|
|
|
|
| 58 |
name = parts[-1]
|
| 59 |
if is_dir:
|
| 60 |
return name in IGNORE_DIRS
|
| 61 |
+
# Catches any suffix variant regardless of extension — uraas.db-wal,
|
| 62 |
+
# uraas.db-shm, uraas.db-journal, timestamped backups like
|
| 63 |
+
# uraas.db.20260629, etc. — not just the exact names/extensions above.
|
| 64 |
+
if name.startswith("uraas.db"):
|
| 65 |
+
return True
|
| 66 |
return (
|
| 67 |
name in IGNORE_FILES
|
| 68 |
+
or os.path.splitext(name)[1].lower() in IGNORE_EXTS
|
| 69 |
)
|
| 70 |
|
| 71 |
|
uraas/database.py
CHANGED
|
@@ -393,3 +393,49 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
| 393 |
|
| 394 |
def init_db():
|
| 395 |
Base.metadata.create_all(bind=engine, checkfirst=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
|
| 394 |
def init_db():
|
| 395 |
Base.metadata.create_all(bind=engine, checkfirst=True)
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
def sync_schema_columns():
|
| 399 |
+
"""Add any column declared on an ORM model but missing from the actual
|
| 400 |
+
database table.
|
| 401 |
+
|
| 402 |
+
create_all() only creates whole tables that don't exist yet — it never
|
| 403 |
+
alters an existing table, so a database that predates some column
|
| 404 |
+
(e.g. a persistent volume surviving across deploys) is silently left on
|
| 405 |
+
its old schema forever, and the first query touching that column
|
| 406 |
+
crashes. Confirmed live 2026-07-19: an HF Space failed to start
|
| 407 |
+
entirely because its persistent DB predated Community.unit_type, and a
|
| 408 |
+
synthetic even-older test schema also turned up a second, completely
|
| 409 |
+
undocumented gap (Community.ror) with no dedicated migration script at
|
| 410 |
+
all — one-column-at-a-time migration scripts don't scale to catching
|
| 411 |
+
every possible drift. This is generic instead: it walks every declared
|
| 412 |
+
ORM column and ALTERs in whatever the live table is missing, so it
|
| 413 |
+
self-heals regardless of which columns happen to be absent or when they
|
| 414 |
+
were added to the models.
|
| 415 |
+
"""
|
| 416 |
+
from sqlalchemy import inspect, text
|
| 417 |
+
|
| 418 |
+
insp = inspect(engine)
|
| 419 |
+
existing_tables = set(insp.get_table_names())
|
| 420 |
+
added = 0
|
| 421 |
+
with engine.begin() as conn:
|
| 422 |
+
for table in Base.metadata.sorted_tables:
|
| 423 |
+
if table.name not in existing_tables:
|
| 424 |
+
continue # brand new table — create_all() already made it correctly
|
| 425 |
+
existing_cols = {c["name"] for c in insp.get_columns(table.name)}
|
| 426 |
+
for column in table.columns:
|
| 427 |
+
if column.name in existing_cols:
|
| 428 |
+
continue
|
| 429 |
+
try:
|
| 430 |
+
ddl_type = column.type.compile(dialect=engine.dialect)
|
| 431 |
+
except Exception as e:
|
| 432 |
+
print(f" [WARN] cannot compile DDL type for {table.name}.{column.name}: {e}")
|
| 433 |
+
continue
|
| 434 |
+
stmt = f"ALTER TABLE {table.name} ADD COLUMN {column.name} {ddl_type}"
|
| 435 |
+
try:
|
| 436 |
+
conn.execute(text(stmt))
|
| 437 |
+
print(f" -> {stmt}")
|
| 438 |
+
added += 1
|
| 439 |
+
except Exception as e:
|
| 440 |
+
print(f" [WARN] {stmt} failed: {e}")
|
| 441 |
+
print(f" schema sync: {added} missing column(s) added" if added else " schema sync: already in sync")
|