skeleton: package, schema, config, CLI, alembic, CI, deploy, docs
ci / lint-test-leaks (pull_request) Canceled after 0s
ci / lint-test-leaks (pull_request) Canceled after 0s
- 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)
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user