File size: 5,255 Bytes
c6cfb1f | 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 157 158 159 160 161 162 163 164 | """Filing schema tests — pure Python, no OpenAI. Covers the required-field,
validator, currency, and CIK-padding contracts."""
from __future__ import annotations
from datetime import date
import pytest
from pydantic import ValidationError
from src.schemas import Filing, FilingCover, FilingFinancials, RiskFactor
from src.schemas.registry import get_schema, list_doc_types
def _cover(**overrides) -> FilingCover:
return FilingCover(company_name="Apple Inc.", **overrides)
def _fin(**overrides) -> FilingFinancials:
return FilingFinancials(**overrides)
# --- Cover ----------------------------------------------------------------
def test_cover_requires_company_name_only():
c = FilingCover(company_name="Northwind Software, Inc.")
assert c.company_name == "Northwind Software, Inc."
assert c.form_type == "10-K"
assert c.cik is None
assert c.ticker is None
def test_cover_ticker_uppercased_and_stripped():
c = _cover(ticker="aapl")
assert c.ticker == "AAPL"
c2 = _cover(ticker=" msft ")
assert c2.ticker == "MSFT"
def test_cover_ticker_blank_becomes_none():
c = _cover(ticker=" ")
assert c.ticker is None
def test_cik_gets_zero_padded_from_str_int():
assert _cover(cik="320193").cik == "0000320193"
assert _cover(cik=320193).cik == "0000320193"
# A CIK already at 10 digits stays the same.
assert _cover(cik="0000320193").cik == "0000320193"
def test_cik_non_numeric_passes_through_untouched():
# Belt-and-suspenders: if the LLM returns something weird, don't crash.
assert _cover(cik="CIK-320193").cik == "CIK-320193"
def test_cover_dates_parsed_from_iso_strings():
c = _cover(fiscal_year_end="2024-09-28", filing_date="2024-11-01")
assert c.fiscal_year_end == date(2024, 9, 28)
assert c.filing_date == date(2024, 11, 1)
# --- Financials -----------------------------------------------------------
def test_financials_defaults_currency_to_usd():
assert FilingFinancials().currency == "USD"
def test_financials_currency_normalized():
assert _fin(currency="usd").currency == "USD"
assert _fin(currency=" eur ").currency == "EUR"
def test_financials_currency_blank_falls_back_to_usd():
# Model returning "" or spaces shouldn't produce an invalid currency.
assert _fin(currency="").currency == "USD"
assert _fin(currency=" ").currency == "USD"
def test_financials_money_rounds_to_two_decimals():
f = _fin(revenue=391035000000.123456, net_income=93736000000.789)
assert f.revenue == 391035000000.12
assert f.net_income == 93736000000.79
def test_financials_fiscal_year_range_enforced():
_fin(fiscal_year=2024) # OK
with pytest.raises(ValidationError):
_fin(fiscal_year=1500)
with pytest.raises(ValidationError):
_fin(fiscal_year=2200)
def test_financials_all_money_fields_optional():
# Building with no fields shouldn't raise — 10-K may omit any of them.
f = FilingFinancials()
assert f.revenue is None
assert f.total_debt is None
assert f.free_cash_flow is None
# --- Risk factors + top-level Filing -------------------------------------
def test_risk_factor_requires_both_theme_and_summary():
RiskFactor(theme="supply chain", summary="Depends on China manufacturing.")
with pytest.raises(ValidationError):
RiskFactor(theme="supply chain") # missing summary
def test_filing_composes_all_three_subschemas():
f = Filing(
cover=_cover(cik="0000320193", ticker="AAPL", fiscal_year_end="2024-09-28"),
financials=_fin(fiscal_year=2024, revenue=391035000000, net_income=93736000000, eps_diluted=6.11),
top_risk_factors=[
RiskFactor(theme="supply chain concentration", summary="China dependency."),
RiskFactor(theme="FX exposure", summary="Non-US sales volatility."),
],
)
assert f.cover.ticker == "AAPL"
assert f.financials.revenue == 391035000000.0
assert len(f.top_risk_factors) == 2
def test_top_risk_factors_capped_at_five():
with pytest.raises(ValidationError):
Filing(
cover=_cover(),
financials=_fin(),
top_risk_factors=[
RiskFactor(theme=str(i), summary="x") for i in range(6) # 6 > max_length=5
],
)
def test_top_risk_factors_default_empty_list():
# A 10-K with no risk section shouldn't fail extraction.
f = Filing(cover=_cover(), financials=_fin())
assert f.top_risk_factors == []
def test_filing_forbids_extra_fields():
with pytest.raises(ValidationError):
Filing.model_validate({
"cover": {"company_name": "X"},
"financials": {},
"top_risk_factors": [],
"extra_field": "nope", # extra="forbid" should reject this
})
# --- Registry hookup ------------------------------------------------------
def test_filing_registered_and_returned_by_get_schema():
assert get_schema("filing") is Filing
def test_list_doc_types_includes_filing():
assert "filing" in list_doc_types()
def test_filing_case_insensitive_lookup():
# get_schema lowercases the input.
assert get_schema("FILING") is Filing
assert get_schema("Filing") is Filing
|