mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-28 21:12:16 +00:00
* 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 <>
988 lines
34 KiB
Python
988 lines
34 KiB
Python
"""Tests for the FRED-native search, series and regional data models."""
|
|
|
|
from datetime import date
|
|
|
|
import pytest
|
|
from openbb_core.app.model.abstract.error import OpenBBError
|
|
from openbb_core.provider.utils.errors import EmptyDataError
|
|
|
|
from openbb_fred.models.regional import FredRegionalDataFetcher, FredRegionalQueryParams
|
|
from openbb_fred.models.search import FredSearchFetcher
|
|
from openbb_fred.models.series import FredSeriesFetcher
|
|
|
|
CREDENTIALS = {"fred_api_key": "test-key"}
|
|
|
|
GDP = {
|
|
"id": "GDPC1",
|
|
"realtime_start": "2024-10-01",
|
|
"realtime_end": "2024-10-01",
|
|
"title": "Real Gross Domestic Product",
|
|
"observation_start": "1947-01-01",
|
|
"observation_end": "2024-07-01",
|
|
"frequency": "Quarterly",
|
|
"frequency_short": "Q",
|
|
"units": "Billions of Chained 2017 Dollars",
|
|
"units_short": "Bil. of Chn. 2017 $",
|
|
"seasonal_adjustment": "Seasonally Adjusted Annual Rate",
|
|
"seasonal_adjustment_short": "SAAR",
|
|
"last_updated": "2024-09-26 07:56:01-05",
|
|
"popularity": 91,
|
|
"group_popularity": 91,
|
|
"notes": "BEA Account Code: A191RX",
|
|
}
|
|
UNEMPLOYMENT = {
|
|
"id": "UNRATE",
|
|
"realtime_start": "2024-10-01",
|
|
"realtime_end": "2024-10-01",
|
|
"title": "Unemployment Rate",
|
|
"observation_start": "1948-01-01",
|
|
"observation_end": "2024-08-01",
|
|
"frequency": "Monthly",
|
|
"frequency_short": "M",
|
|
"units": "Percent",
|
|
"units_short": "%",
|
|
"seasonal_adjustment": "Seasonally Adjusted",
|
|
"seasonal_adjustment_short": "SA",
|
|
"last_updated": "2024-09-06 07:44:02-05",
|
|
"popularity": 85,
|
|
"group_popularity": 85,
|
|
"notes": "The unemployment rate represents the number of unemployed.",
|
|
}
|
|
SERIESS = {"count": 2, "seriess": [GDP, UNEMPLOYMENT]}
|
|
RELEASES = {
|
|
"releases": [
|
|
{
|
|
"id": 82,
|
|
"realtime_start": "2024-10-01",
|
|
"realtime_end": "2024-10-01",
|
|
"name": "Gross Domestic Product",
|
|
"press_release": True,
|
|
"link": "https://www.bea.gov/data/gdp/gross-domestic-product",
|
|
},
|
|
{
|
|
"id": 10,
|
|
"realtime_start": "2024-10-01",
|
|
"realtime_end": "2024-10-01",
|
|
"name": "Consumer Price Index",
|
|
"press_release": True,
|
|
"link": "https://www.bls.gov/cpi/",
|
|
},
|
|
]
|
|
}
|
|
SERIES_GROUP = {
|
|
"series_group": {
|
|
"title": "Per Capita Personal Income",
|
|
"region_type": "state",
|
|
"series_group": 882,
|
|
"season": "NSA",
|
|
"units": "Dollars",
|
|
"frequency": "Annual",
|
|
"min_date": "1929-01-01",
|
|
"max_date": "2023-01-01",
|
|
}
|
|
}
|
|
REGIONAL = {
|
|
"meta": {
|
|
"title": "Per Capita Personal Income by State",
|
|
"region": "state",
|
|
"seasonality": "Not Seasonally Adjusted",
|
|
"units": "Dollars",
|
|
"frequency": "Annual",
|
|
"data": {
|
|
"2022-01-01": [
|
|
{
|
|
"region": "Alabama",
|
|
"code": 1,
|
|
"value": "50637",
|
|
"series_id": "ALPCPI",
|
|
},
|
|
{
|
|
"region": "Alaska",
|
|
"code": 2,
|
|
"value": "66130",
|
|
"series_id": "AKPCPI",
|
|
},
|
|
],
|
|
"2023-01-01": [
|
|
{
|
|
"region": "Alabama",
|
|
"code": 1,
|
|
"value": "52034",
|
|
"series_id": "ALPCPI",
|
|
},
|
|
{
|
|
"region": "Alaska",
|
|
"code": 2,
|
|
"value": "69414",
|
|
"series_id": "AKPCPI",
|
|
},
|
|
],
|
|
},
|
|
}
|
|
}
|
|
|
|
|
|
def _transport(answer, seen=None):
|
|
"""Return a ``fred_get`` stand-in answering from ``answer``."""
|
|
from copy import deepcopy
|
|
|
|
async def fake(url, **kwargs):
|
|
if seen is not None:
|
|
seen.append(url)
|
|
return deepcopy(answer(url) if callable(answer) else answer)
|
|
|
|
return fake
|
|
|
|
|
|
def _serve(monkeypatch, answer, seen=None):
|
|
"""Point the FRED transport at ``answer`` for the duration of a test."""
|
|
monkeypatch.setattr(
|
|
"openbb_fred.utils.rate_limiter.fred_get", _transport(answer, seen)
|
|
)
|
|
|
|
|
|
def _raiser(error):
|
|
"""Return a ``fred_get`` stand-in that fails with ``error``."""
|
|
|
|
async def fake(url, **kwargs):
|
|
raise error
|
|
|
|
return fake
|
|
|
|
|
|
class TestSearchQueryTransform:
|
|
"""How a search request is read before it is sent."""
|
|
|
|
def test_a_release_id_alone_asks_for_that_release(self):
|
|
query = FredSearchFetcher.transform_query({"release_id": 82})
|
|
|
|
assert query.search_type == "release"
|
|
|
|
def test_an_empty_request_lists_the_releases(self):
|
|
query = FredSearchFetcher.transform_query({})
|
|
|
|
assert query.search_type == "release" and query.release_id is None
|
|
|
|
def test_a_query_is_read_as_a_full_text_search(self):
|
|
query = FredSearchFetcher.transform_query({"query": "gdp"})
|
|
|
|
assert query.search_type == "full_text"
|
|
|
|
def test_a_full_text_search_without_a_query_is_reported(self):
|
|
with pytest.raises(OpenBBError, match="A query is required"):
|
|
FredSearchFetcher.transform_query(
|
|
{"release_id": 82, "search_type": "full_text"}
|
|
)
|
|
|
|
def test_a_series_id_search_without_a_query_is_reported(self):
|
|
with pytest.raises(OpenBBError, match="A query is required"):
|
|
FredSearchFetcher.transform_query(
|
|
{"release_id": 82, "search_type": "series_id"}
|
|
)
|
|
|
|
def test_a_series_id_lookup_needs_no_query(self):
|
|
query = FredSearchFetcher.transform_query({"series_id": "ALPCPI"})
|
|
|
|
assert query.series_id == "ALPCPI"
|
|
|
|
def test_a_text_search_with_nothing_to_search_for_lists_the_releases(self):
|
|
query = FredSearchFetcher.transform_query({"search_type": "full_text"})
|
|
|
|
assert query.search_type == "release"
|
|
|
|
def test_excluding_tags_without_naming_tags_is_reported(self):
|
|
with pytest.raises(OpenBBError, match="requires 'tag_names' to be set"):
|
|
FredSearchFetcher.transform_query(
|
|
{"query": "gdp", "exclude_tag_names": "oecd"}
|
|
)
|
|
|
|
def test_excluded_tags_are_accepted_alongside_tags(self):
|
|
query = FredSearchFetcher.transform_query(
|
|
{"query": "gdp", "tag_names": "usa", "exclude_tag_names": "oecd"}
|
|
)
|
|
|
|
assert query.exclude_tag_names == "oecd"
|
|
|
|
|
|
class TestSeriesGroupLookup:
|
|
"""Resolving a series id to the group that carries its regional data."""
|
|
|
|
async def test_every_named_series_is_looked_up(self, monkeypatch):
|
|
seen: list = []
|
|
_serve(monkeypatch, SERIES_GROUP, seen)
|
|
results = await FredSearchFetcher.fetch_data(
|
|
{"series_id": "ALPCPI,AKPCPI"}, CREDENTIALS
|
|
)
|
|
|
|
assert sorted(r.series_id for r in results) == ["AKPCPI", "ALPCPI"]
|
|
assert len(seen) == 2
|
|
|
|
async def test_the_lookup_reads_the_geographic_endpoint(self, monkeypatch):
|
|
seen: list = []
|
|
_serve(monkeypatch, SERIES_GROUP, seen)
|
|
await FredSearchFetcher.fetch_data({"series_id": "ALPCPI"}, CREDENTIALS)
|
|
|
|
assert seen[0].startswith("https://api.stlouisfed.org/geofred/series/group?")
|
|
assert "series_id=ALPCPI" in seen[0]
|
|
|
|
async def test_the_group_id_is_returned_as_text(self, monkeypatch):
|
|
_serve(monkeypatch, SERIES_GROUP)
|
|
results = await FredSearchFetcher.fetch_data(
|
|
{"series_id": "ALPCPI"}, CREDENTIALS
|
|
)
|
|
|
|
assert results[0].series_group == "882"
|
|
|
|
async def test_the_group_carries_the_fields_a_regional_query_needs(
|
|
self, monkeypatch
|
|
):
|
|
_serve(monkeypatch, SERIES_GROUP)
|
|
results = await FredSearchFetcher.fetch_data(
|
|
{"series_id": "ALPCPI"}, CREDENTIALS
|
|
)
|
|
found = results[0]
|
|
|
|
assert found.region_type == "state"
|
|
assert found.units == "Dollars"
|
|
assert found.frequency == "Annual"
|
|
assert found.seasonal_adjustment == "NSA"
|
|
assert found.observation_start == date(1929, 1, 1)
|
|
|
|
async def test_a_series_without_geographic_data_is_dropped(self, monkeypatch):
|
|
def answer(url):
|
|
return SERIES_GROUP if "ALPCPI" in url else {"series_group": None}
|
|
|
|
_serve(monkeypatch, answer)
|
|
results = await FredSearchFetcher.fetch_data(
|
|
{"series_id": "ALPCPI,SP500"}, CREDENTIALS
|
|
)
|
|
|
|
assert [r.series_id for r in results] == ["ALPCPI"]
|
|
|
|
async def test_a_series_id_overrides_the_other_parameters(self, monkeypatch):
|
|
seen: list = []
|
|
_serve(monkeypatch, SERIES_GROUP, seen)
|
|
results = await FredSearchFetcher.fetch_data(
|
|
{"query": "gdp", "series_id": "ALPCPI", "limit": 1}, CREDENTIALS
|
|
)
|
|
|
|
assert seen[0].startswith("https://api.stlouisfed.org/geofred/series/group?")
|
|
assert [r.series_id for r in results] == ["ALPCPI"]
|
|
|
|
async def test_no_geographic_data_at_all_is_reported(self, monkeypatch):
|
|
_serve(monkeypatch, {"series_group": None})
|
|
|
|
with pytest.raises(EmptyDataError, match="No results found for the provided"):
|
|
await FredSearchFetcher.fetch_data({"series_id": "SP500"}, CREDENTIALS)
|
|
|
|
|
|
class TestReleaseListing:
|
|
"""Listing every release FRED publishes."""
|
|
|
|
async def test_every_release_is_listed(self, monkeypatch):
|
|
_serve(monkeypatch, RELEASES)
|
|
results = await FredSearchFetcher.fetch_data({}, CREDENTIALS)
|
|
|
|
assert [r.name for r in results] == [
|
|
"Gross Domestic Product",
|
|
"Consumer Price Index",
|
|
]
|
|
|
|
async def test_a_release_carries_the_link_to_its_publisher(self, monkeypatch):
|
|
_serve(monkeypatch, RELEASES)
|
|
results = await FredSearchFetcher.fetch_data({}, CREDENTIALS)
|
|
|
|
assert results[1].url == "https://www.bls.gov/cpi/"
|
|
assert results[1].press_release is True
|
|
|
|
async def test_the_listing_reads_the_releases_endpoint(self, monkeypatch):
|
|
seen: list = []
|
|
_serve(monkeypatch, RELEASES, seen)
|
|
await FredSearchFetcher.fetch_data({}, CREDENTIALS)
|
|
|
|
assert seen == [
|
|
"https://api.stlouisfed.org/fred/releases?api_key=test-key&file_type=json"
|
|
]
|
|
|
|
async def test_the_release_id_is_returned_as_text(self, monkeypatch):
|
|
_serve(monkeypatch, RELEASES)
|
|
results = await FredSearchFetcher.fetch_data({}, CREDENTIALS)
|
|
|
|
assert [r.release_id for r in results] == ["82", "10"]
|
|
|
|
async def test_a_listing_is_narrowed_by_the_query(self, monkeypatch):
|
|
_serve(monkeypatch, RELEASES)
|
|
results = await FredSearchFetcher.fetch_data(
|
|
{"query": "Consumer", "search_type": "release"}, CREDENTIALS
|
|
)
|
|
|
|
assert [r.release_id for r in results] == ["10"]
|
|
|
|
async def test_an_unexpected_listing_response_is_reported(self, monkeypatch):
|
|
_serve(monkeypatch, {})
|
|
|
|
with pytest.raises(OpenBBError, match="Unexpected result while retrieving"):
|
|
await FredSearchFetcher.fetch_data({}, CREDENTIALS)
|
|
|
|
|
|
class TestReleaseSeries:
|
|
"""Reading the series that belong to one release."""
|
|
|
|
async def test_the_series_of_a_release_are_read(self, monkeypatch):
|
|
seen: list = []
|
|
_serve(monkeypatch, SERIESS, seen)
|
|
results = await FredSearchFetcher.fetch_data({"release_id": 82}, CREDENTIALS)
|
|
|
|
assert "/fred/release/series?" in seen[0]
|
|
assert "release_id=82" in seen[0]
|
|
assert [r.series_id for r in results] == ["UNRATE", "GDPC1"]
|
|
|
|
async def test_the_search_text_is_not_sent_for_a_release(self, monkeypatch):
|
|
seen: list = []
|
|
_serve(monkeypatch, SERIESS, seen)
|
|
await FredSearchFetcher.fetch_data(
|
|
{"release_id": 82, "query": "rate"}, CREDENTIALS
|
|
)
|
|
|
|
assert "search_text" not in seen[0]
|
|
|
|
async def test_search_rank_ordering_is_not_sent_for_a_release(self, monkeypatch):
|
|
seen: list = []
|
|
_serve(monkeypatch, SERIESS, seen)
|
|
await FredSearchFetcher.fetch_data(
|
|
{"release_id": 82, "order_by": "search_rank"}, CREDENTIALS
|
|
)
|
|
|
|
assert "order_by" not in seen[0]
|
|
|
|
async def test_another_ordering_is_sent_for_a_release(self, monkeypatch):
|
|
seen: list = []
|
|
_serve(monkeypatch, SERIESS, seen)
|
|
await FredSearchFetcher.fetch_data(
|
|
{"release_id": 82, "order_by": "popularity"}, CREDENTIALS
|
|
)
|
|
|
|
assert "order_by=popularity" in seen[0]
|
|
|
|
async def test_the_limit_is_applied_here_and_not_sent(self, monkeypatch):
|
|
seen: list = []
|
|
_serve(monkeypatch, SERIESS, seen)
|
|
results = await FredSearchFetcher.fetch_data(
|
|
{"release_id": 82, "limit": 1}, CREDENTIALS
|
|
)
|
|
|
|
assert "limit" not in seen[0]
|
|
assert len(results) == 1
|
|
|
|
|
|
class TestFullTextSearch:
|
|
"""Searching the whole of FRED for matching series."""
|
|
|
|
async def test_the_search_endpoint_carries_the_query(self, monkeypatch):
|
|
seen: list = []
|
|
_serve(monkeypatch, SERIESS, seen)
|
|
await FredSearchFetcher.fetch_data({"query": "rate"}, CREDENTIALS)
|
|
|
|
assert "/fred/series/search?" in seen[0]
|
|
assert "search_text=rate" in seen[0]
|
|
|
|
async def test_a_term_matches_any_field_of_a_row(self, monkeypatch):
|
|
_serve(monkeypatch, SERIESS)
|
|
results = await FredSearchFetcher.fetch_data({"query": "rate"}, CREDENTIALS)
|
|
|
|
assert sorted(r.series_id for r in results) == ["GDPC1", "UNRATE"]
|
|
|
|
async def test_a_row_the_term_misses_is_dropped(self, monkeypatch):
|
|
_serve(monkeypatch, SERIESS)
|
|
results = await FredSearchFetcher.fetch_data({"query": "gdp"}, CREDENTIALS)
|
|
|
|
assert [r.series_id for r in results] == ["GDPC1"]
|
|
|
|
async def test_every_term_of_a_query_must_match(self, monkeypatch):
|
|
_serve(monkeypatch, SERIESS)
|
|
results = await FredSearchFetcher.fetch_data(
|
|
{"query": "gdp;quarterly"}, CREDENTIALS
|
|
)
|
|
|
|
assert [r.series_id for r in results] == ["GDPC1"]
|
|
|
|
async def test_a_query_no_row_satisfies_is_reported(self, monkeypatch):
|
|
_serve(monkeypatch, SERIESS)
|
|
|
|
with pytest.raises(EmptyDataError, match="No results found for the provided"):
|
|
await FredSearchFetcher.fetch_data({"query": "gdp;monthly"}, CREDENTIALS)
|
|
|
|
async def test_a_tag_narrows_the_results_further(self, monkeypatch):
|
|
_serve(monkeypatch, SERIESS)
|
|
results = await FredSearchFetcher.fetch_data(
|
|
{"query": "rate", "tag_names": "quarterly"}, CREDENTIALS
|
|
)
|
|
|
|
assert [r.series_id for r in results] == ["GDPC1"]
|
|
|
|
async def test_a_series_id_search_is_not_narrowed(self, monkeypatch):
|
|
_serve(monkeypatch, SERIESS)
|
|
results = await FredSearchFetcher.fetch_data(
|
|
{"query": "gdp", "search_type": "series_id", "tag_names": "monthly"},
|
|
CREDENTIALS,
|
|
)
|
|
|
|
assert sorted(r.series_id for r in results) == ["GDPC1", "UNRATE"]
|
|
|
|
async def test_an_empty_result_set_is_reported(self, monkeypatch):
|
|
_serve(monkeypatch, {"count": 0, "seriess": []})
|
|
|
|
with pytest.raises(EmptyDataError, match="The request was returned empty"):
|
|
await FredSearchFetcher.fetch_data({"query": "rate"}, CREDENTIALS)
|
|
|
|
async def test_a_fred_error_code_is_reported(self, monkeypatch):
|
|
_serve(
|
|
monkeypatch,
|
|
{"error_code": 400, "error_message": "Bad Request. Variable is not valid."},
|
|
)
|
|
|
|
with pytest.raises(OpenBBError, match="Status Code: 400"):
|
|
await FredSearchFetcher.fetch_data({"query": "rate"}, CREDENTIALS)
|
|
|
|
async def test_the_error_message_is_passed_through(self, monkeypatch):
|
|
_serve(
|
|
monkeypatch, {"error_code": 400, "error_message": "Variable is invalid."}
|
|
)
|
|
|
|
with pytest.raises(OpenBBError, match="Variable is invalid."):
|
|
await FredSearchFetcher.fetch_data({"query": "rate"}, CREDENTIALS)
|
|
|
|
async def test_a_response_that_is_not_a_search_result_is_reported(
|
|
self, monkeypatch
|
|
):
|
|
_serve(monkeypatch, [])
|
|
|
|
with pytest.raises(OpenBBError, match="Unexpected response format"):
|
|
await FredSearchFetcher.fetch_data({"query": "rate"}, CREDENTIALS)
|
|
|
|
async def test_a_missing_value_reads_as_no_value(self, monkeypatch):
|
|
_serve(monkeypatch, {"count": 1, "seriess": [{**GDP, "notes": None}]})
|
|
results = await FredSearchFetcher.fetch_data({"query": "gdp"}, CREDENTIALS)
|
|
|
|
assert results[0].notes is None
|
|
|
|
|
|
class TestSearchOrdering:
|
|
"""Ordering and truncating what a search returned."""
|
|
|
|
async def test_the_default_order_is_the_newest_observation_first(self, monkeypatch):
|
|
_serve(monkeypatch, SERIESS)
|
|
results = await FredSearchFetcher.fetch_data({"query": "rate"}, CREDENTIALS)
|
|
|
|
assert [r.series_id for r in results] == ["UNRATE", "GDPC1"]
|
|
|
|
async def test_the_results_are_ordered_by_the_chosen_attribute(self, monkeypatch):
|
|
_serve(monkeypatch, SERIESS)
|
|
results = await FredSearchFetcher.fetch_data(
|
|
{"query": "rate", "order_by": "popularity"}, CREDENTIALS
|
|
)
|
|
|
|
assert [r.popularity for r in results] == [91, 85]
|
|
|
|
async def test_the_order_can_be_reversed(self, monkeypatch):
|
|
_serve(monkeypatch, SERIESS)
|
|
results = await FredSearchFetcher.fetch_data(
|
|
{"query": "rate", "order_by": "popularity", "sort_order": "asc"},
|
|
CREDENTIALS,
|
|
)
|
|
|
|
assert [r.popularity for r in results] == [85, 91]
|
|
|
|
async def test_an_attribute_the_rows_lack_leaves_the_order_alone(self, monkeypatch):
|
|
_serve(monkeypatch, RELEASES)
|
|
results = await FredSearchFetcher.fetch_data(
|
|
{"search_type": "release", "order_by": "popularity"}, CREDENTIALS
|
|
)
|
|
|
|
assert [r.release_id for r in results] == ["82", "10"]
|
|
|
|
async def test_the_limit_truncates_the_results(self, monkeypatch):
|
|
_serve(monkeypatch, SERIESS)
|
|
results = await FredSearchFetcher.fetch_data(
|
|
{"query": "rate", "limit": 1}, CREDENTIALS
|
|
)
|
|
|
|
assert [r.series_id for r in results] == ["UNRATE"]
|
|
|
|
async def test_a_limit_above_the_result_count_keeps_everything(self, monkeypatch):
|
|
_serve(monkeypatch, SERIESS)
|
|
results = await FredSearchFetcher.fetch_data(
|
|
{"query": "rate", "limit": 10}, CREDENTIALS
|
|
)
|
|
|
|
assert len(results) == 2
|
|
|
|
|
|
class TestSeriesQuery:
|
|
"""How a series request is read before it is sent."""
|
|
|
|
def test_the_symbol_is_upper_cased(self):
|
|
assert FredSeriesFetcher.transform_query({"symbol": "sp500"}).symbol == "SP500"
|
|
|
|
def test_the_cache_is_used_by_default(self):
|
|
assert FredSeriesFetcher.transform_query({"symbol": "SP500"}).use_cache is True
|
|
|
|
|
|
def _series_transport(observations, metadata=None, seen=None):
|
|
"""Return a ``fred_get`` stand-in serving observations and series metadata."""
|
|
from copy import deepcopy
|
|
|
|
meta = metadata or {}
|
|
|
|
async def fake(url, **kwargs):
|
|
if seen is not None:
|
|
seen.append(url)
|
|
|
|
series_id = url.split("series_id=")[1].split("&")[0]
|
|
|
|
if "series/observations?" in url:
|
|
return deepcopy({"observations": observations.get(series_id, [])})
|
|
|
|
return deepcopy({"seriess": [meta.get(series_id, {})]})
|
|
|
|
return fake
|
|
|
|
|
|
def _observation(day: str, value: str) -> dict:
|
|
"""Return one FRED observation as the API writes it."""
|
|
return {
|
|
"realtime_start": "2024-10-01",
|
|
"realtime_end": "2024-10-01",
|
|
"date": day,
|
|
"value": value,
|
|
}
|
|
|
|
|
|
SP500_META = {
|
|
"title": "S&P 500",
|
|
"units": "Index",
|
|
"frequency": "Daily, Close",
|
|
"seasonal_adjustment": "Not Seasonally Adjusted",
|
|
"notes": "The observations are not seasonally adjusted.",
|
|
}
|
|
DGS10_META = {
|
|
"title": "Market Yield on U.S. Treasury Securities at 10-Year Constant Maturity",
|
|
"units": "Percent",
|
|
"frequency": "Daily",
|
|
"seasonal_adjustment": "Not Seasonally Adjusted",
|
|
"notes": "Treasury Yield Curve Methodology.",
|
|
}
|
|
|
|
|
|
class TestSeriesExtract:
|
|
"""Reading the observations and metadata of one or more series."""
|
|
|
|
async def test_one_symbol_reads_its_observations_and_its_metadata(
|
|
self, monkeypatch
|
|
):
|
|
seen: list = []
|
|
monkeypatch.setattr(
|
|
"openbb_fred.utils.rate_limiter.fred_get",
|
|
_series_transport(
|
|
{"SP500": [_observation("2024-01-02", "4742.83")]},
|
|
{"SP500": SP500_META},
|
|
seen,
|
|
),
|
|
)
|
|
await FredSeriesFetcher.fetch_data({"symbol": "SP500"}, CREDENTIALS)
|
|
|
|
assert "/fred/series/observations?" in seen[0]
|
|
assert seen[1].startswith("https://api.stlouisfed.org/fred/series?")
|
|
|
|
async def test_every_symbol_of_a_comma_separated_request_is_read(self, monkeypatch):
|
|
monkeypatch.setattr(
|
|
"openbb_fred.utils.rate_limiter.fred_get",
|
|
_series_transport(
|
|
{
|
|
"SP500": [_observation("2024-01-02", "4742.83")],
|
|
"DGS10": [_observation("2024-01-02", "3.95")],
|
|
},
|
|
{"SP500": SP500_META, "DGS10": DGS10_META},
|
|
),
|
|
)
|
|
response = await FredSeriesFetcher.fetch_data(
|
|
{"symbol": "SP500,DGS10"}, CREDENTIALS
|
|
)
|
|
|
|
assert response.result[0].SP500 == 4742.83
|
|
assert response.result[0].DGS10 == 3.95
|
|
|
|
async def test_the_metadata_describes_each_series(self, monkeypatch):
|
|
monkeypatch.setattr(
|
|
"openbb_fred.utils.rate_limiter.fred_get",
|
|
_series_transport(
|
|
{"SP500": [_observation("2024-01-02", "4742.83")]},
|
|
{"SP500": SP500_META},
|
|
),
|
|
)
|
|
response = await FredSeriesFetcher.fetch_data({"symbol": "SP500"}, CREDENTIALS)
|
|
|
|
assert response.metadata["SP500"]["title"] == "S&P 500"
|
|
assert response.metadata["SP500"]["units"] == "Index"
|
|
assert "data" not in response.metadata["SP500"]
|
|
|
|
async def test_the_observations_are_returned_in_date_order(self, monkeypatch):
|
|
monkeypatch.setattr(
|
|
"openbb_fred.utils.rate_limiter.fred_get",
|
|
_series_transport(
|
|
{
|
|
"SP500": [
|
|
_observation("2024-01-03", "4704.81"),
|
|
_observation("2024-01-02", "4742.83"),
|
|
]
|
|
},
|
|
{"SP500": SP500_META},
|
|
),
|
|
)
|
|
response = await FredSeriesFetcher.fetch_data({"symbol": "SP500"}, CREDENTIALS)
|
|
|
|
assert [r.date for r in response.result] == [date(2024, 1, 2), date(2024, 1, 3)]
|
|
|
|
async def test_an_unpublished_observation_is_dropped(self, monkeypatch):
|
|
monkeypatch.setattr(
|
|
"openbb_fred.utils.rate_limiter.fred_get",
|
|
_series_transport(
|
|
{
|
|
"SP500": [
|
|
_observation("2024-01-01", "."),
|
|
_observation("2024-01-02", "4742.83"),
|
|
]
|
|
},
|
|
{"SP500": SP500_META},
|
|
),
|
|
)
|
|
response = await FredSeriesFetcher.fetch_data({"symbol": "SP500"}, CREDENTIALS)
|
|
|
|
assert [r.date for r in response.result] == [date(2024, 1, 2)]
|
|
|
|
async def test_a_date_one_series_lacks_reads_as_no_value(self, monkeypatch):
|
|
monkeypatch.setattr(
|
|
"openbb_fred.utils.rate_limiter.fred_get",
|
|
_series_transport(
|
|
{
|
|
"SP500": [
|
|
_observation("2024-01-02", "4742.83"),
|
|
_observation("2024-01-03", "4704.81"),
|
|
],
|
|
"DGS10": [_observation("2024-01-02", "3.95")],
|
|
},
|
|
{"SP500": SP500_META, "DGS10": DGS10_META},
|
|
),
|
|
)
|
|
response = await FredSeriesFetcher.fetch_data(
|
|
{"symbol": "SP500,DGS10"}, CREDENTIALS
|
|
)
|
|
|
|
assert response.result[1].DGS10 is None
|
|
|
|
async def test_a_series_with_no_observations_is_dropped(self, monkeypatch):
|
|
monkeypatch.setattr(
|
|
"openbb_fred.utils.rate_limiter.fred_get",
|
|
_series_transport(
|
|
{"SP500": [_observation("2024-01-02", "4742.83")], "DGS10": []},
|
|
{"SP500": SP500_META, "DGS10": DGS10_META},
|
|
),
|
|
)
|
|
response = await FredSeriesFetcher.fetch_data(
|
|
{"symbol": "SP500,DGS10"}, CREDENTIALS
|
|
)
|
|
|
|
assert "DGS10" not in response.metadata
|
|
assert not hasattr(response.result[0], "DGS10")
|
|
|
|
async def test_a_request_answered_by_nothing_returns_no_rows(self, monkeypatch):
|
|
monkeypatch.setattr(
|
|
"openbb_fred.utils.rate_limiter.fred_get",
|
|
_series_transport({"SP500": []}, {"SP500": SP500_META}),
|
|
)
|
|
response = await FredSeriesFetcher.fetch_data({"symbol": "SP500"}, CREDENTIALS)
|
|
|
|
assert response.result == []
|
|
|
|
async def test_a_response_that_is_not_an_object_reads_as_nothing(self, monkeypatch):
|
|
_serve(monkeypatch, [])
|
|
response = await FredSeriesFetcher.fetch_data({"symbol": "SP500"}, CREDENTIALS)
|
|
|
|
assert response.result == []
|
|
|
|
async def test_the_cache_switch_reaches_the_transport(self, monkeypatch):
|
|
seen: list = []
|
|
|
|
async def fake(url, **kwargs):
|
|
seen.append(kwargs.get("use_cache"))
|
|
return {"observations": [], "seriess": [{}]}
|
|
|
|
monkeypatch.setattr("openbb_fred.utils.rate_limiter.fred_get", fake)
|
|
await FredSeriesFetcher.fetch_data(
|
|
{"symbol": "SP500", "use_cache": False}, CREDENTIALS
|
|
)
|
|
|
|
assert seen == [False, False]
|
|
|
|
|
|
class TestSeriesFailures:
|
|
"""What a failed series read reports."""
|
|
|
|
async def test_a_reported_error_is_passed_through_unchanged(self, monkeypatch):
|
|
original = OpenBBError("FRED API key is invalid.")
|
|
monkeypatch.setattr(
|
|
"openbb_fred.utils.rate_limiter.fred_get", _raiser(original)
|
|
)
|
|
|
|
with pytest.raises(OpenBBError) as raised:
|
|
await FredSeriesFetcher.fetch_data({"symbol": "SP500"}, CREDENTIALS)
|
|
|
|
assert raised.value is original
|
|
|
|
async def test_an_unexpected_failure_is_reported_as_an_error(self, monkeypatch):
|
|
failure = ValueError("the connection dropped")
|
|
monkeypatch.setattr("openbb_fred.utils.rate_limiter.fred_get", _raiser(failure))
|
|
|
|
with pytest.raises(OpenBBError, match="the connection dropped") as raised:
|
|
await FredSeriesFetcher.fetch_data({"symbol": "SP500"}, CREDENTIALS)
|
|
|
|
assert raised.value.__cause__ is failure
|
|
|
|
async def test_a_failure_with_no_message_names_its_type(self, monkeypatch):
|
|
monkeypatch.setattr(
|
|
"openbb_fred.utils.rate_limiter.fred_get", _raiser(TimeoutError())
|
|
)
|
|
|
|
with pytest.raises(
|
|
OpenBBError, match=r"FRED request failed \(TimeoutError\)\."
|
|
):
|
|
await FredSeriesFetcher.fetch_data({"symbol": "SP500"}, CREDENTIALS)
|
|
|
|
|
|
class TestRegionalQueryValidation:
|
|
"""What a regional request must carry, and the start date it is given."""
|
|
|
|
def test_a_series_group_without_a_frequency_is_reported(self):
|
|
with pytest.raises(OpenBBError, match="frequency is a required field"):
|
|
FredRegionalQueryParams(
|
|
symbol="882", is_series_group=True, region_type="state", units="Dollars"
|
|
)
|
|
|
|
def test_a_series_group_without_a_region_type_is_reported(self):
|
|
with pytest.raises(OpenBBError, match="region_type is a required field"):
|
|
FredRegionalQueryParams(
|
|
symbol="882", is_series_group=True, frequency="a", units="Dollars"
|
|
)
|
|
|
|
def test_a_series_group_without_units_is_reported(self):
|
|
with pytest.raises(OpenBBError, match="units is a required field"):
|
|
FredRegionalQueryParams(
|
|
symbol="882", is_series_group=True, frequency="a", region_type="state"
|
|
)
|
|
|
|
def test_a_series_group_reads_the_whole_record_by_default(self):
|
|
query = FredRegionalQueryParams(
|
|
symbol="882",
|
|
is_series_group=True,
|
|
frequency="a",
|
|
region_type="state",
|
|
units="Dollars",
|
|
)
|
|
|
|
assert query.start_date == date(1900, 1, 1)
|
|
|
|
def test_a_given_start_date_is_kept_for_a_series_group(self):
|
|
query = FredRegionalQueryParams(
|
|
symbol="882",
|
|
is_series_group=True,
|
|
frequency="a",
|
|
region_type="state",
|
|
units="Dollars",
|
|
start_date=date(2000, 1, 1),
|
|
)
|
|
|
|
assert query.start_date == date(2000, 1, 1)
|
|
|
|
def test_a_series_is_given_no_start_date(self):
|
|
query = FredRegionalQueryParams(symbol="ALPCPI", is_series_group=False)
|
|
|
|
assert query.start_date is None
|
|
|
|
def test_a_given_start_date_is_kept_for_a_series(self):
|
|
query = FredRegionalQueryParams(
|
|
symbol="ALPCPI", is_series_group=False, start_date=date(2000, 1, 1)
|
|
)
|
|
|
|
assert query.start_date == date(2000, 1, 1)
|
|
|
|
|
|
class TestRegionalExtract:
|
|
"""Which geographic endpoint a regional request reads."""
|
|
|
|
async def test_a_series_group_reads_the_regional_endpoint(self, monkeypatch):
|
|
seen: list = []
|
|
_serve(monkeypatch, REGIONAL, seen)
|
|
await FredRegionalDataFetcher.fetch_data(
|
|
{
|
|
"symbol": "882",
|
|
"is_series_group": True,
|
|
"frequency": "a",
|
|
"region_type": "state",
|
|
"units": "Dollars",
|
|
},
|
|
CREDENTIALS,
|
|
)
|
|
|
|
assert seen[0].startswith("https://api.stlouisfed.org/geofred/regional/data?")
|
|
assert "series_group=882" in seen[0]
|
|
assert "region_type=state" in seen[0]
|
|
|
|
async def test_the_season_is_sent_in_upper_case(self, monkeypatch):
|
|
seen: list = []
|
|
_serve(monkeypatch, REGIONAL, seen)
|
|
await FredRegionalDataFetcher.fetch_data(
|
|
{
|
|
"symbol": "882",
|
|
"is_series_group": True,
|
|
"frequency": "a",
|
|
"region_type": "state",
|
|
"units": "Dollars",
|
|
"season": "sa",
|
|
},
|
|
CREDENTIALS,
|
|
)
|
|
|
|
assert "season=SA" in seen[0]
|
|
|
|
async def test_a_series_reads_the_series_endpoint(self, monkeypatch):
|
|
seen: list = []
|
|
_serve(monkeypatch, REGIONAL, seen)
|
|
await FredRegionalDataFetcher.fetch_data(
|
|
{"symbol": "ALPCPI", "is_series_group": False}, CREDENTIALS
|
|
)
|
|
|
|
assert seen[0].startswith("https://api.stlouisfed.org/geofred/series/data?")
|
|
assert "series_id=ALPCPI" in seen[0]
|
|
|
|
async def test_the_group_only_parameters_are_not_sent_for_a_series(
|
|
self, monkeypatch
|
|
):
|
|
seen: list = []
|
|
_serve(monkeypatch, REGIONAL, seen)
|
|
await FredRegionalDataFetcher.fetch_data(
|
|
{
|
|
"symbol": "ALPCPI",
|
|
"is_series_group": False,
|
|
"region_type": "state",
|
|
"units": "Dollars",
|
|
"season": "sa",
|
|
},
|
|
CREDENTIALS,
|
|
)
|
|
|
|
assert "region_type" not in seen[0]
|
|
assert "season" not in seen[0]
|
|
assert "units" not in seen[0]
|
|
|
|
async def test_the_limit_and_end_date_are_not_sent(self, monkeypatch):
|
|
seen: list = []
|
|
_serve(monkeypatch, REGIONAL, seen)
|
|
await FredRegionalDataFetcher.fetch_data(
|
|
{
|
|
"symbol": "ALPCPI",
|
|
"is_series_group": False,
|
|
"end_date": date(2023, 1, 1),
|
|
"limit": 10,
|
|
},
|
|
CREDENTIALS,
|
|
)
|
|
|
|
assert "limit" not in seen[0]
|
|
assert "end_date" not in seen[0]
|
|
|
|
|
|
class TestRegionalTransform:
|
|
"""Flattening the regional response into one row per region and date."""
|
|
|
|
async def test_every_region_is_returned_for_every_date(self, monkeypatch):
|
|
_serve(monkeypatch, REGIONAL)
|
|
response = await FredRegionalDataFetcher.fetch_data(
|
|
{"symbol": "ALPCPI", "is_series_group": False}, CREDENTIALS
|
|
)
|
|
|
|
assert [(r.date, r.region) for r in response.result] == [
|
|
(date(2022, 1, 1), "Alabama"),
|
|
(date(2022, 1, 1), "Alaska"),
|
|
(date(2023, 1, 1), "Alabama"),
|
|
(date(2023, 1, 1), "Alaska"),
|
|
]
|
|
|
|
async def test_each_row_carries_its_region_code_and_series(self, monkeypatch):
|
|
_serve(monkeypatch, REGIONAL)
|
|
response = await FredRegionalDataFetcher.fetch_data(
|
|
{"symbol": "ALPCPI", "is_series_group": False}, CREDENTIALS
|
|
)
|
|
first = response.result[0]
|
|
|
|
assert first.code == 1
|
|
assert first.series_id == "ALPCPI"
|
|
assert first.value == 50637
|
|
|
|
async def test_each_row_carries_the_units_of_the_response(self, monkeypatch):
|
|
_serve(monkeypatch, REGIONAL)
|
|
response = await FredRegionalDataFetcher.fetch_data(
|
|
{"symbol": "ALPCPI", "is_series_group": False}, CREDENTIALS
|
|
)
|
|
|
|
assert response.result[0].units == "Dollars"
|
|
|
|
async def test_the_metadata_describes_the_response_without_the_data(
|
|
self, monkeypatch
|
|
):
|
|
_serve(monkeypatch, REGIONAL)
|
|
response = await FredRegionalDataFetcher.fetch_data(
|
|
{"symbol": "ALPCPI", "is_series_group": False}, CREDENTIALS
|
|
)
|
|
|
|
assert response.metadata["title"] == "Per Capita Personal Income by State"
|
|
assert response.metadata["frequency"] == "Annual"
|
|
assert "data" not in response.metadata
|
|
|
|
async def test_observations_after_the_end_date_are_dropped(self, monkeypatch):
|
|
_serve(monkeypatch, REGIONAL)
|
|
response = await FredRegionalDataFetcher.fetch_data(
|
|
{
|
|
"symbol": "ALPCPI",
|
|
"is_series_group": False,
|
|
"end_date": date(2022, 6, 30),
|
|
},
|
|
CREDENTIALS,
|
|
)
|
|
|
|
assert {r.date for r in response.result} == {date(2022, 1, 1)}
|
|
|
|
async def test_a_response_describing_no_periods_returns_no_rows(self, monkeypatch):
|
|
_serve(monkeypatch, {"meta": {"units": "Dollars", "data": {}}})
|
|
response = await FredRegionalDataFetcher.fetch_data(
|
|
{"symbol": "ALPCPI", "is_series_group": False}, CREDENTIALS
|
|
)
|
|
|
|
assert response.result == []
|
|
assert response.metadata == {"units": "Dollars"}
|
|
|
|
async def test_a_response_carrying_nothing_is_reported(self, monkeypatch):
|
|
_serve(monkeypatch, {})
|
|
|
|
with pytest.raises(EmptyDataError):
|
|
await FredRegionalDataFetcher.fetch_data(
|
|
{"symbol": "ALPCPI", "is_series_group": False}, CREDENTIALS
|
|
)
|
|
|
|
async def test_a_response_with_an_empty_description_is_reported(self, monkeypatch):
|
|
_serve(monkeypatch, {"meta": None})
|
|
|
|
with pytest.raises(EmptyDataError):
|
|
await FredRegionalDataFetcher.fetch_data(
|
|
{"symbol": "ALPCPI", "is_series_group": False}, CREDENTIALS
|
|
)
|