smriti/backend/app/config.py
Himanshu Dongre f22056418d Add optional local-first SQLite mode (Phase A core)
Smriti can now run against a file-backed SQLite database with no Docker
and no Postgres, removing the operate-a-backend burden for solo builders.
Postgres remains the stronger shared/team mode, unchanged.

- Portable column types (app/db/types.py): JSON renders as JSONB on
  PostgreSQL and generic JSON on SQLite; the pgvector embedding column
  renders as JSON on SQLite. Same models and create_all on both backends.
- Mode resolution (config.py): SMRITI_DB_MODE=local|postgres, defaulting
  to local when unconfigured. An explicitly-set Postgres DATABASE_URL
  preserves Postgres behavior, so existing setups are unaffected. Local
  DB defaults to ~/.smriti/smriti.db; SMRITI_LOCAL_DB_PATH overrides.
- SQLite engine setup (database.py): check_same_thread plus foreign_keys
  / WAL / busy_timeout pragmas; a lazy first-run create_all bootstrap on
  first DB use, which keeps the integration test suite insulated.
- Removed the integration-test JSONB/VECTOR DDL substitution hack — the
  models are genuinely portable now, so conftest needs no type patching.
- Alembic resolves its URL through the same logic (Postgres mode only).
- New persistent file-backed SQLite smoke test, plus mode-resolution and
  per-dialect type-rendering tests.

Local mode uses create_all, not Alembic. No schema changes.
2026-05-16 22:26:05 +05:30

68 lines
2.3 KiB
Python

"""Application settings and database-mode resolution."""
from pathlib import Path
from pydantic_settings import BaseSettings
DEFAULT_POSTGRES_URL = "postgresql://smriti:smriti@localhost:5432/smriti"
DEFAULT_LOCAL_DB_PATH = "~/.smriti/smriti.db"
class Settings(BaseSettings):
"""Application settings loaded from environment variables."""
# App
app_name: str = "Smriti"
debug: bool = False
# Database — see db_mode / resolved_database_url for how these resolve.
# database_url: the raw DATABASE_URL env var. Empty string means "unset".
database_url: str = ""
# smriti_db_mode: the raw SMRITI_DB_MODE env var — "local" | "postgres".
# Empty string means "derive" (see db_mode).
smriti_db_mode: str = ""
# smriti_local_db_path: SQLite file path for local mode. Empty string
# means the default, ~/.smriti/smriti.db.
smriti_local_db_path: str = ""
# OpenAI (for extraction service)
openai_api_key: str = ""
openai_model: str = "gpt-4o-mini"
# CORS
cors_origins: list[str] = ["http://localhost:5173", "http://localhost:3000"]
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
@property
def db_mode(self) -> str:
"""Resolved database mode: 'local' or 'postgres'.
An explicit SMRITI_DB_MODE wins. Otherwise, an explicitly-set
Postgres DATABASE_URL preserves Postgres behavior (backward
compatibility for existing setups); an otherwise-unconfigured
environment defaults to local, so a solo user needs no database
server.
"""
mode = self.smriti_db_mode.strip().lower()
if mode in ("local", "postgres"):
return mode
if self.database_url.strip().startswith(("postgresql://", "postgres://")):
return "postgres"
return "local"
@property
def local_db_path(self) -> Path:
"""Absolute path to the local-mode SQLite database file."""
raw = self.smriti_local_db_path.strip() or DEFAULT_LOCAL_DB_PATH
return Path(raw).expanduser().resolve()
@property
def resolved_database_url(self) -> str:
"""The SQLAlchemy URL the engine should connect to, per db_mode."""
if self.db_mode == "postgres":
return self.database_url.strip() or DEFAULT_POSTGRES_URL
return f"sqlite:///{self.local_db_path}"
settings = Settings()