import json
import lzma
import pytest
import requests
from openbb_ecb.utils import generate_cache as gc
_NS = (
'xmlns:mes="http://www.sdmx.org/resources/sdmxml/schemas/v2_1/message" '
'xmlns:str="http://www.sdmx.org/resources/sdmxml/schemas/v2_1/structure" '
'xmlns:com="http://www.sdmx.org/resources/sdmxml/schemas/v2_1/common"'
)
def _doc(body: str) -> bytes:
return f"{body}".encode()
DATAFLOWS = _doc(
""
''
'Exchange Rates'
''
""
''
""
""
)
DSDS = _doc(
""
''
""
''
''
''
""
''
""
''
""
""
""
""
)
CODELISTS = _doc(
""
''
'Daily'
''
""
""
""
""
)
CONCEPTS = _doc(
""
''
'Frequency'
""
""
)
CATSCHEME = _doc(
""
''
'Monetary'
'desc'
'Sub'
""
'NoId'
""
)
CATEGORISATION = _doc(
""
''
''
''
''
''
''
''
''
""
""
)
CONTENT_CONSTRAINTS = _doc(
""
''
''
""
''
'DM'
'USD'
''
""
''
''
""
''
'D'
""
''
''
'D'
""
''
''
""
''
''
""
''
""
""
)
_PUB_TABLE_HTML = (
"
Test Table | ECB Data Portal"
'"
'"
)
_PUB_CATEGORY_HTML = 't'
_DSET_INFO_HTML = (
"Exchange Rates - EXR | ECB Data Portal"
'CSV'
'"
''
''
""
)
_CONCEPTS_INDEX_HTML = (
'xy'
)
_CONCEPT_HTML = (
"Exchange rates | ECB Data Portal"
'x'
)
class _FakeResp:
def __init__(self, status_code=200, content=b""):
self.status_code = status_code
self.content = content
@property
def text(self):
return self.content.decode("utf-8")
def raise_for_status(self):
if self.status_code >= 400:
raise requests.HTTPError(str(self.status_code))
_HTML_ROUTES = (
(("/data/concepts/", "data-information"), _CONCEPT_HTML),
(("/data/concepts",), _CONCEPTS_INDEX_HTML),
(("/data/datasets/", "data-information"), _DSET_INFO_HTML),
(("/data/publications/",), _PUB_TABLE_HTML),
(("/publications/",), _PUB_CATEGORY_HTML),
)
_XML_ROUTES = {
"/data/BSI/": b"KEY,TIME_PERIOD,OBS_VALUE\nBSI.M.U2.X,2024-01,1.0\n",
"/dataflow/": DATAFLOWS,
"/datastructure/": DSDS,
"/codelist/": CODELISTS,
"/conceptscheme/": CONCEPTS,
"/categoryscheme/": CATSCHEME,
"/categorisation/": CATEGORISATION,
"/contentconstraint/": CONTENT_CONSTRAINTS,
}
def _dispatch(url, **kwargs):
for fragments, body in _HTML_ROUTES:
if all(fragment in url for fragment in fragments):
return _FakeResp(200, body.encode())
for fragment, body in _XML_ROUTES.items():
if fragment in url:
return _FakeResp(200, body)
return _FakeResp(404, b"")
@pytest.fixture
def _mock_session(monkeypatch):
monkeypatch.setattr(gc._session, "get", _dispatch)
monkeypatch.setattr(gc.time, "sleep", lambda *a, **k: None)
def test_get_success(_mock_session):
assert gc._get(f"{gc.BASE_URL}/dataflow/ECB") is not None
def test_get_404_returns_none(monkeypatch):
monkeypatch.setattr(gc._session, "get", lambda *a, **k: _FakeResp(404))
assert gc._get("http://x") is None
def test_get_429_then_success(monkeypatch):
monkeypatch.setattr(gc.time, "sleep", lambda *a, **k: None)
seq = [_FakeResp(429), _FakeResp(200, DATAFLOWS)]
monkeypatch.setattr(gc._session, "get", lambda *a, **k: seq.pop(0))
assert gc._get("http://x") is not None
def test_get_retries_then_raises(monkeypatch):
monkeypatch.setattr(gc.time, "sleep", lambda *a, **k: None)
def _boom(*a, **k):
raise requests.ConnectionError("down")
monkeypatch.setattr(gc._session, "get", _boom)
with pytest.raises(requests.RequestException):
gc._get("http://x", retries=2)
def test_get_all_429_exhausts(monkeypatch):
monkeypatch.setattr(gc.time, "sleep", lambda *a, **k: None)
monkeypatch.setattr(gc._session, "get", lambda *a, **k: _FakeResp(429))
with pytest.raises(requests.RequestException):
gc._get("http://x", retries=2)
def test_en_helper():
import xml.etree.ElementTree as ET
assert gc._en(None, "Name") == ""
elem = ET.fromstring(
f""
'Bonjour'
'Hello'
)
assert gc._en(elem, "Name") == "Hello"
only_fr = ET.fromstring(
f'Bonjour'
)
assert gc._en(only_fr, "Name") == "Bonjour"
def test_fetch_functions(_mock_session):
dataflows = gc.fetch_dataflows()
assert dataflows["EXR"]["dsd_id"] == "ECB_EXR1"
assert dataflows["NOSTRUCT"]["dsd_id"] is None
dsds = gc.fetch_datastructures()
assert dsds["ECB_EXR1"]["dimensions"][0]["codelist_id"] == "CL_FREQ"
assert dsds["ECB_EXR1"]["time_dimension"] == {"id": "TIME_PERIOD"}
codelists = gc.fetch_codelists()
assert codelists["CL_FREQ"] == {"D": "Daily", "X": "X"}
concepts = gc.fetch_concepts()
assert concepts["FREQ"] == "Frequency"
categories, df_to_cat, cat_to_df = gc.fetch_categories()
assert categories["01"]["name"] == "Monetary"
assert categories["0101"]["name"] == "Sub"
assert df_to_cat == {"EXR": ["01"]}
assert cat_to_df == {"01": ["EXR"]}
def test_fetch_functions_empty(monkeypatch):
monkeypatch.setattr(gc._session, "get", lambda *a, **k: _FakeResp(404))
monkeypatch.setattr(gc.time, "sleep", lambda *a, **k: None)
assert gc.fetch_dataflows() == {}
assert gc.fetch_datastructures() == {}
assert gc.fetch_codelists() == {}
assert gc.fetch_concepts() == {}
assert gc.fetch_categories() == ({}, {}, {})
assert gc.fetch_presentation_tables() == {}
assert gc.fetch_content_constraints() == {}
assert gc.fetch_dataflow_info(["X"]) == {}
assert gc.fetch_portal_concepts(set()) == {}
def test_fetch_content_constraints(_mock_session):
out = gc.fetch_content_constraints()
assert out == {"EXR": {"FREQ": ["D", "M"], "CURRENCY": ["USD"]}}
def test_get_text_success(monkeypatch):
monkeypatch.setattr(
gc._session, "get", lambda *a, **k: _FakeResp(200, b"hi
")
)
assert gc._get_text("http://x") == "hi
"
def test_get_text_non_200(monkeypatch):
monkeypatch.setattr(gc._session, "get", lambda *a, **k: _FakeResp(404))
assert gc._get_text("http://x") == ""
def test_get_text_retry_then_success(monkeypatch):
monkeypatch.setattr(gc.time, "sleep", lambda *a, **k: None)
seq = [_FakeResp(503), _FakeResp(200, b"ok")]
monkeypatch.setattr(gc._session, "get", lambda *a, **k: seq.pop(0))
assert gc._get_text("http://x") == "ok"
def test_get_text_all_5xx(monkeypatch):
monkeypatch.setattr(gc.time, "sleep", lambda *a, **k: None)
monkeypatch.setattr(gc._session, "get", lambda *a, **k: _FakeResp(500))
assert gc._get_text("http://x", retries=2) == ""
def test_get_text_request_error(monkeypatch):
monkeypatch.setattr(gc.time, "sleep", lambda *a, **k: None)
calls = {"n": 0}
def _boom(*a, **k):
calls["n"] += 1
raise requests.ConnectionError("down")
monkeypatch.setattr(gc._session, "get", _boom)
assert gc._get_text("http://x", retries=2) == ""
assert calls["n"] == 2
def test_clean_html():
assert gc._clean_html("Hi there\nx") == "Hi there x"
def test_crawl_publication_tree(monkeypatch):
category_html = (
't'
'a'
'e'
)
pages = {f"/publications/{c}": category_html for c in gc.PUBLICATION_CATEGORIES}
pages["/publications/money-credit-and-banking/1"] = (
't'
'b'
)
pages["/publications/money-credit-and-banking/2"] = (
'a'
)
monkeypatch.setattr(
gc, "_get_text", lambda url: pages.get(url.replace(gc.PORTAL_URL, ""), "")
)
assert gc._crawl_publication_tree() == {"TBL01", "TBL02"}
_PARSE_PAGES = {
"GOOD": _PUB_TABLE_HTML,
"NOSCRIPT": "X | ECBno settings
",
"BADJSON": '',
"NOKEYS": '',
"NOTITLE": '',
"ONECRUMB": "Solo | ECB"
'"
'',
}
def test_parse_publication_table(monkeypatch):
monkeypatch.setattr(
gc, "_get_text", lambda url: _PARSE_PAGES.get(url.rsplit("/", 1)[-1], "")
)
good = gc._parse_publication_table("GOOD")
assert good["title"] == "Test Table"
assert good["category"] == "Money, credit and banking"
assert good["subcategory"] == "Monetary aggregates"
assert good["rows"] == [
{"flow": "BSI", "key": "M.U2.X"},
{"flow": "BSI", "key": "M.U2.Y"},
]
assert gc._parse_publication_table("MISSING") is None
assert gc._parse_publication_table("NOSCRIPT") is None
assert gc._parse_publication_table("BADJSON") is None
assert gc._parse_publication_table("NOKEYS") is None
notitle = gc._parse_publication_table("NOTITLE")
assert notitle["title"] == "NOTITLE"
assert notitle["category"] == "" and notitle["subcategory"] == ""
one = gc._parse_publication_table("ONECRUMB")
assert one["category"] == "Solo Category" and one["subcategory"] == ""
def test_fetch_presentation_tables(_mock_session):
tables = gc.fetch_presentation_tables()
assert set(tables) == {"TBL01"}
assert tables["TBL01"]["title"] == "Test Table"
assert tables["TBL01"]["rows"][0] == {"flow": "BSI", "key": "M.U2.X"}
def test_fetch_presentation_tables_drops_unparsed(monkeypatch):
monkeypatch.setattr(gc, "_crawl_publication_tree", lambda: {"A", "B"})
monkeypatch.setattr(
gc,
"_parse_publication_table",
lambda tid: {"id": tid, "rows": [1]} if tid == "A" else None,
)
assert set(gc.fetch_presentation_tables()) == {"A"}
def test_sanitize_dsetinfo():
out = gc._sanitize_dsetinfo(
'Hi L
'
'z Lz'
def test_extract_dataset_info():
info = gc._extract_dataset_info(_DSET_INFO_HTML)
assert info["title"] == "Exchange Rates - EXR"
assert [f["label"] for f in info["fields"]] == ["Scope", "Legal"]
scope = info["fields"][0]["html"]
assert scope == '
Summary EXR.
'
assert "