Initial commit: CI Agent competitive-intelligence monitoring app

FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring,
source collection, LLM-based change analysis, enrichment, and account
security (Turnstile, escalating lockout, email verification).
This commit is contained in:
2026-08-05 10:48:20 -04:00
commit 1a4c80958f
365 changed files with 43541 additions and 0 deletions
@@ -0,0 +1,64 @@
"""create users and refresh tokens
Revision ID: 05a3ccfe49b9
Revises:
Create Date: 2026-07-31 18:14:41.012285
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "05a3ccfe49b9"
down_revision: str | None = None
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"users",
sa.Column("email", sa.String(length=320), nullable=False),
sa.Column("password_hash", sa.String(length=255), nullable=True),
sa.Column("display_name", sa.String(length=120), nullable=False),
sa.Column("timezone", sa.String(length=64), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=False),
sa.Column("is_admin", sa.Boolean(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
op.create_table(
"refresh_tokens",
sa.Column("user_id", sa.Uuid(), nullable=False),
sa.Column("token_hash", sa.String(length=64), nullable=False),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(
op.f("ix_refresh_tokens_token_hash"), "refresh_tokens", ["token_hash"], unique=True
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_refresh_tokens_token_hash"), table_name="refresh_tokens")
op.drop_table("refresh_tokens")
op.drop_index(op.f("ix_users_email"), table_name="users")
op.drop_table("users")
# ### end Alembic commands ###
@@ -0,0 +1,57 @@
"""add per-source scheduling override columns
Revision ID: 06e7f03cea36
Revises: 60a25ddfc6a3
Create Date: 2026-08-03 17:22:37.278775
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "06e7f03cea36"
down_revision: str | None = "60a25ddfc6a3"
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"sources",
sa.Column(
"frequency_type",
sa.Enum(
"HOURLY",
"EVERY_6_HOURS",
"EVERY_12_HOURS",
"DAILY",
"EVERY_2_DAYS",
"WEEKLY",
"EVERY_2_WEEKS",
"MONTHLY",
"CUSTOM",
name="monitoringfrequency",
native_enum=False,
length=20,
),
nullable=True,
),
)
op.add_column("sources", sa.Column("interval_minutes", sa.Integer(), nullable=True))
op.add_column("sources", sa.Column("cron_expression", sa.String(length=120), nullable=True))
op.add_column("sources", sa.Column("next_check", sa.DateTime(timezone=True), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("sources", "next_check")
op.drop_column("sources", "cron_expression")
op.drop_column("sources", "interval_minutes")
op.drop_column("sources", "frequency_type")
# ### end Alembic commands ###
@@ -0,0 +1,155 @@
"""create sources source_documents and snapshots
Revision ID: 484419ccd357
Revises: 8880af3b8aa1
Create Date: 2026-07-31 18:55:01.926914
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "484419ccd357"
down_revision: str | None = "8880af3b8aa1"
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"sources",
sa.Column("company_id", sa.Uuid(), nullable=False),
sa.Column(
"source_type",
sa.Enum(
"WEBSITE",
"RSS",
"CUSTOM_URL",
"SEC_EDGAR",
"GITHUB",
"JOB_POSTING",
"PATENT",
"REVIEW",
name="sourcetype",
native_enum=False,
length=20,
),
nullable=False,
),
sa.Column("name", sa.String(length=200), nullable=False),
sa.Column("base_url", sa.String(length=500), nullable=True),
sa.Column("active", sa.Boolean(), nullable=False),
sa.Column(
"status",
sa.Enum(
"ACTIVE",
"DISABLED",
"RATE_LIMITED",
"AUTH_REQUIRED",
"BLOCKED_BY_POLICY",
"FAILED",
name="sourcestatus",
native_enum=False,
length=20,
),
nullable=False,
),
sa.Column("trust_score", sa.Float(), nullable=False),
sa.Column("last_checked", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_successful_check", sa.DateTime(timezone=True), nullable=True),
sa.Column("failure_count", sa.Integer(), nullable=False),
sa.Column("configuration_metadata", sa.JSON(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(op.f("ix_sources_company_id"), "sources", ["company_id"], unique=False)
op.create_table(
"snapshots",
sa.Column("company_id", sa.Uuid(), nullable=False),
sa.Column("source_id", sa.Uuid(), nullable=False),
sa.Column("snapshot_type", sa.String(length=50), nullable=False),
sa.Column("hash", sa.String(length=64), nullable=False),
sa.Column("structured_summary", sa.JSON(), nullable=False),
sa.Column("text_summary", sa.Text(), nullable=True),
sa.Column("monitoring_run_id", sa.Uuid(), nullable=True),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["source_id"], ["sources.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(op.f("ix_snapshots_company_id"), "snapshots", ["company_id"], unique=False)
op.create_index(op.f("ix_snapshots_hash"), "snapshots", ["hash"], unique=False)
op.create_index(
op.f("ix_snapshots_monitoring_run_id"), "snapshots", ["monitoring_run_id"], unique=False
)
op.create_index(op.f("ix_snapshots_source_id"), "snapshots", ["source_id"], unique=False)
op.create_table(
"source_documents",
sa.Column("source_id", sa.Uuid(), nullable=False),
sa.Column("company_id", sa.Uuid(), nullable=False),
sa.Column("url", sa.String(length=1000), nullable=False),
sa.Column("canonical_url", sa.String(length=1000), nullable=False),
sa.Column("title", sa.String(length=500), nullable=True),
sa.Column("author", sa.String(length=200), nullable=True),
sa.Column("publication_date", sa.DateTime(timezone=True), nullable=True),
sa.Column("retrieved_date", sa.DateTime(timezone=True), nullable=False),
sa.Column("content_text", sa.Text(), nullable=False),
sa.Column("content_hash", sa.String(length=64), nullable=False),
sa.Column("metadata_json", sa.JSON(), nullable=False),
sa.Column("language", sa.String(length=16), nullable=True),
sa.Column("http_status", sa.Integer(), nullable=True),
sa.Column("extraction_method", sa.String(length=50), nullable=False),
sa.Column("trust_score", sa.Float(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["source_id"], ["sources.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(
op.f("ix_source_documents_canonical_url"),
"source_documents",
["canonical_url"],
unique=False,
)
op.create_index(
op.f("ix_source_documents_company_id"), "source_documents", ["company_id"], unique=False
)
op.create_index(
op.f("ix_source_documents_content_hash"), "source_documents", ["content_hash"], unique=False
)
op.create_index(
op.f("ix_source_documents_source_id"), "source_documents", ["source_id"], unique=False
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_source_documents_source_id"), table_name="source_documents")
op.drop_index(op.f("ix_source_documents_content_hash"), table_name="source_documents")
op.drop_index(op.f("ix_source_documents_company_id"), table_name="source_documents")
op.drop_index(op.f("ix_source_documents_canonical_url"), table_name="source_documents")
op.drop_table("source_documents")
op.drop_index(op.f("ix_snapshots_source_id"), table_name="snapshots")
op.drop_index(op.f("ix_snapshots_monitoring_run_id"), table_name="snapshots")
op.drop_index(op.f("ix_snapshots_hash"), table_name="snapshots")
op.drop_index(op.f("ix_snapshots_company_id"), table_name="snapshots")
op.drop_table("snapshots")
op.drop_index(op.f("ix_sources_company_id"), table_name="sources")
op.drop_table("sources")
# ### end Alembic commands ###
@@ -0,0 +1,120 @@
"""add notification_destination_companies join table
Notification destinations were previously user-global with no concept of
"which company is this for" - every destination got every one of the user's
companies' alerts. This migration adds the join table and then, in the same
step, (1) backfills links so every existing destination is linked to every
company the same user currently has (this is a no-op behavior change - it's
exactly what already happened implicitly before this table existed), and (2)
deduplicates destinations that share the same (user, type, value) - the
wizard previously created a brand new row per company even when the same
email/phone was reused, which is the duplicate-rows bug this whole feature
was built to fix. The earliest-created row per duplicate group is kept;
later duplicates are deleted (cascading to their NotificationDelivery
history, which is acceptable one-time cleanup - see KNOWN_LIMITATIONS.md).
Revision ID: 60a25ddfc6a3
Revises: f01919a99ee9
Create Date: 2026-08-02 11:47:04.760190
"""
from __future__ import annotations
from collections.abc import Sequence
from datetime import UTC, datetime
import sqlalchemy as sa
from alembic import op
revision: str = "60a25ddfc6a3"
down_revision: str | None = "f01919a99ee9"
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"notification_destination_companies",
sa.Column("destination_id", sa.Uuid(), nullable=False),
sa.Column("company_id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(
["destination_id"], ["notification_destinations.id"], ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("destination_id", "company_id"),
)
# ### end Alembic commands ###
_backfill_links_and_dedupe_destinations()
def _backfill_links_and_dedupe_destinations() -> None:
bind = op.get_bind()
companies_t = sa.table(
"companies",
sa.column("id", sa.Uuid()),
sa.column("user_id", sa.Uuid()),
)
destinations_t = sa.table(
"notification_destinations",
sa.column("id", sa.Uuid()),
sa.column("user_id", sa.Uuid()),
sa.column("type", sa.String()),
sa.column("destination_value", sa.String()),
sa.column("created_at", sa.DateTime(timezone=True)),
)
links_t = sa.table(
"notification_destination_companies",
sa.column("destination_id", sa.Uuid()),
sa.column("company_id", sa.Uuid()),
sa.column("created_at", sa.DateTime(timezone=True)),
sa.column("updated_at", sa.DateTime(timezone=True)),
)
all_destinations = bind.execute(
sa.select(
destinations_t.c.id,
destinations_t.c.user_id,
destinations_t.c.type,
destinations_t.c.destination_value,
).order_by(destinations_t.c.user_id, destinations_t.c.created_at)
).fetchall()
companies_by_user: dict = {}
for company_id, user_id in bind.execute(
sa.select(companies_t.c.id, companies_t.c.user_id)
).fetchall():
companies_by_user.setdefault(user_id, []).append(company_id)
now = datetime.now(UTC)
link_rows = [
{"destination_id": dest_id, "company_id": company_id, "created_at": now, "updated_at": now}
for dest_id, user_id, _type, _value in all_destinations
for company_id in companies_by_user.get(user_id, [])
]
if link_rows:
bind.execute(sa.insert(links_t), link_rows)
seen: dict[tuple, object] = {}
duplicate_ids: list = []
for dest_id, user_id, dtype, value in all_destinations:
normalized_value = value.strip().lower() if dtype == "email" else value.strip()
key = (user_id, dtype, normalized_value)
if key in seen:
duplicate_ids.append(dest_id)
else:
seen[key] = dest_id
if duplicate_ids:
bind.execute(sa.delete(destinations_t).where(destinations_t.c.id.in_(duplicate_ids)))
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("notification_destination_companies")
# ### end Alembic commands ###
@@ -0,0 +1,47 @@
"""add user_api_keys table
Revision ID: 6d296b533f7c
Revises: 7e0adee63b3b
Create Date: 2026-08-05 10:40:15.989218
"""
from __future__ import annotations
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
revision: str = '6d296b533f7c'
down_revision: str | None = '7e0adee63b3b'
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
op.create_table(
'user_api_keys',
sa.Column('user_id', sa.Uuid(), nullable=False),
sa.Column(
'provider',
sa.Enum(
'ANTHROPIC', 'BRAVE_SEARCH', 'NINJAPEAR', 'USPTO',
name='apikeyprovider', native_enum=False, length=16,
),
nullable=False,
),
sa.Column('encrypted_key', sa.Text(), nullable=False),
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'provider', name='uq_user_api_keys_user_provider'),
)
op.create_index(op.f('ix_user_api_keys_user_id'), 'user_api_keys', ['user_id'], unique=False)
def downgrade() -> None:
op.drop_index(op.f('ix_user_api_keys_user_id'), table_name='user_api_keys')
op.drop_table('user_api_keys')
@@ -0,0 +1,64 @@
"""create company enrichments table
Revision ID: 79e3aa041131
Revises: 06e7f03cea36
Create Date: 2026-08-03 18:43:27.311271
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "79e3aa041131"
down_revision: str | None = "06e7f03cea36"
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"company_enrichments",
sa.Column("company_id", sa.Uuid(), nullable=False),
sa.Column(
"status",
sa.Enum(
"PENDING",
"PARTIAL",
"COMPLETE",
"FAILED",
name="enrichmentstatus",
native_enum=False,
length=20,
),
nullable=False,
),
sa.Column("data", sa.JSON(), nullable=False),
sa.Column("errors", sa.JSON(), nullable=False),
sa.Column("credits_spent", sa.Integer(), nullable=True),
sa.Column("fetched_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(
op.f("ix_company_enrichments_company_id"),
"company_enrichments",
["company_id"],
unique=True,
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_company_enrichments_company_id"), table_name="company_enrichments")
op.drop_table("company_enrichments")
# ### end Alembic commands ###
@@ -0,0 +1,45 @@
"""add password_history_entries table
Revision ID: 7e0adee63b3b
Revises: ef56f181dbb9
Create Date: 2026-08-05 07:24:33.677085
"""
from __future__ import annotations
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
revision: str = "7e0adee63b3b"
down_revision: str | None = "ef56f181dbb9"
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
op.create_table(
"password_history_entries",
sa.Column("user_id", sa.Uuid(), nullable=False),
sa.Column("password_hash", sa.String(length=255), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_password_history_entries_user_id"),
"password_history_entries",
["user_id"],
unique=False,
)
def downgrade() -> None:
op.drop_index(
op.f("ix_password_history_entries_user_id"), table_name="password_history_entries"
)
op.drop_table("password_history_entries")
@@ -0,0 +1,186 @@
"""create companies monitoring and notification tables
Revision ID: 8880af3b8aa1
Revises: 05a3ccfe49b9
Create Date: 2026-07-31 18:29:28.820437
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "8880af3b8aa1"
down_revision: str | None = "05a3ccfe49b9"
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"companies",
sa.Column("user_id", sa.Uuid(), nullable=False),
sa.Column("name", sa.String(length=200), nullable=False),
sa.Column("slug", sa.String(length=220), nullable=False),
sa.Column("official_website", sa.String(length=500), nullable=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("monitoring_focus", sa.Text(), nullable=True),
sa.Column("industry", sa.String(length=120), nullable=True),
sa.Column("country", sa.String(length=120), nullable=True),
sa.Column("region", sa.String(length=120), nullable=True),
sa.Column(
"status",
sa.Enum("ACTIVE", "PAUSED", name="companystatus", native_enum=False, length=20),
nullable=False,
),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(op.f("ix_companies_slug"), "companies", ["slug"], unique=False)
op.create_index(op.f("ix_companies_user_id"), "companies", ["user_id"], unique=False)
op.create_table(
"notification_destinations",
sa.Column("user_id", sa.Uuid(), nullable=False),
sa.Column(
"type",
sa.Enum(
"EMAIL", "SMS", "CONSOLE", name="notificationtype", native_enum=False, length=20
),
nullable=False,
),
sa.Column("destination_value", sa.String(length=320), nullable=False),
sa.Column("verified", sa.Boolean(), nullable=False),
sa.Column("enabled", sa.Boolean(), nullable=False),
sa.Column(
"minimum_severity",
sa.Enum(
"CRITICAL",
"HIGH",
"MEDIUM",
"LOW",
name="severitylevel",
native_enum=False,
length=20,
),
nullable=False,
),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(
op.f("ix_notification_destinations_user_id"),
"notification_destinations",
["user_id"],
unique=False,
)
op.create_table(
"company_aliases",
sa.Column("company_id", sa.Uuid(), nullable=False),
sa.Column("alias", sa.String(length=200), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(
op.f("ix_company_aliases_company_id"), "company_aliases", ["company_id"], unique=False
)
op.create_table(
"competitors",
sa.Column("company_id", sa.Uuid(), nullable=False),
sa.Column("name", sa.String(length=200), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(op.f("ix_competitors_company_id"), "competitors", ["company_id"], unique=False)
op.create_table(
"monitor_configurations",
sa.Column("company_id", sa.Uuid(), nullable=False),
sa.Column(
"frequency_type",
sa.Enum(
"HOURLY",
"EVERY_6_HOURS",
"EVERY_12_HOURS",
"DAILY",
"EVERY_2_DAYS",
"WEEKLY",
"EVERY_2_WEEKS",
"MONTHLY",
"CUSTOM",
name="monitoringfrequency",
native_enum=False,
length=30,
),
nullable=False,
),
sa.Column("interval_minutes", sa.Integer(), nullable=True),
sa.Column("cron_expression", sa.String(length=120), nullable=True),
sa.Column("timezone", sa.String(length=64), nullable=False),
sa.Column("enabled", sa.Boolean(), nullable=False),
sa.Column("next_run", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_run", sa.DateTime(timezone=True), nullable=True),
sa.Column(
"severity_threshold",
sa.Enum(
"CRITICAL",
"HIGH",
"MEDIUM",
"LOW",
name="severitylevel",
native_enum=False,
length=20,
),
nullable=False,
),
sa.Column("source_configuration", sa.JSON(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(
op.f("ix_monitor_configurations_company_id"),
"monitor_configurations",
["company_id"],
unique=True,
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_monitor_configurations_company_id"), table_name="monitor_configurations")
op.drop_table("monitor_configurations")
op.drop_index(op.f("ix_competitors_company_id"), table_name="competitors")
op.drop_table("competitors")
op.drop_index(op.f("ix_company_aliases_company_id"), table_name="company_aliases")
op.drop_table("company_aliases")
op.drop_index(
op.f("ix_notification_destinations_user_id"), table_name="notification_destinations"
)
op.drop_table("notification_destinations")
op.drop_index(op.f("ix_companies_user_id"), table_name="companies")
op.drop_index(op.f("ix_companies_slug"), table_name="companies")
op.drop_table("companies")
# ### end Alembic commands ###
@@ -0,0 +1,96 @@
"""create monitoring runs
Revision ID: 8ceefbd0a6a5
Revises: 484419ccd357
Create Date: 2026-07-31 19:10:32.580895
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "8ceefbd0a6a5"
down_revision: str | None = "484419ccd357"
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"monitoring_runs",
sa.Column("company_id", sa.Uuid(), nullable=False),
sa.Column(
"trigger_type",
sa.Enum(
"SCHEDULED",
"MANUAL",
"INITIAL",
"RETRY",
name="monitoringruntrigger",
native_enum=False,
length=20,
),
nullable=False,
),
sa.Column(
"status",
sa.Enum(
"QUEUED",
"RUNNING",
"SUCCESSFUL",
"PARTIAL",
"FAILED",
name="monitoringrunstatus",
native_enum=False,
length=20,
),
nullable=False,
),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("sources_attempted", sa.Integer(), nullable=False),
sa.Column("sources_successful", sa.Integer(), nullable=False),
sa.Column("sources_failed", sa.Integer(), nullable=False),
sa.Column("items_collected", sa.Integer(), nullable=False),
sa.Column("changes_detected", sa.Integer(), nullable=False),
sa.Column("error_summary", sa.Text(), nullable=True),
sa.Column("worker_task_id", sa.String(length=255), nullable=True),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(
op.f("ix_monitoring_runs_company_id"), "monitoring_runs", ["company_id"], unique=False
)
# ### end Alembic commands ###
# snapshots.monitoring_run_id was added in the previous migration without
# a FK constraint, since monitoring_runs didn't exist yet - add it now
# that the table this phase creates exists. batch_alter_table so this
# also works on SQLite (which can't ALTER TABLE ADD CONSTRAINT directly).
with op.batch_alter_table("snapshots") as batch_op:
batch_op.create_foreign_key(
"fk_snapshots_monitoring_run_id",
"monitoring_runs",
["monitoring_run_id"],
["id"],
ondelete="SET NULL",
)
def downgrade() -> None:
with op.batch_alter_table("snapshots") as batch_op:
batch_op.drop_constraint("fk_snapshots_monitoring_run_id", type_="foreignkey")
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_monitoring_runs_company_id"), table_name="monitoring_runs")
op.drop_table("monitoring_runs")
# ### end Alembic commands ###
@@ -0,0 +1,46 @@
"""add system secrets table
Revision ID: 8db5bd1c31f8
Revises: 6d296b533f7c
Create Date: 2026-08-05 07:48:50.750288
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "8db5bd1c31f8"
down_revision: str | None = "6d296b533f7c"
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
op.create_table(
"system_secrets",
sa.Column(
"key",
sa.Enum(
"TURNSTILE_SITE_KEY",
"TURNSTILE_SECRET",
name="systemsecretkey",
native_enum=False,
length=32,
),
nullable=False,
),
sa.Column("encrypted_value", sa.Text(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("key"),
)
def downgrade() -> None:
op.drop_table("system_secrets")
@@ -0,0 +1,121 @@
"""create alerts and notification deliveries
Revision ID: b5e0d93c89ac
Revises: c06c845cdd10
Create Date: 2026-07-31 22:15:07.310833
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "b5e0d93c89ac"
down_revision: str | None = "c06c845cdd10"
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"alerts",
sa.Column("company_id", sa.Uuid(), nullable=False),
sa.Column("detected_change_id", sa.Uuid(), nullable=False),
sa.Column("user_id", sa.Uuid(), nullable=False),
sa.Column("title", sa.String(length=200), nullable=False),
sa.Column("summary", sa.Text(), nullable=False),
sa.Column("why_it_matters", sa.Text(), nullable=False),
sa.Column(
"severity",
sa.Enum(
"CRITICAL",
"HIGH",
"MEDIUM",
"LOW",
name="severitylevel",
native_enum=False,
length=20,
),
nullable=False,
),
sa.Column("confidence", sa.Float(), nullable=False),
sa.Column("read", sa.Boolean(), nullable=False),
sa.Column("resolved", sa.Boolean(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(
["detected_change_id"], ["detected_changes.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(op.f("ix_alerts_company_id"), "alerts", ["company_id"], unique=False)
op.create_index(
op.f("ix_alerts_detected_change_id"), "alerts", ["detected_change_id"], unique=False
)
op.create_index(op.f("ix_alerts_user_id"), "alerts", ["user_id"], unique=False)
op.create_table(
"notification_deliveries",
sa.Column("alert_id", sa.Uuid(), nullable=False),
sa.Column("destination_id", sa.Uuid(), nullable=False),
sa.Column("provider", sa.String(length=50), nullable=False),
sa.Column(
"status",
sa.Enum(
"PENDING",
"SENT",
"FAILED",
name="notificationdeliverystatus",
native_enum=False,
length=20,
),
nullable=False,
),
sa.Column("attempt_count", sa.Integer(), nullable=False),
sa.Column("last_attempt", sa.DateTime(timezone=True), nullable=True),
sa.Column("external_message_id", sa.String(length=255), nullable=True),
sa.Column("error_message", sa.Text(), nullable=True),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["alert_id"], ["alerts.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(
["destination_id"], ["notification_destinations.id"], ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(
op.f("ix_notification_deliveries_alert_id"),
"notification_deliveries",
["alert_id"],
unique=False,
)
op.create_index(
op.f("ix_notification_deliveries_destination_id"),
"notification_deliveries",
["destination_id"],
unique=False,
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
op.f("ix_notification_deliveries_destination_id"), table_name="notification_deliveries"
)
op.drop_index(op.f("ix_notification_deliveries_alert_id"), table_name="notification_deliveries")
op.drop_table("notification_deliveries")
op.drop_index(op.f("ix_alerts_user_id"), table_name="alerts")
op.drop_index(op.f("ix_alerts_detected_change_id"), table_name="alerts")
op.drop_index(op.f("ix_alerts_company_id"), table_name="alerts")
op.drop_table("alerts")
# ### end Alembic commands ###
@@ -0,0 +1,68 @@
"""create reports
Revision ID: c06c845cdd10
Revises: cb61ba9570fb
Create Date: 2026-07-31 19:52:22.947563
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "c06c845cdd10"
down_revision: str | None = "cb61ba9570fb"
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"reports",
sa.Column("company_id", sa.Uuid(), nullable=False),
sa.Column("monitoring_run_id", sa.Uuid(), nullable=True),
sa.Column(
"report_type",
sa.Enum(
"BASELINE",
"UPDATE",
"MONTHLY",
"MANUAL",
name="reporttype",
native_enum=False,
length=20,
),
nullable=False,
),
sa.Column("title", sa.String(length=300), nullable=False),
sa.Column("executive_summary", sa.Text(), nullable=False),
sa.Column("structured_report", sa.JSON(), nullable=False),
sa.Column("markdown_content", sa.Text(), nullable=False),
sa.Column("model_provider", sa.String(length=50), nullable=False),
sa.Column("model_name", sa.String(length=100), nullable=False),
sa.Column("prompt_version", sa.String(length=20), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["monitoring_run_id"], ["monitoring_runs.id"], ondelete="SET NULL"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(op.f("ix_reports_company_id"), "reports", ["company_id"], unique=False)
op.create_index(
op.f("ix_reports_monitoring_run_id"), "reports", ["monitoring_run_id"], unique=False
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_reports_monitoring_run_id"), table_name="reports")
op.drop_index(op.f("ix_reports_company_id"), table_name="reports")
op.drop_table("reports")
# ### end Alembic commands ###
@@ -0,0 +1,41 @@
"""add user known ips table
Revision ID: c34c769afc07
Revises: 8db5bd1c31f8
Create Date: 2026-08-05 09:13:20.293276
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "c34c769afc07"
down_revision: str | None = "8db5bd1c31f8"
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
op.create_table(
"user_known_ips",
sa.Column("user_id", sa.Uuid(), nullable=False),
sa.Column("ip_address", sa.String(length=45), nullable=False),
sa.Column("first_seen_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("user_id", "ip_address", name="uq_user_known_ips_user_ip"),
)
op.create_index(op.f("ix_user_known_ips_user_id"), "user_known_ips", ["user_id"], unique=False)
def downgrade() -> None:
op.drop_index(op.f("ix_user_known_ips_user_id"), table_name="user_known_ips")
op.drop_table("user_known_ips")
@@ -0,0 +1,107 @@
"""create detected changes
Revision ID: cb61ba9570fb
Revises: 8ceefbd0a6a5
Create Date: 2026-07-31 19:34:49.775190
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "cb61ba9570fb"
down_revision: str | None = "8ceefbd0a6a5"
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"detected_changes",
sa.Column("company_id", sa.Uuid(), nullable=False),
sa.Column("source_id", sa.Uuid(), nullable=False),
sa.Column("monitoring_run_id", sa.Uuid(), nullable=False),
sa.Column("previous_snapshot_id", sa.Uuid(), nullable=True),
sa.Column("current_snapshot_id", sa.Uuid(), nullable=False),
sa.Column(
"change_type",
sa.Enum(
"NEW_DOCUMENT",
"REMOVED_DOCUMENT",
"CONTENT_MODIFIED",
"PRICE_CHANGE",
"LEADERSHIP_CHANGE",
"FILING_NEW",
name="changetype",
native_enum=False,
length=30,
),
nullable=False,
),
sa.Column("raw_diff", sa.JSON(), nullable=False),
sa.Column("significance_score", sa.Float(), nullable=False),
sa.Column("confidence_score", sa.Float(), nullable=False),
sa.Column(
"severity",
sa.Enum(
"CRITICAL",
"HIGH",
"MEDIUM",
"LOW",
name="severitylevel",
native_enum=False,
length=20,
),
nullable=False,
),
sa.Column(
"status",
sa.Enum(
"NEW",
"ACKNOWLEDGED",
"DISMISSED",
name="changestatus",
native_enum=False,
length=20,
),
nullable=False,
),
sa.Column("summary", sa.String(length=500), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["current_snapshot_id"], ["snapshots.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["monitoring_run_id"], ["monitoring_runs.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["previous_snapshot_id"], ["snapshots.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["source_id"], ["sources.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(
op.f("ix_detected_changes_company_id"), "detected_changes", ["company_id"], unique=False
)
op.create_index(
op.f("ix_detected_changes_monitoring_run_id"),
"detected_changes",
["monitoring_run_id"],
unique=False,
)
op.create_index(
op.f("ix_detected_changes_source_id"), "detected_changes", ["source_id"], unique=False
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_detected_changes_source_id"), table_name="detected_changes")
op.drop_index(op.f("ix_detected_changes_monitoring_run_id"), table_name="detected_changes")
op.drop_index(op.f("ix_detected_changes_company_id"), table_name="detected_changes")
op.drop_table("detected_changes")
# ### end Alembic commands ###
@@ -0,0 +1,159 @@
"""add email verification, ip throttle/ban, unban requests, user security events
Revision ID: ef56f181dbb9
Revises: 79e3aa041131
Create Date: 2026-08-05 00:46:28.371645
"""
from __future__ import annotations
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
revision: str = "ef56f181dbb9"
down_revision: str | None = "79e3aa041131"
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"ip_bans",
sa.Column("ip_address", sa.String(length=45), nullable=False),
sa.Column("banned_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("reason", sa.String(length=255), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(op.f("ix_ip_bans_ip_address"), "ip_bans", ["ip_address"], unique=True)
op.create_table(
"ip_throttle_state",
sa.Column("ip_address", sa.String(length=45), nullable=False),
sa.Column(
"action",
sa.Enum(
"RESEND_VERIFICATION",
"RESEND_RESET",
"FAILED_LOGIN",
name="throttleaction",
native_enum=False,
length=24,
),
nullable=False,
),
sa.Column("attempt_count", sa.Integer(), nullable=False),
sa.Column("next_allowed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("timeout_until", sa.DateTime(timezone=True), nullable=True),
sa.Column("offense_count", sa.Integer(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
sa.UniqueConstraint("ip_address", "action", name="uq_ip_throttle_state_ip_action"),
)
op.create_index(
op.f("ix_ip_throttle_state_ip_address"), "ip_throttle_state", ["ip_address"], unique=False
)
op.create_table(
"unban_requests",
sa.Column("ip_address", sa.String(length=45), nullable=False),
sa.Column("message", sa.Text(), nullable=True),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(
op.f("ix_unban_requests_ip_address"), "unban_requests", ["ip_address"], unique=False
)
op.create_table(
"email_codes",
sa.Column("user_id", sa.Uuid(), nullable=False),
sa.Column(
"purpose",
sa.Enum(
"VERIFY_EMAIL",
"PASSWORD_RESET",
name="emailcodepurpose",
native_enum=False,
length=20,
),
nullable=False,
),
sa.Column("code_hash", sa.String(length=64), nullable=False),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("used_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(op.f("ix_email_codes_code_hash"), "email_codes", ["code_hash"], unique=False)
op.create_table(
"user_security_events",
sa.Column("user_id", sa.Uuid(), nullable=False),
sa.Column(
"event_type",
sa.Enum(
"LOGIN_SUCCESS",
"LOGIN_FAILED",
"ACCOUNT_LOCKED",
"PASSWORD_RESET_REQUESTED",
"PASSWORD_RESET_COMPLETED",
"EMAIL_VERIFICATION_SENT",
"EMAIL_VERIFIED",
name="securityeventtype",
native_enum=False,
length=32,
),
nullable=False,
),
sa.Column("ip_address", sa.String(length=45), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_index(
op.f("ix_user_security_events_user_id"), "user_security_events", ["user_id"], unique=False
)
op.add_column(
"users",
sa.Column("email_verified", sa.Boolean(), nullable=False, server_default=sa.false()),
)
op.add_column(
"users", sa.Column("failed_login_count", sa.Integer(), nullable=False, server_default="0")
)
op.add_column("users", sa.Column("locked_at", sa.DateTime(timezone=True), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("users", "locked_at")
op.drop_column("users", "failed_login_count")
op.drop_column("users", "email_verified")
op.drop_index(op.f("ix_user_security_events_user_id"), table_name="user_security_events")
op.drop_table("user_security_events")
op.drop_index(op.f("ix_email_codes_code_hash"), table_name="email_codes")
op.drop_table("email_codes")
op.drop_index(op.f("ix_unban_requests_ip_address"), table_name="unban_requests")
op.drop_table("unban_requests")
op.drop_index(op.f("ix_ip_throttle_state_ip_address"), table_name="ip_throttle_state")
op.drop_table("ip_throttle_state")
op.drop_index(op.f("ix_ip_bans_ip_address"), table_name="ip_bans")
op.drop_table("ip_bans")
# ### end Alembic commands ###
@@ -0,0 +1,39 @@
"""add company headquarters and public_identifiers
Revision ID: f01919a99ee9
Revises: b5e0d93c89ac
Create Date: 2026-08-01 15:28:37.144461
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "f01919a99ee9"
down_revision: str | None = "b5e0d93c89ac"
branch_labels: Sequence[str] | str | None = None
depends_on: Sequence[str] | str | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("companies", sa.Column("headquarters", sa.String(length=200), nullable=True))
# server_default backfills existing rows; NOT NULL alone would fail
# against a non-empty companies table (Postgres rejects adding a NOT
# NULL column with no default when rows already exist).
op.add_column(
"companies",
sa.Column("public_identifiers", sa.JSON(), nullable=False, server_default="{}"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("companies", "public_identifiers")
op.drop_column("companies", "headquarters")
# ### end Alembic commands ###