Spaces:
Sleeping
Sleeping
fix: chronological sort of periods in sections_db (Q12027 before Q32026)
Browse files- storage/sections_db.py +29 -12
- tests/test_sections_db.py +13 -0
storage/sections_db.py
CHANGED
|
@@ -77,8 +77,31 @@ def get_section(ticker: str, period: str, section: str) -> Optional[str]:
|
|
| 77 |
return row[0] if row else None
|
| 78 |
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
def get_periods_for_ticker(ticker: str, form_type: Optional[str] = None) -> list[str]:
|
| 81 |
-
"""Return all period strings stored for a ticker, sorted newest first.
|
| 82 |
|
| 83 |
Optionally filtered by form_type (e.g. '10-Q').
|
| 84 |
"""
|
|
@@ -87,20 +110,14 @@ def get_periods_for_ticker(ticker: str, form_type: Optional[str] = None) -> list
|
|
| 87 |
with sqlite3.connect(SECTIONS_DB_PATH) as conn:
|
| 88 |
if form_type:
|
| 89 |
rows = conn.execute(
|
| 90 |
-
""
|
| 91 |
-
SELECT DISTINCT period FROM sections
|
| 92 |
-
WHERE ticker=? AND form_type=?
|
| 93 |
-
ORDER BY period DESC
|
| 94 |
-
""",
|
| 95 |
(ticker.upper(), form_type),
|
| 96 |
).fetchall()
|
| 97 |
else:
|
| 98 |
rows = conn.execute(
|
| 99 |
-
""
|
| 100 |
-
SELECT DISTINCT period FROM sections
|
| 101 |
-
WHERE ticker=?
|
| 102 |
-
ORDER BY period DESC
|
| 103 |
-
""",
|
| 104 |
(ticker.upper(),),
|
| 105 |
).fetchall()
|
| 106 |
-
|
|
|
|
|
|
|
|
|
| 77 |
return row[0] if row else None
|
| 78 |
|
| 79 |
|
| 80 |
+
def _period_sort_key(period: str) -> tuple[int, int]:
|
| 81 |
+
"""Parse 'Q12027' → (2027, 1) for correct chronological sort (newest first).
|
| 82 |
+
|
| 83 |
+
Falls back to (0, 0) for unparseable strings (e.g. 'FY2024').
|
| 84 |
+
"""
|
| 85 |
+
if not period:
|
| 86 |
+
return (0, 0)
|
| 87 |
+
if period.startswith("Q") and len(period) >= 6:
|
| 88 |
+
try:
|
| 89 |
+
body = period[1:] # "12027"
|
| 90 |
+
year = int(body[-4:]) # 2027
|
| 91 |
+
quarter = int(body[:-4]) # 1
|
| 92 |
+
return (year, quarter)
|
| 93 |
+
except (ValueError, IndexError):
|
| 94 |
+
pass
|
| 95 |
+
if period.startswith("FY") and len(period) == 6:
|
| 96 |
+
try:
|
| 97 |
+
return (int(period[2:]), 0)
|
| 98 |
+
except ValueError:
|
| 99 |
+
pass
|
| 100 |
+
return (0, 0)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
def get_periods_for_ticker(ticker: str, form_type: Optional[str] = None) -> list[str]:
|
| 104 |
+
"""Return all period strings stored for a ticker, sorted newest first (chronologically).
|
| 105 |
|
| 106 |
Optionally filtered by form_type (e.g. '10-Q').
|
| 107 |
"""
|
|
|
|
| 110 |
with sqlite3.connect(SECTIONS_DB_PATH) as conn:
|
| 111 |
if form_type:
|
| 112 |
rows = conn.execute(
|
| 113 |
+
"SELECT DISTINCT period FROM sections WHERE ticker=? AND form_type=?",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
(ticker.upper(), form_type),
|
| 115 |
).fetchall()
|
| 116 |
else:
|
| 117 |
rows = conn.execute(
|
| 118 |
+
"SELECT DISTINCT period FROM sections WHERE ticker=?",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
(ticker.upper(),),
|
| 120 |
).fetchall()
|
| 121 |
+
periods = [r[0] for r in rows]
|
| 122 |
+
periods.sort(key=_period_sort_key, reverse=True)
|
| 123 |
+
return periods
|
tests/test_sections_db.py
CHANGED
|
@@ -61,6 +61,19 @@ def test_get_periods_returns_empty_when_no_db(tmp_path):
|
|
| 61 |
assert get_periods_for_ticker("AAPL") == []
|
| 62 |
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
def test_ticker_is_case_insensitive(tmp_path):
|
| 65 |
db_path = _tmp_db(tmp_path)
|
| 66 |
with patch("storage.sections_db.SECTIONS_DB_PATH", db_path):
|
|
|
|
| 61 |
assert get_periods_for_ticker("AAPL") == []
|
| 62 |
|
| 63 |
|
| 64 |
+
def test_get_periods_chronological_sort(tmp_path):
|
| 65 |
+
"""Q12027 must come before Q32026 — chronological not alphabetical."""
|
| 66 |
+
db_path = _tmp_db(tmp_path)
|
| 67 |
+
with patch("storage.sections_db.SECTIONS_DB_PATH", db_path):
|
| 68 |
+
from storage.sections_db import init_sections_db, upsert_section, get_periods_for_ticker
|
| 69 |
+
init_sections_db()
|
| 70 |
+
for period in ["Q32026", "Q22026", "Q12027", "Q12026"]:
|
| 71 |
+
upsert_section("NVDA", period, "10-Q", "mda", f"text {period}")
|
| 72 |
+
periods = get_periods_for_ticker("NVDA", form_type="10-Q")
|
| 73 |
+
assert periods[0] == "Q12027", f"Expected Q12027 first, got {periods}"
|
| 74 |
+
assert periods[1] == "Q32026", f"Expected Q32026 second, got {periods}"
|
| 75 |
+
|
| 76 |
+
|
| 77 |
def test_ticker_is_case_insensitive(tmp_path):
|
| 78 |
db_path = _tmp_db(tmp_path)
|
| 79 |
with patch("storage.sections_db.SECTIONS_DB_PATH", db_path):
|