Files
cayce 34a5f331ac
ci / lint-test-leaks (pull_request) Canceled after 0s
skeleton: package, schema, config, CLI, alembic, CI, deploy, docs
- cayceops core: env-only config (CAYCE_OPS_*), engine/session, ORM models
  with state machines (proposal/mail/content) and audited writes
- provider-agnostic LLM interface (null provider default, stdlib openai-compat)
- Typer CLI with lazy component loading (scanner/executor/maild/publisher/monitor)
- alembic 0001 initial schema; sqlite-portable JSON columns
- tests: 7 passing on throwaway sqlite; fixtures invented, never real data
- ci: ruff + alembic smoke + pytest + gitleaks(full history) in
  .gitea/workflows + .github/workflows; local gate = make check
- deploy: docker-compose (postgres 16) + hardened systemd units/timers
- docs: DECISIONS D1-D9; README project (grant text retired)
2026-10-19 21:35:50 +00:00

82 lines
2.0 KiB
Python

"""Database engine, sessions and schema lifecycle helpers."""
from __future__ import annotations
from collections.abc import Iterator
from contextlib import contextmanager
from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session, sessionmaker
from cayceops.models import Base
_engine = None
_factory: sessionmaker | None = None
def engine(url: str | None = None):
"""Return (and cache) the process SQLAlchemy engine."""
global _engine
if _engine is None:
from cayceops.config import load
_engine = create_engine(url or load().database_url, future=True)
return _engine
def reset_engine() -> None:
"""Drop the cached engine (used by tests and CLI re-entry)."""
global _engine, _factory
if _engine is not None:
_engine.dispose()
_engine = None
_factory = None
def session_factory():
global _factory
if _factory is None:
_factory = sessionmaker(bind=engine(), expire_on_commit=False, future=True)
return _factory
@contextmanager
def session_scope() -> Iterator[Session]:
"""Transactional scope: commit on success, rollback on error."""
s = session_factory()()
try:
yield s
s.commit()
except Exception:
s.rollback()
raise
finally:
s.close()
def init_db() -> None:
"""Create all tables from metadata (dev/test convenience).
Production uses Alembic migrations (``cayceops db upgrade``).
"""
Base.metadata.create_all(engine())
def upgrade_db() -> None:
"""Run Alembic migrations to head."""
from alembic.config import Config as AlembicConfig
from alembic import command
from cayceops.config import load
cfg = AlembicConfig("alembic.ini")
cfg.set_main_option("script_location", "alembic")
cfg.set_main_option("sqlalchemy.url", load().database_url)
command.upgrade(cfg, "head")
def current_revision() -> str | None:
with engine().connect() as conn:
row = conn.execute(text("SELECT version_num FROM alembic_version")).first()
return row[0] if row else None