fix: make SEC quality gating terminal-safe
This commit is contained in:
@@ -4,6 +4,8 @@ Revision ID: 028
|
||||
Revises: 027
|
||||
Create Date: 2026-08-03 00:00:00.000000
|
||||
"""
|
||||
from datetime import date, datetime, timezone
|
||||
import json
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
@@ -28,11 +30,118 @@ def upgrade() -> None:
|
||||
sa.Column("coregistrant_ciks_json", sa.Text(), nullable=True),
|
||||
sa.Column("first_seen_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("last_attempted_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("escalated_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.UniqueConstraint("accession", name="uq_sec_filing_gaps_accession"),
|
||||
)
|
||||
op.create_index("ix_sec_filing_gaps_cik", "sec_filing_gaps", ["cik"])
|
||||
_backfill_retry_queue()
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_sec_filing_gaps_cik", table_name="sec_filing_gaps")
|
||||
op.drop_table("sec_filing_gaps")
|
||||
|
||||
|
||||
def _as_date(value) -> date | None:
|
||||
if isinstance(value, datetime):
|
||||
return value.date()
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return date.fromisoformat(value)
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _backfill_retry_queue() -> None:
|
||||
"""Materialize pre-queue promoted gaps once; runtime never scans history."""
|
||||
bind = op.get_bind()
|
||||
runs = sa.table(
|
||||
"data_import_runs",
|
||||
sa.column("source", sa.String()),
|
||||
sa.column("status", sa.String()),
|
||||
sa.column("validation_json", sa.Text()),
|
||||
sa.column("source_max_date", sa.Date()),
|
||||
sa.column("started_at", sa.DateTime(timezone=True)),
|
||||
)
|
||||
snapshots = sa.table(
|
||||
"fundamental_snapshots",
|
||||
sa.column("cik", sa.String()),
|
||||
sa.column("accession", sa.String()),
|
||||
sa.column("filed_date", sa.Date()),
|
||||
)
|
||||
gaps = sa.table(
|
||||
"sec_filing_gaps",
|
||||
sa.column("cik", sa.String()),
|
||||
sa.column("accession", sa.String()),
|
||||
sa.column("form", sa.String()),
|
||||
sa.column("index_date", sa.Date()),
|
||||
sa.column("reason", sa.String()),
|
||||
sa.column("coregistrant_ciks_json", sa.Text()),
|
||||
sa.column("first_seen_at", sa.DateTime(timezone=True)),
|
||||
sa.column("last_attempted_at", sa.DateTime(timezone=True)),
|
||||
sa.column("escalated_at", sa.DateTime(timezone=True)),
|
||||
)
|
||||
|
||||
snapshot_rows = bind.execute(
|
||||
sa.select(snapshots.c.cik, snapshots.c.accession, snapshots.c.filed_date)
|
||||
).all()
|
||||
resolved_accessions = {row.accession for row in snapshot_rows}
|
||||
latest_filed_by_cik: dict[str, date] = {}
|
||||
for row in snapshot_rows:
|
||||
if row.filed_date is not None:
|
||||
current = latest_filed_by_cik.get(row.cik)
|
||||
if current is None or row.filed_date > current:
|
||||
latest_filed_by_cik[row.cik] = row.filed_date
|
||||
|
||||
audit_rows = bind.execute(
|
||||
sa.select(
|
||||
runs.c.validation_json,
|
||||
runs.c.source_max_date,
|
||||
runs.c.started_at,
|
||||
).where(
|
||||
runs.c.source == "sec_facts",
|
||||
runs.c.status == "promoted",
|
||||
runs.c.validation_json.is_not(None),
|
||||
)
|
||||
).all()
|
||||
now = datetime.now(timezone.utc)
|
||||
candidates: dict[str, dict] = {}
|
||||
for audit in audit_rows:
|
||||
try:
|
||||
summary = json.loads(audit.validation_json)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if not isinstance(summary, dict):
|
||||
continue
|
||||
for item in summary.get("missing_xbrl") or []:
|
||||
accession = item.get("accession")
|
||||
raw_cik = item.get("cik")
|
||||
if not accession or raw_cik is None or accession in resolved_accessions:
|
||||
continue
|
||||
cik = str(raw_cik).zfill(10)
|
||||
index_date = _as_date(item.get("index_date")) or _as_date(
|
||||
audit.source_max_date
|
||||
)
|
||||
later_filed = latest_filed_by_cik.get(cik)
|
||||
if index_date is not None and later_filed is not None and later_filed > index_date:
|
||||
continue
|
||||
first_seen = audit.started_at or now
|
||||
existing = candidates.get(accession)
|
||||
if existing is not None and existing["first_seen_at"] <= first_seen:
|
||||
continue
|
||||
candidates[accession] = {
|
||||
"cik": cik,
|
||||
"accession": accession,
|
||||
"form": item.get("form"),
|
||||
"index_date": index_date,
|
||||
"reason": item.get("reason") or "not_in_companyfacts",
|
||||
"coregistrant_ciks_json": json.dumps(item.get("coregistrants") or []),
|
||||
"first_seen_at": first_seen,
|
||||
"last_attempted_at": first_seen,
|
||||
"escalated_at": None,
|
||||
}
|
||||
if candidates:
|
||||
op.bulk_insert(gaps, list(candidates.values()))
|
||||
|
||||
Reference in New Issue
Block a user