mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-29 13:32:08 +00:00
* merge branch develop with main * bump desktop version * fix the merge * sync Cargo.lock to desktop 2.0.0 * fix cli conftest.py --------- Co-authored-by: deeleeramone <>
1023 lines
38 KiB
Python
1023 lines
38 KiB
Python
"""Tests for progressive validation logic in ImfParamsBuilder and ImfQueryBuilder."""
|
|
|
|
# ruff: noqa: I001
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
class TestImfParamsBuilderInit:
|
|
"""Tests for ImfParamsBuilder initialization."""
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_valid_dataflow_initializes(self, mock_metadata_cls):
|
|
"""Valid dataflow should initialize successfully."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {
|
|
"structureRef": {"id": "DSD_BOP"},
|
|
"agencyID": "IMF",
|
|
}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [
|
|
{"id": "REF_AREA", "position": 1},
|
|
{"id": "INDICATOR", "position": 2},
|
|
{"id": "FREQ", "position": 3},
|
|
],
|
|
}
|
|
}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfParamsBuilder("BOP")
|
|
assert builder.dataflow_id == "BOP"
|
|
assert builder._dimensions == ["REF_AREA", "INDICATOR", "FREQ"]
|
|
assert builder.current_dimension == "REF_AREA"
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_invalid_dataflow_raises_keyerror(self, mock_metadata_cls):
|
|
"""Invalid dataflow should raise KeyError with helpful message."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {"BOP": {}, "IFS": {}}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
with pytest.raises(KeyError) as exc_info:
|
|
ImfParamsBuilder("INVALID")
|
|
|
|
assert "INVALID" in str(exc_info.value)
|
|
assert "Available dataflows" in str(exc_info.value)
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_dimensions_sorted_by_position(self, mock_metadata_cls):
|
|
"""Dimensions should be sorted by position."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"TEST": {
|
|
"structureRef": {"id": "DSD_TEST"},
|
|
"agencyID": "IMF",
|
|
}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_TEST": {
|
|
"id": "DSD_TEST",
|
|
"dimensions": [
|
|
{"id": "THIRD", "position": 3},
|
|
{"id": "FIRST", "position": 1},
|
|
{"id": "SECOND", "position": 2},
|
|
],
|
|
}
|
|
}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfParamsBuilder("TEST")
|
|
assert builder._dimensions == ["FIRST", "SECOND", "THIRD"]
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_time_period_excluded_from_dimensions(self, mock_metadata_cls):
|
|
"""TIME_PERIOD should be excluded from dimensions list."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"TEST": {
|
|
"structureRef": {"id": "DSD_TEST"},
|
|
}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_TEST": {
|
|
"dimensions": [
|
|
{"id": "REF_AREA", "position": 1},
|
|
{"id": "TIME_PERIOD", "position": 2},
|
|
{"id": "INDICATOR", "position": 3},
|
|
],
|
|
}
|
|
}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfParamsBuilder("TEST")
|
|
assert "TIME_PERIOD" not in builder._dimensions
|
|
assert builder._dimensions == ["REF_AREA", "INDICATOR"]
|
|
|
|
|
|
class TestDimensionSelection:
|
|
"""Tests for set_dimension and selection tracking."""
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_set_dimension_updates_selection(self, mock_metadata_cls):
|
|
"""Setting a dimension should update selections."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {"BOP": {"structureRef": {"id": "DSD_BOP"}}}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"dimensions": [
|
|
{"id": "REF_AREA", "position": 1},
|
|
{"id": "INDICATOR", "position": 2},
|
|
],
|
|
}
|
|
}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfParamsBuilder("BOP")
|
|
result = builder.set_dimension(("REF_AREA", "US"))
|
|
|
|
assert result["REF_AREA"] == "US"
|
|
assert result["INDICATOR"] is None
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_set_dimension_clears_downstream(self, mock_metadata_cls):
|
|
"""Setting an upstream dimension should clear downstream selections."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {"BOP": {"structureRef": {"id": "DSD_BOP"}}}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"dimensions": [
|
|
{"id": "REF_AREA", "position": 1},
|
|
{"id": "INDICATOR", "position": 2},
|
|
{"id": "FREQ", "position": 3},
|
|
],
|
|
}
|
|
}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfParamsBuilder("BOP")
|
|
builder._selections = {"REF_AREA": "US", "INDICATOR": "IND1", "FREQ": "A"}
|
|
|
|
result = builder.set_dimension(("REF_AREA", "GB"))
|
|
|
|
assert result["REF_AREA"] == "GB"
|
|
assert result["INDICATOR"] is None
|
|
assert result["FREQ"] is None
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_set_invalid_dimension_raises_keyerror(self, mock_metadata_cls):
|
|
"""Setting an invalid dimension should raise KeyError."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {"BOP": {"structureRef": {"id": "DSD_BOP"}}}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfParamsBuilder("BOP")
|
|
|
|
with pytest.raises(KeyError) as exc_info:
|
|
builder.set_dimension(("INVALID_DIM", "VALUE"))
|
|
|
|
assert "INVALID_DIM" in str(exc_info.value)
|
|
assert "Valid dimensions" in str(exc_info.value)
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_get_next_dimension_to_select(self, mock_metadata_cls):
|
|
"""get_next_dimension_to_select should return first unselected dimension."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {"BOP": {"structureRef": {"id": "DSD_BOP"}}}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"dimensions": [
|
|
{"id": "DIM1", "position": 1},
|
|
{"id": "DIM2", "position": 2},
|
|
{"id": "DIM3", "position": 3},
|
|
],
|
|
}
|
|
}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfParamsBuilder("BOP")
|
|
|
|
assert builder.get_next_dimension_to_select() == "DIM1"
|
|
|
|
builder.set_dimension(("DIM1", "VAL1"))
|
|
assert builder.get_next_dimension_to_select() == "DIM2"
|
|
|
|
builder.set_dimension(("DIM2", "VAL2"))
|
|
assert builder.get_next_dimension_to_select() == "DIM3"
|
|
|
|
builder.set_dimension(("DIM3", "VAL3"))
|
|
assert builder.get_next_dimension_to_select() is None
|
|
|
|
|
|
class TestGetOptionsForDimension:
|
|
"""Tests for get_options_for_dimension constraint checking."""
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_get_options_returns_available_values(self, mock_metadata_cls):
|
|
"""get_options_for_dimension should return available values from constraints."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [{"id": "REF_AREA", "values": ["US", "GB", "DE"]}]
|
|
}
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfParamsBuilder("BOP")
|
|
options = builder.get_options_for_dimension("REF_AREA")
|
|
|
|
assert len(options) == 3
|
|
assert {"label": "US", "value": "US"} in options
|
|
assert {"label": "GB", "value": "GB"} in options
|
|
assert {"label": "DE", "value": "DE"} in options
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_get_options_uses_codelist_labels(self, mock_metadata_cls):
|
|
"""get_options_for_dimension should use codelist for human-readable labels."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [{"id": "REF_AREA", "values": ["US", "GB"]}]
|
|
}
|
|
mock_metadata._resolve_codelist_id.return_value = "CL_AREA"
|
|
mock_metadata._get_codelist_map.return_value = {
|
|
"US": "United States",
|
|
"GB": "United Kingdom",
|
|
}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfParamsBuilder("BOP")
|
|
options = builder.get_options_for_dimension("REF_AREA")
|
|
|
|
assert {"label": "United States", "value": "US"} in options
|
|
assert {"label": "United Kingdom", "value": "GB"} in options
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_get_options_for_invalid_dimension_raises(self, mock_metadata_cls):
|
|
"""get_options_for_dimension with invalid dimension should raise ValueError."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {"BOP": {"structureRef": {"id": "DSD_BOP"}}}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfParamsBuilder("BOP")
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
builder.get_options_for_dimension("INVALID")
|
|
|
|
assert "INVALID" in str(exc_info.value)
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_get_options_uses_wildcards_for_unselected(self, mock_metadata_cls):
|
|
"""Unselected dimensions should use '*' wildcard in key."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [
|
|
{"id": "DIM1", "position": 1},
|
|
{"id": "DIM2", "position": 2},
|
|
{"id": "DIM3", "position": 3},
|
|
],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {"key_values": []}
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfParamsBuilder("BOP")
|
|
builder.set_dimension(("DIM1", "VAL1"))
|
|
builder.get_options_for_dimension("DIM2")
|
|
|
|
call_args = mock_metadata.get_available_constraints.call_args
|
|
assert call_args.kwargs["key"] == "VAL1.*.*"
|
|
|
|
|
|
class TestValidateDimensionConstraints:
|
|
"""Tests for ImfQueryBuilder.validate_dimension_constraints."""
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_valid_values_pass_validation(self, mock_metadata_cls):
|
|
"""Valid dimension values should pass validation without error."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [
|
|
{"id": "REF_AREA", "position": 1},
|
|
{"id": "INDICATOR", "position": 2},
|
|
],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [
|
|
{"id": "REF_AREA", "values": ["US", "GB"]},
|
|
{"id": "INDICATOR", "values": ["IND1", "IND2"]},
|
|
]
|
|
}
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
builder.validate_dimension_constraints(
|
|
dataflow="BOP", REF_AREA="US", INDICATOR="IND1"
|
|
)
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_invalid_value_raises_valueerror(self, mock_metadata_cls):
|
|
"""Invalid dimension value should raise ValueError with details."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [
|
|
{"id": "REF_AREA", "position": 1},
|
|
],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [
|
|
{"id": "REF_AREA", "values": ["US", "GB"]},
|
|
]
|
|
}
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
builder.validate_dimension_constraints(
|
|
dataflow="BOP", REF_AREA="INVALID_COUNTRY"
|
|
)
|
|
|
|
error_msg = str(exc_info.value)
|
|
assert "INVALID_COUNTRY" in error_msg
|
|
assert "REF_AREA" in error_msg
|
|
assert "available values" in error_msg.lower()
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_wildcard_always_valid(self, mock_metadata_cls):
|
|
"""Wildcard '*' should always be accepted."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {"key_values": []}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
builder.validate_dimension_constraints(dataflow="BOP", REF_AREA="*")
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_comma_separated_values_validated(self, mock_metadata_cls):
|
|
"""Comma-separated values should all be validated."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [
|
|
{"id": "REF_AREA", "values": ["US", "GB", "DE"]},
|
|
]
|
|
}
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
builder.validate_dimension_constraints(dataflow="BOP", REF_AREA="US,GB")
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_comma_separated_with_invalid_raises(self, mock_metadata_cls):
|
|
"""Comma-separated with one invalid value should raise."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [
|
|
{"id": "REF_AREA", "values": ["US", "GB"]},
|
|
]
|
|
}
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
builder.validate_dimension_constraints(
|
|
dataflow="BOP", REF_AREA="US,INVALID"
|
|
)
|
|
|
|
assert "INVALID" in str(exc_info.value)
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_plus_separated_values_validated(self, mock_metadata_cls):
|
|
"""Plus-separated values should all be validated."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [
|
|
{"id": "REF_AREA", "values": ["US", "GB", "DE"]},
|
|
]
|
|
}
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
builder.validate_dimension_constraints(dataflow="BOP", REF_AREA="US+GB+DE")
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_empty_value_skipped(self, mock_metadata_cls):
|
|
"""Empty/None values should be skipped in validation."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
builder.validate_dimension_constraints(dataflow="BOP", REF_AREA="")
|
|
builder.validate_dimension_constraints(dataflow="BOP", REF_AREA=None)
|
|
|
|
|
|
class TestTimePeriodValidation:
|
|
"""Tests for time period range validation."""
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_start_date_after_available_range_raises(self, mock_metadata_cls):
|
|
"""Start date after available data range should raise ValueError."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [{"id": "REF_AREA", "values": ["US"]}],
|
|
"full_response": {
|
|
"data": {
|
|
"dataConstraints": [
|
|
{
|
|
"annotations": [
|
|
{"id": "time_period_start", "title": "2000-01"},
|
|
{"id": "time_period_end", "title": "2023-12"},
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
}
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
builder.validate_dimension_constraints(
|
|
dataflow="BOP", REF_AREA="US", start_date="2025-01"
|
|
)
|
|
|
|
error_msg = str(exc_info.value)
|
|
assert "2025-01" in error_msg
|
|
assert "after" in error_msg.lower()
|
|
assert "2023-12" in error_msg
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_end_date_before_available_range_raises(self, mock_metadata_cls):
|
|
"""End date before available data range should raise ValueError."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [{"id": "REF_AREA", "values": ["US"]}],
|
|
"full_response": {
|
|
"data": {
|
|
"dataConstraints": [
|
|
{
|
|
"annotations": [
|
|
{"id": "time_period_start", "title": "2000-01"},
|
|
{"id": "time_period_end", "title": "2023-12"},
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
}
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
builder.validate_dimension_constraints(
|
|
dataflow="BOP", REF_AREA="US", end_date="1990-01"
|
|
)
|
|
|
|
error_msg = str(exc_info.value)
|
|
assert "1990-01" in error_msg
|
|
assert "before" in error_msg.lower()
|
|
assert "2000-01" in error_msg
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_valid_date_range_passes(self, mock_metadata_cls):
|
|
"""Valid date range within available data should pass."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [{"id": "REF_AREA", "values": ["US"]}],
|
|
"full_response": {
|
|
"data": {
|
|
"dataConstraints": [
|
|
{
|
|
"annotations": [
|
|
{"id": "time_period_start", "title": "2000-01"},
|
|
{"id": "time_period_end", "title": "2023-12"},
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
}
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
builder.validate_dimension_constraints(
|
|
dataflow="BOP", REF_AREA="US", start_date="2010-01", end_date="2020-12"
|
|
)
|
|
|
|
|
|
class TestProgressiveConstraintPropagation:
|
|
"""Tests for how prior selections affect subsequent option availability."""
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_prior_selection_constrains_options(self, mock_metadata_cls):
|
|
"""Prior selections should constrain available options for subsequent dimensions."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [
|
|
{"id": "REF_AREA", "position": 1},
|
|
{"id": "INDICATOR", "position": 2},
|
|
],
|
|
}
|
|
}
|
|
|
|
call_count = [0]
|
|
|
|
def mock_constraints(dataflow_id, key, component_id):
|
|
call_count[0] += 1
|
|
if component_id == "INDICATOR" and "US" in key:
|
|
return {"key_values": [{"id": "INDICATOR", "values": ["IND1", "IND2"]}]}
|
|
elif component_id == "INDICATOR" and "GB" in key:
|
|
return {
|
|
"key_values": [
|
|
{"id": "INDICATOR", "values": ["IND1", "IND2", "IND3", "IND4"]}
|
|
]
|
|
}
|
|
return {"key_values": []}
|
|
|
|
mock_metadata.get_available_constraints.side_effect = mock_constraints
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfParamsBuilder("BOP")
|
|
|
|
builder.set_dimension(("REF_AREA", "US"))
|
|
us_options = builder.get_options_for_dimension("INDICATOR")
|
|
assert len(us_options) == 2
|
|
|
|
builder._selections = {"REF_AREA": None, "INDICATOR": None}
|
|
builder.set_dimension(("REF_AREA", "GB"))
|
|
gb_options = builder.get_options_for_dimension("INDICATOR")
|
|
assert len(gb_options) == 4
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_validate_considers_prior_selections_in_error(self, mock_metadata_cls):
|
|
"""Error message should include prior selections for context."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [
|
|
{"id": "REF_AREA", "position": 1},
|
|
{"id": "INDICATOR", "position": 2},
|
|
],
|
|
}
|
|
}
|
|
|
|
def mock_constraints(dataflow_id, key, component_id):
|
|
if component_id == "REF_AREA":
|
|
return {"key_values": [{"id": "REF_AREA", "values": ["US", "GB"]}]}
|
|
elif component_id == "INDICATOR":
|
|
return {"key_values": [{"id": "INDICATOR", "values": ["IND1"]}]}
|
|
return {"key_values": []}
|
|
|
|
mock_metadata.get_available_constraints.side_effect = mock_constraints
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
builder.validate_dimension_constraints(
|
|
dataflow="BOP", REF_AREA="US", INDICATOR="IND_NOT_AVAILABLE_FOR_US"
|
|
)
|
|
|
|
error_msg = str(exc_info.value)
|
|
assert "IND_NOT_AVAILABLE_FOR_US" in error_msg
|
|
assert "REF_AREA" in error_msg
|
|
|
|
|
|
class TestValidationEdgeCases:
|
|
"""Edge cases in progressive validation."""
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_no_key_values_in_constraints_response(self, mock_metadata_cls):
|
|
"""Empty key_values should result in no available values."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {"key_values": []}
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfParamsBuilder("BOP")
|
|
options = builder.get_options_for_dimension("REF_AREA")
|
|
|
|
assert options == []
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_missing_dimension_in_constraints_response(self, mock_metadata_cls):
|
|
"""Missing dimension in key_values should return empty options."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [
|
|
{"id": "REF_AREA", "position": 1},
|
|
{"id": "INDICATOR", "position": 2},
|
|
],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [{"id": "REF_AREA", "values": ["US"]}]
|
|
}
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfParamsBuilder("BOP")
|
|
options = builder.get_options_for_dimension("INDICATOR")
|
|
|
|
assert options == []
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_list_input_validated(self, mock_metadata_cls):
|
|
"""List input for dimension value should be validated."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [{"id": "REF_AREA", "values": ["US", "GB"]}]
|
|
}
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
builder.validate_dimension_constraints(dataflow="BOP", REF_AREA=["US", "GB"])
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_whitespace_in_comma_separated_trimmed(self, mock_metadata_cls):
|
|
"""Whitespace around comma-separated values should be trimmed."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [{"id": "REF_AREA", "values": ["US", "GB"]}]
|
|
}
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
builder.validate_dimension_constraints(dataflow="BOP", REF_AREA=" US , GB ")
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_metadata_keyerror_warns_instead_of_raising(self, mock_metadata_cls):
|
|
"""KeyError from metadata should warn, not raise."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
import warnings
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {} # No dataflows
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
|
|
with warnings.catch_warnings(record=True) as w:
|
|
warnings.simplefilter("always")
|
|
builder.validate_dimension_constraints(
|
|
dataflow="NONEXISTENT", REF_AREA="US"
|
|
)
|
|
|
|
assert len(w) == 1
|
|
assert "could not validate" in str(w[0].message).lower()
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_no_time_constraints_in_response(self, mock_metadata_cls):
|
|
"""Missing time period constraints should not cause errors."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [{"id": "REF_AREA", "values": ["US"]}],
|
|
}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
builder.validate_dimension_constraints(dataflow="BOP", REF_AREA="US")
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_irfcl_compatibility_filtering(self, mock_metadata_cls):
|
|
"""Test that IRFCL hierarchies are filtered for compatibility."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"IRFCL": {"structureRef": {"id": "DSD_IRFCL"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_IRFCL": {
|
|
"id": "DSD_IRFCL",
|
|
"dimensions": [{"id": "INDICATOR", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [{"id": "INDICATOR", "values": ["RAF_USD"]}]
|
|
}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
|
|
builder.validate_dimension_constraints(dataflow="IRFCL", INDICATOR="RAF_USD")
|
|
|
|
with pytest.raises(ValueError):
|
|
builder.validate_dimension_constraints(
|
|
dataflow="IRFCL", INDICATOR="INCOMPATIBLE"
|
|
)
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_long_multi_code_input_wildcard(self, mock_metadata_cls):
|
|
"""Test that long multi-code inputs are treated as wildcards during validation."""
|
|
from openbb_imf.utils.query_builder import ImfQueryBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [{"id": "REF_AREA", "values": ["US", "GB", "FR"]}]
|
|
}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
|
|
long_list = ["US"] * 50
|
|
|
|
builder.validate_dimension_constraints(dataflow="BOP", REF_AREA=long_list)
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [{"id": "REF_AREA", "position": 1}],
|
|
}
|
|
}
|
|
mock_metadata.get_available_constraints.return_value = {
|
|
"key_values": [{"id": "REF_AREA", "values": ["US"]}],
|
|
"full_response": {"data": {}},
|
|
}
|
|
mock_metadata._resolve_codelist_id.return_value = None
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfQueryBuilder()
|
|
builder.validate_dimension_constraints(
|
|
dataflow="BOP", REF_AREA="US", start_date="2020-01", end_date="2023-12"
|
|
)
|
|
|
|
|
|
class TestBuildUrl:
|
|
"""Tests for ImfParamsBuilder.build_url."""
|
|
|
|
@patch("openbb_imf.utils.query_builder.ImfMetadata")
|
|
def test_build_url_uses_selections(self, mock_metadata_cls):
|
|
"""build_url should use current selections."""
|
|
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
|
|
|
|
mock_metadata = MagicMock()
|
|
mock_metadata.dataflows = {
|
|
"BOP": {"structureRef": {"id": "DSD_BOP"}, "agencyID": "IMF"}
|
|
}
|
|
mock_metadata.datastructures = {
|
|
"DSD_BOP": {
|
|
"id": "DSD_BOP",
|
|
"dimensions": [
|
|
{"id": "REF_AREA", "position": 1},
|
|
{"id": "INDICATOR", "position": 2},
|
|
],
|
|
}
|
|
}
|
|
mock_metadata_cls.return_value = mock_metadata
|
|
|
|
builder = ImfParamsBuilder("BOP")
|
|
builder.set_dimension(("REF_AREA", "US"))
|
|
builder.set_dimension(("INDICATOR", "IND1"))
|
|
|
|
with patch.object(
|
|
builder._builder, "build_url", return_value="http://test.url"
|
|
) as mock_build:
|
|
result = builder.build_url(start_date="2020-01", end_date="2023-12")
|
|
|
|
mock_build.assert_called_once_with(
|
|
dataflow="BOP",
|
|
start_date="2020-01",
|
|
end_date="2023-12",
|
|
REF_AREA="US",
|
|
INDICATOR="IND1",
|
|
)
|
|
assert result == "http://test.url"
|