mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-28 13:01:38 +00:00
* stash some changes * add more robust testing * mypy * point PR at V5 * introduce spec file * codespell * test fix * fix workflow environment setup * fix workflow environment setup * fix workflow environment setup * add pyyaml to dependencies * split lint jobs * fix workflow environment setup * fix workflow environment setup * workflow env setup * workflow env setup * clean up code comments * add auth hook entrypoints * codespell * add codegen feature * codespell * move _unpack into dispatchers for consistency with codegen packages * surface nested models in the response * fix missing coverage in CI * socrata updates * test fix * detect plotly output * add --include and --exclude flags from generate-extension command * refactor openbb-platform-api for V5 operations and spec file handling * delete poetry.lock * add test workflow * add temp branch id for test workflow * add httpx to dev dependencies * add a conftest.py * tweak conftest * tweak conftest * python 3.10 issues * more python 3.10 * more python 3.10 * more python 3.10 * more python 3.10 * fix url template resolution * multi-spec support * fix a windows path issue * merge branch develop * table builder, integration tests, and more * linting * merge branch develop * bit of cleanup * some more updates, cleanup, and apps.json * codespell * black * integration test params and linting * mypy * pylint * black * more pylint * recapture teest cassettes * black again * more linting * table presentation choices * black * fix merge conflict * split up metadata module and reformat cache to use msgpack * mypy * regenerate lock file * use json and include dependencies to make openbb-oecd a standalone install * pylint * more linting * serve metadata as dependency instead of assign and initialize * fix integration_tests_testers to not count dependency injections as paramters * add assets to git ignore * ruff * v5 refactor and publish workflow * test coverage and ty fixes * codespell * update lock file * workflow setup * ty check * workflow setup * Scope PR to OECD changes only Restore non-OECD files in the diff against v5 to their v5 state. The branch was originally created off feature/cli-2 and inherited 186 files of unrelated changes (cli, agent_server, mcp_server, platform_api, core/, .github/workflows/* not oecd). Those have nothing to do with the openbb-oecd v5 refactor and were polluting the PR diff. * scope cleanup: include leaked v5-resident files (general-linting workflow, platform_api test fixtures) --------- Co-authored-by: deeleeramone <> Co-authored-by: Copilot <copilot@github.com>
1239 lines
52 KiB
Python
1239 lines
52 KiB
Python
"""Unit tests targeting 100% coverage for openbb_oecd.models."""
|
|
|
|
from datetime import date
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from openbb_core.app.model.abstract.error import OpenBBError
|
|
from openbb_core.provider.utils.errors import EmptyDataError
|
|
|
|
from openbb_oecd.models.available_indicators import (
|
|
OecdAvailableIndicatorsFetcher,
|
|
OecdAvailableIndicatorsQueryParams,
|
|
_build_also_in,
|
|
)
|
|
from openbb_oecd.models.balance_of_payments import (
|
|
OECDBalanceOfPaymentsFetcher,
|
|
OECDBalanceOfPaymentsQueryParams,
|
|
_format_end_period,
|
|
_format_start_period,
|
|
)
|
|
from openbb_oecd.models.composite_leading_indicator import (
|
|
OECDCompositeLeadingIndicatorFetcher,
|
|
OECDCompositeLeadingIndicatorQueryParams,
|
|
)
|
|
from openbb_oecd.models.consumer_price_index import (
|
|
OECDCPIFetcher,
|
|
OECDCPIQueryParams,
|
|
)
|
|
from openbb_oecd.models.country_interest_rates import (
|
|
OecdCountryInterestRatesFetcher,
|
|
OecdCountryInterestRatesQueryParams,
|
|
)
|
|
from openbb_oecd.models.gdp_forecast import (
|
|
OECDGdpForecastFetcher,
|
|
OECDGdpForecastQueryParams,
|
|
)
|
|
from openbb_oecd.models.gdp_nominal import (
|
|
OECDGdpNominalFetcher,
|
|
OECDGdpNominalQueryParams,
|
|
)
|
|
from openbb_oecd.models.gdp_real import (
|
|
OECDGdpRealFetcher,
|
|
OECDGdpRealQueryParams,
|
|
)
|
|
from openbb_oecd.models.house_price_index import (
|
|
OECDHousePriceIndexFetcher,
|
|
OECDHousePriceIndexQueryParams,
|
|
)
|
|
from openbb_oecd.models.share_price_index import (
|
|
OECDSharePriceIndexFetcher,
|
|
OECDSharePriceIndexQueryParams,
|
|
)
|
|
from openbb_oecd.models.unemployment import (
|
|
OECDUnemploymentFetcher,
|
|
OECDUnemploymentQueryParams,
|
|
)
|
|
|
|
QB_PATH = "openbb_oecd.utils.query_builder.OecdQueryBuilder"
|
|
META_PATH = "openbb_oecd.utils.metadata.OecdMetadata"
|
|
|
|
|
|
def _mock_qb(records=None, raise_exc=None):
|
|
"""Build a MagicMock OecdQueryBuilder with configurable fetch_data behaviour."""
|
|
qb = MagicMock()
|
|
qb.metadata.resolve_country_codes.return_value = ["USA"]
|
|
if raise_exc is not None:
|
|
qb.fetch_data.side_effect = raise_exc
|
|
else:
|
|
qb.fetch_data.return_value = {"data": records if records is not None else []}
|
|
return qb
|
|
|
|
|
|
class TestAvailableIndicators:
|
|
"""Cover available_indicators.py missing branches."""
|
|
|
|
def test_build_also_in_empty_indicator(self):
|
|
"""Empty indicator returns empty list (line 25)."""
|
|
assert _build_also_in("", "DF", {}, {}, MagicMock()) == []
|
|
|
|
def test_build_also_in_unknown_indicator(self):
|
|
"""Indicator missing from code_to_dataflows returns empty list (line 25)."""
|
|
assert _build_also_in("CODE", "DF", {}, {}, MagicMock()) == []
|
|
|
|
def test_build_also_in_resolves_via_metadata(self):
|
|
"""Missing name in cache triggers _resolve_dataflow_id fallback (lines 39-45)."""
|
|
meta = MagicMock()
|
|
meta._resolve_dataflow_id.return_value = "DF_FULL"
|
|
code_to_dfs = {"IND": ["DF_OTHER"]}
|
|
df_name_cache = {"DF_FULL": "Other Flow"}
|
|
out = _build_also_in("IND", "DF_THIS", code_to_dfs, df_name_cache, meta)
|
|
assert out == ["DF_OTHER (Other Flow)"]
|
|
|
|
def test_build_also_in_resolve_raises(self):
|
|
"""metadata._resolve_dataflow_id raising is swallowed (lines 44-45)."""
|
|
meta = MagicMock()
|
|
meta._resolve_dataflow_id.side_effect = RuntimeError("nope")
|
|
code_to_dfs = {"IND": ["DF_OTHER"]}
|
|
out = _build_also_in("IND", "DF_THIS", code_to_dfs, {}, meta)
|
|
assert out == ["DF_OTHER"]
|
|
|
|
def test_build_also_in_truncates_after_ten(self):
|
|
"""More than 10 entries adds 'and N more' suffix."""
|
|
codes = [f"DF{i}" for i in range(12)]
|
|
df_name_cache = {c: f"Name{c}" for c in codes}
|
|
out = _build_also_in("IND", "DF_X", {"IND": codes}, df_name_cache, MagicMock())
|
|
assert out[-1] == "... and 2 more"
|
|
|
|
def test_enrich_results_missing_full_id_fallback(self):
|
|
"""full_id resolved from datastructures when short id missing (line 208)."""
|
|
meta = MagicMock()
|
|
meta._short_id_map = {}
|
|
meta.datastructures = {"DF_FULL": {"dimensions": []}}
|
|
meta._dataflow_constraints = {"DF_FULL": {}}
|
|
meta.codelists = {}
|
|
results = [{"dataflow_id": "DF_FULL", "indicator": "IND"}]
|
|
out = OecdAvailableIndicatorsFetcher._enrich_results(results, meta, {}, {})
|
|
assert out[0]["series_id"] == "DF_FULL::IND"
|
|
|
|
def test_enrich_results_skips_unresolved_dataflow(self):
|
|
"""Datastructure missing → loop skips this df (line 210)."""
|
|
meta = MagicMock()
|
|
meta._short_id_map = {}
|
|
meta.datastructures = {}
|
|
meta._dataflow_constraints = {}
|
|
meta.codelists = {}
|
|
results = [{"dataflow_id": "DF_X", "indicator": "IND"}]
|
|
out = OecdAvailableIndicatorsFetcher._enrich_results(results, meta, {}, {})
|
|
assert out[0]["frequencies"] == []
|
|
assert out[0]["transformations"] == []
|
|
|
|
def test_extract_data_dataflows_string_splits(self):
|
|
"""Comma-separated dataflows string is parsed into a list (line 262)."""
|
|
meta = MagicMock()
|
|
meta.search_indicators.return_value = []
|
|
meta.list_dataflows.return_value = []
|
|
with patch("openbb_oecd.utils.metadata.OecdMetadata", return_value=meta):
|
|
q = OecdAvailableIndicatorsQueryParams(dataflows="DF_KEI, DF_QNA")
|
|
OecdAvailableIndicatorsFetcher.extract_data(q, None)
|
|
args, kwargs = meta.search_indicators.call_args
|
|
assert kwargs["dataflows"] == ["DF_KEI", "DF_QNA"]
|
|
|
|
def test_extract_data_topic_intersects_with_dataflows(self):
|
|
"""Topic + dataflow intersection path (lines 267-274)."""
|
|
meta = MagicMock()
|
|
meta.list_dataflows.return_value = [
|
|
{"value": "AGENCY:DSD_KEI@DF_KEI"},
|
|
{"value": "AGENCY:DSD_QNA@DF_QNA"},
|
|
]
|
|
meta.search_indicators.return_value = []
|
|
with patch("openbb_oecd.utils.metadata.OecdMetadata", return_value=meta):
|
|
q = OecdAvailableIndicatorsQueryParams(topic="ECO", dataflows=["DF_KEI"])
|
|
OecdAvailableIndicatorsFetcher.extract_data(q, None)
|
|
_, kwargs = meta.search_indicators.call_args
|
|
assert kwargs["dataflows"] == ["AGENCY:DSD_KEI@DF_KEI"]
|
|
|
|
def test_extract_data_topic_no_intersection_keeps_explicit(self):
|
|
"""Empty intersection falls back to explicit dataflows (line 274)."""
|
|
meta = MagicMock()
|
|
meta.list_dataflows.return_value = [{"value": "AGENCY,DF_OTHER,1.0"}]
|
|
meta.search_indicators.return_value = []
|
|
with patch("openbb_oecd.utils.metadata.OecdMetadata", return_value=meta):
|
|
q = OecdAvailableIndicatorsQueryParams(topic="ECO", dataflows=["DF_KEI"])
|
|
OecdAvailableIndicatorsFetcher.extract_data(q, None)
|
|
_, kwargs = meta.search_indicators.call_args
|
|
assert kwargs["dataflows"] == ["DF_KEI"]
|
|
|
|
def test_extract_data_topic_without_dataflows(self):
|
|
"""Topic alone expands to its dataflows (line 276)."""
|
|
meta = MagicMock()
|
|
meta.list_dataflows.return_value = [{"value": "AGENCY:DSD_KEI@DF_KEI"}]
|
|
meta.search_indicators.return_value = []
|
|
with patch("openbb_oecd.utils.metadata.OecdMetadata", return_value=meta):
|
|
q = OecdAvailableIndicatorsQueryParams(topic="ECO")
|
|
OecdAvailableIndicatorsFetcher.extract_data(q, None)
|
|
_, kwargs = meta.search_indicators.call_args
|
|
assert kwargs["dataflows"] == ["AGENCY:DSD_KEI@DF_KEI"]
|
|
|
|
def test_extract_data_keywords_string_splits(self):
|
|
"""Comma-separated keywords string is parsed (line 281)."""
|
|
meta = MagicMock()
|
|
meta.search_indicators.return_value = []
|
|
with patch("openbb_oecd.utils.metadata.OecdMetadata", return_value=meta):
|
|
q = OecdAvailableIndicatorsQueryParams(keywords="gdp, growth")
|
|
OecdAvailableIndicatorsFetcher.extract_data(q, None)
|
|
_, kwargs = meta.search_indicators.call_args
|
|
assert kwargs["keywords"] == ["gdp", "growth"]
|
|
|
|
def test_extract_data_empty_results_returns_early(self):
|
|
"""Empty search_indicators short-circuits enrichment (line 290)."""
|
|
meta = MagicMock()
|
|
meta.search_indicators.return_value = []
|
|
with patch("openbb_oecd.utils.metadata.OecdMetadata", return_value=meta):
|
|
q = OecdAvailableIndicatorsQueryParams()
|
|
out = OecdAvailableIndicatorsFetcher.extract_data(q, None)
|
|
assert out == []
|
|
|
|
def test_transform_query_passthrough(self):
|
|
"""transform_query returns query params (line 152)."""
|
|
q = OecdAvailableIndicatorsFetcher.transform_query({"query": "gdp"})
|
|
assert q.query == "gdp"
|
|
|
|
def test_build_enrichment_indexes(self):
|
|
"""Reverse indexes built from cached dataflows (lines 162-185)."""
|
|
meta = MagicMock()
|
|
meta._dataflow_indicators_cache = {
|
|
"FULL_A": [
|
|
{"indicator": "GDP", "dataflow_id": "DF_A"},
|
|
{"indicator": "CPI", "dataflow_id": "DF_A"},
|
|
{"indicator": ""},
|
|
],
|
|
}
|
|
meta.dataflows = {
|
|
"FULL_A": {"short_id": "DF_A", "name": "Flow A"},
|
|
"FULL_B": {"short_id": "", "name": "Flow B"},
|
|
}
|
|
c2d, names = OecdAvailableIndicatorsFetcher._build_enrichment_indexes(meta)
|
|
assert c2d["GDP"] == ["DF_A"]
|
|
assert c2d["CPI"] == ["DF_A"]
|
|
assert names["DF_A"] == "Flow A"
|
|
assert names["FULL_B"] == "Flow B"
|
|
|
|
def test_enrich_results_with_constraints(self):
|
|
"""Frequency/transformation labels built from constraints (lines 225-227)."""
|
|
meta = MagicMock()
|
|
meta._short_id_map = {"DF_X": "FULL_X"}
|
|
meta.datastructures = {
|
|
"FULL_X": {
|
|
"dimensions": [
|
|
{"id": "FREQ", "codelist_id": "CL_FREQ"},
|
|
{"id": "TRANSFORMATION", "codelist_id": "CL_TR"},
|
|
]
|
|
}
|
|
}
|
|
meta._dataflow_constraints = {
|
|
"FULL_X": {"FREQ": ["A"], "TRANSFORMATION": ["G1"]}
|
|
}
|
|
meta.codelists = {"CL_FREQ": {"A": "Annual"}, "CL_TR": {"G1": "Growth"}}
|
|
results = [{"dataflow_id": "DF_X", "indicator": "GDP"}]
|
|
out = OecdAvailableIndicatorsFetcher._enrich_results(results, meta, {}, {})
|
|
assert out[0]["frequencies"] == ["A (Annual)"]
|
|
assert out[0]["transformations"] == ["G1 (Growth)"]
|
|
|
|
def test_extract_data_with_results_enriches(self):
|
|
"""Non-empty results trigger enrichment path (lines 293-297)."""
|
|
meta = MagicMock()
|
|
meta._dataflow_indicators_cache = {"FULL": [{"indicator": "GDP"}]}
|
|
meta.dataflows = {"FULL": {"short_id": "DF", "name": "Flow"}}
|
|
meta.search_indicators.return_value = [
|
|
{"dataflow_id": "DF", "indicator": "GDP"}
|
|
]
|
|
meta._short_id_map = {"DF": "FULL"}
|
|
meta.datastructures = {"FULL": {"dimensions": []}}
|
|
meta._dataflow_constraints = {"FULL": {}}
|
|
meta.codelists = {}
|
|
with patch("openbb_oecd.utils.metadata.OecdMetadata", return_value=meta):
|
|
q = OecdAvailableIndicatorsQueryParams()
|
|
out = OecdAvailableIndicatorsFetcher.extract_data(q, None)
|
|
assert out and out[0]["series_id"] == "DF::GDP"
|
|
|
|
def test_transform_data(self):
|
|
"""transform_data validates each row (line 308)."""
|
|
data = [
|
|
{
|
|
"series_id": "DF::IND",
|
|
"indicator": "IND",
|
|
"dataflow_id": "DF",
|
|
}
|
|
]
|
|
out = OecdAvailableIndicatorsFetcher.transform_data(
|
|
OecdAvailableIndicatorsQueryParams(), data
|
|
)
|
|
assert len(out) == 1
|
|
assert out[0].symbol == "DF::IND"
|
|
|
|
|
|
class TestBalanceOfPayments:
|
|
"""Cover balance_of_payments.py missing branches."""
|
|
|
|
def test_format_start_period_quarterly(self):
|
|
"""Quarterly start formats year-Qn (line 63-64)."""
|
|
assert _format_start_period(date(2024, 5, 1), "Q") == "2024-Q2"
|
|
|
|
def test_format_start_period_monthly(self):
|
|
"""Monthly start formats year-MM (line 65)."""
|
|
assert _format_start_period(date(2024, 3, 15), "M") == "2024-03"
|
|
|
|
def test_format_end_period_quarterly(self):
|
|
"""Quarterly end formats year-Qn (line 72-73)."""
|
|
assert _format_end_period(date(2024, 8, 1), "Q") == "2024-Q3"
|
|
|
|
def test_format_end_period_monthly(self):
|
|
"""Monthly end formats year-MM (line 74)."""
|
|
assert _format_end_period(date(2024, 11, 1), "M") == "2024-11"
|
|
|
|
def test_validator_normalizes_country(self):
|
|
"""Spaces and case normalized in the validator."""
|
|
q = OECDBalanceOfPaymentsQueryParams(country="United States")
|
|
assert q.country == "united_states"
|
|
|
|
def test_extract_data_non_200_raises(self):
|
|
"""HTTP non-200 raises OpenBBError (line 179)."""
|
|
resp = MagicMock(status_code=500, reason="Server", text="")
|
|
with (
|
|
patch("openbb_core.provider.utils.helpers.make_request", return_value=resp),
|
|
patch("openbb_oecd.utils.metadata.OecdMetadata") as MetaCls,
|
|
):
|
|
MetaCls.return_value.resolve_country_codes.return_value = ["USA"]
|
|
with pytest.raises(OpenBBError, match="BOP request failed"):
|
|
OECDBalanceOfPaymentsFetcher.extract_data(
|
|
OECDBalanceOfPaymentsQueryParams(
|
|
country="united_states",
|
|
start_date=date(2020, 1, 1),
|
|
end_date=date(2021, 1, 1),
|
|
),
|
|
None,
|
|
)
|
|
|
|
def test_extract_data_empty_text_raises(self):
|
|
"""Empty body raises OpenBBError wrapping EmptyDataError (line 186)."""
|
|
resp = MagicMock(status_code=200, reason="OK", text=" ")
|
|
with (
|
|
patch("openbb_core.provider.utils.helpers.make_request", return_value=resp),
|
|
patch("openbb_oecd.utils.metadata.OecdMetadata") as MetaCls,
|
|
):
|
|
MetaCls.return_value.resolve_country_codes.return_value = ["USA"]
|
|
with pytest.raises(OpenBBError):
|
|
OECDBalanceOfPaymentsFetcher.extract_data(
|
|
OECDBalanceOfPaymentsQueryParams(), None
|
|
)
|
|
|
|
def test_extract_data_csv_parse_error(self):
|
|
"""read_csv failure raises OpenBBError (lines 192-193)."""
|
|
resp = MagicMock(status_code=200, reason="OK", text="rows here")
|
|
with (
|
|
patch("openbb_core.provider.utils.helpers.make_request", return_value=resp),
|
|
patch("openbb_oecd.utils.metadata.OecdMetadata") as MetaCls,
|
|
patch("pandas.read_csv", side_effect=ValueError("bad csv")),
|
|
):
|
|
MetaCls.return_value.resolve_country_codes.return_value = ["USA"]
|
|
with pytest.raises(OpenBBError, match="Failed to parse"):
|
|
OECDBalanceOfPaymentsFetcher.extract_data(
|
|
OECDBalanceOfPaymentsQueryParams(), None
|
|
)
|
|
|
|
def test_extract_data_empty_dataframe(self):
|
|
"""Empty parsed DataFrame raises OpenBBError (line 198)."""
|
|
from pandas import DataFrame
|
|
|
|
resp = MagicMock(status_code=200, reason="OK", text="A,B\n")
|
|
with (
|
|
patch("openbb_core.provider.utils.helpers.make_request", return_value=resp),
|
|
patch("openbb_oecd.utils.metadata.OecdMetadata") as MetaCls,
|
|
patch("pandas.read_csv", return_value=DataFrame()),
|
|
):
|
|
MetaCls.return_value.resolve_country_codes.return_value = ["USA"]
|
|
with pytest.raises(OpenBBError, match="No BOP data rows"):
|
|
OECDBalanceOfPaymentsFetcher.extract_data(
|
|
OECDBalanceOfPaymentsQueryParams(), None
|
|
)
|
|
|
|
def test_extract_data_empty_string_column(self):
|
|
"""All-null string column triggers sample.empty branch (line 225)."""
|
|
from pandas import DataFrame
|
|
|
|
df = DataFrame(
|
|
{
|
|
"MEASURE": ["CA"],
|
|
"ACCOUNTING_ENTRY": ["B"],
|
|
"UNIT_MEASURE": ["USD_EXC"],
|
|
"TIME_PERIOD": ["2020"],
|
|
"OBS_VALUE": [1.0],
|
|
"EMPTY_COL": [None],
|
|
"REF_AREA": ["USA"],
|
|
}
|
|
)
|
|
df["EMPTY_COL"] = df["EMPTY_COL"].astype("string")
|
|
resp = MagicMock(status_code=200, reason="OK", text="ignored")
|
|
with (
|
|
patch("openbb_core.provider.utils.helpers.make_request", return_value=resp),
|
|
patch("openbb_oecd.utils.metadata.OecdMetadata") as MetaCls,
|
|
patch("pandas.read_csv", return_value=df),
|
|
):
|
|
MetaCls.return_value.resolve_country_codes.return_value = ["USA"]
|
|
out = OECDBalanceOfPaymentsFetcher.extract_data(
|
|
OECDBalanceOfPaymentsQueryParams(), None
|
|
)
|
|
assert out and out[0]["OBS_VALUE"] == 1.0
|
|
|
|
def test_extract_data_no_colon_in_string_column(self):
|
|
"""String column without ': ' assigns label = code (line 234)."""
|
|
from pandas import DataFrame
|
|
|
|
df = DataFrame(
|
|
{
|
|
"MEASURE": ["CA"],
|
|
"ACCOUNTING_ENTRY": ["B"],
|
|
"UNIT_MEASURE": ["USD_EXC"],
|
|
"TIME_PERIOD": ["2020"],
|
|
"OBS_VALUE": [1.0],
|
|
"REF_AREA": ["USA"],
|
|
"TAG": ["plain_text"],
|
|
}
|
|
)
|
|
resp = MagicMock(status_code=200, reason="OK", text="ignored")
|
|
with (
|
|
patch("openbb_core.provider.utils.helpers.make_request", return_value=resp),
|
|
patch("openbb_oecd.utils.metadata.OecdMetadata") as MetaCls,
|
|
patch("pandas.read_csv", return_value=df),
|
|
):
|
|
MetaCls.return_value.resolve_country_codes.return_value = ["USA"]
|
|
out = OECDBalanceOfPaymentsFetcher.extract_data(
|
|
OECDBalanceOfPaymentsQueryParams(), None
|
|
)
|
|
assert out and out[0].get("OBS_VALUE") == 1.0
|
|
|
|
def test_transform_data_skips_missing_value(self):
|
|
"""Row with empty OBS_VALUE skipped (line 257)."""
|
|
rows = [{"OBS_VALUE": None, "TIME_PERIOD": "2020"}]
|
|
q = OECDBalanceOfPaymentsQueryParams()
|
|
out = OECDBalanceOfPaymentsFetcher.transform_data(q, rows)
|
|
assert out == []
|
|
|
|
def test_transform_data_skips_invalid_date(self):
|
|
"""Row with unparsable TIME_PERIOD skipped (line 262)."""
|
|
rows = [{"OBS_VALUE": 1.0, "TIME_PERIOD": "not-a-date"}]
|
|
q = OECDBalanceOfPaymentsQueryParams()
|
|
out = OECDBalanceOfPaymentsFetcher.transform_data(q, rows)
|
|
assert out == []
|
|
|
|
def test_transform_data_skips_before_start(self):
|
|
"""Row before start_date skipped (line 265)."""
|
|
rows = [
|
|
{
|
|
"OBS_VALUE": 1.0,
|
|
"TIME_PERIOD": "2010",
|
|
"MEASURE": "CA",
|
|
"ACCOUNTING_ENTRY": "B",
|
|
"UNIT_MEASURE": "USD_EXC",
|
|
"REF_AREA": "USA",
|
|
}
|
|
]
|
|
q = OECDBalanceOfPaymentsQueryParams(start_date=date(2020, 1, 1))
|
|
out = OECDBalanceOfPaymentsFetcher.transform_data(q, rows)
|
|
assert out == []
|
|
|
|
def test_transform_data_skips_after_end(self):
|
|
"""Row after end_date skipped (line 268)."""
|
|
rows = [
|
|
{
|
|
"OBS_VALUE": 1.0,
|
|
"TIME_PERIOD": "2030",
|
|
"MEASURE": "CA",
|
|
"ACCOUNTING_ENTRY": "B",
|
|
"UNIT_MEASURE": "USD_EXC",
|
|
"REF_AREA": "USA",
|
|
}
|
|
]
|
|
q = OECDBalanceOfPaymentsQueryParams(end_date=date(2020, 1, 1))
|
|
out = OECDBalanceOfPaymentsFetcher.transform_data(q, rows)
|
|
assert out == []
|
|
|
|
def test_transform_data_unmapped_column_skipped(self):
|
|
"""Row with unmapped MEASURE/ENTRY/UNIT combo skipped (line 276)."""
|
|
rows = [
|
|
{
|
|
"OBS_VALUE": 1.0,
|
|
"TIME_PERIOD": "2020",
|
|
"MEASURE": "ZZZ",
|
|
"ACCOUNTING_ENTRY": "B",
|
|
"UNIT_MEASURE": "USD_EXC",
|
|
"REF_AREA": "USA",
|
|
}
|
|
]
|
|
q = OECDBalanceOfPaymentsQueryParams()
|
|
out = OECDBalanceOfPaymentsFetcher.transform_data(q, rows)
|
|
assert out == []
|
|
|
|
def test_transform_data_happy_path(self):
|
|
"""Valid row maps into BOP data object."""
|
|
rows = [
|
|
{
|
|
"OBS_VALUE": 12.5,
|
|
"TIME_PERIOD": "2020",
|
|
"MEASURE": "CA",
|
|
"ACCOUNTING_ENTRY": "B",
|
|
"UNIT_MEASURE": "USD_EXC",
|
|
"REF_AREA": "USA",
|
|
"REF_AREA_label": "United States",
|
|
}
|
|
]
|
|
q = OECDBalanceOfPaymentsQueryParams()
|
|
out = OECDBalanceOfPaymentsFetcher.transform_data(q, rows)
|
|
assert len(out) == 1
|
|
assert out[0].balance_total == 12.5
|
|
|
|
def test_format_start_period_annual(self):
|
|
"""Annual start formats year only (line 62)."""
|
|
assert _format_start_period(date(2024, 6, 1), "A") == "2024"
|
|
|
|
def test_format_end_period_annual(self):
|
|
"""Annual end formats year only (line 71)."""
|
|
assert _format_end_period(date(2024, 6, 1), "A") == "2024"
|
|
|
|
def test_transform_query_returns_params(self):
|
|
"""transform_query returns query (line 130)."""
|
|
q = OECDBalanceOfPaymentsFetcher.transform_query({"country": "united_states"})
|
|
assert q.country == "united_states"
|
|
|
|
def test_extract_data_with_dates_and_colon_columns(self):
|
|
"""Covers dim_filter with dates, colon-split labels, and percent path (lines 161-167, 205, 228-234, 286)."""
|
|
from pandas import DataFrame
|
|
|
|
df = DataFrame(
|
|
{
|
|
"MEASURE: Measure": ["CA: Current account", "S: Services"],
|
|
"ACCOUNTING_ENTRY: Entry": ["B: Balance", "B: Balance"],
|
|
"UNIT_MEASURE: Unit": ["PT_B1GQ: Percent", "USD_EXC: USD"],
|
|
"TIME_PERIOD": ["2020", "2020"],
|
|
"OBS_VALUE": [2.5, 100.0],
|
|
"REF_AREA: Area": ["USA: United States", "USA: United States"],
|
|
}
|
|
)
|
|
resp = MagicMock(status_code=200, reason="OK", text="ignored")
|
|
with (
|
|
patch("openbb_core.provider.utils.helpers.make_request", return_value=resp),
|
|
patch("openbb_oecd.utils.metadata.OecdMetadata") as MetaCls,
|
|
patch("pandas.read_csv", return_value=df),
|
|
):
|
|
MetaCls.return_value.resolve_country_codes.return_value = ["USA"]
|
|
q = OECDBalanceOfPaymentsQueryParams(
|
|
country="united_states",
|
|
frequency="annual",
|
|
start_date=date(2020, 1, 1),
|
|
end_date=date(2020, 12, 31),
|
|
)
|
|
rows = OECDBalanceOfPaymentsFetcher.extract_data(q, None)
|
|
out = OECDBalanceOfPaymentsFetcher.transform_data(q, rows)
|
|
assert out
|
|
balance_pct = [r for r in out if r.balance_percent_of_gdp is not None]
|
|
assert balance_pct and balance_pct[0].balance_percent_of_gdp == 0.025
|
|
|
|
|
|
class TestCompositeLeadingIndicator:
|
|
"""Cover composite_leading_indicator.py missing branches."""
|
|
|
|
def test_validator_none_returns_g20(self):
|
|
"""None country normalizes to g20 (line 50)."""
|
|
q = OECDCompositeLeadingIndicatorQueryParams(country=None)
|
|
assert q.country == "g20"
|
|
|
|
def test_validator_list_input(self):
|
|
"""List input bypasses split branch (lines 54-55)."""
|
|
q = OECDCompositeLeadingIndicatorQueryParams(country=["g20"])
|
|
assert q.country == "g20"
|
|
|
|
def test_validator_all_returns_all(self):
|
|
"""Token 'all' short-circuits (line 57)."""
|
|
q = OECDCompositeLeadingIndicatorQueryParams(country="all,g20")
|
|
assert q.country == "all"
|
|
|
|
def test_validator_unknown_country_warns_and_skips(self):
|
|
"""Unknown country warned and dropped (line 60)."""
|
|
with pytest.warns(UserWarning, match="not supported"):
|
|
q = OECDCompositeLeadingIndicatorQueryParams(country="g20,atlantis")
|
|
assert q.country == "g20"
|
|
|
|
def test_validator_no_valid_raises(self):
|
|
"""All-invalid input raises (line 64)."""
|
|
with (
|
|
pytest.warns(UserWarning, match="not supported"),
|
|
pytest.raises(OpenBBError, match="No valid countries"),
|
|
):
|
|
OECDCompositeLeadingIndicatorQueryParams(country="atlantis")
|
|
|
|
def test_transform_query_fills_defaults(self):
|
|
"""None dates and country filled in (lines 88, 95, 98)."""
|
|
q = OECDCompositeLeadingIndicatorFetcher.transform_query(
|
|
{"start_date": None, "end_date": None, "country": None}
|
|
)
|
|
assert q.start_date == date(1947, 1, 1)
|
|
assert q.end_date.year == date.today().year
|
|
assert q.country == "g20"
|
|
|
|
def test_transform_query_all_uses_recent_start(self):
|
|
"""country='all' yields 2020 start (line 88)."""
|
|
q = OECDCompositeLeadingIndicatorFetcher.transform_query(
|
|
{"start_date": None, "country": "all"}
|
|
)
|
|
assert q.start_date == date(2020, 1, 1)
|
|
|
|
def test_extract_data_growth_clears_adjustment(self):
|
|
"""growth_rate=True wipes adjustment (line 116)."""
|
|
qb = _mock_qb(records=[{"TIME_PERIOD": "2020", "OBS_VALUE": 1.0}])
|
|
with patch(
|
|
"openbb_oecd.utils.query_builder.OecdQueryBuilder",
|
|
return_value=qb,
|
|
):
|
|
q = OECDCompositeLeadingIndicatorQueryParams(
|
|
country="g20", growth_rate=True
|
|
)
|
|
OECDCompositeLeadingIndicatorFetcher.extract_data(q, None)
|
|
kwargs = qb.fetch_data.call_args.kwargs
|
|
assert kwargs["ADJUSTMENT"] == ""
|
|
assert kwargs["TRANSFORMATION"] == "GY"
|
|
|
|
def test_extract_data_wraps_exception(self):
|
|
"""fetch_data exceptions wrap as OpenBBError (lines 134-135)."""
|
|
qb = _mock_qb(raise_exc=RuntimeError("boom"))
|
|
with patch(
|
|
"openbb_oecd.utils.query_builder.OecdQueryBuilder",
|
|
return_value=qb,
|
|
):
|
|
with pytest.raises(OpenBBError, match="boom"):
|
|
OECDCompositeLeadingIndicatorFetcher.extract_data(
|
|
OECDCompositeLeadingIndicatorQueryParams(country="g20"), None
|
|
)
|
|
|
|
def test_extract_data_empty_raises(self):
|
|
"""Empty records raise OpenBBError (line 140)."""
|
|
qb = _mock_qb(records=[])
|
|
with patch(
|
|
"openbb_oecd.utils.query_builder.OecdQueryBuilder",
|
|
return_value=qb,
|
|
):
|
|
with pytest.raises(OpenBBError, match="No data"):
|
|
OECDCompositeLeadingIndicatorFetcher.extract_data(
|
|
OECDCompositeLeadingIndicatorQueryParams(country="g20"), None
|
|
)
|
|
|
|
def test_transform_data_skips_bad_date(self):
|
|
"""Bad TIME_PERIOD skipped (line 160)."""
|
|
q = OECDCompositeLeadingIndicatorQueryParams(country="g20")
|
|
out = OECDCompositeLeadingIndicatorFetcher.transform_data(
|
|
q, [{"TIME_PERIOD": "bad", "OBS_VALUE": 1.0}]
|
|
)
|
|
assert out == []
|
|
|
|
def test_transform_data_skips_missing_value(self):
|
|
"""Missing OBS_VALUE skipped (line 165)."""
|
|
q = OECDCompositeLeadingIndicatorQueryParams(country="g20")
|
|
out = OECDCompositeLeadingIndicatorFetcher.transform_data(
|
|
q, [{"TIME_PERIOD": "2020", "OBS_VALUE": None}]
|
|
)
|
|
assert out == []
|
|
|
|
def test_transform_data_growth_scales(self):
|
|
"""growth_rate=True divides value by 100 (line 170)."""
|
|
q = OECDCompositeLeadingIndicatorQueryParams(country="g20", growth_rate=True)
|
|
out = OECDCompositeLeadingIndicatorFetcher.transform_data(
|
|
q, [{"TIME_PERIOD": "2020", "OBS_VALUE": 50.0, "REF_AREA": "USA"}]
|
|
)
|
|
assert out[0].value == 0.5
|
|
|
|
|
|
class TestConsumerPriceIndex:
|
|
"""Cover consumer_price_index.py missing branches."""
|
|
|
|
def test_validate_expenditure_string(self):
|
|
"""Expenditure parsed and lowercased (line 150)."""
|
|
q = OECDCPIQueryParams(country="united_states", expenditure="Total, Energy")
|
|
assert q.expenditure == "total,energy"
|
|
|
|
def test_validate_expenditure_unknown_raises(self):
|
|
"""Unknown expenditure raises (lines 152-155)."""
|
|
with pytest.raises(ValueError, match="not a valid choice"):
|
|
OECDCPIQueryParams(country="united_states", expenditure="nonsense")
|
|
|
|
def test_validate_expenditure_all(self):
|
|
"""'all' keyword accepted (line 156)."""
|
|
q = OECDCPIQueryParams(country="united_states", expenditure="all")
|
|
assert q.expenditure == "all"
|
|
|
|
def test_transform_query_defaults(self):
|
|
"""transform_query fills nulls (lines 184, 186, 188)."""
|
|
q = OECDCPIFetcher.transform_query(
|
|
{"start_date": None, "end_date": None, "country": None}
|
|
)
|
|
assert q.start_date == date(1950, 1, 1)
|
|
assert q.end_date.year == date.today().year
|
|
assert q.country == "united_states"
|
|
|
|
def test_extract_data_harmonized_quarter_becomes_monthly(self):
|
|
"""harmonized+quarter forced to monthly (line 209)."""
|
|
qb = _mock_qb(records=[{"TIME_PERIOD": "2020", "OBS_VALUE": 1.0}])
|
|
with patch(
|
|
"openbb_oecd.utils.query_builder.OecdQueryBuilder",
|
|
return_value=qb,
|
|
):
|
|
q = OECDCPIQueryParams(
|
|
country="united_states", harmonized=True, frequency="quarter"
|
|
)
|
|
OECDCPIFetcher.extract_data(q, None)
|
|
assert qb.fetch_data.call_args.kwargs["FREQ"] == "M"
|
|
|
|
def test_extract_data_expenditure_all_clears_code(self):
|
|
"""'all' expenditure clears the code list (line 215)."""
|
|
qb = _mock_qb(records=[{"TIME_PERIOD": "2020", "OBS_VALUE": 1.0}])
|
|
with patch(
|
|
"openbb_oecd.utils.query_builder.OecdQueryBuilder",
|
|
return_value=qb,
|
|
):
|
|
q = OECDCPIQueryParams(country="united_states", expenditure="all")
|
|
OECDCPIFetcher.extract_data(q, None)
|
|
assert qb.fetch_data.call_args.kwargs["EXPENDITURE"] == ""
|
|
|
|
def test_extract_data_wraps_exception(self):
|
|
"""Exceptions wrap as OpenBBError (lines 233-234)."""
|
|
qb = _mock_qb(raise_exc=RuntimeError("bad"))
|
|
with patch(
|
|
"openbb_oecd.utils.query_builder.OecdQueryBuilder",
|
|
return_value=qb,
|
|
):
|
|
with pytest.raises(OpenBBError, match="No data"):
|
|
OECDCPIFetcher.extract_data(
|
|
OECDCPIQueryParams(country="united_states"), None
|
|
)
|
|
|
|
def test_extract_data_empty_raises(self):
|
|
"""Empty records raise (line 239)."""
|
|
qb = _mock_qb(records=[])
|
|
with patch(
|
|
"openbb_oecd.utils.query_builder.OecdQueryBuilder",
|
|
return_value=qb,
|
|
):
|
|
with pytest.raises(OpenBBError):
|
|
OECDCPIFetcher.extract_data(
|
|
OECDCPIQueryParams(country="united_states"), None
|
|
)
|
|
|
|
def test_transform_data_filters(self):
|
|
"""Bad date/range/value rows skipped (lines 260, 263, 266, 271)."""
|
|
q = OECDCPIQueryParams(
|
|
country="united_states",
|
|
start_date=date(2020, 1, 1),
|
|
end_date=date(2020, 12, 31),
|
|
transform="index",
|
|
)
|
|
rows = [
|
|
{"TIME_PERIOD": "bad", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2010", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2030", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2020", "OBS_VALUE": None},
|
|
{
|
|
"TIME_PERIOD": "2020",
|
|
"OBS_VALUE": 100.0,
|
|
"REF_AREA": "USA",
|
|
"EXPENDITURE": "_T",
|
|
"FREQ": "A",
|
|
"TRANSFORMATION_label": "Not applicable",
|
|
},
|
|
]
|
|
out = OECDCPIFetcher.transform_data(q, rows)
|
|
assert len(out) == 1
|
|
assert out[0].value == 100.0
|
|
|
|
def test_transform_data_yoy_scales_and_appends_transform(self):
|
|
"""YoY value scales by 100 and transform label appended (lines 276, 291)."""
|
|
q = OECDCPIQueryParams(country="united_states", transform="yoy")
|
|
rows = [
|
|
{
|
|
"TIME_PERIOD": "2020",
|
|
"OBS_VALUE": 50.0,
|
|
"REF_AREA": "USA",
|
|
"EXPENDITURE": "_T",
|
|
"TRANSFORMATION_label": "Year-on-year growth",
|
|
}
|
|
]
|
|
out = OECDCPIFetcher.transform_data(q, rows)
|
|
assert out[0].value == 0.5
|
|
assert "Year-on-year growth" in out[0].title
|
|
|
|
|
|
class TestCountryInterestRates:
|
|
"""Cover country_interest_rates.py missing branches."""
|
|
|
|
def test_transform_query_all_uses_2020(self):
|
|
"""country=all uses 2020 start (line 72)."""
|
|
q = OecdCountryInterestRatesFetcher.transform_query(
|
|
{"start_date": None, "country": "all"}
|
|
)
|
|
assert q.start_date == date(2020, 1, 1)
|
|
|
|
def test_transform_query_default_end_and_country(self):
|
|
"""None end and country defaulted (lines 78, 80)."""
|
|
q = OecdCountryInterestRatesFetcher.transform_query(
|
|
{"start_date": None, "end_date": None, "country": None}
|
|
)
|
|
assert q.end_date.year == date.today().year
|
|
assert q.country == "united_states"
|
|
|
|
def test_extract_data_wraps_exception(self):
|
|
"""Exception wrapped (lines 115-116)."""
|
|
qb = _mock_qb(raise_exc=RuntimeError("oops"))
|
|
with patch(
|
|
"openbb_oecd.utils.query_builder.OecdQueryBuilder",
|
|
return_value=qb,
|
|
):
|
|
with pytest.raises(OpenBBError, match="oops"):
|
|
OecdCountryInterestRatesFetcher.extract_data(
|
|
OecdCountryInterestRatesQueryParams(country="united_states"), None
|
|
)
|
|
|
|
def test_extract_data_empty_raises(self):
|
|
"""Empty records raise EmptyDataError (line 121)."""
|
|
qb = _mock_qb(records=[])
|
|
with patch(
|
|
"openbb_oecd.utils.query_builder.OecdQueryBuilder",
|
|
return_value=qb,
|
|
):
|
|
with pytest.raises(EmptyDataError):
|
|
OecdCountryInterestRatesFetcher.extract_data(
|
|
OecdCountryInterestRatesQueryParams(country="united_states"), None
|
|
)
|
|
|
|
def test_extract_data_returns_records(self):
|
|
"""Non-empty rows returned (line 123)."""
|
|
rows = [{"TIME_PERIOD": "2020", "OBS_VALUE": 1.0}]
|
|
qb = _mock_qb(records=rows)
|
|
with patch(
|
|
"openbb_oecd.utils.query_builder.OecdQueryBuilder",
|
|
return_value=qb,
|
|
):
|
|
out = OecdCountryInterestRatesFetcher.extract_data(
|
|
OecdCountryInterestRatesQueryParams(country="united_states"), None
|
|
)
|
|
assert out == rows
|
|
|
|
def test_transform_data_filters(self):
|
|
"""Bad date/missing value rows skipped (lines 140, 145)."""
|
|
q = OecdCountryInterestRatesQueryParams(country="united_states")
|
|
rows = [
|
|
{"TIME_PERIOD": "bad", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2020", "OBS_VALUE": None},
|
|
{"TIME_PERIOD": "2020", "OBS_VALUE": 5.0, "REF_AREA": "USA"},
|
|
]
|
|
out = OecdCountryInterestRatesFetcher.transform_data(q, rows)
|
|
assert len(out) == 1
|
|
assert out[0].value == 0.05
|
|
|
|
|
|
class TestGdpForecast:
|
|
"""Cover gdp_forecast.py missing branches."""
|
|
|
|
def test_transform_query_defaults(self):
|
|
"""Missing country/dates filled in (lines 76, 79, 84)."""
|
|
q = OECDGdpForecastFetcher.transform_query(
|
|
{"country": None, "start_date": None, "end_date": None}
|
|
)
|
|
assert q.country == "all"
|
|
assert q.start_date.year == date.today().year
|
|
assert q.end_date.year == date.today().year + 2
|
|
|
|
def test_extract_data_capita_quarter_warns(self):
|
|
"""capita+quarter combo forces annual + warns (lines 104-107)."""
|
|
qb = _mock_qb(records=[{"TIME_PERIOD": "2025", "OBS_VALUE": 1.0}])
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
q = OECDGdpForecastQueryParams(
|
|
country="all", units="capita", frequency="quarter"
|
|
)
|
|
with pytest.warns(UserWarning, match="not available"):
|
|
OECDGdpForecastFetcher.extract_data(q, None)
|
|
assert qb.fetch_data.call_args.kwargs["FREQ"] == "A"
|
|
|
|
def test_extract_data_wraps_exception(self):
|
|
"""Exception wrapped (lines 122-123)."""
|
|
qb = _mock_qb(raise_exc=RuntimeError("nope"))
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
with pytest.raises(OpenBBError, match="nope"):
|
|
OECDGdpForecastFetcher.extract_data(
|
|
OECDGdpForecastQueryParams(country="all"), None
|
|
)
|
|
|
|
def test_extract_data_empty_raises(self):
|
|
"""Empty records → EmptyDataError (line 127)."""
|
|
qb = _mock_qb(records=[])
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
with pytest.raises(EmptyDataError):
|
|
OECDGdpForecastFetcher.extract_data(
|
|
OECDGdpForecastQueryParams(country="all"), None
|
|
)
|
|
|
|
def test_transform_data_filters_and_scales(self):
|
|
"""Bad date / missing value / growth scaling / non-positive skip (lines 144, 147, 150, 154)."""
|
|
q = OECDGdpForecastQueryParams(country="all", units="growth")
|
|
rows = [
|
|
{"TIME_PERIOD": "bad", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2025", "OBS_VALUE": None},
|
|
{"TIME_PERIOD": "2025", "OBS_VALUE": -1.0, "REF_AREA": "USA"},
|
|
{"TIME_PERIOD": "2025", "OBS_VALUE": 5.0, "REF_AREA": "USA"},
|
|
]
|
|
out = OECDGdpForecastFetcher.transform_data(q, rows)
|
|
assert len(out) == 1
|
|
assert out[0].value == 0.05
|
|
|
|
def test_transform_data_int_cast_path(self):
|
|
"""units='current_prices' triggers int cast branch."""
|
|
q = OECDGdpForecastQueryParams(country="all", units="current_prices")
|
|
rows = [{"TIME_PERIOD": "2025", "OBS_VALUE": 1.5, "REF_AREA": "USA"}]
|
|
out = OECDGdpForecastFetcher.transform_data(q, rows)
|
|
assert out[0].value == 1
|
|
|
|
|
|
class TestGdpNominal:
|
|
"""Cover gdp_nominal.py missing branches."""
|
|
|
|
def test_transform_query_all_recent_start(self):
|
|
"""country=all yields 2020 start (line 84)."""
|
|
q = OECDGdpNominalFetcher.transform_query(
|
|
{"start_date": None, "country": "all"}
|
|
)
|
|
assert q.start_date == date(2020, 1, 1)
|
|
|
|
def test_transform_query_default_country(self):
|
|
"""None country default (lines 90, 92)."""
|
|
q = OECDGdpNominalFetcher.transform_query(
|
|
{"start_date": None, "end_date": None, "country": None}
|
|
)
|
|
assert q.country == "united_states"
|
|
assert q.end_date.year == date.today().year
|
|
|
|
def test_extract_data_index_volume_uses_dr(self):
|
|
"""units='index' + price_base 'current_prices' flips to DR (line 113)."""
|
|
qb = _mock_qb(records=[{"TIME_PERIOD": "2020", "OBS_VALUE": 1.0}])
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
q = OECDGdpNominalQueryParams(
|
|
country="united_states", units="index", price_base="current_prices"
|
|
)
|
|
OECDGdpNominalFetcher.extract_data(q, None)
|
|
assert qb.fetch_data.call_args.kwargs["PRICE_BASE"] == "DR"
|
|
|
|
def test_extract_data_wraps_exception(self):
|
|
"""Exception wrapped (lines 132-133)."""
|
|
qb = _mock_qb(raise_exc=RuntimeError("ugh"))
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
with pytest.raises(OpenBBError, match="ugh"):
|
|
OECDGdpNominalFetcher.extract_data(
|
|
OECDGdpNominalQueryParams(country="united_states"), None
|
|
)
|
|
|
|
def test_extract_data_empty_raises(self):
|
|
"""Empty records → EmptyDataError (line 137)."""
|
|
qb = _mock_qb(records=[])
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
with pytest.raises(EmptyDataError):
|
|
OECDGdpNominalFetcher.extract_data(
|
|
OECDGdpNominalQueryParams(country="united_states"), None
|
|
)
|
|
|
|
def test_transform_data_filters(self):
|
|
"""Date/range/value filters (lines 155, 157, 159, 162)."""
|
|
q = OECDGdpNominalQueryParams(
|
|
country="united_states",
|
|
start_date=date(2020, 1, 1),
|
|
end_date=date(2020, 12, 31),
|
|
units="level",
|
|
)
|
|
rows = [
|
|
{"TIME_PERIOD": "bad", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2010", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2030", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2020", "OBS_VALUE": None},
|
|
{"TIME_PERIOD": "2020", "OBS_VALUE": 1.0, "REF_AREA": "USA"},
|
|
]
|
|
out = OECDGdpNominalFetcher.transform_data(q, rows)
|
|
assert len(out) == 1
|
|
|
|
|
|
class TestGdpReal:
|
|
"""Cover gdp_real.py missing branches."""
|
|
|
|
def test_transform_query_all_start(self):
|
|
"""country=all → 2020 start (line 64)."""
|
|
q = OECDGdpRealFetcher.transform_query({"start_date": None, "country": "all"})
|
|
assert q.start_date == date(2020, 1, 1)
|
|
|
|
def test_transform_query_default_country(self):
|
|
"""None country default (lines 70, 72)."""
|
|
q = OECDGdpRealFetcher.transform_query(
|
|
{"start_date": None, "end_date": None, "country": None}
|
|
)
|
|
assert q.country == "united_states"
|
|
|
|
def test_extract_data_wraps_exception(self):
|
|
"""Exception wrapped (lines 107-108)."""
|
|
qb = _mock_qb(raise_exc=RuntimeError("oops"))
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
with pytest.raises(OpenBBError, match="oops"):
|
|
OECDGdpRealFetcher.extract_data(
|
|
OECDGdpRealQueryParams(country="united_states"), None
|
|
)
|
|
|
|
def test_extract_data_empty_raises(self):
|
|
"""Empty rows → EmptyDataError (line 112)."""
|
|
qb = _mock_qb(records=[])
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
with pytest.raises(EmptyDataError):
|
|
OECDGdpRealFetcher.extract_data(
|
|
OECDGdpRealQueryParams(country="united_states"), None
|
|
)
|
|
|
|
def test_extract_data_returns_records(self):
|
|
"""Non-empty records returned (line 114)."""
|
|
rows = [{"TIME_PERIOD": "2020", "OBS_VALUE": 1.0, "REF_AREA": "USA"}]
|
|
qb = _mock_qb(records=rows)
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
out = OECDGdpRealFetcher.extract_data(
|
|
OECDGdpRealQueryParams(country="united_states"), None
|
|
)
|
|
assert out == rows
|
|
|
|
def test_transform_data_filters(self):
|
|
"""Date/value filters (lines 129, 131, 133, 136)."""
|
|
q = OECDGdpRealQueryParams(
|
|
country="united_states",
|
|
start_date=date(2020, 1, 1),
|
|
end_date=date(2020, 12, 31),
|
|
)
|
|
rows = [
|
|
{"TIME_PERIOD": "bad", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2010", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2030", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2020", "OBS_VALUE": None},
|
|
{"TIME_PERIOD": "2020", "OBS_VALUE": 2.5, "REF_AREA": "USA"},
|
|
]
|
|
out = OECDGdpRealFetcher.transform_data(q, rows)
|
|
assert len(out) == 1
|
|
assert out[0].value == 2_500_000
|
|
|
|
|
|
class TestHousePriceIndex:
|
|
"""Cover house_price_index.py missing branches."""
|
|
|
|
def test_transform_query_all_start(self):
|
|
"""country=all → 2000 start (line 64)."""
|
|
q = OECDHousePriceIndexFetcher.transform_query(
|
|
{"start_date": None, "country": "all"}
|
|
)
|
|
assert q.start_date == date(2000, 1, 1)
|
|
|
|
def test_transform_query_default_country(self):
|
|
"""None country default (lines 70, 72)."""
|
|
q = OECDHousePriceIndexFetcher.transform_query(
|
|
{"start_date": None, "end_date": None, "country": None}
|
|
)
|
|
assert q.country == "united_states"
|
|
|
|
def test_extract_data_monthly_fallback_to_quarterly(self):
|
|
"""Monthly failure falls back to quarterly (lines 106-127)."""
|
|
qb = MagicMock()
|
|
qb.metadata.resolve_country_codes.return_value = ["USA"]
|
|
qb.fetch_data.side_effect = [
|
|
RuntimeError("no monthly"),
|
|
{"data": [{"TIME_PERIOD": "2020", "OBS_VALUE": 1.0, "REF_AREA": "USA"}]},
|
|
]
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
with pytest.warns(UserWarning, match="quarterly"):
|
|
out = OECDHousePriceIndexFetcher.extract_data(
|
|
OECDHousePriceIndexQueryParams(
|
|
country="united_states", frequency="monthly"
|
|
),
|
|
None,
|
|
)
|
|
assert out and qb.fetch_data.call_count == 2
|
|
|
|
def test_extract_data_monthly_fallback_fails(self):
|
|
"""Quarterly fallback failure raises OpenBBError (lines 128-129)."""
|
|
qb = MagicMock()
|
|
qb.metadata.resolve_country_codes.return_value = ["USA"]
|
|
qb.fetch_data.side_effect = [RuntimeError("m"), RuntimeError("q")]
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
with pytest.warns(UserWarning):
|
|
with pytest.raises(OpenBBError, match="q"):
|
|
OECDHousePriceIndexFetcher.extract_data(
|
|
OECDHousePriceIndexQueryParams(
|
|
country="united_states", frequency="monthly"
|
|
),
|
|
None,
|
|
)
|
|
|
|
def test_extract_data_non_monthly_exception(self):
|
|
"""Non-monthly failure raises directly (line 131)."""
|
|
qb = _mock_qb(raise_exc=RuntimeError("boom"))
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
with pytest.raises(OpenBBError, match="boom"):
|
|
OECDHousePriceIndexFetcher.extract_data(
|
|
OECDHousePriceIndexQueryParams(
|
|
country="united_states", frequency="quarter"
|
|
),
|
|
None,
|
|
)
|
|
|
|
def test_extract_data_empty_raises(self):
|
|
"""Empty records → EmptyDataError (line 135)."""
|
|
qb = _mock_qb(records=[])
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
with pytest.raises(EmptyDataError):
|
|
OECDHousePriceIndexFetcher.extract_data(
|
|
OECDHousePriceIndexQueryParams(country="united_states"), None
|
|
)
|
|
|
|
def test_transform_data_filters_and_scales(self):
|
|
"""Date / value filters + percent scaling (lines 151, 156, 159)."""
|
|
q = OECDHousePriceIndexQueryParams(country="united_states", transform="yoy")
|
|
rows = [
|
|
{"TIME_PERIOD": "bad", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2020", "OBS_VALUE": None},
|
|
{"TIME_PERIOD": "2020", "OBS_VALUE": 50.0, "REF_AREA": "USA"},
|
|
]
|
|
out = OECDHousePriceIndexFetcher.transform_data(q, rows)
|
|
assert out[0].value == 0.5
|
|
|
|
|
|
class TestSharePriceIndex:
|
|
"""Cover share_price_index.py missing branches."""
|
|
|
|
def test_transform_query_all_uses_2000(self):
|
|
"""country=all → 2000 start (line 54)."""
|
|
q = OECDSharePriceIndexFetcher.transform_query(
|
|
{"start_date": None, "country": "all"}
|
|
)
|
|
assert q.start_date == date(2000, 1, 1)
|
|
|
|
def test_transform_query_default_country(self):
|
|
"""None country defaulted (lines 61, 64)."""
|
|
q = OECDSharePriceIndexFetcher.transform_query(
|
|
{"start_date": None, "end_date": None, "country": None}
|
|
)
|
|
assert q.country == "united_states"
|
|
assert q.end_date.year == date.today().year
|
|
|
|
def test_extract_data_wraps_exception(self):
|
|
"""Exception wrapped (lines 95-96)."""
|
|
qb = _mock_qb(raise_exc=RuntimeError("ugh"))
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
with pytest.raises(OpenBBError, match="ugh"):
|
|
OECDSharePriceIndexFetcher.extract_data(
|
|
OECDSharePriceIndexQueryParams(country="united_states"), None
|
|
)
|
|
|
|
def test_extract_data_empty_raises(self):
|
|
"""Empty records → OpenBBError (line 101)."""
|
|
qb = _mock_qb(records=[])
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
with pytest.raises(OpenBBError, match="no data rows"):
|
|
OECDSharePriceIndexFetcher.extract_data(
|
|
OECDSharePriceIndexQueryParams(country="united_states"), None
|
|
)
|
|
|
|
def test_extract_data_returns_records(self):
|
|
"""Non-empty rows returned (line 105)."""
|
|
rows = [{"TIME_PERIOD": "2020", "OBS_VALUE": 1.0}]
|
|
qb = _mock_qb(records=rows)
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
out = OECDSharePriceIndexFetcher.extract_data(
|
|
OECDSharePriceIndexQueryParams(country="united_states"), None
|
|
)
|
|
assert out == rows
|
|
|
|
def test_transform_data_filters(self):
|
|
"""Bad date / missing value (lines 119, 124)."""
|
|
q = OECDSharePriceIndexQueryParams(country="united_states")
|
|
rows = [
|
|
{"TIME_PERIOD": "bad", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2020", "OBS_VALUE": None},
|
|
{"TIME_PERIOD": "2020", "OBS_VALUE": 100.0, "REF_AREA": "USA"},
|
|
]
|
|
out = OECDSharePriceIndexFetcher.transform_data(q, rows)
|
|
assert len(out) == 1
|
|
assert out[0].value == 100.0
|
|
|
|
|
|
class TestUnemployment:
|
|
"""Cover unemployment.py missing branches."""
|
|
|
|
def test_validator_normalizes(self):
|
|
"""Validator strips and lowercases (line 64)."""
|
|
q = OECDUnemploymentQueryParams(country="United States")
|
|
assert q.country == "united_states"
|
|
|
|
def test_transform_query_all_uses_2010(self):
|
|
"""country=all → 2010 start (line 81)."""
|
|
q = OECDUnemploymentFetcher.transform_query(
|
|
{"start_date": None, "end_date": None, "country": "all"}
|
|
)
|
|
assert q.start_date == date(2010, 1, 1)
|
|
|
|
def test_transform_query_end_default(self):
|
|
"""None end_date filled in (line 87)."""
|
|
q = OECDUnemploymentFetcher.transform_query(
|
|
{"start_date": date(2020, 1, 1), "end_date": None}
|
|
)
|
|
assert q.end_date.year == date.today().year
|
|
|
|
def test_extract_data_wraps_exception(self):
|
|
"""Exception wrapped (lines 123-124)."""
|
|
qb = _mock_qb(raise_exc=RuntimeError("nope"))
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
with pytest.raises(OpenBBError, match="nope"):
|
|
OECDUnemploymentFetcher.extract_data(
|
|
OECDUnemploymentQueryParams(country="united_states"), None
|
|
)
|
|
|
|
def test_extract_data_empty_raises(self):
|
|
"""Empty records → EmptyDataError (line 128)."""
|
|
qb = _mock_qb(records=[])
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
with pytest.raises(EmptyDataError):
|
|
OECDUnemploymentFetcher.extract_data(
|
|
OECDUnemploymentQueryParams(country="united_states"), None
|
|
)
|
|
|
|
def test_extract_data_returns_records(self):
|
|
"""Non-empty rows returned (line 130)."""
|
|
rows = [{"TIME_PERIOD": "2020", "OBS_VALUE": 1.0}]
|
|
qb = _mock_qb(records=rows)
|
|
with patch("openbb_oecd.utils.query_builder.OecdQueryBuilder", return_value=qb):
|
|
out = OECDUnemploymentFetcher.extract_data(
|
|
OECDUnemploymentQueryParams(country="united_states"), None
|
|
)
|
|
assert out == rows
|
|
|
|
def test_transform_data_filters(self):
|
|
"""Date and value filters (lines 143, 145, 147, 150)."""
|
|
q = OECDUnemploymentQueryParams(
|
|
country="united_states",
|
|
start_date=date(2020, 1, 1),
|
|
end_date=date(2020, 12, 31),
|
|
)
|
|
rows = [
|
|
{"TIME_PERIOD": "bad", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2010", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2030", "OBS_VALUE": 1.0},
|
|
{"TIME_PERIOD": "2020", "OBS_VALUE": None},
|
|
{"TIME_PERIOD": "2020", "OBS_VALUE": 5.0, "REF_AREA": "USA"},
|
|
]
|
|
out = OECDUnemploymentFetcher.transform_data(q, rows)
|
|
assert len(out) == 1
|
|
assert out[0].value == 0.05
|