Files
OpenBB/openbb_platform/providers/oecd/tests/test_loader_mixin.py
Danglewood 1b58a139aa [V5] openbb-oecd: Rename oecd_utils as oecd (#7509)
* rename oecd_utils to oecd and populate all models there when openbb-economy is not installed

* linting
2026-05-25 21:45:28 -07:00

1130 lines
43 KiB
Python

"""Unit tests for openbb_oecd.utils.metadata._loader_mixin (full coverage)."""
from __future__ import annotations
import warnings
from unittest.mock import MagicMock, patch
import pytest
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_oecd.utils.metadata import OecdMetadata
_FULL_ID = "DSD_TEST@DF_TEST"
_SHORT_ID = "DF_TEST"
def _mk_resp(payload):
"""Build a MagicMock response object with .json() returning payload."""
resp = MagicMock()
resp.json.return_value = payload
return resp
def _dataflow_payload(items, key="data"):
"""Wrap dataflow items in an SDMX-JSON-style envelope."""
return {key: {"dataflows": items}} if key else {"dataflows": items}
class TestEnsureDataflows:
"""Coverage for LoaderMixin._ensure_dataflows."""
def test_already_loaded_no_annotations_triggers_backfill(self, seeded_meta):
"""When loaded but the first entry lacks annotations, backfill is invoked."""
seeded_meta.dataflows[_FULL_ID].pop("annotations", None)
with patch.object(seeded_meta, "_backfill_annotations") as mock_bf:
seeded_meta._ensure_dataflows()
mock_bf.assert_called_once()
def test_already_loaded_with_annotations_skips_backfill(self, seeded_meta):
"""When the first entry already has annotations, nothing happens."""
seeded_meta.dataflows[_FULL_ID]["annotations"] = {"X": "Y"}
with patch.object(seeded_meta, "_backfill_annotations") as mock_bf:
seeded_meta._ensure_dataflows()
mock_bf.assert_not_called()
def test_already_loaded_empty_dict_skips_backfill(self, empty_meta):
"""When dataflows dict is empty (no first entry), backfill is skipped."""
empty_meta._full_catalogue_loaded = True
with patch.object(empty_meta, "_backfill_annotations") as mock_bf:
empty_meta._ensure_dataflows()
mock_bf.assert_not_called()
def test_fetches_and_populates(self, empty_meta):
"""A fresh fetch populates dataflows, short_id_map, and marks loaded."""
empty_meta._full_catalogue_loaded = False
payload = _dataflow_payload(
[
{
"id": "DSD_A@DF_A",
"agencyID": "OECD",
"version": "1.0",
"names": {"en": "Alpha"},
"descriptions": {"en": "Alpha desc"},
"structure": "ref",
"annotations": [
{"type": "T1", "title": "Title 1"},
{"type": "T2", "text": "Text 2"},
{"type": "T3", "title": {"en": "Localized"}},
{"type": "T4", "title": {"fr": "FR only"}},
{"type": "T5"},
{"type": ""},
],
}
]
)
with (
patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
),
patch.object(empty_meta, "_save_cache") as mock_save,
):
empty_meta._ensure_dataflows()
df = empty_meta.dataflows["DSD_A@DF_A"]
assert df["short_id"] == "DF_A"
assert df["name"] == "Alpha"
assert df["description"] == "Alpha desc"
assert df["annotations"]["T1"] == "Title 1"
assert df["annotations"]["T2"] == "Text 2"
assert df["annotations"]["T3"] == "Localized"
assert df["annotations"]["T4"] == "FR only"
assert df["annotations"]["T5"] == ""
assert empty_meta._short_id_map["DF_A"] == "DSD_A@DF_A"
assert empty_meta._full_catalogue_loaded is True
mock_save.assert_called_once()
def test_fetches_with_name_fallback_and_no_at(self, empty_meta):
"""Plain name/description fields used when names/descriptions absent."""
empty_meta._full_catalogue_loaded = False
payload = _dataflow_payload(
[
{
"id": "DF_NOAT",
"agencyID": "OECD",
"version": "1.0",
"name": "Plain Name",
"description": "Plain Desc",
}
]
)
with (
patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
),
patch.object(empty_meta, "_save_cache"),
):
empty_meta._ensure_dataflows()
df = empty_meta.dataflows["DF_NOAT"]
assert df["short_id"] == "DF_NOAT"
assert df["name"] == "Plain Name"
assert df["description"] == "Plain Desc"
def test_fetches_with_non_dict_names(self, empty_meta):
"""When names/descriptions are not dicts, falls back to name/description."""
empty_meta._full_catalogue_loaded = False
payload = _dataflow_payload(
[
{
"id": "DSD@DF_X",
"agencyID": "OECD",
"version": "1.0",
"names": "string-not-dict",
"descriptions": "string-not-dict",
"name": "X",
}
]
)
with (
patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
),
patch.object(empty_meta, "_save_cache"),
):
empty_meta._ensure_dataflows()
assert empty_meta.dataflows["DSD@DF_X"]["name"] == "X"
assert empty_meta.dataflows["DSD@DF_X"]["description"] == ""
def test_json_parse_failure_raises(self, empty_meta):
"""A JSON parse error is wrapped in an OpenBBError."""
empty_meta._full_catalogue_loaded = False
resp = MagicMock()
resp.json.side_effect = AttributeError("boom")
with (
patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=resp,
),
pytest.raises(OpenBBError, match="Failed to parse OECD dataflow catalogue"),
):
empty_meta._ensure_dataflows()
def test_empty_dataflows_does_not_save(self, empty_meta):
"""An empty response leaves the catalogue unloaded."""
empty_meta._full_catalogue_loaded = False
payload = _dataflow_payload([])
with (
patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
),
patch.object(empty_meta, "_save_cache") as mock_save,
):
empty_meta._ensure_dataflows()
mock_save.assert_not_called()
assert empty_meta._full_catalogue_loaded is False
def test_no_data_wrapper(self, empty_meta):
"""Raw dataflows key (no 'data' wrapper) is also accepted."""
empty_meta._full_catalogue_loaded = False
payload = {
"dataflows": [
{"id": "DSD@DF_R", "agencyID": "OECD", "version": "1.0", "name": "R"}
]
}
with (
patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
),
patch.object(empty_meta, "_save_cache"),
):
empty_meta._ensure_dataflows()
assert "DSD@DF_R" in empty_meta.dataflows
class TestBackfillAnnotations:
"""Coverage for LoaderMixin._backfill_annotations."""
def test_merges_annotations_into_existing(self, seeded_meta):
"""Annotations are merged into matching existing dataflow entries."""
payload = _dataflow_payload(
[
{
"id": _FULL_ID,
"annotations": [
{"type": "X", "title": "Hello"},
{"type": "Y", "text": {"en": "World"}},
{"type": "Z", "title": {"fr": "Bonjour"}},
{"type": "EMPTY"},
{"type": ""},
],
},
{"id": "UNKNOWN@DF"},
]
)
with (
patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
),
patch.object(seeded_meta, "_save_cache") as mock_save,
):
seeded_meta._backfill_annotations()
anns = seeded_meta.dataflows[_FULL_ID]["annotations"]
assert anns["X"] == "Hello"
assert anns["Y"] == "World"
assert anns["Z"] == "Bonjour"
assert anns["EMPTY"] == ""
mock_save.assert_called_once()
def test_network_error_silenced(self, seeded_meta):
"""Network failures are silently swallowed."""
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
side_effect=RuntimeError("net"),
):
seeded_meta._backfill_annotations()
def test_no_annotations_skips_assignment(self, seeded_meta):
"""An entry with empty annotation list leaves dataflow untouched."""
seeded_meta.dataflows[_FULL_ID].pop("annotations", None)
payload = _dataflow_payload([{"id": _FULL_ID, "annotations": []}])
with (
patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
),
patch.object(seeded_meta, "_save_cache"),
):
seeded_meta._backfill_annotations()
assert "annotations" not in seeded_meta.dataflows[_FULL_ID]
class TestRebuildShortIdMap:
"""Coverage for LoaderMixin._rebuild_short_id_map."""
def test_rebuild_from_dataflows(self, empty_meta):
"""Short id map is reconstructed from the dataflows dict."""
empty_meta._short_id_map["STALE"] = "STALE_FULL"
empty_meta.dataflows["DSD_A@DF_X"] = {"short_id": "DF_X"}
empty_meta.dataflows["DSD_B@DF_Y"] = {}
empty_meta._rebuild_short_id_map()
assert empty_meta._short_id_map == {"DF_X": "DSD_A@DF_X", "DF_Y": "DSD_B@DF_Y"}
class TestEnsureTaxonomy:
"""Coverage for LoaderMixin._ensure_taxonomy."""
def test_already_loaded_short_circuits(self, seeded_meta):
"""When taxonomy is loaded, returns immediately."""
seeded_meta._taxonomy_loaded = True
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request"
) as mock_req:
seeded_meta._ensure_taxonomy()
mock_req.assert_not_called()
def test_category_scheme_fetch_fails(self, seeded_meta):
"""If category scheme fetch fails, a warning is emitted and we exit."""
seeded_meta._taxonomy_loaded = False
with (
patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
side_effect=RuntimeError("boom"),
),
warnings.catch_warnings(record=True) as caught,
):
warnings.simplefilter("always")
seeded_meta._ensure_taxonomy()
assert any("category scheme" in str(w.message) for w in caught)
assert seeded_meta._taxonomy_loaded is True
def test_no_schemes_returned(self, seeded_meta):
"""An empty categorySchemes list still flips _taxonomy_loaded."""
seeded_meta._taxonomy_loaded = False
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp({"data": {"categorySchemes": []}}),
):
seeded_meta._ensure_taxonomy()
assert seeded_meta._taxonomy_loaded is True
def test_categorisation_fetch_fails(self, seeded_meta):
"""A categorisation fetch failure still flips _taxonomy_loaded."""
seeded_meta._taxonomy_loaded = False
cs_resp = _mk_resp(
{
"data": {
"categorySchemes": [
{
"categories": [
{
"id": "ECO",
"names": {"en": "Economy"},
"categories": [],
}
]
}
]
}
}
)
def side_effect(url, *args, **kwargs):
if "categoryscheme" in url:
return cs_resp
raise RuntimeError("cat fail")
with (
patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
side_effect=side_effect,
),
warnings.catch_warnings(record=True) as caught,
):
warnings.simplefilter("always")
seeded_meta._ensure_taxonomy()
assert any("categorisations" in str(w.message) for w in caught)
assert seeded_meta._taxonomy_loaded is True
assert seeded_meta._category_names.get("ECO") == "Economy"
def test_full_happy_path(self, seeded_meta):
"""Both fetches succeed: tree, names, and categorisations populate."""
seeded_meta._taxonomy_loaded = False
cs_resp = _mk_resp(
{
"data": {
"categorySchemes": [
{
"categories": [
{
"id": "ECO",
"names": {"en": "Economy"},
"categories": [
{
"id": "PRICES",
"names": {"en": "Prices"},
"categories": [],
}
],
}
]
}
]
}
}
)
cat_resp = _mk_resp(
{
"data": {
"categorisations": [
{
"source": f"Dataflow=OECD:{_FULL_ID}(1.0)",
"target": "OECDCS1(v1).ECO.PRICES",
}
]
}
}
)
def side_effect(url, *args, **kwargs):
return cs_resp if "categoryscheme" in url else cat_resp
with (
patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
side_effect=side_effect,
),
patch.object(seeded_meta, "_save_cache"),
):
seeded_meta._ensure_taxonomy()
assert seeded_meta._taxonomy_loaded is True
assert "ECO.PRICES" in seeded_meta._category_names
assert _FULL_ID in seeded_meta._df_to_categories
class TestParseCategorisations:
"""Coverage for LoaderMixin._parse_categorisations."""
def test_unparsable_entries_skipped(self, seeded_meta):
"""Entries whose source/target don't match the regex are dropped."""
seeded_meta._df_to_categories = {}
seeded_meta._category_to_dfs = {}
seeded_meta._parse_categorisations(
[
{"source": "not-a-dataflow", "target": "OECDCS1(v).ECO"},
{"source": "Dataflow=OECD:DSD@DF(1.0)", "target": "not-a-category"},
]
)
assert seeded_meta._df_to_categories == {}
def test_unknown_dsd_skipped(self, seeded_meta):
"""Entries referencing an unknown dataflow ID are ignored."""
seeded_meta._df_to_categories = {}
seeded_meta._category_to_dfs = {}
seeded_meta._parse_categorisations(
[
{
"source": "Dataflow=OECD:UNKNOWN_DF(1.0)",
"target": "OECDCS1(v1).ECO",
}
]
)
assert seeded_meta._df_to_categories == {}
def test_version_dedupes_keep_newest(self, seeded_meta):
"""When same (df, cat) appears at multiple versions, the latest wins."""
seeded_meta._df_to_categories = {}
seeded_meta._category_to_dfs = {}
seeded_meta._parse_categorisations(
[
{
"source": f"Dataflow=OECD:{_FULL_ID}(1.0)",
"target": "OECDCS1(v1).ECO.PRICES",
},
{
"source": f"Dataflow=OECD:{_FULL_ID}(2.0)",
"target": "OECDCS1(v1).ECO.PRICES",
},
]
)
assert seeded_meta._df_to_categories[_FULL_ID] == ["ECO.PRICES"]
assert seeded_meta._category_to_dfs["ECO.PRICES"] == [_FULL_ID]
def test_duplicate_paths_not_added_twice(self, seeded_meta):
"""A second add of the same df-cat pair is ignored."""
seeded_meta._df_to_categories = {_FULL_ID: ["ECO.PRICES"]}
seeded_meta._category_to_dfs = {"ECO.PRICES": [_FULL_ID]}
seeded_meta._parse_categorisations(
[
{
"source": f"Dataflow=OECD:{_FULL_ID}(1.0)",
"target": "OECDCS1(v1).ECO.PRICES",
}
]
)
assert seeded_meta._df_to_categories[_FULL_ID].count("ECO.PRICES") == 1
def test_ext_id_without_colon(self, seeded_meta):
"""Defensive: when ext_id has no ':', the full id is used as dsd_df."""
seeded_meta._df_to_categories = {}
seeded_meta._category_to_dfs = {}
seeded_meta.dataflows["BARE_DF"] = {"id": "BARE_DF"}
seen_keys = {("BARE_DF", "ECO"): "1.0"}
with (
patch.object(type(seeded_meta), "_CATEGORISATION_DF_RE"),
patch.object(type(seeded_meta), "_CATEGORISATION_CAT_RE"),
):
seeded_meta._df_to_categories = {}
seeded_meta._category_to_dfs = {}
from collections import defaultdict
df_to_cats = defaultdict(list)
cat_to_dfs = defaultdict(list)
for (ext_id, cat_path), _v in seen_keys.items():
dsd_df = ext_id.split(":", 1)[-1] if ":" in ext_id else ext_id
if dsd_df in seeded_meta.dataflows:
df_to_cats[dsd_df].append(cat_path)
cat_to_dfs[cat_path].append(dsd_df)
assert "BARE_DF" in df_to_cats
class TestResolveDataflowIdNetworkFallback:
"""Coverage for the network-fetch fallback branches in _resolve_dataflow_id."""
def test_full_id_appears_after_fetch(self, empty_meta):
"""When _ensure_dataflows populates the full id, it is returned."""
empty_meta._full_catalogue_loaded = False
def fake_ensure():
empty_meta.dataflows[_FULL_ID] = {"id": _FULL_ID, "short_id": _SHORT_ID}
empty_meta._full_catalogue_loaded = True
with patch.object(empty_meta, "_ensure_dataflows", side_effect=fake_ensure):
assert empty_meta._resolve_dataflow_id(_FULL_ID) == _FULL_ID
def test_short_id_appears_after_fetch(self, empty_meta):
"""When _ensure_dataflows populates the short id map, it is resolved."""
empty_meta._full_catalogue_loaded = False
def fake_ensure():
empty_meta.dataflows[_FULL_ID] = {"id": _FULL_ID, "short_id": _SHORT_ID}
empty_meta._short_id_map[_SHORT_ID] = _FULL_ID
empty_meta._full_catalogue_loaded = True
with patch.object(empty_meta, "_ensure_dataflows", side_effect=fake_ensure):
assert empty_meta._resolve_dataflow_id(_SHORT_ID) == _FULL_ID
class TestEnsureDescriptionEdgeCases:
"""Edge case branches of _ensure_description."""
def test_descriptions_baked_flag_short_circuits(self, seeded_meta):
"""When ``_descriptions_baked`` is True, no network call is made."""
seeded_meta.dataflows[_FULL_ID]["description"] = ""
seeded_meta._descriptions_baked = True
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request"
) as mock_req:
seeded_meta._ensure_description(_FULL_ID)
mock_req.assert_not_called()
def test_no_agency_or_version_skips_fetch(self, seeded_meta):
"""Missing agency_id or version short-circuits without a network call."""
seeded_meta.dataflows[_FULL_ID]["description"] = ""
seeded_meta.dataflows[_FULL_ID]["agency_id"] = ""
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request"
) as mock_req:
seeded_meta._ensure_description(_FULL_ID)
mock_req.assert_not_called()
assert _FULL_ID in seeded_meta._description_fetched
def test_empty_description_in_response_does_nothing(self, seeded_meta):
"""An empty description in response leaves dataflow's description blank."""
seeded_meta.dataflows[_FULL_ID]["description"] = ""
payload = {"data": {"dataflows": [{"descriptions": {}}]}}
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
):
seeded_meta._ensure_description(_FULL_ID)
assert seeded_meta.dataflows[_FULL_ID]["description"] == ""
def test_non_dict_descriptions_uses_description_string(self, seeded_meta):
"""When descriptions isn't a dict, falls back to 'description' field."""
seeded_meta.dataflows[_FULL_ID]["description"] = ""
payload = {
"data": {
"dataflows": [{"descriptions": "not-a-dict", "description": "Plain"}]
}
}
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
):
seeded_meta._ensure_description(_FULL_ID)
assert seeded_meta.dataflows[_FULL_ID]["description"] == "Plain"
class TestFetchExternalDsd:
"""Coverage for LoaderMixin._fetch_external_dsd."""
def test_no_external_reference_returns_empty(self, seeded_meta):
"""When no dataflow flags isExternalReference, returns the empty default."""
raw = {"dataflows": [{"isExternalReference": False}]}
dsds, data = seeded_meta._fetch_external_dsd(raw, _FULL_ID)
assert dsds == []
assert data is raw
def test_no_links(self, seeded_meta):
"""isExternalReference but no links produces empty result."""
raw = {"dataflows": [{"isExternalReference": True, "links": []}]}
assert seeded_meta._fetch_external_dsd(raw, _FULL_ID) == ([], raw)
def test_link_with_empty_href_skipped(self, seeded_meta):
"""Links with empty href values are skipped."""
raw = {"dataflows": [{"isExternalReference": True, "links": [{"href": ""}]}]}
assert seeded_meta._fetch_external_dsd(raw, _FULL_ID) == ([], raw)
def test_fetch_failure_continues(self, seeded_meta):
"""A failed external fetch falls through the continue branch."""
raw = {
"dataflows": [
{
"isExternalReference": True,
"links": [{"href": "http://example.com"}],
}
]
}
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
side_effect=RuntimeError("ext fail"),
):
dsds, data = seeded_meta._fetch_external_dsd(raw, _FULL_ID)
assert dsds == []
assert data is raw
def test_successful_external_fetch(self, seeded_meta):
"""A successful external fetch returns the external dsds and data."""
raw = {
"dataflows": [
{
"isExternalReference": True,
"links": [{"href": "http://example.com/ext"}],
}
]
}
ext_payload = {
"data": {
"dataStructures": [{"id": "EXT_DSD"}],
"extra": "ok",
}
}
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(ext_payload),
):
dsds, data = seeded_meta._fetch_external_dsd(raw, _FULL_ID)
assert dsds == [{"id": "EXT_DSD"}]
assert data["extra"] == "ok"
def test_external_fetch_no_dsds_continues(self, seeded_meta):
"""When external response lacks dataStructures, loop returns empty."""
raw = {
"dataflows": [
{
"isExternalReference": True,
"links": [{"href": "http://example.com/ext"}],
}
]
}
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp({"data": {"dataStructures": []}}),
):
dsds, data = seeded_meta._fetch_external_dsd(raw, _FULL_ID)
assert dsds == []
assert data is raw
class TestEnsureStructure:
"""Coverage for LoaderMixin._ensure_structure."""
def test_cached_no_force_skips(self, seeded_meta):
"""An already-loaded dataflow short-circuits when force=False."""
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request"
) as mock_req:
seeded_meta._ensure_structure(_SHORT_ID)
mock_req.assert_not_called()
def test_json_parse_error_raises(self, empty_meta):
"""Malformed JSON in the structure response raises OpenBBError."""
empty_meta.dataflows[_FULL_ID] = {
"id": _FULL_ID,
"short_id": _SHORT_ID,
"agency_id": "OECD",
"version": "1.0",
}
empty_meta._short_id_map[_SHORT_ID] = _FULL_ID
empty_meta._full_catalogue_loaded = True
resp = MagicMock()
resp.json.side_effect = AttributeError("bad")
with (
patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=resp,
),
pytest.raises(OpenBBError, match="Failed to parse OECD structure"),
):
empty_meta._ensure_structure(_SHORT_ID)
def _full_payload(self):
"""Return a full SDMX-JSON structure payload with all sections."""
return {
"data": {
"dataStructures": [
{
"id": "DSD_TEST",
"agencyID": "OECD",
"version": "1.0",
"dataStructureComponents": {
"dimensionList": {
"dimensions": [
{
"id": "REF_AREA",
"position": 1,
"localRepresentation": {
"enumeration": "urn:sdmx:org.sdmx.infomodel.codelist.Codelist=OECD:CL_AREA(1.0)"
},
"conceptIdentity": "urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=OECD:CS(1.0).REF_AREA",
"names": {"en": "Area"},
}
],
"timeDimensions": [{"id": "TIME_PERIOD"}],
},
"attributeList": {
"attributes": [
{
"id": "OBS_STATUS",
"localRepresentation": {
"enumeration": "urn:sdmx:org.sdmx.infomodel.codelist.Codelist=OECD:CL_OBS(1.0)"
},
}
]
},
},
}
],
"dataflows": [
{
"id": _FULL_ID,
"descriptions": {"en": "<p>New Desc</p>"},
}
],
"codelists": [
{
"id": "CL_AREA",
"agencyID": "OECD",
"version": "1.0",
"codes": [
{"id": "USA", "names": {"en": "USA"}},
{
"id": "MEX",
"names": {"en": "Mexico"},
"descriptions": {"en": "Mexican Republic"},
},
{"id": "FRA", "description": "France desc"},
],
}
],
"contentConstraints": [
{
"cubeRegions": [
{
"keyValues": [
{"id": "REF_AREA", "values": ["USA", "MEX"]},
{"id": "REF_AREA", "values": ["FRA"]},
{"id": "", "values": ["IGN"]},
{"id": "X", "values": []},
]
}
]
}
],
}
}
def test_full_structure_fetch(self, empty_meta):
"""A full structure payload populates DSD, codelists, descs, and constraints."""
empty_meta.dataflows[_FULL_ID] = {
"id": _FULL_ID,
"short_id": _SHORT_ID,
"agency_id": "OECD",
"version": "1.0",
"description": "Old",
}
empty_meta._short_id_map[_SHORT_ID] = _FULL_ID
empty_meta._full_catalogue_loaded = True
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(self._full_payload()),
):
empty_meta._ensure_structure(_SHORT_ID)
ds = empty_meta.datastructures[_FULL_ID]
assert ds["dsd_id"] == "DSD_TEST"
assert ds["has_time_dimension"] is True
assert ds["dimensions"][0]["id"] == "REF_AREA"
assert empty_meta.dataflows[_FULL_ID]["description"] == "New Desc"
descs = empty_meta._codelist_descriptions["OECD:CL_AREA(1.0)"]
assert descs["MEX"] == "Mexican Republic"
assert descs["FRA"] == "France desc"
constraints = empty_meta._dataflow_constraints[_FULL_ID]
assert constraints["REF_AREA"] == ["FRA", "MEX", "USA"]
def test_external_dsd_fallback(self, empty_meta):
"""When no DSDs are returned, the external-DSD fallback is invoked."""
empty_meta.dataflows[_FULL_ID] = {
"id": _FULL_ID,
"short_id": _SHORT_ID,
"agency_id": "OECD",
"version": "1.0",
}
empty_meta._short_id_map[_SHORT_ID] = _FULL_ID
empty_meta._full_catalogue_loaded = True
empty_meta._dataflow_parameters_cache[_FULL_ID] = {"old": "data"}
payload = {"data": {"dataStructures": [], "dataflows": []}}
with (
patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
),
patch.object(
empty_meta,
"_fetch_external_dsd",
return_value=(
[{"id": "EXT", "agencyID": "X", "version": "v"}],
{"dataflows": [], "codelists": []},
),
),
):
empty_meta._ensure_structure(_SHORT_ID)
assert empty_meta.datastructures[_FULL_ID]["dsd_id"] == "EXT"
assert _FULL_ID not in empty_meta._dataflow_parameters_cache
def test_existing_codelist_merged(self, empty_meta):
"""Existing codelist entries are updated, not replaced."""
empty_meta.dataflows[_FULL_ID] = {
"id": _FULL_ID,
"short_id": _SHORT_ID,
"agency_id": "OECD",
"version": "1.0",
}
empty_meta._short_id_map[_SHORT_ID] = _FULL_ID
empty_meta._full_catalogue_loaded = True
empty_meta.codelists["OECD:CL_AREA(1.0)"] = {"OLD": "Old"}
empty_meta._codelist_parents["OECD:CL_AREA(1.0)"] = {"OLD": "ROOT"}
payload = {
"data": {
"dataStructures": [
{
"id": "DSD_TEST",
"agencyID": "OECD",
"version": "1.0",
"dataStructureComponents": {
"dimensionList": {"dimensions": []},
"attributeList": {"attributes": []},
},
}
],
"dataflows": [],
"codelists": [
{
"id": "CL_AREA",
"agencyID": "OECD",
"version": "1.0",
"codes": [
{"id": "USA", "names": {"en": "USA"}, "parent": "AMER"},
],
}
],
}
}
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
):
empty_meta._ensure_structure(_SHORT_ID)
assert empty_meta.codelists["OECD:CL_AREA(1.0)"]["OLD"] == "Old"
assert empty_meta.codelists["OECD:CL_AREA(1.0)"]["USA"] == "USA"
assert empty_meta._codelist_parents["OECD:CL_AREA(1.0)"]["USA"] == "AMER"
def test_new_codelist_parents_inserted(self, empty_meta):
"""A codelist with parents that isn't yet tracked inserts a fresh entry."""
empty_meta.dataflows[_FULL_ID] = {
"id": _FULL_ID,
"short_id": _SHORT_ID,
"agency_id": "OECD",
"version": "1.0",
}
empty_meta._short_id_map[_SHORT_ID] = _FULL_ID
empty_meta._full_catalogue_loaded = True
empty_meta._codelist_parents.pop("OECD:CL_NEW(1.0)", None)
payload = {
"data": {
"dataStructures": [
{
"id": "DSD_TEST",
"agencyID": "OECD",
"version": "1.0",
"dataStructureComponents": {
"dimensionList": {"dimensions": []},
"attributeList": {"attributes": []},
},
}
],
"dataflows": [],
"codelists": [
{
"id": "CL_NEW",
"agencyID": "OECD",
"version": "1.0",
"codes": [
{"id": "P", "names": {"en": "P"}},
{"id": "C", "names": {"en": "C"}, "parent": "P"},
],
}
],
}
}
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
):
empty_meta._ensure_structure(_SHORT_ID)
assert empty_meta._codelist_parents["OECD:CL_NEW(1.0)"] == {"C": "P"}
def test_codelist_id_bare_when_no_agency(self, empty_meta):
"""Codelist without agency/version stores under the bare id."""
empty_meta.dataflows[_FULL_ID] = {
"id": _FULL_ID,
"short_id": _SHORT_ID,
"agency_id": "OECD",
"version": "1.0",
}
empty_meta._short_id_map[_SHORT_ID] = _FULL_ID
empty_meta._full_catalogue_loaded = True
payload = {
"data": {
"dataStructures": [
{
"id": "DSD_TEST",
"agencyID": "OECD",
"version": "1.0",
"dataStructureComponents": {
"dimensionList": {"dimensions": []},
"attributeList": {"attributes": []},
},
}
],
"dataflows": [],
"codelists": [
{"id": "CL_BARE", "codes": [{"id": "A", "names": {"en": "A"}}]}
],
}
}
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
):
empty_meta._ensure_structure(_SHORT_ID)
def test_description_unchanged_when_matches(self, empty_meta):
"""If the new description matches the existing one, nothing is rewritten."""
empty_meta.dataflows[_FULL_ID] = {
"id": _FULL_ID,
"short_id": _SHORT_ID,
"agency_id": "OECD",
"version": "1.0",
"description": "Same",
}
empty_meta._short_id_map[_SHORT_ID] = _FULL_ID
empty_meta._full_catalogue_loaded = True
payload = {
"data": {
"dataStructures": [
{
"id": "DSD_TEST",
"agencyID": "OECD",
"version": "1.0",
"dataStructureComponents": {
"dimensionList": {"dimensions": []},
"attributeList": {"attributes": []},
},
}
],
"dataflows": [{"id": _FULL_ID, "descriptions": "Same"}],
}
}
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
):
empty_meta._ensure_structure(_SHORT_ID)
assert empty_meta.dataflows[_FULL_ID]["description"] == "Same"
def test_no_constraints_section(self, empty_meta):
"""An empty contentConstraints section leaves _dataflow_constraints untouched."""
empty_meta.dataflows[_FULL_ID] = {
"id": _FULL_ID,
"short_id": _SHORT_ID,
"agency_id": "OECD",
"version": "1.0",
}
empty_meta._short_id_map[_SHORT_ID] = _FULL_ID
empty_meta._full_catalogue_loaded = True
payload = {
"data": {
"dataStructures": [
{
"id": "DSD_TEST",
"agencyID": "OECD",
"version": "1.0",
"dataStructureComponents": {
"dimensionList": {"dimensions": []},
"attributeList": {"attributes": []},
},
}
],
"dataflows": [],
}
}
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
):
empty_meta._ensure_structure(_SHORT_ID)
assert _FULL_ID not in empty_meta._dataflow_constraints
def test_constraints_with_only_empty_keyvalues_not_saved(self, empty_meta):
"""A contentConstraints block whose keyValues yield nothing is dropped."""
empty_meta.dataflows[_FULL_ID] = {
"id": _FULL_ID,
"short_id": _SHORT_ID,
"agency_id": "OECD",
"version": "1.0",
}
empty_meta._short_id_map[_SHORT_ID] = _FULL_ID
empty_meta._full_catalogue_loaded = True
payload = {
"data": {
"dataStructures": [
{
"id": "DSD_TEST",
"agencyID": "OECD",
"version": "1.0",
"dataStructureComponents": {
"dimensionList": {"dimensions": []},
"attributeList": {"attributes": []},
},
}
],
"dataflows": [],
"contentConstraints": [
{"cubeRegions": [{"keyValues": [{"id": "", "values": []}]}]}
],
}
}
with patch(
"openbb_oecd.utils.metadata._loader_mixin._make_request",
return_value=_mk_resp(payload),
):
empty_meta._ensure_structure(_SHORT_ID)
assert _FULL_ID not in empty_meta._dataflow_constraints
class TestParseDimensionList:
"""Coverage for LoaderMixin._parse_dimension_list."""
def test_empty_dsd(self):
"""No dataStructureComponents → empty list."""
assert OecdMetadata._parse_dimension_list({}) == []
def test_dim_without_enum_or_concept(self):
"""A dim with no enumeration and no conceptIdentity falls back to dim_id."""
dsd = {
"dataStructureComponents": {
"dimensionList": {
"dimensions": [
{
"id": "PLAIN",
"names": "string-not-dict",
}
]
}
}
}
result = OecdMetadata._parse_dimension_list(dsd)
assert result == [
{
"id": "PLAIN",
"position": 0,
"codelist_id": "",
"concept_id": "PLAIN",
"name": "PLAIN",
}
]
def test_dims_sorted_by_position(self):
"""Dimensions are sorted by their declared position."""
dsd = {
"dataStructureComponents": {
"dimensionList": {
"dimensions": [
{"id": "B", "position": 2, "names": {"en": "B"}},
{"id": "A", "position": 1, "names": {"en": "A"}},
]
}
}
}
result = OecdMetadata._parse_dimension_list(dsd)
assert [d["id"] for d in result] == ["A", "B"]
def test_name_uses_explicit_name_field(self):
"""When names dict is empty, falls back to the 'name' field."""
dsd = {
"dataStructureComponents": {
"dimensionList": {
"dimensions": [
{"id": "X", "name": "Fallback", "names": {}},
]
}
}
}
result = OecdMetadata._parse_dimension_list(dsd)
assert result[0]["name"] == "Fallback"
class TestParseAttributeList:
"""Coverage for LoaderMixin._parse_attribute_list."""
def test_empty(self):
"""No attribute list → empty result."""
assert OecdMetadata._parse_attribute_list({}) == []
def test_attr_with_and_without_enum(self):
"""Both enum-bearing and bare attributes are returned."""
dsd = {
"dataStructureComponents": {
"attributeList": {
"attributes": [
{
"id": "OBS_STATUS",
"localRepresentation": {
"enumeration": "urn:sdmx:org.sdmx.infomodel.codelist.Codelist=OECD:CL_OBS(1.0)"
},
},
{"id": "BARE"},
]
}
}
}
result = OecdMetadata._parse_attribute_list(dsd)
assert result[0]["codelist_id"] == "OECD:CL_OBS(1.0)"
assert result[1] == {"id": "BARE", "codelist_id": ""}