admin / Bid-Sentinel
publicBid Scrape and Tracking Application with AI Capability
Bid-Sentinel / bid-sentinel-v2 / backend / app / seed.py
3518 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | """Database initialization: create tables and seed the portal registry. The administrator account is no longer auto-seeded — it is created on first run via the setup screen (POST /auth/setup). See app/routers/auth.py. """ import logging from sqlalchemy import select, text from app.database import AsyncSessionLocal, Base, engine from app.models import Portal from app.scraper.portals import PORTALS logger = logging.getLogger("seed") # Lightweight, idempotent migrations for columns added after the first release. # (No Alembic in this project; create_all only creates missing tables.) _MIGRATIONS = [ "ALTER TABLE tenders ADD COLUMN IF NOT EXISTS outcome VARCHAR(16) NOT NULL DEFAULT 'Awaiting'", "ALTER TABLE tenders ADD COLUMN IF NOT EXISTS required_certs VARCHAR(1024) NOT NULL DEFAULT ''", "ALTER TABLE tenders ADD COLUMN IF NOT EXISTS fit_score INTEGER", "ALTER TABLE tenders ADD COLUMN IF NOT EXISTS fit_matched VARCHAR(1024) NOT NULL DEFAULT ''", "ALTER TABLE tenders ADD COLUMN IF NOT EXISTS tech_requirements TEXT NOT NULL DEFAULT ''", "ALTER TABLE tenders ADD COLUMN IF NOT EXISTS ai_summary TEXT NOT NULL DEFAULT ''", "ALTER TABLE portals ADD COLUMN IF NOT EXISTS custom BOOLEAN NOT NULL DEFAULT false", "ALTER TABLE cpv_codes ADD COLUMN IF NOT EXISTS enabled BOOLEAN NOT NULL DEFAULT true", "ALTER TABLE suppressed_opportunities ADD COLUMN IF NOT EXISTS batch_id VARCHAR(64)", ] # Enum-type changes are run one-per-transaction and tolerated if they fail # (e.g. the value already exists). ADD VALUE IF NOT EXISTS needs PostgreSQL 12+. _TYPE_MIGRATIONS = [ "ALTER TYPE user_role ADD VALUE IF NOT EXISTS 'analyst'", ] async def create_tables() -> None: async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) for stmt in _MIGRATIONS: await conn.execute(text(stmt)) for stmt in _TYPE_MIGRATIONS: try: async with engine.begin() as conn: await conn.execute(text(stmt)) except Exception as exc: # noqa: BLE001 - isolated; must not block startup logger.warning("Type migration skipped (%s): %s", stmt, exc) logger.info("Database tables ensured.") async def seed_portals() -> None: """Insert any registry portals missing from the DB (default enabled=True). Existing rows are left untouched so a user's enabled/disabled choices are preserved across restarts; only name/url/scope/live are kept in sync with the registry. """ async with AsyncSessionLocal() as db: existing = await db.execute(select(Portal.key)) existing_keys = {k for (k,) in existing.all()} added = 0 for p in PORTALS: if p.key in existing_keys: row = ( await db.execute(select(Portal).where(Portal.key == p.key)) ).scalar_one() row.name, row.url, row.scope, row.live = p.name, p.url, p.scope, p.live row.custom = False else: db.add( Portal( key=p.key, name=p.name, url=p.url, scope=p.scope, live=p.live, enabled=True, custom=False, ) ) added += 1 await db.commit() if added: logger.info("Seeded %d new portal(s).", added) |