"""Tests for the FRED release table and economic calendar models.""" import json from datetime import date, datetime, timedelta from urllib.parse import parse_qs, urlparse import pytest from openbb_core.app.model.abstract.error import OpenBBError from openbb_core.provider.utils.errors import EmptyDataError from openbb_fred.models.economic_calendar import ( FredEconomicCalendarFetcher, FredEconomicCalendarQueryParams, ) from openbb_fred.models.release_table import FredReleaseTableFetcher, _quarter_start from openbb_fred.models.search import FredSearchData CREDENTIALS = {"fred_api_key": "test-key"} SECTIONS = { "name": "Gross Domestic Product", "element_id": 12344, "release_id": "53", "elements": { "12345": { "element_id": 12345, "release_id": 53, "parent_id": None, "type": "table", "name": "Table 1. Gross Domestic Product", "level": "0", "children": [{"element_id": 12346}], }, "12346": { "element_id": 12346, "release_id": 53, "parent_id": 12345, "type": "section", "name": "Personal consumption expenditures", "level": "1", "children": [], }, }, } def _tables(*rows: dict) -> dict: """Return the release tables payload carrying these elements.""" return { "name": "SOFR Averages", "element_id": 1217633, "release_id": "483", "elements": {str(row["element_id"]): dict(row) for row in rows}, } def _series(element_id: int, value: str, **fields) -> dict: """Return one series element of a release table.""" return { "element_id": element_id, "release_id": 483, "parent_id": 1217633, "type": "series", "level": "1", "series_id": f"SERIES{element_id}", "name": f"Series {element_id}", "observation_date": "2024-07-12", "observation_value": value, "children": [], **fields, } def _periods(asked: list) -> list: """Return the observation date each release table request asked for.""" return [ parse_qs(urlparse(url).query).get("observation_date", [None])[0] for url in asked ] @pytest.fixture(name="serve_tables") def serve_tables_fixture(monkeypatch): """Answer the release tables endpoint from recorded payloads.""" def serve(payloads: dict, frequency: str = "D", units: dict | None = None) -> list: asked: list = [] async def release_info(params, credentials=None, **kwargs): return [ FredSearchData.model_validate({"id": "A", "frequency_short": frequency}) ] async def series_units(release_id, credentials): return units or {} async def get(url, **kwargs): asked.append(url) period = parse_qs(urlparse(url).query).get("observation_date", ["*"])[0] return payloads.get(period, payloads.get("*", {})) monkeypatch.setattr( "openbb_fred.models.search.FredSearchFetcher.fetch_data", release_info ) monkeypatch.setattr("openbb_fred.models.release_table.get_units", series_units) monkeypatch.setattr("openbb_fred.utils.rate_limiter.fred_get", get) return asked return serve class TestQuarterStart: """The first day of the quarter a date falls in.""" def test_a_january_date_starts_its_quarter_in_january(self): assert _quarter_start("2024-01-31") == "2024-01-01" def test_a_may_date_starts_its_quarter_in_april(self): assert _quarter_start("2024-05-17") == "2024-04-01" def test_an_august_date_starts_its_quarter_in_july(self): assert _quarter_start("2024-08-02") == "2024-07-01" def test_a_december_date_starts_its_quarter_in_october(self): assert _quarter_start("2024-12-31") == "2024-10-01" def test_the_first_day_of_a_quarter_is_left_alone(self): assert _quarter_start("2024-07-01") == "2024-07-01" class TestRequestedPeriods: """Which period a requested date is read as, given the release frequency.""" async def test_a_daily_release_reads_the_day_asked_for(self, serve_tables): asked = serve_tables({"*": _tables(_series(1, "5.0"))}) await FredReleaseTableFetcher.fetch_data( {"release_id": "483", "date": "2024-07-12"}, CREDENTIALS ) assert _periods(asked) == ["2024-07-12"] async def test_a_monthly_release_reads_the_first_of_the_month(self, serve_tables): asked = serve_tables({"*": _tables(_series(1, "5.0"))}, frequency="M") await FredReleaseTableFetcher.fetch_data( {"release_id": "483", "date": "2024-07-12"}, CREDENTIALS ) assert _periods(asked) == ["2024-07-01"] async def test_a_quarterly_release_reads_the_first_of_the_quarter( self, serve_tables ): asked = serve_tables({"*": _tables(_series(1, "5.0"))}, frequency="Q") await FredReleaseTableFetcher.fetch_data( {"release_id": "53", "date": "2024-05-17"}, CREDENTIALS ) assert _periods(asked) == ["2024-04-01"] async def test_two_days_in_one_month_are_read_as_one_period(self, serve_tables): asked = serve_tables({"*": _tables(_series(1, "5.0"))}, frequency="M") await FredReleaseTableFetcher.fetch_data( {"release_id": "483", "date": "2024-07-05,2024-07-27"}, CREDENTIALS ) assert _periods(asked) == ["2024-07-01"] async def test_two_days_in_one_quarter_are_read_as_one_period(self, serve_tables): asked = serve_tables({"*": _tables(_series(1, "5.0"))}, frequency="Q") await FredReleaseTableFetcher.fetch_data( {"release_id": "53", "date": "2024-05-17,2024-06-30"}, CREDENTIALS ) assert _periods(asked) == ["2024-04-01"] async def test_each_requested_period_is_read(self, serve_tables): asked = serve_tables({"*": _tables(_series(1, "5.0"))}, frequency="M") await FredReleaseTableFetcher.fetch_data( {"release_id": "483", "date": "2024-06-05,2024-07-27"}, CREDENTIALS ) assert sorted(_periods(asked)) == ["2024-06-01", "2024-07-01"] async def test_no_date_reads_the_latest_observation(self, serve_tables): asked = serve_tables({"*": _tables(_series(1, "5.0"))}) await FredReleaseTableFetcher.fetch_data({"release_id": "483"}, CREDENTIALS) assert _periods(asked) == [None] class TestMissingRelease: """A release id the search does not know.""" async def test_a_release_without_information_is_reported(self, monkeypatch): async def release_info(params, credentials=None, **kwargs): return [] monkeypatch.setattr( "openbb_fred.models.search.FredSearchFetcher.fetch_data", release_info ) with pytest.raises(OpenBBError, match="No release information found for, 999."): await FredReleaseTableFetcher.fetch_data({"release_id": "999"}, CREDENTIALS) class TestTableElements: """A release that answers with its table of contents rather than values.""" async def test_every_element_is_returned(self, serve_tables): serve_tables({"*": SECTIONS}) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "53"}, CREDENTIALS ) assert [r.element_id for r in rows] == ["12345", "12346"] async def test_an_element_carries_its_kind_and_name(self, serve_tables): serve_tables({"*": SECTIONS}) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "53"}, CREDENTIALS ) assert (rows[0].element_type, rows[0].name) == ( "table", "Table 1. Gross Domestic Product", ) async def test_the_identifiers_are_returned_as_text(self, serve_tables): serve_tables({"*": SECTIONS}) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "53"}, CREDENTIALS ) assert (rows[1].element_id, rows[1].parent_id) == ("12346", "12345") async def test_a_top_level_element_has_no_parent(self, serve_tables): serve_tables({"*": SECTIONS}) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "53"}, CREDENTIALS ) assert rows[0].parent_id is None async def test_an_element_has_no_observation(self, serve_tables): serve_tables({"*": SECTIONS}) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "53"}, CREDENTIALS ) assert (rows[0].date, rows[0].value, rows[0].symbol) == (None, None, None) async def test_the_child_list_and_release_id_are_dropped(self, serve_tables): serve_tables({"*": SECTIONS}) query = FredReleaseTableFetcher.transform_query({"release_id": "53"}) extracted = await FredReleaseTableFetcher.aextract_data(query, CREDENTIALS) assert "children" not in extracted[0] assert "release_id" not in extracted[0] async def test_an_element_is_not_repeated_across_periods(self, serve_tables): asked = serve_tables({"*": SECTIONS}, frequency="M") rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "53", "date": "2024-06-05,2024-07-27"}, CREDENTIALS ) assert len(asked) == 2 assert [r.element_id for r in rows] == ["12345", "12346"] class TestReleaseObservationRows: """A release table that answers with observation values.""" async def test_an_observation_becomes_a_row(self, serve_tables): serve_tables({"*": _tables(_series(1217635, "5.33808"))}) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "483"}, CREDENTIALS ) assert (rows[0].symbol, rows[0].value, rows[0].date) == ( "SERIES1217635", 5.33808, date(2024, 7, 12), ) async def test_a_value_written_with_separators_is_read_as_a_number( self, serve_tables ): serve_tables({"*": _tables(_series(1, "1,234,567.89"))}) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "483"}, CREDENTIALS ) assert rows[0].value == 1234567.89 async def test_a_missing_observation_is_dropped(self, serve_tables): serve_tables({"*": _tables(_series(1, "."), _series(2, "5.0"))}) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "483"}, CREDENTIALS ) assert [r.element_id for r in rows] == ["2"] async def test_a_header_row_is_dropped(self, serve_tables): serve_tables( {"*": _tables(_series(1, "5.0", type="header"), _series(2, "5.0"))} ) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "483"}, CREDENTIALS ) assert [r.element_id for r in rows] == ["2"] async def test_the_units_of_the_series_are_attached(self, serve_tables): serve_tables({"*": _tables(_series(1, "5.0"))}, units={"SERIES1": "Percent"}) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "483"}, CREDENTIALS ) assert rows[0].units == "Percent" async def test_a_series_without_published_units_carries_none(self, serve_tables): serve_tables({"*": _tables(_series(1, "5.0"))}, units={"OTHER": "Percent"}) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "483"}, CREDENTIALS ) assert rows[0].units is None async def test_the_children_of_a_row_are_listed(self, serve_tables): serve_tables( { "*": _tables( _series(100, "5.0"), _series(101, "1.0", parent_id=100), _series(102, "2.0", parent_id=100), ) } ) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "483"}, CREDENTIALS ) listed = {r.element_id: r.children for r in rows} assert listed["100"] == "101,102" async def test_a_row_without_children_lists_none(self, serve_tables): serve_tables( { "*": _tables( _series(100, "5.0"), _series(101, "1.0", parent_id=100), ) } ) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "483"}, CREDENTIALS ) listed = {r.element_id: r.children for r in rows} assert listed["101"] is None class TestLineNumbers: """The line a row occupies in the published table.""" async def test_a_line_number_is_read_as_a_number(self, serve_tables): serve_tables({"*": _tables(_series(1, "5.0", line="7"))}) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "483"}, CREDENTIALS ) assert rows[0].line == 7 async def test_a_line_that_is_not_a_number_is_dropped(self, serve_tables): serve_tables({"*": _tables(_series(1, "5.0", line="n/a"))}) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "483"}, CREDENTIALS ) assert rows[0].line is None async def test_a_release_that_publishes_no_lines_carries_none(self, serve_tables): serve_tables({"*": _tables(_series(1, "5.0", line=None))}) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "483"}, CREDENTIALS ) assert rows[0].line is None class TestReleaseObservationDates: """How the period a release table row belongs to is written.""" @pytest.mark.parametrize( ("published", "expected"), [ ("Q1 2024", date(2024, 3, 31)), ("Q2 2024", date(2024, 6, 30)), ("Q3 2024", date(2024, 9, 30)), ("Q4 2024", date(2024, 12, 31)), ], ) async def test_a_quarter_becomes_its_last_day( self, serve_tables, published, expected ): serve_tables({"*": _tables(_series(1, "5.0", observation_date=published))}) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "53"}, CREDENTIALS ) assert rows[0].date == expected async def test_a_month_and_year_becomes_the_first_of_the_month(self, serve_tables): serve_tables({"*": _tables(_series(1, "5.0", observation_date="Jul 2024"))}) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "483"}, CREDENTIALS ) assert rows[0].date == date(2024, 7, 1) async def test_an_unreadable_period_is_passed_through_unchanged(self, serve_tables): serve_tables({"*": _tables(_series(1, "5.0", observation_date="not-a-date"))}) query = FredReleaseTableFetcher.transform_query({"release_id": "483"}) extracted = await FredReleaseTableFetcher.aextract_data(query, CREDENTIALS) assert extracted[0]["observation_date"] == "not-a-date" class TestEmptyRelease: """A release that publishes no table.""" async def test_a_response_without_elements_is_reported(self, serve_tables): serve_tables({"*": {"name": "Nothing"}}) with pytest.raises(EmptyDataError, match="No tables were found"): await FredReleaseTableFetcher.fetch_data({"release_id": "483"}, CREDENTIALS) async def test_a_release_of_only_missing_values_is_reported(self, serve_tables): serve_tables({"*": _tables(_series(1, "."), _series(2, "."))}) with pytest.raises(EmptyDataError, match="No tables were found"): await FredReleaseTableFetcher.fetch_data({"release_id": "483"}, CREDENTIALS) async def test_the_message_points_at_the_search_command(self, serve_tables): serve_tables({"*": {}}) with pytest.raises(EmptyDataError, match="fred_search"): await FredReleaseTableFetcher.fetch_data({"release_id": "483"}, CREDENTIALS) class TestRowOrder: """The order the rows of a release table are returned in.""" async def test_rows_are_ordered_by_period(self, serve_tables): serve_tables( { "2024-06-01": _tables(_series(2, "2.0", observation_date="2024-06-30")), "2024-07-01": _tables(_series(1, "1.0", observation_date="2024-07-31")), }, frequency="M", ) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "483", "date": "2024-06-15,2024-07-15"}, CREDENTIALS ) assert [r.date for r in rows] == [date(2024, 6, 30), date(2024, 7, 31)] async def test_rows_of_one_period_are_ordered_by_line(self, serve_tables): serve_tables( { "*": _tables( _series(1, "1.0", line="9"), _series(2, "2.0", line="3"), ) } ) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "483"}, CREDENTIALS ) assert [r.line for r in rows] == [3, 9] async def test_a_row_without_a_line_is_ordered_last(self, serve_tables): serve_tables( { "*": _tables( _series(1, "1.0", line="n/a"), _series(2, "2.0", line="3"), ) } ) rows = await FredReleaseTableFetcher.fetch_data( {"release_id": "483"}, CREDENTIALS ) assert [r.line for r in rows] == [3, None] class _Page: """A response carrying one rendered page of the releases calendar.""" def __init__(self, payload: dict) -> None: self._payload = payload async def text(self) -> str: """Return the response body.""" return json.dumps(self._payload) def _day(label: str) -> str: """Return the row that opens one day of the calendar.""" return f'
| Date | Release |
|---|
no table here
"}), None ) monkeypatch.setattr("openbb_fred.utils.rate_limiter.fred_get", get) with pytest.raises(OpenBBError, match="No tables found"): await FredEconomicCalendarFetcher.fetch_data( {"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)} ) class TestCalendarValidation: """Turning the read rows into economic calendar events.""" def test_a_row_becomes_an_event(self): query = FredEconomicCalendarQueryParams() rows = FredEconomicCalendarFetcher.transform_data( query, [ { "date": "2025-07-01 07:00:00-05:00", "event": "SOFR Averages and Index Data", "release_id": "483", } ], ) assert (rows[0].event, rows[0].release_id) == ( "SOFR Averages and Index Data", 483, ) def test_a_release_id_is_optional(self): query = FredEconomicCalendarQueryParams() rows = FredEconomicCalendarFetcher.transform_data( query, [{"date": "2025-07-01 07:00:00-05:00", "event": "Something"}] ) assert rows[0].release_id is None def test_a_release_filter_is_carried_by_the_query(self): query = FredEconomicCalendarFetcher.transform_query({"release_id": 483}) assert query.release_id == 483