Files
OpenBB/openbb_platform/providers/sec/tests/test_statement_widget.py
Danglewood dc4b26030f [V5] openbb-sec: Refactor for V5 (#7525)
* refactor openbb-sec for v5

* linting

* ty

* normalize compare_company_facts

* add workflows

* python 3.10 fix

* add more thorough url validation and tests

* ty check

* ty for python 3.10

* reconfigure with apps

* temp pyproject overrride

* pyproject

* pyproject

* pyproject

* codespell

* pyproject

* fix uk schema file links in schema_files

* test coverage

* test_cli

* port #7547 into this pr

* no hard coded prefix

* improve filing rendering capabilities

* fix xml text detection

* make codeql happy

* missed a file in the commit

* test coverage

* codespell

* fix flakey test

* some updates

* management_ownership --> management_profiles

* proxy statement markdown

* ruff

* fix test coverage

* more tests

* test coverage

* fix lint errors

* Add SEC adviser search, profile, and universe routes (#7559)

* Add SEC institutional data fetchers

* Add SEC filing index and Form D routes

* Add SEC investment adviser people data

* Expose SEC adviser and institutional routes

* fix(sec): address adviser review feedback

* refactor(sec): narrow adviser support to IAPD search

* feat(sec): add adviser firm and individual searches

* feat(sec): add adviser profile drill-down

* fix: repair pre-commit configuration

* feat(sec): add adviser universe route

* feat(sec): expose complete Form ADV data

---------

Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com>

* fix test coverage

---------

Co-authored-by: deeleeramone <>
Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>
2026-09-24 08:47:49 -07:00

249 lines
7.3 KiB
Python

"""Unit tests for ``openbb_sec.utils.statement_widget``."""
import asyncio
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
import pytest
from openbb_sec.utils.statement_widget import get_statement_widget_rows
def _records():
return [
{
"period_ending": "2024-12-31",
"fiscal_period": "FY",
"fiscal_year": 2024,
"tag": "total_assets",
"value": 100.0,
"label": "Total Assets",
"description": "All assets.",
"source": "10-K filed 2025-02",
},
{
"period_ending": "2023-12-31",
"fiscal_period": "FY",
"fiscal_year": 2023,
"tag": "total_assets",
"value": 90.0,
"label": "Total Assets",
"description": "All assets.",
"source": "10-K filed 2024-02",
},
{
"period_ending": "2024-12-31",
"fiscal_period": "FY",
"fiscal_year": 2024,
"tag": "total_liabilities",
"value": 40.0,
"label": "Total Liabilities",
"description": "",
"source": "",
},
]
def _run(records=None, **kwargs):
fake = SimpleNamespace(
balance_sheet=_records() if records is None else records,
income_statement=[],
cash_flow=[],
)
kwargs.setdefault("symbol", "AAPL")
kwargs.setdefault("statement_type", "balance")
with patch(
"openbb_sec.utils.company_facts.get_standardized_financials",
AsyncMock(return_value=fake),
):
return asyncio.run(get_statement_widget_rows(**kwargs))
def test_transposed_with_provenance():
rows = _run(period="FY", transform="None", transpose=True, limit=10)
assert {r["Line Item"] for r in rows} == {"Total Assets", "Total Liabilities"}
assets = next(r for r in rows if r["Line Item"] == "Total Assets")
assert assets["FY 2024-12-31"] == 100.0
assert assets["FY 2023-12-31"] == 90.0
assert "10-K filed 2025-02" in assets["provenance"]
def test_no_source_provenance_fallback():
rows = _run(transpose=True)
liabilities = next(r for r in rows if r["Line Item"] == "Total Liabilities")
assert "No source detail" in liabilities["provenance"]
def test_provenance_collapses_when_source_uniform():
records = [
{
"period_ending": "2024-12-31",
"fiscal_period": "FY",
"tag": "total_assets",
"value": 1,
"label": "Total Assets",
"source": "us-gaap:Assets",
},
{
"period_ending": "2023-12-31",
"fiscal_period": "FY",
"tag": "total_assets",
"value": 2,
"label": "Total Assets",
"source": "us-gaap:Assets",
},
]
prov = _run(records=records, transpose=True)[0]["provenance"]
assert prov == "us-gaap:Assets"
assert "FY" not in prov
def test_untransposed():
rows = _run(transpose=False, limit=10)
assert rows[0]["Period"].startswith("FY")
assert "Total Assets" in rows[0]
def test_limit_restricts_periods():
rows = _run(transpose=True, limit=1)
period_cols = [
k for k in rows[0] if k not in ("Line Item", "description", "provenance")
]
assert len(period_cols) == 1
def test_empty_records():
assert _run(records=[]) == []
def test_unknown_statement_type():
with pytest.raises(ValueError, match="Unknown statement_type"):
_run(statement_type="bogus")
def test_ttm_pop_invokes_pct_change():
with patch(
"openbb_sec.utils.company_facts._compute_pct_change",
lambda records, mode: records,
):
rows = _run(period="TTM", transform="% PoP")
assert rows
def test_invalid_combo_falls_back_to_base_period():
rows = _run(period="FY", transform="% PoP")
assert {r["Line Item"] for r in rows} == {"Total Assets", "Total Liabilities"}
def test_drops_line_items_empty_across_all_periods():
records = [
{
"period_ending": "2024-12-31",
"fiscal_period": "FY",
"tag": "total_assets",
"value": 100,
"label": "Total Assets",
},
{
"period_ending": "2024-12-31",
"fiscal_period": "FY",
"tag": "restricted_cash",
"value": None,
"label": "Restricted Cash",
},
{
"period_ending": "2023-12-31",
"fiscal_period": "FY",
"tag": "restricted_cash",
"value": None,
"label": "Restricted Cash",
},
]
items = {r["Line Item"] for r in _run(records=records, transpose=True)}
assert "Total Assets" in items
assert "Restricted Cash" not in items
def test_line_items_sorted_by_model_field_order():
records = [
{
"period_ending": "2024-12-31",
"fiscal_period": "FY",
"tag": "total_liabilities",
"value": 1,
"label": "Total Liabilities",
},
{
"period_ending": "2024-12-31",
"fiscal_period": "FY",
"tag": "cash_and_equivalents",
"value": 2,
"label": "Cash",
},
{
"period_ending": "2024-12-31",
"fiscal_period": "FY",
"tag": "total_current_assets",
"value": 3,
"label": "Total Current Assets",
},
]
rows = _run(records=records, transpose=True)
assert [r["Line Item"] for r in rows] == [
"Cash",
"Total Current Assets",
"Total Liabilities",
]
def test_line_items_resolve_through_parent_and_orphans():
records = [
{
"period_ending": "2024-12-31",
"fiscal_period": "FY",
"tag": "total_current_assets",
"value": 1,
"label": "Total Current Assets",
},
# Custom tag anchored to a model field via its parent (depth 1).
{
"period_ending": "2024-12-31",
"fiscal_period": "FY",
"tag": "custom_receivable",
"value": 2,
"label": "Custom Receivable",
"parent": "total_current_assets",
"sequence": 3.0,
},
# Orphan tag: not a model field and no parent -> resolves to nothing.
{
"period_ending": "2024-12-31",
"fiscal_period": "FY",
"tag": "mystery_item",
"value": 4,
"label": "Mystery Item",
},
# Cyclic parents: each points at the other, so resolution bails out.
{
"period_ending": "2024-12-31",
"fiscal_period": "FY",
"tag": "loop_a",
"value": 5,
"label": "Loop A",
"parent": "loop_b",
},
{
"period_ending": "2024-12-31",
"fiscal_period": "FY",
"tag": "loop_b",
"value": 6,
"label": "Loop B",
"parent": "loop_a",
},
]
items = [r["Line Item"] for r in _run(records=records, transpose=True)]
# The child anchors just above its resolved parent (anchor index - 0.5).
assert items.index("Custom Receivable") == items.index("Total Current Assets") - 1
# Unresolvable tags sink to the end but are still present.
assert {"Mystery Item", "Loop A", "Loop B"}.issubset(set(items))