mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-12 12:22:35 +00:00
* refactor bls for v5, add 100 widgets * some CI fixes * no python 3.15 * fix missing dev group in pyproject.toml * more pyproject fallout * action
212 lines
7.8 KiB
Python
212 lines
7.8 KiB
Python
"""Tests for the ``BlsSearchFetcher``."""
|
|
|
|
import pytest
|
|
from openbb_core.provider.utils.errors import EmptyDataError
|
|
|
|
from openbb_bls.models.search import (
|
|
BlsSearchData,
|
|
BlsSearchFetcher,
|
|
BlsSearchQueryParams,
|
|
_fill_titles,
|
|
_is_blank_title,
|
|
_rank_by_recency,
|
|
_synthesize_title,
|
|
)
|
|
|
|
|
|
def test_rank_by_recency_orders_current_first():
|
|
"""Series with the most recent ``end_year`` (then period) sort first."""
|
|
from pandas import DataFrame
|
|
|
|
df = DataFrame(
|
|
[
|
|
{"series_id": "OLD", "end_year": "2003", "end_period": "M12"},
|
|
{"series_id": "NEW", "end_year": "2026", "end_period": "M04"},
|
|
{"series_id": "MID", "end_year": "2015", "end_period": "M06"},
|
|
]
|
|
)
|
|
assert list(_rank_by_recency(df)["series_id"]) == ["NEW", "MID", "OLD"]
|
|
|
|
|
|
def test_rank_by_recency_pushes_missing_year_to_bottom():
|
|
"""Missing / non-numeric ``end_year`` is treated as oldest."""
|
|
from pandas import DataFrame
|
|
|
|
df = DataFrame(
|
|
[
|
|
{"series_id": "A", "end_year": None, "end_period": "M01"},
|
|
{"series_id": "B", "end_year": "2026", "end_period": "M01"},
|
|
{"series_id": "C", "end_year": "bad", "end_period": "M01"},
|
|
]
|
|
)
|
|
assert _rank_by_recency(df).iloc[0]["series_id"] == "B"
|
|
|
|
|
|
def test_rank_by_recency_without_end_period_column():
|
|
"""Ranking still works when only ``end_year`` is present."""
|
|
from pandas import DataFrame
|
|
|
|
df = DataFrame(
|
|
[{"series_id": "A", "end_year": "2000"}, {"series_id": "B", "end_year": "2024"}]
|
|
)
|
|
assert list(_rank_by_recency(df)["series_id"]) == ["B", "A"]
|
|
|
|
|
|
def test_rank_by_recency_no_end_year_is_noop():
|
|
"""A frame without ``end_year`` is returned unchanged."""
|
|
from pandas import DataFrame
|
|
|
|
df = DataFrame([{"series_id": "A"}, {"series_id": "B"}])
|
|
assert list(_rank_by_recency(df)["series_id"]) == ["A", "B"]
|
|
|
|
|
|
def test_is_blank_title_branches():
|
|
"""``_is_blank_title`` flags None / placeholder strings, keeps real text."""
|
|
assert _is_blank_title(None)
|
|
assert _is_blank_title("")
|
|
assert _is_blank_title(" ")
|
|
assert _is_blank_title("NaN")
|
|
assert _is_blank_title("-")
|
|
assert not _is_blank_title("All items in U.S. city average")
|
|
|
|
|
|
def test_synthesize_title_combines_resolved_fields():
|
|
"""A title is built in reading order with the seasonal-adjustment suffix."""
|
|
row = {
|
|
"sector_code": "Manufacturing, Nondurable Goods",
|
|
"measure_code": "Output per worker",
|
|
"duration_code": "% Change from previous quarter",
|
|
"seasonal": "S",
|
|
}
|
|
assert _synthesize_title(row) == (
|
|
"Manufacturing, Nondurable Goods — Output per worker"
|
|
" — % Change from previous quarter (seasonally adjusted)"
|
|
)
|
|
|
|
|
|
def test_synthesize_title_dedupes_skips_blanks_and_marks_unadjusted():
|
|
"""Duplicate / blank parts are dropped; ``U`` yields the unadjusted suffix."""
|
|
row = {
|
|
"industry_code": "Total Nonfarm",
|
|
"region_code": "Total Nonfarm", # duplicate -> collapsed
|
|
"area_code": "-", # blank -> skipped
|
|
"dataelement_code": "Hires",
|
|
"seasonal": "U",
|
|
}
|
|
assert _synthesize_title(row) == "Total Nonfarm — Hires (not seasonally adjusted)"
|
|
|
|
|
|
def test_synthesize_title_seasonal_only_and_empty():
|
|
"""With no descriptive fields, fall back to the seasonal label or None."""
|
|
assert _synthesize_title({"seasonal": "S"}) == "Seasonally adjusted"
|
|
assert _synthesize_title({"sector_code": "-", "seasonal": ""}) is None
|
|
|
|
|
|
def test_fill_titles_fills_blanks_and_preserves_existing():
|
|
"""``_fill_titles`` keeps real titles and synthesizes the missing ones."""
|
|
from pandas import DataFrame
|
|
|
|
df = DataFrame(
|
|
[
|
|
{"series_id": "A", "series_title": "Real title", "sector_code": "X"},
|
|
{
|
|
"series_id": "B",
|
|
"series_title": None,
|
|
"sector_code": "Mining",
|
|
"dataelement_code": "Hires",
|
|
"seasonal": "U",
|
|
},
|
|
]
|
|
)
|
|
out = _fill_titles(df)
|
|
assert out.loc[0, "series_title"] == "Real title"
|
|
assert out.loc[1, "series_title"] == "Mining — Hires (not seasonally adjusted)"
|
|
|
|
|
|
def test_fill_titles_adds_missing_column():
|
|
"""A category without a ``series_title`` column gets one synthesized."""
|
|
from pandas import DataFrame
|
|
|
|
df = DataFrame(
|
|
[{"series_id": "A", "industry_code": "Total Nonfarm", "seasonal": "S"}]
|
|
)
|
|
out = _fill_titles(df)
|
|
assert out.loc[0, "series_title"] == "Total Nonfarm (seasonally adjusted)"
|
|
|
|
|
|
def test_transform_query_validates_category():
|
|
"""``transform_query`` builds the params model with a valid category."""
|
|
params = BlsSearchFetcher.transform_query({"category": "cpi", "query": "Food"})
|
|
assert isinstance(params, BlsSearchQueryParams)
|
|
assert params.category == "cpi"
|
|
assert params.query == "Food"
|
|
|
|
|
|
def test_transform_query_rejects_invalid_category():
|
|
"""Pydantic rejects categories outside ``SURVEY_CATEGORIES``."""
|
|
from pydantic import ValidationError
|
|
|
|
with pytest.raises(ValidationError):
|
|
BlsSearchFetcher.transform_query({"category": "not-a-cat"})
|
|
|
|
|
|
def test_extract_data_no_query_returns_all_rows(stub_cache_path):
|
|
"""An empty query string returns every row in the category."""
|
|
query = BlsSearchFetcher.transform_query({"category": "cpi", "query": ""})
|
|
rows = BlsSearchFetcher.extract_data(query, credentials=None)
|
|
assert len(rows) == 2
|
|
assert {r["series_id"] for r in rows} == {"CUUR0000SA0", "CUUR0000SAF1"}
|
|
|
|
|
|
def test_extract_data_applies_and_filter(stub_cache_path):
|
|
"""Semicolon-separated terms apply AND across rows."""
|
|
query = BlsSearchFetcher.transform_query({"category": "cpi", "query": "Food;city"})
|
|
rows = BlsSearchFetcher.extract_data(query, credentials=None)
|
|
assert len(rows) == 1
|
|
assert rows[0]["series_id"] == "CUUR0000SAF1"
|
|
|
|
|
|
def test_extract_data_empty_match_raises(stub_cache_path):
|
|
"""A non-matching query bubbles up as ``EmptyDataError``."""
|
|
query = BlsSearchFetcher.transform_query(
|
|
{"category": "cpi", "query": "no-such-term"}
|
|
)
|
|
with pytest.raises(EmptyDataError):
|
|
BlsSearchFetcher.extract_data(query, credentials=None)
|
|
|
|
|
|
def test_extract_data_include_extras_keeps_all_columns(stub_cache_path):
|
|
"""``include_extras=True`` returns every column from the series table."""
|
|
query = BlsSearchFetcher.transform_query(
|
|
{"category": "cpi", "query": "Food", "include_extras": True}
|
|
)
|
|
rows = BlsSearchFetcher.extract_data(query, credentials=None)
|
|
assert "area_code" in rows[0]
|
|
|
|
|
|
def test_extract_data_default_trims_columns(stub_cache_path):
|
|
"""Without extras the result is trimmed to the canonical 3 columns."""
|
|
query = BlsSearchFetcher.transform_query({"category": "cpi", "query": "Food"})
|
|
rows = BlsSearchFetcher.extract_data(query, credentials=None)
|
|
assert set(rows[0]) == {"series_id", "series_title", "survey_name"}
|
|
|
|
|
|
def test_transform_data_attaches_codes_when_requested(stub_cache_path):
|
|
"""``include_code_map=True`` populates the AnnotatedResult metadata."""
|
|
query = BlsSearchFetcher.transform_query(
|
|
{"category": "cpi", "query": "Food", "include_code_map": True}
|
|
)
|
|
rows = BlsSearchFetcher.extract_data(query, credentials=None)
|
|
result = BlsSearchFetcher.transform_data(query, rows)
|
|
assert result.metadata == {"cu": {"area_code": {"0000": "U.S. city average"}}}
|
|
assert all(isinstance(r, BlsSearchData) for r in result.result)
|
|
|
|
|
|
def test_transform_data_without_code_map(stub_cache_path):
|
|
"""Default ``include_code_map=False`` returns empty metadata."""
|
|
query = BlsSearchFetcher.transform_query({"category": "cpi", "query": "Food"})
|
|
rows = BlsSearchFetcher.extract_data(query, credentials=None)
|
|
result = BlsSearchFetcher.transform_data(query, rows)
|
|
assert result.metadata == {}
|
|
assert len(result.result) == 1
|