File size: 5,250 Bytes
559c2ff
 
d1e793b
 
 
559c2ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d1e793b
559c2ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8dbbf97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e6496c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559c2ff
8dbbf97
559c2ff
 
 
 
 
 
 
 
8dbbf97
559c2ff
 
 
 
8dbbf97
559c2ff
 
8dbbf97
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
"""storage/sections_db.py β€” persist raw section text across filing periods.

Stores verbatim Business, Segments/Geography, MD&A, Risk Factors, and
transcript text keyed by (ticker, period, section). Used by company-profile
generation and analysis/textdiff.py without re-fetching source documents.

This is append-only at ingest time; textdiff reads it at runtime.
"""
from __future__ import annotations

import sqlite3
from pathlib import Path
from typing import Optional

SECTIONS_DB_PATH = Path("data/sections.db")

_SCHEMA = """
CREATE TABLE IF NOT EXISTS sections (
    ticker      TEXT NOT NULL,
    period      TEXT NOT NULL,
    form_type   TEXT NOT NULL,
    section     TEXT NOT NULL,
    text        TEXT NOT NULL DEFAULT '',
    ingested_at TEXT,
    PRIMARY KEY (ticker, period, section)
)
"""


def init_sections_db() -> None:
    SECTIONS_DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    with sqlite3.connect(SECTIONS_DB_PATH) as conn:
        conn.execute(_SCHEMA)


def upsert_section(
    ticker: str,
    period: str,
    form_type: str,
    section: str,
    text: str,
) -> None:
    """Write or overwrite a supported filing or transcript section."""
    from datetime import datetime, timezone
    SECTIONS_DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    with sqlite3.connect(SECTIONS_DB_PATH) as conn:
        conn.execute(_SCHEMA)
        conn.execute(
            """
            INSERT INTO sections (ticker, period, form_type, section, text, ingested_at)
            VALUES (?, ?, ?, ?, ?, ?)
            ON CONFLICT(ticker, period, section) DO UPDATE SET
                form_type   = excluded.form_type,
                text        = excluded.text,
                ingested_at = excluded.ingested_at
            """,
            (
                ticker.upper(),
                period,
                form_type,
                section,
                text or "",
                datetime.now(timezone.utc).isoformat(),
            ),
        )


def get_section(ticker: str, period: str, section: str) -> Optional[str]:
    """Return the stored text for (ticker, period, section), or None if absent."""
    if not SECTIONS_DB_PATH.exists():
        return None
    with sqlite3.connect(SECTIONS_DB_PATH) as conn:
        row = conn.execute(
            "SELECT text FROM sections WHERE ticker=? AND period=? AND section=?",
            (ticker.upper(), period, section),
        ).fetchone()
    return row[0] if row else None


def _period_sort_key(period: str) -> tuple[int, int]:
    """Parse 'Q12027' β†’ (2027, 1) for correct chronological sort (newest first).

    Falls back to (0, 0) for unparseable strings (e.g. 'FY2024').
    """
    if not period:
        return (0, 0)
    if period.startswith("Q") and len(period) >= 6:
        try:
            body = period[1:]          # "12027"
            year = int(body[-4:])      # 2027
            quarter = int(body[:-4])   # 1
            return (year, quarter)
        except (ValueError, IndexError):
            pass
    if period.startswith("FY") and len(period) == 6:
        try:
            return (int(period[2:]), 0)
        except ValueError:
            pass
    return (0, 0)


def _transcript_sort_key(period: str) -> tuple[int, int]:
    """Chronological sort key that places FY calls as Q4 of their year.

    'Q12026' β†’ (2026, 1); 'FY2025' β†’ (2025, 4) so the Q4/FY earnings call
    sorts between Q32025 and Q12026. Unparseable strings β†’ (0, 0).
    """
    if period and period.startswith("FY") and len(period) == 6:
        try:
            return (int(period[2:]), 4)
        except ValueError:
            return (0, 0)
    return _period_sort_key(period)


def get_recent_transcripts(ticker: str, n: int = 4) -> list[tuple[str, str]]:
    """Return [(period, text)] for the n most recent non-empty transcripts.

    Mixes 10-Q and 10-K (FY) earnings calls; FY periods sort as Q4 of their
    year. Results are in chronological order (oldest first).
    """
    if not SECTIONS_DB_PATH.exists():
        return []
    with sqlite3.connect(SECTIONS_DB_PATH) as conn:
        rows = conn.execute(
            "SELECT period, text FROM sections "
            "WHERE ticker=? AND section='transcript' AND length(text) > 0",
            (ticker.upper(),),
        ).fetchall()
    rows.sort(key=lambda r: _transcript_sort_key(r[0]))
    return [(r[0], r[1]) for r in rows[-n:]]


def get_periods_for_ticker(ticker: str, form_type: Optional[str] = None) -> list[str]:
    """Return all period strings stored for a ticker, sorted newest first (chronologically).

    Optionally filtered by form_type (e.g. '10-Q').
    """
    if not SECTIONS_DB_PATH.exists():
        return []
    with sqlite3.connect(SECTIONS_DB_PATH) as conn:
        if form_type:
            rows = conn.execute(
                "SELECT DISTINCT period FROM sections WHERE ticker=? AND form_type=?",
                (ticker.upper(), form_type),
            ).fetchall()
        else:
            rows = conn.execute(
                "SELECT DISTINCT period FROM sections WHERE ticker=?",
                (ticker.upper(),),
            ).fetchall()
    periods = [r[0] for r in rows]
    periods.sort(key=_period_sort_key, reverse=True)
    return periods