mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-13 12:51:49 +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>
382 lines
15 KiB
Python
382 lines
15 KiB
Python
"""Coverage tests for QueryMixin methods on OecdMetadata."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
_FULL_ID = "DSD_TEST@DF_TEST"
|
|
_SHORT_ID = "DF_TEST"
|
|
|
|
|
|
class TestResolveDataflowTriplet:
|
|
def test_returns_agency_full_id_version(self, seeded_meta):
|
|
agency, full_id, version = seeded_meta.resolve_dataflow_triplet(_SHORT_ID)
|
|
assert agency == "OECD"
|
|
assert full_id == _FULL_ID
|
|
assert version == "1.0"
|
|
|
|
|
|
class TestBuildDataUrl:
|
|
def test_basic_url_shape(self, seeded_meta):
|
|
url = seeded_meta.build_data_url(_SHORT_ID)
|
|
assert "DSD_TEST%40DF_TEST" in url
|
|
assert "dimensionAtObservation=TIME_PERIOD" in url
|
|
assert "detail=dataonly" in url
|
|
|
|
def test_last_n_appended(self, seeded_meta):
|
|
url = seeded_meta.build_data_url(_SHORT_ID, last_n=5)
|
|
assert "lastNObservations=5" in url
|
|
|
|
def test_first_n_appended(self, seeded_meta):
|
|
url = seeded_meta.build_data_url(_SHORT_ID, first_n=3)
|
|
assert "firstNObservations=3" in url
|
|
|
|
def test_last_n_blocked_for_blocked_dataflow(self, seeded_meta):
|
|
blocked_full = "DSD_BATIS@DF_BATIS"
|
|
seeded_meta.dataflows[blocked_full] = dict(seeded_meta.dataflows[_FULL_ID])
|
|
seeded_meta.dataflows[blocked_full]["id"] = blocked_full
|
|
seeded_meta.dataflows[blocked_full]["short_id"] = "DF_BATIS"
|
|
seeded_meta._short_id_map["DF_BATIS"] = blocked_full
|
|
seeded_meta.datastructures[blocked_full] = seeded_meta.datastructures[_FULL_ID]
|
|
url = seeded_meta.build_data_url("DF_BATIS", last_n=5)
|
|
assert "lastNObservations" not in url
|
|
|
|
def test_last_n_blocked_for_blocked_agency(self, seeded_meta):
|
|
seeded_meta.dataflows[_FULL_ID]["agency_id"] = "OECD.STI"
|
|
url = seeded_meta.build_data_url(_SHORT_ID, last_n=5)
|
|
assert "lastNObservations" not in url
|
|
|
|
def test_no_time_dimension_skips_param(self, seeded_meta):
|
|
seeded_meta.datastructures[_FULL_ID]["has_time_dimension"] = False
|
|
url = seeded_meta.build_data_url(_SHORT_ID)
|
|
assert "dimensionAtObservation" not in url
|
|
|
|
|
|
class TestBuildDimensionFilter:
|
|
def test_default_all_wildcards(self, seeded_meta):
|
|
result = seeded_meta.build_dimension_filter(_SHORT_ID)
|
|
assert result == "*.*.*"
|
|
|
|
def test_specific_dimensions(self, seeded_meta):
|
|
result = seeded_meta.build_dimension_filter(_SHORT_ID, REF_AREA="USA", FREQ="A")
|
|
parts = result.split(".")
|
|
assert parts[0] == "USA"
|
|
assert parts[1] == "*"
|
|
assert parts[2] == "A"
|
|
|
|
def test_empty_value_becomes_wildcard(self, seeded_meta):
|
|
result = seeded_meta.build_dimension_filter(_SHORT_ID, REF_AREA="")
|
|
assert result.split(".")[0] == "*"
|
|
|
|
|
|
class TestClassifyDimensions:
|
|
def test_basic_classification(self, seeded_meta):
|
|
result = seeded_meta.classify_dimensions(_SHORT_ID)
|
|
country_ids = [d["id"] for d in result["country"]]
|
|
freq_ids = [d["id"] for d in result["freq"]]
|
|
assert "REF_AREA" in country_ids
|
|
assert "FREQ" in freq_ids
|
|
|
|
def test_time_period_excluded(self, seeded_meta):
|
|
seeded_meta.datastructures[_FULL_ID]["dimensions"].append(
|
|
{
|
|
"id": "TIME_PERIOD",
|
|
"position": 4,
|
|
"codelist_id": "",
|
|
"concept_id": "TIME_PERIOD",
|
|
"name": "Time",
|
|
}
|
|
)
|
|
result = seeded_meta.classify_dimensions(_SHORT_ID)
|
|
assert all("TIME_PERIOD" not in [d["id"] for d in v] for v in result.values())
|
|
|
|
def test_fixed_single_value_dim(self, seeded_meta):
|
|
seeded_meta.codelists["OECD:CL_MEASURE(1.0)"] = {"CPI": "Consumer Price Index"}
|
|
result = seeded_meta.classify_dimensions(_SHORT_ID)
|
|
fixed_ids = [d["id"] for d in result["fixed"]]
|
|
assert "MEASURE" in fixed_ids
|
|
|
|
def test_axis_large_codelist(self, seeded_meta):
|
|
seeded_meta.codelists["OECD:CL_MEASURE(1.0)"] = {
|
|
f"C{i}": f"Code {i}" for i in range(60)
|
|
}
|
|
result = seeded_meta.classify_dimensions(_SHORT_ID)
|
|
axis_ids = [d["id"] for d in result["axis"]]
|
|
assert "MEASURE" in axis_ids
|
|
|
|
def test_selector_role(self, seeded_meta):
|
|
result = seeded_meta.classify_dimensions(_SHORT_ID)
|
|
selector_ids = [d["id"] for d in result["selector"]]
|
|
assert "MEASURE" in selector_ids
|
|
|
|
def test_missing_codelist_returns_empty_values(self, seeded_meta):
|
|
for dim in seeded_meta.datastructures[_FULL_ID]["dimensions"]:
|
|
if dim["id"] == "MEASURE":
|
|
dim["codelist_id"] = "UNKNOWN:NOPE(1.0)"
|
|
result = seeded_meta.classify_dimensions(_SHORT_ID)
|
|
measure = next(
|
|
(d for v in result.values() for d in v if d["id"] == "MEASURE"), None
|
|
)
|
|
assert measure is not None
|
|
assert measure["codelist_size"] == 0
|
|
|
|
|
|
class TestGetTableParameters:
|
|
def test_returns_per_dim(self, seeded_meta):
|
|
params = seeded_meta.get_table_parameters(_SHORT_ID)
|
|
assert "REF_AREA" in params
|
|
assert "FREQ" in params
|
|
assert "MEASURE" in params
|
|
|
|
def test_freq_default_picks_annual(self, seeded_meta):
|
|
params = seeded_meta.get_table_parameters(_SHORT_ID)
|
|
assert params["FREQ"]["default"] == "A"
|
|
|
|
def test_freq_default_falls_back_to_first(self, seeded_meta):
|
|
seeded_meta.codelists["OECD:CL_FREQ(1.0)"] = {"M": "Monthly", "Q": "Quarterly"}
|
|
params = seeded_meta.get_table_parameters(_SHORT_ID)
|
|
assert params["FREQ"]["default"] in {"M", "Q"}
|
|
|
|
def test_freq_default_empty_codelist(self, seeded_meta):
|
|
seeded_meta.codelists["OECD:CL_FREQ(1.0)"] = {}
|
|
params = seeded_meta.get_table_parameters(_SHORT_ID)
|
|
assert params["FREQ"]["default"] == "*"
|
|
|
|
def test_fixed_default_picks_only_value(self, seeded_meta):
|
|
seeded_meta.codelists["OECD:CL_MEASURE(1.0)"] = {"CPI": "Consumer Price Index"}
|
|
params = seeded_meta.get_table_parameters(_SHORT_ID)
|
|
assert params["MEASURE"]["default"] == "CPI"
|
|
|
|
|
|
class TestBuildTableQuery:
|
|
def test_basic_default_query(self, seeded_meta):
|
|
q = seeded_meta.build_table_query(_SHORT_ID)
|
|
assert q.count(".") == 2
|
|
|
|
def test_country_string_inserted(self, seeded_meta):
|
|
q = seeded_meta.build_table_query(_SHORT_ID, country="USA")
|
|
parts = q.split(".")
|
|
assert parts[0] == "USA"
|
|
|
|
def test_country_list_joined_with_plus(self, seeded_meta):
|
|
q = seeded_meta.build_table_query(_SHORT_ID, country=["USA", "GBR"])
|
|
assert "USA+GBR" in q
|
|
|
|
def test_country_all_keyword_wildcard(self, seeded_meta):
|
|
q = seeded_meta.build_table_query(_SHORT_ID, country="ALL")
|
|
assert q.split(".")[0] == "*"
|
|
|
|
def test_country_empty_string_wildcard(self, seeded_meta):
|
|
q = seeded_meta.build_table_query(_SHORT_ID, country="")
|
|
assert q.split(".")[0] == "*"
|
|
|
|
def test_frequency_override(self, seeded_meta):
|
|
q = seeded_meta.build_table_query(_SHORT_ID, frequency="Q")
|
|
assert q.split(".")[2] == "Q"
|
|
|
|
def test_selector_override(self, seeded_meta):
|
|
q = seeded_meta.build_table_query(_SHORT_ID, MEASURE="PPI")
|
|
assert "PPI" in q.split(".")
|
|
|
|
def test_secondary_country_dim_becomes_wildcard(self, seeded_meta):
|
|
seeded_meta.datastructures[_FULL_ID]["dimensions"].insert(
|
|
1,
|
|
{
|
|
"id": "COUNTERPART_AREA",
|
|
"position": 2,
|
|
"codelist_id": "OECD:CL_AREA(1.0)",
|
|
"concept_id": "REF_AREA",
|
|
"name": "Counterpart Area",
|
|
},
|
|
)
|
|
seeded_meta.datastructures[_FULL_ID]["dimensions"][2]["position"] = 3
|
|
seeded_meta.datastructures[_FULL_ID]["dimensions"][3]["position"] = 4
|
|
q = seeded_meta.build_table_query(_SHORT_ID, country="USA")
|
|
parts = q.split(".")
|
|
assert parts[0] == "USA"
|
|
assert parts[1] == "*"
|
|
|
|
def test_time_period_position_filled_with_wildcard(self, seeded_meta):
|
|
seeded_meta.datastructures[_FULL_ID]["dimensions"].append(
|
|
{
|
|
"id": "TIME_PERIOD",
|
|
"position": 4,
|
|
"codelist_id": "",
|
|
"concept_id": "TIME_PERIOD",
|
|
"name": "Time",
|
|
}
|
|
)
|
|
q = seeded_meta.build_table_query(_SHORT_ID)
|
|
assert q.split(".")[3] == "*"
|
|
|
|
def test_fixed_role_uses_default(self, seeded_meta):
|
|
seeded_meta.codelists["OECD:CL_MEASURE(1.0)"] = {"CPI": "Consumer Price Index"}
|
|
q = seeded_meta.build_table_query(_SHORT_ID)
|
|
assert "CPI" in q.split(".")
|
|
|
|
|
|
class TestDescribeTableDimensions:
|
|
def test_returns_one_entry_per_dimension(self, seeded_meta):
|
|
rows = seeded_meta.describe_table_dimensions(_SHORT_ID)
|
|
ids = {r["id"] for r in rows}
|
|
assert {"REF_AREA", "MEASURE", "FREQ"} <= ids
|
|
|
|
def test_includes_sample_values(self, seeded_meta):
|
|
rows = seeded_meta.describe_table_dimensions(_SHORT_ID)
|
|
ref_area = next(r for r in rows if r["id"] == "REF_AREA")
|
|
assert ref_area["sample_values"]
|
|
assert all("code" in s and "label" in s for s in ref_area["sample_values"])
|
|
|
|
def test_empty_values_yield_empty_sample(self, seeded_meta):
|
|
seeded_meta.codelists["OECD:CL_MEASURE(1.0)"] = {}
|
|
rows = seeded_meta.describe_table_dimensions(_SHORT_ID)
|
|
measure = next(r for r in rows if r["id"] == "MEASURE")
|
|
assert measure["sample_values"] == []
|
|
|
|
|
|
class TestFetchAvailability:
|
|
def _availability_response(self, **dim_values):
|
|
return {
|
|
"data": {
|
|
"contentConstraints": [
|
|
{
|
|
"cubeRegions": [
|
|
{
|
|
"keyValues": [
|
|
{"id": k, "values": v}
|
|
for k, v in dim_values.items()
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
def test_returns_cached_result_on_second_call(self, seeded_meta):
|
|
sentinel = {"REF_AREA": ["USA"]}
|
|
seeded_meta._availability_cache[f"{_FULL_ID}::"] = sentinel
|
|
result = seeded_meta.fetch_availability(_SHORT_ID)
|
|
assert result is sentinel
|
|
|
|
def test_parses_availability_response(self, seeded_meta):
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = self._availability_response(
|
|
REF_AREA=["USA", "GBR"], MEASURE=["CPI"]
|
|
)
|
|
with patch(
|
|
"openbb_oecd.utils.metadata._query_mixin._make_request",
|
|
return_value=mock_resp,
|
|
):
|
|
result = seeded_meta.fetch_availability(_SHORT_ID)
|
|
assert result["REF_AREA"] == ["GBR", "USA"]
|
|
assert result["MEASURE"] == ["CPI"]
|
|
|
|
def test_fills_missing_dims_from_codelist(self, seeded_meta):
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = self._availability_response(REF_AREA=["USA"])
|
|
with patch(
|
|
"openbb_oecd.utils.metadata._query_mixin._make_request",
|
|
return_value=mock_resp,
|
|
):
|
|
result = seeded_meta.fetch_availability(_SHORT_ID)
|
|
assert "FREQ" in result
|
|
assert "MEASURE" in result
|
|
|
|
def test_pinned_dims_drive_cache_key(self, seeded_meta):
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = self._availability_response(REF_AREA=["USA"])
|
|
with patch(
|
|
"openbb_oecd.utils.metadata._query_mixin._make_request",
|
|
return_value=mock_resp,
|
|
):
|
|
seeded_meta.fetch_availability(_SHORT_ID, pinned={"FREQ": "A"})
|
|
keys = list(seeded_meta._availability_cache.keys())
|
|
assert any("FREQ=A" in k for k in keys)
|
|
|
|
def test_skips_time_period_in_keyvalues(self, seeded_meta):
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = self._availability_response(
|
|
TIME_PERIOD=["2024"], REF_AREA=["USA"]
|
|
)
|
|
with patch(
|
|
"openbb_oecd.utils.metadata._query_mixin._make_request",
|
|
return_value=mock_resp,
|
|
):
|
|
result = seeded_meta.fetch_availability(_SHORT_ID)
|
|
assert "TIME_PERIOD" not in result
|
|
|
|
def test_request_error_falls_back_to_codelist(self, seeded_meta):
|
|
with patch(
|
|
"openbb_oecd.utils.metadata._query_mixin._make_request",
|
|
side_effect=RuntimeError("network down"),
|
|
):
|
|
result = seeded_meta.fetch_availability(_SHORT_ID)
|
|
assert "REF_AREA" in result
|
|
assert "USA" in result["REF_AREA"]
|
|
|
|
def test_request_error_uses_empty_when_no_codelist(self, seeded_meta, monkeypatch):
|
|
for dim in seeded_meta.datastructures[_FULL_ID]["dimensions"]:
|
|
dim["codelist_id"] = "UNKNOWN:X(1.0)"
|
|
monkeypatch.setattr(
|
|
type(seeded_meta),
|
|
"_ensure_structure",
|
|
lambda self, full_id, force=False: None,
|
|
)
|
|
with patch(
|
|
"openbb_oecd.utils.metadata._query_mixin._make_request",
|
|
side_effect=RuntimeError("net"),
|
|
):
|
|
result = seeded_meta.fetch_availability(_SHORT_ID)
|
|
for v in result.values():
|
|
assert v == []
|
|
|
|
def test_constraints_filter_availability(self, seeded_meta):
|
|
seeded_meta._dataflow_constraints[_FULL_ID] = {"REF_AREA": ["USA"]}
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = self._availability_response(
|
|
REF_AREA=["USA", "GBR", "DEU"]
|
|
)
|
|
with patch(
|
|
"openbb_oecd.utils.metadata._query_mixin._make_request",
|
|
return_value=mock_resp,
|
|
):
|
|
result = seeded_meta.fetch_availability(_SHORT_ID)
|
|
assert result["REF_AREA"] == ["USA"]
|
|
|
|
def test_constraint_for_missing_dim_does_not_crash(self, seeded_meta):
|
|
seeded_meta._dataflow_constraints[_FULL_ID] = {"NONEXISTENT": ["X"]}
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = self._availability_response(REF_AREA=["USA"])
|
|
with patch(
|
|
"openbb_oecd.utils.metadata._query_mixin._make_request",
|
|
return_value=mock_resp,
|
|
):
|
|
result = seeded_meta.fetch_availability(_SHORT_ID)
|
|
assert "REF_AREA" in result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"agency,short,blocked",
|
|
[
|
|
("OECD.STI", "DF_REGULAR", True),
|
|
("OECD.DCD", "DF_REGULAR", True),
|
|
("OECD", "DF_REGULAR", False),
|
|
],
|
|
)
|
|
def test_last_n_block_matrix(seeded_meta, agency, short, blocked):
|
|
full_id = f"DSD_X@{short}"
|
|
seeded_meta.dataflows[full_id] = dict(seeded_meta.dataflows[_FULL_ID])
|
|
seeded_meta.dataflows[full_id]["id"] = full_id
|
|
seeded_meta.dataflows[full_id]["short_id"] = short
|
|
seeded_meta.dataflows[full_id]["agency_id"] = agency
|
|
seeded_meta._short_id_map[short] = full_id
|
|
seeded_meta.datastructures[full_id] = seeded_meta.datastructures[_FULL_ID]
|
|
url = seeded_meta.build_data_url(short, last_n=10)
|
|
if blocked:
|
|
assert "lastNObservations" not in url
|
|
else:
|
|
assert "lastNObservations=10" in url
|