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,59 @@
|
||||
"""Alembic environment — URL injected at runtime from CAYCE_OPS_DATABASE_URL."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
from alembic import context
|
||||
from cayceops.models import Base
|
||||
|
||||
config = context.config
|
||||
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
if not config.get_main_option("sqlalchemy.url"):
|
||||
import os
|
||||
|
||||
config.set_main_option(
|
||||
"sqlalchemy.url",
|
||||
os.environ.get("CAYCE_OPS_DATABASE_URL", "sqlite:///cayce-ops.db"),
|
||||
)
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
context.configure(
|
||||
url=config.get_main_option("sqlalchemy.url"),
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
render_as_batch=True,
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
render_as_batch=True, # required for SQLite ALTER support
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
${imports if imports else ""}
|
||||
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,144 @@
|
||||
"""initial schema: proposals, audit_log, mail_messages, content_items, daemon_runs
|
||||
|
||||
Revision ID: 0001
|
||||
Revises:
|
||||
Create Date: 2026-10-19
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "0001"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"proposals",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column(
|
||||
"source",
|
||||
sa.Enum("SCANNER", "MANUAL", name="proposalsource"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("kind", sa.String(length=64), nullable=False),
|
||||
sa.Column("title", sa.String(length=512), nullable=False),
|
||||
sa.Column("url", sa.String(length=1024), nullable=True),
|
||||
sa.Column("detail", sa.JSON(), nullable=False),
|
||||
sa.Column("risk_score", sa.Float(), nullable=False),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum(
|
||||
"PROPOSED", "APPROVED", "REJECTED", "RUNNING", "DONE", "FAILED",
|
||||
name="proposalstatus",
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("attempts", sa.Integer(), nullable=False),
|
||||
sa.Column("dedupe_key", sa.String(length=256), nullable=True),
|
||||
sa.Column("approved_by", sa.String(length=128), nullable=True),
|
||||
sa.Column("approved_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.UniqueConstraint("dedupe_key"),
|
||||
)
|
||||
op.create_index("ix_proposals_status", "proposals", ["status"])
|
||||
|
||||
op.create_table(
|
||||
"audit_log",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("ts", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("actor", sa.String(length=128), nullable=False),
|
||||
sa.Column("proposal_id", sa.Integer(), nullable=True),
|
||||
sa.Column("action", sa.String(length=64), nullable=False),
|
||||
sa.Column("detail", sa.JSON(), nullable=False),
|
||||
sa.Column("result", sa.String(length=32), nullable=False),
|
||||
sa.ForeignKeyConstraint(["proposal_id"], ["proposals.id"], ondelete="SET NULL"),
|
||||
)
|
||||
op.create_index("ix_audit_log_ts", "audit_log", ["ts"])
|
||||
op.create_index("ix_audit_log_proposal_id", "audit_log", ["proposal_id"])
|
||||
|
||||
op.create_table(
|
||||
"mail_messages",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column(
|
||||
"direction",
|
||||
sa.Enum("INBOUND", "OUTBOUND", name="maildirection"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("message_id", sa.String(length=998), nullable=True),
|
||||
sa.Column("in_reply_to", sa.String(length=998), nullable=True),
|
||||
sa.Column("from_addr", sa.String(length=320), nullable=True),
|
||||
sa.Column("to_addr", sa.String(length=320), nullable=True),
|
||||
sa.Column("subject", sa.String(length=998), nullable=True),
|
||||
sa.Column("body", sa.Text(), nullable=True),
|
||||
sa.Column(
|
||||
"classification",
|
||||
sa.Enum(
|
||||
"APPROVAL", "REJECTION", "QUESTION", "OTHER", "NEEDS_REVIEW",
|
||||
name="mailclassification",
|
||||
),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("confidence", sa.Float(), nullable=False),
|
||||
sa.Column("proposal_id", sa.Integer(), nullable=True),
|
||||
sa.Column(
|
||||
"draft_status",
|
||||
sa.Enum(
|
||||
"NONE", "DRAFTED", "APPROVED", "SENT", "SKIPPED", name="draftstatus"
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("draft_reply", sa.Text(), nullable=True),
|
||||
sa.Column("processed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(["proposal_id"], ["proposals.id"], ondelete="SET NULL"),
|
||||
)
|
||||
op.create_index("ix_mail_messages_message_id", "mail_messages", ["message_id"])
|
||||
op.create_index("ix_mail_messages_proposal_id", "mail_messages", ["proposal_id"])
|
||||
|
||||
op.create_table(
|
||||
"content_items",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("theme", sa.String(length=128), nullable=False),
|
||||
sa.Column("channel", sa.String(length=32), nullable=False),
|
||||
sa.Column("caption", sa.Text(), nullable=False),
|
||||
sa.Column("media_path", sa.String(length=1024), nullable=True),
|
||||
sa.Column("link", sa.String(length=1024), nullable=True),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum("DRAFT", "APPROVED", "PUBLISHED", name="contentstatus"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("scheduled_for", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("published_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("post_ref", sa.String(length=512), nullable=True),
|
||||
sa.Column("error", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
)
|
||||
op.create_index("ix_content_items_status", "content_items", ["status"])
|
||||
|
||||
op.create_table(
|
||||
"daemon_runs",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("daemon", sa.String(length=32), nullable=False),
|
||||
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("status", sa.String(length=16), nullable=False),
|
||||
sa.Column("detail", sa.JSON(), nullable=False),
|
||||
)
|
||||
op.create_index("ix_daemon_runs_daemon", "daemon_runs", ["daemon"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("daemon_runs")
|
||||
op.drop_table("content_items")
|
||||
op.drop_table("mail_messages")
|
||||
op.drop_table("audit_log")
|
||||
op.drop_table("proposals")
|
||||
for enum_name in (
|
||||
"proposalsource", "proposalstatus", "maildirection",
|
||||
"mailclassification", "draftstatus", "contentstatus",
|
||||
):
|
||||
op.execute(f"DROP TYPE IF EXISTS {enum_name}")
|
||||
Reference in New Issue
Block a user