Files
OpenBB/openbb_platform/providers/fred/tests/test_release_models.py
Danglewood 869cfed125 [Release] openbb-v5.0.0 (#7741)
* merge branch develop with main

* bump desktop version

* fix the merge

* sync Cargo.lock to desktop 2.0.0

* fix cli conftest.py

---------

Co-authored-by: deeleeramone <>
2026-09-29 10:58:44 -07:00

1015 lines
34 KiB
Python

"""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'<tr><td colspan="2"><span>{label}</span></td></tr>'
def _event(when: str, name: str, rid: int) -> str:
"""Return the row one scheduled release occupies."""
return f'<tr><td>{when}</td><td><a href="/release?rid={rid}">{name}</a></td></tr>'
def _pager(*rows: str) -> str:
"""Return the table one calendar page renders."""
return (
"<table><thead><tr><th>Date</th><th>Release</th></tr></thead>"
f"<tbody>{''.join(rows)}</tbody></table>"
)
def _page(scheduled: int, *rows: str) -> dict:
"""Return the payload one calendar request answers with."""
return {"ptic": scheduled, "pager": _pager(*rows)}
def _days(asked: list) -> list:
"""Return the day each calendar request asked for."""
return [parse_qs(urlparse(url).query)["vs"][0] for url in asked]
class _UnnamedDay:
"""A day of the requested range that carries no label."""
@staticmethod
def strftime(pattern: str) -> str:
"""Return the label of the day."""
return ""
ONE_DAY = _page(
2,
_day("Tuesday July 01, 2025"),
_event("1:00 am", "Euro Short Term Rate", 502),
_event("7:00 am", "SOFR Averages and Index Data", 483),
)
@pytest.fixture(name="serve_calendar")
def serve_calendar_fixture(monkeypatch):
"""Answer the releases calendar from recorded pages."""
def serve(by_date: dict) -> list:
asked: list = []
async def get(url, *, response_callback=None, **kwargs):
asked.append(url)
query = parse_qs(urlparse(url).query, keep_blank_values=True)
pages = by_date.get(query["vs"][0], by_date.get("*", []))
return await response_callback(
_Page(pages[int(query.get("pageID", ["1"])[0]) - 1]), None
)
monkeypatch.setattr("openbb_fred.utils.rate_limiter.fred_get", get)
return asked
return serve
class TestCalendarDay:
"""Reading one day of the release calendar."""
async def test_each_scheduled_release_becomes_an_event(self, serve_calendar):
serve_calendar({"*": [ONE_DAY]})
rows = await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
assert [r.event for r in rows] == [
"Euro Short Term Rate",
"SOFR Averages and Index Data",
]
async def test_the_day_header_is_not_an_event(self, serve_calendar):
serve_calendar({"*": [ONE_DAY]})
rows = await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
assert len(rows) == 2
async def test_the_release_id_is_read_from_the_link(self, serve_calendar):
serve_calendar({"*": [ONE_DAY]})
rows = await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
assert [r.release_id for r in rows] == [502, 483]
async def test_an_event_takes_its_day_from_the_header_above_it(
self, serve_calendar
):
serve_calendar({"*": [ONE_DAY]})
rows = await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
assert rows[0].date.strftime("%Y-%m-%d %H:%M") == "2025-07-01 01:00"
@pytest.mark.parametrize(
("day", "label", "abbreviation", "offset"),
[
(date(2025, 1, 7), "Tuesday January 07, 2025", "CST", -6),
(date(2025, 7, 1), "Tuesday July 01, 2025", "CDT", -5),
],
)
async def test_the_times_are_read_in_the_central_zone(
self, serve_calendar, day, label, abbreviation, offset
):
serve_calendar(
{
"*": [
_page(
1,
_day(label),
_event("1:00 am", "Euro Short Term Rate", 502),
)
]
}
)
rows = await FredEconomicCalendarFetcher.fetch_data(
{"start_date": day, "end_date": day}
)
assert str(rows[0].date.tzinfo) == "US/Central"
assert rows[0].date.tzname() == abbreviation
assert rows[0].date.utcoffset() == timedelta(hours=offset)
async def test_an_updated_marker_is_stripped_from_the_day(self, serve_calendar):
serve_calendar(
{
"*": [
_page(
1,
_day("Tuesday July 01, 2025 Updated"),
_event("1:00 am", "Euro Short Term Rate", 502),
)
]
}
)
rows = await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
assert rows[0].date.strftime("%Y-%m-%d") == "2025-07-01"
async def test_a_revised_marker_is_stripped_from_the_day(self, serve_calendar):
serve_calendar(
{
"*": [
_page(
1,
_day("Tuesday July 01, 2025 Revised"),
_event("1:00 am", "Euro Short Term Rate", 502),
)
]
}
)
rows = await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
assert rows[0].date.strftime("%Y-%m-%d") == "2025-07-01"
async def test_a_blank_time_repeats_the_time_above_it(self, serve_calendar):
serve_calendar(
{
"*": [
_page(
2,
_day("Tuesday July 01, 2025"),
_event("7:00 am", "SOFR Averages and Index Data", 483),
_event("", "Secured Overnight Financing Rate Data", 445),
)
]
}
)
rows = await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
assert rows[1].date.strftime("%H:%M") == "07:00"
async def test_a_time_written_as_not_available_repeats_the_time_above_it(
self, serve_calendar
):
serve_calendar(
{
"*": [
_page(
2,
_day("Tuesday July 01, 2025"),
_event("7:00 am", "SOFR Averages and Index Data", 483),
_event("N/A", "ICE BofA Indices", 209),
)
]
}
)
rows = await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
assert rows[1].date.strftime("%H:%M") == "07:00"
class TestCalendarPageWithoutADay:
"""A page of the calendar read without the day it belongs to."""
async def test_a_row_without_a_day_keeps_its_published_time(
self, serve_calendar, monkeypatch
):
monkeypatch.setattr(
"pandas.date_range", lambda *args, **kwargs: [_UnnamedDay()]
)
serve_calendar(
{"*": [_page(1, _event("7:00 am", "SOFR Averages and Index Data", 483))]}
)
rows = await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
assert rows[0].date.strftime("%H:%M") == "07:00"
assert str(rows[0].date.tzinfo) == "US/Central"
class TestCalendarRange:
"""Reading a range of days."""
async def test_every_day_of_the_range_is_read(self, serve_calendar):
asked = serve_calendar({"*": [ONE_DAY]})
await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 3)}
)
assert _days(asked) == ["2025-07-01", "2025-07-02", "2025-07-03"]
async def test_a_start_date_alone_reads_the_following_week(self, serve_calendar):
asked = serve_calendar({"*": [ONE_DAY]})
await FredEconomicCalendarFetcher.fetch_data({"start_date": date(2025, 7, 1)})
assert _days(asked)[-1] == "2025-07-08"
async def test_no_dates_read_from_yesterday_to_tomorrow(self, serve_calendar):
asked = serve_calendar({"*": [ONE_DAY]})
opened = datetime.today().date()
await FredEconomicCalendarFetcher.fetch_data({})
closed = datetime.today().date()
spans = [
[(day + timedelta(days=offset)).isoformat() for offset in (-1, 0, 1)]
for day in {opened, closed}
]
assert [day[:10] for day in _days(asked)] in spans
async def test_the_events_of_the_range_are_ordered_by_time(self, serve_calendar):
serve_calendar(
{
"2025-07-01": [
_page(
1,
_day("Tuesday July 01, 2025"),
_event("7:00 am", "SOFR Averages and Index Data", 483),
)
],
"2025-07-02": [
_page(
1,
_day("Wednesday July 02, 2025"),
_event("1:00 am", "Euro Short Term Rate", 502),
)
],
}
)
rows = await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 2)}
)
assert [r.date.strftime("%Y-%m-%d %H:%M") for r in rows] == [
"2025-07-01 07:00",
"2025-07-02 01:00",
]
class TestCalendarPaging:
"""Reading a day that spans more than one page."""
@staticmethod
def _spread(scheduled: int) -> dict:
"""Return a day of that many releases, one to a page."""
return {
"*": [
_page(
scheduled,
_day("Tuesday July 01, 2025"),
_event("1:00 am", "Euro Short Term Rate", 502),
),
_page(scheduled, _event("7:00 am", "SOFR Averages", 483)),
_page(scheduled, _event("8:00 am", "Federal Funds Data", 378)),
]
}
async def test_a_second_page_is_read_when_the_first_falls_short(
self, serve_calendar
):
serve_calendar(self._spread(60))
rows = await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
assert [r.release_id for r in rows] == [502, 483]
async def test_a_continuation_page_takes_its_day_from_the_request(
self, serve_calendar
):
serve_calendar(self._spread(60))
rows = await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
assert rows[1].date.strftime("%Y-%m-%d %H:%M") == "2025-07-01 07:00"
async def test_only_as_many_pages_as_the_count_calls_for_are_read(
self, serve_calendar
):
asked = serve_calendar(self._spread(60))
await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
assert [
parse_qs(urlparse(url).query).get("pageID", ["1"])[0] for url in asked
] == ["1", "2"]
async def test_a_further_page_is_read_for_every_fifty_releases(
self, serve_calendar
):
asked = serve_calendar(self._spread(101))
rows = await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
assert len(asked) == 3
assert [r.release_id for r in rows] == [502, 483, 378]
async def test_a_complete_first_page_is_not_followed(self, serve_calendar):
asked = serve_calendar({"*": [ONE_DAY]})
await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
assert len(asked) == 1
class TestCalendarReleaseFilter:
"""Reading the calendar of a single release."""
async def test_a_release_id_reads_the_whole_range_in_one_request(
self, serve_calendar
):
asked = serve_calendar({"*": [ONE_DAY]})
await FredEconomicCalendarFetcher.fetch_data(
{
"start_date": date(2025, 7, 1),
"end_date": date(2025, 7, 31),
"release_id": 483,
}
)
query = parse_qs(urlparse(asked[0]).query)
assert len(asked) == 1
assert (query["vs"][0], query["ve"][0], query["rid"][0]) == (
"2025-07-01",
"2025-07-31",
"483",
)
class TestCalendarWithoutEvents:
"""A calendar request that finds nothing scheduled."""
async def test_a_range_with_nothing_scheduled_is_reported(self, serve_calendar):
serve_calendar({"*": [_page(0)]})
with pytest.raises(OpenBBError, match="returned no data"):
await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 2)}
)
async def test_a_day_with_nothing_scheduled_is_left_out(self, serve_calendar):
serve_calendar(
{
"2025-07-01": [_page(0)],
"2025-07-02": [
_page(
1,
_day("Wednesday July 02, 2025"),
_event("1:00 am", "Euro Short Term Rate", 502),
)
],
}
)
rows = await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 2)}
)
assert [r.date.strftime("%Y-%m-%d") for r in rows] == ["2025-07-02"]
async def test_a_release_with_nothing_scheduled_is_reported(self, serve_calendar):
serve_calendar({"*": [_page(0)]})
with pytest.raises(OpenBBError, match="returned no data"):
await FredEconomicCalendarFetcher.fetch_data(
{
"start_date": date(2025, 7, 1),
"end_date": date(2025, 7, 2),
"release_id": 483,
}
)
class TestCalendarRequestFailure:
"""A calendar request the source refuses."""
async def test_a_failed_request_is_reported_with_its_message(self, monkeypatch):
async def get(url, **kwargs):
raise RuntimeError("the source is unavailable")
monkeypatch.setattr("openbb_fred.utils.rate_limiter.fred_get", get)
with pytest.raises(OpenBBError, match="the source is unavailable"):
await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
async def test_a_failure_without_a_message_names_its_type(self, monkeypatch):
async def get(url, **kwargs):
raise RuntimeError
monkeypatch.setattr("openbb_fred.utils.rate_limiter.fred_get", get)
with pytest.raises(
OpenBBError, match=r"FRED request failed \(RuntimeError\)\."
):
await FredEconomicCalendarFetcher.fetch_data(
{"start_date": date(2025, 7, 1), "end_date": date(2025, 7, 1)}
)
async def test_an_unreadable_page_is_reported(self, monkeypatch):
async def get(url, *, response_callback=None, **kwargs):
return await response_callback(
_Page({"ptic": 3, "pager": "<p>no table here</p>"}), 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