[Feature] Refactor IMF for SDMX 3 (#7282)

* start the pr

* codespell

* lint

* lint

* lint

* more lint

* allow parameterized dimensions to be set by 'dimension_values' pairs

* missed file in commit

* grammar police

* more lint

* frequency description

* fix nasdaq test

* new lint

* more touchup

* readme

* readme

* fix integration tests

* fix more integration tests

* list_indicators_by_dataflow is redundant

* unused import

* remove integration test for removed utils endpoint

* update dependencies

* add grouping mechanism for port info widgets.

* fix nan string representation propagating in table_presentation

* more presentation table updates

* update lock

* cli lock file
This commit is contained in:
Danglewood
2025-12-27 08:35:03 -08:00
committed by GitHub
parent 97cae93800
commit 0757e157fc
56 changed files with 19688 additions and 2462 deletions

View File

@@ -9,7 +9,7 @@ from openbb_core.provider.utils.descriptions import (
DATA_DESCRIPTIONS,
QUERY_DESCRIPTIONS,
)
from pydantic import Field, field_validator
from pydantic import Field
class ConsumerPriceIndexQueryParams(QueryParams):
@@ -19,16 +19,13 @@ class ConsumerPriceIndexQueryParams(QueryParams):
description=QUERY_DESCRIPTIONS.get("country"),
default="united_states",
)
transform: Literal["index", "yoy", "period"] = Field(
description="Transformation of the CPI data. Period represents the change since previous."
+ " Defaults to change from one year ago (yoy).",
transform: str = Field(
description="Transformation of the CPI data.",
default="yoy",
json_schema_extra={"choices": ["index", "yoy", "period"]},
)
frequency: Literal["annual", "quarter", "monthly"] = Field(
default="monthly",
description=QUERY_DESCRIPTIONS.get("frequency"),
json_schema_extra={"choices": ["annual", "quarter", "monthly"]},
)
harmonized: bool = Field(
default=False, description="If true, returns harmonized data."
@@ -40,12 +37,6 @@ class ConsumerPriceIndexQueryParams(QueryParams):
default=None, description=QUERY_DESCRIPTIONS.get("end_date")
)
@field_validator("country", mode="before", check_fields=False)
@classmethod
def to_lower(cls, v):
"""Convert country to lower case."""
return v.replace(" ", "_").lower()
class ConsumerPriceIndexData(Data):
"""CPI data."""

View File

@@ -14,23 +14,27 @@ from pydantic import Field
class EconomicIndicatorsQueryParams(QueryParams):
"""Economic Indicators Query."""
symbol: str = Field(description=QUERY_DESCRIPTIONS.get("symbol", ""))
country: str | None = Field(
default=None,
description=QUERY_DESCRIPTIONS.get("country", "")
+ " The country represented by the indicator, if available.",
default=None, description=QUERY_DESCRIPTIONS.get("country", "")
)
frequency: str | None = Field(
default=None, description=QUERY_DESCRIPTIONS.get("frequency", "")
)
start_date: dateType | None = Field(
description=QUERY_DESCRIPTIONS.get("start_date", ""), default=None
default=None, description=QUERY_DESCRIPTIONS.get("start_date", "")
)
end_date: dateType | None = Field(
description=QUERY_DESCRIPTIONS.get("end_date", ""), default=None
default=None, description=QUERY_DESCRIPTIONS.get("end_date", "")
)
class EconomicIndicatorsData(Data):
"""Economic Indicators Data."""
date: dateType = Field(description=DATA_DESCRIPTIONS.get("date", ""))
date: dateType | None = Field(
default=None, description=DATA_DESCRIPTIONS.get("date", "")
)
symbol_root: str | None = Field(
default=None, description="The root symbol for the indicator (e.g. GDP)."
)

View File

@@ -103,6 +103,19 @@ def test_economy_calendar(params, headers):
"expenditure": "transport",
}
),
(
{
"country": "portugal,spain",
"transform": "yoy",
"frequency": "quarter",
"harmonized": False,
"start_date": "2020-01-01",
"end_date": "2023-06-06",
"provider": "imf",
"expenditure": "transport",
"limit": None,
}
),
],
)
@pytest.mark.integration
@@ -550,21 +563,15 @@ def test_economy_fred_regional(params, headers):
(
{
"provider": "imf",
"country": "us,uk,jp",
"symbol": "gold_reserves",
"start_date": "2022-01-01",
"end_date": "2023-12-31",
"frequency": "annual",
}
),
(
{
"provider": "imf",
"country": "all",
"symbol": "derivative_assets",
"start_date": "2022-01-01",
"end_date": "2023-12-31",
"frequency": "annual",
"country": "*",
"symbol": "IL::RGV_REVS",
"start_date": "2025-09-30",
"end_date": None,
"frequency": "month",
"transform": None,
"dimension_values": None,
"limit": 1,
"pivot": False,
}
),
],
@@ -585,7 +592,15 @@ def test_economy_indicators(params, headers):
"params",
[
({"provider": "econdb", "use_cache": False}),
({"provider": "imf", "query": "balance sheet;households;debt"}),
(
{
"provider": "imf",
"query": "gold+volume",
"dataflows": None,
"keywords": None,
"symbol": None,
}
),
],
)
@pytest.mark.integration
@@ -1287,11 +1302,13 @@ def test_economy_shipping_chokepoint_info(params, headers):
@pytest.mark.parametrize(
"params",
[
(
{
"provider": "imf",
}
),
{
"provider": "imf",
"port_code": None,
"country": None,
"continent": None,
"limit": None,
}
],
)
@pytest.mark.integration

View File

@@ -62,40 +62,45 @@ def test_economy_calendar(params, obb):
@pytest.mark.parametrize(
"params",
[
(
{
"country": "spain",
"transform": "yoy",
"frequency": "annual",
"harmonized": False,
"start_date": "2020-01-01",
"end_date": "2023-06-06",
"provider": "fred",
}
),
(
{
"country": "portugal,spain",
"transform": "period",
"frequency": "monthly",
"harmonized": True,
"start_date": "2023-01-01",
"end_date": "2023-06-06",
"provider": "fred",
}
),
(
{
"country": "portugal,spain",
"transform": "yoy",
"frequency": "quarter",
"harmonized": False,
"start_date": "2020-01-01",
"end_date": "2023-06-06",
"provider": "oecd",
"expenditure": "transport",
}
),
{
"country": "spain",
"transform": "yoy",
"frequency": "annual",
"harmonized": False,
"start_date": "2020-01-01",
"end_date": "2023-06-06",
"provider": "fred",
},
{
"country": "portugal,spain",
"transform": "period",
"frequency": "monthly",
"harmonized": True,
"start_date": "2023-01-01",
"end_date": "2023-06-06",
"provider": "fred",
},
{
"country": "portugal,spain",
"transform": "yoy",
"frequency": "quarter",
"harmonized": False,
"start_date": "2020-01-01",
"end_date": "2023-06-06",
"provider": "oecd",
"expenditure": "transport",
},
{
"country": "portugal,spain",
"transform": "yoy",
"frequency": "quarter",
"harmonized": False,
"start_date": "2020-01-01",
"end_date": "2023-06-06",
"provider": "imf",
"expenditure": "transport",
"limit": None,
},
],
)
@pytest.mark.integration
@@ -527,7 +532,15 @@ def test_economy_country_profile(params, obb):
"params",
[
({"provider": "econdb", "use_cache": False}),
({"provider": "imf", "query": "balance sheet;households;debt"}),
(
{
"provider": "imf",
"query": "gold+volume",
"dataflows": None,
"keywords": None,
"symbol": None,
}
),
],
)
@pytest.mark.integration
@@ -571,21 +584,15 @@ def test_economy_available_indicators(params, obb):
(
{
"provider": "imf",
"country": "us,uk,jp",
"symbol": "gold_reserves",
"start_date": "2022-01-01",
"end_date": "2023-12-31",
"frequency": "annual",
}
),
(
{
"provider": "imf",
"country": "all",
"symbol": "derivative_assets",
"start_date": "2022-01-01",
"end_date": "2023-12-31",
"frequency": "annual",
"country": "*",
"symbol": "IL::RGV_REVS",
"start_date": "2025-09-30",
"end_date": None,
"frequency": "month",
"transform": None,
"dimension_values": None,
"limit": 1,
"pivot": False,
}
),
],
@@ -1148,13 +1155,13 @@ def test_economy_fomc_documents(params, obb):
@pytest.mark.parametrize(
"params",
[
(
{
"provider": "imf",
"country": "CRI",
"continent": None,
}
),
{
"provider": "imf",
"port_code": None,
"country": None,
"continent": None,
"limit": None,
}
],
)
@pytest.mark.integration

View File

@@ -6,7 +6,7 @@ from typing import Annotated
from fastapi import Body
from openbb_core.app.model.command_context import CommandContext
from openbb_core.app.model.example import APIEx
from openbb_core.app.model.example import APIEx, PythonEx
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.provider_interface import (
ExtraParams,
@@ -79,6 +79,14 @@ async def calendar(
"provider": "oecd",
},
),
PythonEx(
description="Get the latest reported weightings of a country's CPI basket, from IMF.",
code=[
"res = obb.economy.cpi("
+ "provider='imf', country='CAN', transform='weight_percent', expenditure='all', limit=1)",
"print(res.model_dump(include='results')['results'])",
],
),
],
)
async def cpi(
@@ -87,10 +95,7 @@ async def cpi(
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Get Consumer Price Index (CPI).
Returns either the rescaled index value, or a rate of change (inflation).
"""
"""Get Consumer Price Index (CPI) data by country."""
return await OBBject.from_query(Query(**locals()))
@@ -346,7 +351,7 @@ async def available_indicators(
APIEx(parameters={"provider": "econdb", "symbol": "PCOCO"}),
APIEx(
description="Enter the country as the full name, or iso code."
+ " Use `available_indicators()` to get a list of supported indicators from EconDB.",
+ " Use `/economy/available_indicators` to get a list of supported indicators from EconDB.",
parameters={
"symbol": "CPI",
"country": "united_states,jp",
@@ -358,44 +363,29 @@ async def available_indicators(
parameters={"provider": "econdb", "symbol": "main", "country": "eu"},
),
APIEx(
description="When the provider is 'imf', the absence of a symbol will default to 'irfcl_top_lines'."
+ " Use 'IRFCL' to get all the data from the set of indicators.",
parameters={"provider": "imf"},
),
APIEx(
description="When the provider is 'imf', complete tables are returned by using a 'preset'."
+ " Refer to the function's docstring for descriptions of each preset."
+ " When no country is supplied, the data is returned for all countries.",
parameters={"provider": "imf", "symbol": "gold_reserves"},
),
APIEx(
description="When the provider is 'imf', multiple countries and symbols can be supplied."
+ " Enter countries as a two-letter ISO country code, or the country name in lower_snake_case.",
description="IMF indicators are identified by their dataflow and indicator code."
+ " Use `/economy/available_indicators` to get and search a list of supported indicators symbols."
+ " This example gets gold reserves held by countries, measured in Fine Troy Ounces.",
parameters={
"provider": "imf",
"symbol": "RAFA_USD,RAPFA_USD,RAFA_RAPFA_RO",
"country": "us,china,jp,4f,gb",
"start_date": "2010-01-01",
"end_date": "2020-12-31",
"frequency": "annual",
"symbol": "IL::RGV_REVS",
"country": "*",
"frequency": "month",
"limit": 1,
"start_date": "2025-09-30",
},
),
APIEx(
description=(
"When the provider is 'imf', additional presets return the core Financial Soundness Indicators."
"\n 'fsi_core' - Core FSIs"
"\n 'fsi_encouraged_set' - Encouraged Set of FSIs,"
"\n 'fsi_core_underlying' - Underlying data for the Core FSIs."
"\n 'fsi_other' - Additional/Other FSIs that are not in the Core or Encouraged Set."
"\n 'fsi_all' - all FSI data for a single country."
),
description="IMF symbols can also be used for retrieving entire presentation tables."
+ " This example gets the Direct Investment Position (DIP) table."
+ " Use `/imf_utils/list_tables` to get a list of supported presentation table symbols.",
parameters={
"provider": "imf",
"symbol": "fsi_encouraged_set",
"country": "us,fr,gb",
"start_date": "2022-01-01",
"end_date": "2023-12-31",
"symbol": "DIP::H_DIP_INDICATOR",
"country": "BRA",
"frequency": "annual",
"limit": 2,
"pivot": True,
},
),
],

View File

@@ -466,179 +466,101 @@
"groups": []
},
{
"name": "Example IMF App",
"img": "https://www.imf.org/-/media/Images/IMF/About/Factsheets/2022/Hero/imf-logo.ashx",
"description": "Global economic and financial data, including GDP, inflation, trade, and fiscal balances (WEO), financial stability risks (GFSR), and public debt (GFS, QPSD). It tracks exchange rates, interest rates, and money supply (IFS), balance of payments and trade flows (BOPS, DOTS), and banking sector health (FSIs).",
"allowCustomization": true,
"tabs": {
"DirectionOfTrade": {
"id": "DirectionOfTrade",
"name": "Direction Of Trade",
"layout": [
{
"i": "economy_direction_of_trade_imf_obb",
"x": 0,
"y": 2,
"w": 40,
"h": 25,
"state": {
"params": {
"country": "us",
"counterpart": "canada,mexico",
"direction": "all",
"start_date": "2010-01-01"
},
"chartView": {
"enabled": false,
"chartType": "line"
},
"columnState": {
"default_": {
"columnPinning": {
"leftColIds": [
"date",
"symbol",
"title"
],
"rightColIds": []
}
"name": "The United States Congress",
"img": "https://www.congress.gov/img/opengraph1200by630.jpg",
"img_dark": "https://www.congress.gov/img/opengraph1200by630.jpg",
"img_light": "https://www.congress.gov/img/opengraph1200by630.jpg",
"description": "Find and view US federal legislative information and complete bill text.",
"allowCustomization": true,
"tabs": {
"": {
"id": "",
"name": "",
"layout": [
{
"i": "uscongress_bills_congress_gov_obb",
"x": 0,
"y": 0,
"w": 40,
"h": 11,
"state": {
"params": {
"bill_url": "119/hr/1"
},
"chartView": {
"enabled": false,
"chartType": "line"
},
"columnState": {
"default": {
"columnVisibility": {
"hiddenColIds": [
"origin_chamber_code"
]
},
"columnOrder": {
"orderedColIds": [
"update_date",
"latest_action_date",
"bill_url",
"congress",
"bill_number",
"origin_chamber",
"origin_chamber_code",
"bill_type",
"title",
"latest_action",
"update_date_including_text"
]
},
"focusedCell": {
"colId": "bill_url",
"rowIndex": 2,
"rowPinned": null
}
}
}
}
]
},
"Indicators": {
"id": "Indicators",
"name": "Indicators",
"layout": [
{
"i": "economy_available_indicators_imf_obb",
"x": 0,
"y": 2,
"w": 40,
"h": 10,
"state": {
"chartView": {
"enabled": false,
"chartType": "line"
}
}
},
{
"i": "economy_indicators_imf_obb",
"x": 0,
"y": 12,
"w": 40,
"h": 15,
"state": {
"chartView": {
"enabled": false,
"chartType": "line"
}
},
{
"i": "uscongress_bill_text_congress_gov_obb",
"x": 16,
"y": 11,
"w": 24,
"h": 41,
"state": {
"params": {
"bill_url": "119/hr/1"
}
}
]
}
},
"groups": []
},
{
"i": "uscongress_bill_info_congress_gov_obb",
"x": 0,
"y": 11,
"w": 16,
"h": 41,
"state": {
"params": {
"bill_url": "119/hr/1"
}
}
}
]
}
},
{
"name": "The United States Congress",
"img": "https://www.congress.gov/img/opengraph1200by630.jpg",
"img_dark": "https://www.congress.gov/img/opengraph1200by630.jpg",
"img_light": "https://www.congress.gov/img/opengraph1200by630.jpg",
"description": "Find and view US federal legislative information and complete bill text.",
"allowCustomization": true,
"tabs": {
"": {
"id": "",
"name": "",
"layout": [
{
"i": "uscongress_bills_congress_gov_obb",
"x": 0,
"y": 0,
"w": 40,
"h": 11,
"state": {
"params": {
"bill_url": "119/hr/1"
},
"chartView": {
"enabled": false,
"chartType": "line"
},
"columnState": {
"default": {
"columnVisibility": {
"hiddenColIds": [
"origin_chamber_code"
]
},
"columnOrder": {
"orderedColIds": [
"update_date",
"latest_action_date",
"bill_url",
"congress",
"bill_number",
"origin_chamber",
"origin_chamber_code",
"bill_type",
"title",
"latest_action",
"update_date_including_text"
]
},
"focusedCell": {
"colId": "bill_url",
"rowIndex": 2,
"rowPinned": null
}
}
}
}
},
{
"i": "uscongress_bill_text_congress_gov_obb",
"x": 16,
"y": 11,
"w": 24,
"h": 41,
"state": {
"params": {
"bill_url": "119/hr/1"
}
}
},
{
"i": "uscongress_bill_info_congress_gov_obb",
"x": 0,
"y": 11,
"w": 16,
"h": 41,
"state": {
"params": {
"bill_url": "119/hr/1"
}
}
}
]
}
},
"groups": [
{
"name": "Group 1",
"type": "param",
"paramName": "bill_url",
"defaultValue": "119/hr/1",
"widgetIds": [
"uscongress_bills_congress_gov_obb",
"uscongress_bill_text_congress_gov_obb",
"uscongress_bill_info_congress_gov_obb"
]
}
]
}
"groups": [
{
"name": "Group 1",
"type": "param",
"paramName": "bill_url",
"defaultValue": "119/hr/1",
"widgetIds": [
"uscongress_bills_congress_gov_obb",
"uscongress_bill_text_congress_gov_obb",
"uscongress_bill_info_congress_gov_obb"
]
}
]
}
]

View File

@@ -959,7 +959,6 @@
},
{
"field": "name",
"pinned": "left",
"formatterFn": null,
"headerName": "Name",
"headerTooltip": "The name of the series.",
@@ -1901,7 +1900,6 @@
},
{
"field": "series_id",
"pinned": "left",
"formatterFn": null,
"headerName": "Series ID",
"headerTooltip": "The series ID for the item in the release.",
@@ -1909,7 +1907,6 @@
},
{
"field": "name",
"pinned": "left",
"formatterFn": null,
"headerName": "Name",
"headerTooltip": "The name of the release.",
@@ -2618,7 +2615,6 @@
},
{
"field": "name",
"pinned": "left",
"formatterFn": null,
"headerName": "Name",
"headerTooltip": "The name of the series.",
@@ -2953,7 +2949,6 @@
},
{
"field": "series_id",
"pinned": "left",
"formatterFn": null,
"headerName": "Series ID",
"headerTooltip": "The individual series ID for the region.",
@@ -3289,7 +3284,6 @@
},
{
"field": "name",
"pinned": "left",
"formatterFn": null,
"headerName": "Name",
"headerTooltip": "The name of the series.",

View File

@@ -44,6 +44,7 @@ def find_extensions(filter_chart: bool | None = True):
extensions.extend(
[x for x in (ROOT_DIR / "obbject_extensions").iterdir() if x.is_dir()]
)
extensions.extend([x for x in (ROOT_DIR / "providers").iterdir() if x.is_dir()])
extensions = [x for x in extensions if x.name not in filter_ext]
return extensions

View File

@@ -34,6 +34,8 @@ def get_integration_tests(
for extension in find_extensions(filter_charting_ext):
integration_folder = os.path.join(extension, "integration")
if not os.path.exists(integration_folder):
continue
for file in os.listdir(integration_folder):
if file.endswith(file_end):
file_path = os.path.join(integration_folder, file)
@@ -314,7 +316,11 @@ def check_missing_integration_tests(test_type: Literal["api", "python"]) -> list
if route not in tested_functions:
# TODO: See how to handle edge cases that are excluded from the schema
# on purpose. This is currently on the econometrics router.
if test_type == "api" and "econometrics" in route:
if (
test_type == "api"
and "econometrics" in route
or route.endswith(".json")
):
continue
missing_integration_tests.append(
f"Missing {test_type} integration test for route {route}"

View File

@@ -3,7 +3,7 @@
# pylint: disable=unused-argument
from datetime import datetime
from typing import Any, Literal
from typing import Any
from warnings import warn
from openbb_core.app.model.abstract.error import OpenBBError
@@ -24,6 +24,8 @@ class EconDbEconomicIndicatorsQueryParams(EconomicIndicatorsQueryParams):
__json_schema_extra__ = {
"symbol": {"multiple_items_allowed": True},
"country": {"multiple_items_allowed": True},
"transform": {"choices": ["toya", "tpop", "tusd", "tpgp"]},
"frequency": {"choices": ["annual", "quarter", "month"]},
}
symbol: str = Field(
@@ -32,7 +34,12 @@ class EconDbEconomicIndicatorsQueryParams(EconomicIndicatorsQueryParams):
+ " Use `available_indicators()` to get a list of available symbols.",
)
transform: None | Literal["toya", "tpop", "tusd", "tpgp"] = Field(
country: str | None = Field(
default=None,
description=QUERY_DESCRIPTIONS.get("country", "")
+ " ISO country codes or country names.",
)
transform: None | str = Field(
default=None,
description="The transformation to apply to the data, default is None."
+ "\n"
@@ -49,7 +56,7 @@ class EconDbEconomicIndicatorsQueryParams(EconomicIndicatorsQueryParams):
+ " and the original units and scale differ between entities."
+ "\n `tusd` should only be used where values are currencies.",
)
frequency: Literal["annual", "quarter", "month"] = Field(
frequency: str = Field(
default="quarter",
description="The frequency of the data, default is 'quarter'."
+ " Only valid when 'symbol' is 'main'.",

View File

@@ -10,12 +10,10 @@ from openbb_core.provider.standard_models.consumer_price_index import (
ConsumerPriceIndexData,
ConsumerPriceIndexQueryParams,
)
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_core.provider.utils.helpers import check_item
from openbb_fred.models.series import FredSeriesFetcher
from openbb_fred.utils.fred_helpers import CPI_COUNTRIES, CpiCountries, all_cpi_options
from pydantic import Field, field_validator
from openbb_fred.utils.fred_helpers import CPI_COUNTRIES, all_cpi_options
from pydantic import field_validator
class FREDConsumerPriceIndexQueryParams(ConsumerPriceIndexQueryParams):
@@ -26,13 +24,11 @@ class FREDConsumerPriceIndexQueryParams(ConsumerPriceIndexQueryParams):
"multiple_items_allowed": True,
"choices": CPI_COUNTRIES,
},
"transform": {
"choices": ["index", "yoy", "period"],
},
}
country: CpiCountries | str = Field(
description=QUERY_DESCRIPTIONS.get("country"),
default="united_states",
)
@field_validator("country", mode="before", check_fields=False)
@classmethod
def validate_country(cls, c: str):
@@ -40,10 +36,24 @@ class FREDConsumerPriceIndexQueryParams(ConsumerPriceIndexQueryParams):
result: list = []
values = c.replace(" ", "_").split(",")
for v in values:
check_item(v.lower(), CPI_COUNTRIES)
if v.lower() not in CPI_COUNTRIES:
raise ValueError(
f"Invalid country: {v}. Available countries are: {', '.join(CPI_COUNTRIES)}"
)
result.append(v.lower())
return ",".join(result)
@field_validator("transform", mode="before", check_fields=False)
@classmethod
def validate_transform(cls, t: str):
"""Validate transform."""
valid_transforms = ["index", "yoy", "period"]
if t.lower() not in valid_transforms:
raise ValueError(
f"Invalid transform: {t}. Available transforms are: {', '.join(valid_transforms)}"
)
return t.lower()
class FREDConsumerPriceIndexData(ConsumerPriceIndexData):
"""FRED Consumer Price Index Data."""
@@ -92,8 +102,8 @@ class FREDConsumerPriceIndexFetcher(
)
results: dict = {}
temp = await FredSeriesFetcher.fetch_data(item_query, credentials)
result = [d.model_dump() for d in temp.result]
results["metadata"] = {country_map.get(k): v for k, v in temp.metadata.items()}
result = [d.model_dump() for d in temp.result] # type: ignore
results["metadata"] = {country_map.get(k): v for k, v in temp.metadata.items()} # type: ignore
results["data"] = [
{country_map.get(k, k): v for k, v in d.items()} for d in result
]

View File

@@ -1,6 +1,9 @@
# OpenBB IMF Provider Extension
This package adds the `openbb-imf` provider extension to the OpenBB Platform.
This package adds the `openbb-imf` provider extension to the Open Data Platform by OpenBB.
It provides everything you need - endpoints, tools, and metadata - to access and explore the entirety of
https://data.imf.org, without any previous experience working with it.
## Installation
@@ -10,32 +13,140 @@ Install from PyPI with:
pip install openbb-imf
```
## Implementation
Then build the Python static assets by running:
The extension utilizes the JSON RESTful Web Service ((https://datahelp.imf.org/knowledgebase/articles/630877-data-services)[https://datahelp.imf.org/knowledgebase/articles/630877-data-services])
```sh
openbb-build
```
No authorization is required to use, but IP addresses are bound by the limitations described in the link above.
## Quick Start
The fastest way to get started is by connecting to the OpenBB Workspace as a custom backend.
### Start Server
```sh
openbb-api
```
This starts the FastAPI server over localhost on port 6900.
### Add to Workspace
See the documentation [here](https://docs.openbb.co/python/quickstart/workspace) for more details.
### Click to Open App
Once added, click on the app to open the dashboard.
The dashboard contains widgets with metadata and information, as well ones for exploring and retrieving the data.
## Implementation Details
IMF is a SDMX API, and they organize their data by "dataflows". You can think of these as databases.
Each one has their own definitions for parameters and output. Some definitions are shared, others are domain-specific.
In SDMX, query models are expressed as dimensions, and data models as attributes.
The extension comes with a base level of metadata used for normalizing, translating, and validating inputs and outputs.
When mapping requires a code list that is not included, a network request is made to retrieve it.
User input is validated by calling the constraints API for each dimension.
The cached metadata contains all the potential values for parameters,
but the availability of each is determined by other choices - country, frequency, etc.
When making the actual request for data, parameters are tested for compatibility in the sequence defined by the data structure definition.
Invalid parameter combinations are returned as helpful error messages with descriptions of what went wrong and what the valid choices are.
The output converts ID codes into human-readable labels, and includes dataset and series metadata in a separate object.
### Indicators
In this library, we refer to indicators as "indicator-like" dimensions within individual dataflows.
It includes dimensions like `COICOP_1999`, so items such as CPI All Items, or Clothing and footwear,
are considered.
The IMF codes for these values - `_T`, `CP01`, etc. - are used to construct ticker-like symbols.
### Presentation Tables
Presentation tables are built from hierarchical code lists defining the parent-child relationship
between individual series.
Internally, table IDs are prefixed with `H_` - i.e, `H_BOP_BOP_AGG_STANDARD_PRESENTATION`.
The symbology allows entering references to tables, or indicators.
### Symbology
The Open Data Platform generally refers to all time series IDs as a `symbol`.
Requesting time series data or presentation tables requires a symbol constructed from
the dataflow ID and the indicator-like dimensions, split with `::`.
`CPI::CPI` returns all series for `CPI` under the `TYPE_OF_INDEX` dimension.
`CPI::CPI_CP01` returns just component of CPI, Food and non-alcoholic beverages.
The symbol mapper matches the items after `::` intelligently to its corresponding dimension.
Entering, `CPI::CP01`, gets the same result as, `CPI::CPI_CP01`.
`CPI::CPI__T` returns just the top-level, All Items.
`CPI::H_CPI_BY_COMPONENT` returns the entire presentation table, Consumer Price Index (CPI) by Component.
Use, `obb.imf_utils.list_tables()`, for a list of tables and their symbol.
Use, `obb.economy.available_indicators(provider='imf', query=')`, to search for, or list all, individual time series and symbols.
This example lists all the indicators in the CPI and PI dataflows.
```python
from openbb import obb
indicators = obb.economy.available_indicators(provider="imf", dataflows="CPI,PI")
print(indicators.to_dict("records"))
```
```sh
...
{'symbol_root': 'CP01',
'symbol': 'CPI::CP01',
'description': 'Food and non-alcoholic beverages',
'agency_id': 'IMF.STA',
'dataflow_id': 'CPI',
'dataflow_name': 'Consumer Price Index (CPI)',
'structure_id': 'DSD_CPI',
'dimension_id': 'COICOP_1999',
'long_description': 'Food and non-alcholoic beverages consumer price index is produced using prices related to food items and non-alcoholic beverages such as fresh produce, packaged foods, and beverages excluding alcoholic drinks, aggregated by their respective consumer expenditure weights.',
'member_of': ['CPI::H_CPI_BY_COMPONENT']},
...
```
## Coverage
- Databases:
- International Reserves and Foreign Currency Liquidity
- Direction of Trade Statistics
- Financial Soundness Indicators
- Port Watch
All data available from https://data.imf.org/en/Data-Explorer can be retrieved, via `obb.economy.indicators(provider='imf', **kwargs)`.
Coverage:
- All IRFCL tables.
- Individual, or multiple, time series from single or multiple countries.
- Core and Encouraged Set tables, plus all individual underlying series.
- Daily Port and Chokepoints data, with charts for metadata and average annual statistics.
Additionally, there are endpoints for some Port Watch items (not part of the Data Explorer).
The extension creates a router path, `imf_utils`, that exposes utility functions for UI integrations and metadata lookup.
### Endpoints
- `obb.economy.available_indicators`
- `obb.economy.indicators`
- `obb.economy.cpi`
- `obb.economy.direction_of_trade`
- `obb.economy.shipping.chokepoint_info`
- `obb.economy.shipping.chokepoint_volume`
- `obb.economy.shipping.port_info`
- `obb.economy.shipping.port_volume`
- `obb.imf_utils.get_dataflow_dimensions`
- `obb.imf_utils.list_dataflow_choices`
- `obb.imf_utils.list_dataflows`
- `obb.imf_utils.list_indicators_by_dataflow`
- `obb.imf_utils.list_port_id_choices`
- `obb.imf_utils.list_table_choices`
- `obb.imf_utils.list_tables`
- `obb.imf_utils.presentation_table`
- `obb.imf_utils.presentation_table_choices`
"Choices" endpoints are utilized by OpenBB Workspace to populate widget dropdown menus.

View File

@@ -0,0 +1 @@
"""IMF Utilities Router Module Integration Tests."""

View File

@@ -0,0 +1,211 @@
"""Test IMF Utils API endpoints."""
import base64
import pytest
import requests
from openbb_core.env import Env
from openbb_core.provider.utils.helpers import get_querystring
# pylint: disable=redefined-outer-name
@pytest.fixture(scope="session")
def headers():
"""Get the headers for the API request."""
userpass = f"{Env().API_USERNAME}:{Env().API_PASSWORD}"
userpass_bytes = userpass.encode("ascii")
base64_bytes = base64.b64encode(userpass_bytes)
return {"Authorization": f"Basic {base64_bytes.decode('ascii')}"}
@pytest.mark.parametrize(
"params",
[
{
"output_format": "json",
}
],
)
@pytest.mark.integration
def test_imf_utils_list_dataflows(params, headers):
"""Test imf_utils_list_dataflows endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/imf_utils/list_dataflows?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@pytest.mark.parametrize(
"params",
[
{
"dataflow_id": "CPI",
"output_format": "json",
},
],
)
@pytest.mark.integration
def test_imf_utils_get_dataflow_dimensions(params, headers):
"""Test imf_utils_get_dataflow_dimensions endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/imf_utils/get_dataflow_dimensions?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@pytest.mark.parametrize("params", [{}])
@pytest.mark.integration
def test_imf_utils_list_port_id_choices(params, headers):
"""Test imf_utils_list_port_id_choices endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/imf_utils/list_port_id_choices?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@pytest.mark.parametrize("params", [{}])
@pytest.mark.integration
def test_imf_utils_list_tables(params, headers):
"""Test imf_utils_list_tables endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/imf_utils/list_tables?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@pytest.mark.parametrize("params", [{}])
@pytest.mark.integration
def test_imf_utils_list_table_choices(params, headers):
"""Test imf_utils_list_table_choices endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/imf_utils/list_table_choices?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@pytest.mark.parametrize("params", [{}])
@pytest.mark.integration
def test_imf_utils_list_dataflow_choices(params, headers):
"""Test imf_utils_list_dataflow_choices endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/imf_utils/list_dataflow_choices?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@pytest.mark.parametrize(
"params",
[
{
"dataflow_group": None,
"table": None,
"country": None,
"frequency": None,
},
{
"dataflow_group": "cpi",
"table": None,
"country": None,
"frequency": None,
},
{
"dataflow_group": "cpi",
"table": "cpi",
"country": None,
"frequency": None,
},
{
"dataflow_group": "cpi",
"table": "cpi",
"country": "JPN",
"frequency": None,
},
],
)
@pytest.mark.integration
def test_imf_utils_presentation_table_choices(params, headers):
"""Test imf_utils_presentation_table_choices endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/imf_utils/presentation_table_choices?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@pytest.mark.parametrize(
"params",
[
{
"symbol": "CPI::CPI__T",
"country": None,
"frequency": None,
"transform": None,
"dimension_values": None,
},
{
"symbol": "CPI::CPI__T",
"country": "JPN",
"frequency": None,
"transform": None,
"dimension_values": None,
},
],
)
@pytest.mark.integration
def test_imf_utils_indicator_choices(params, headers):
"""Test imf_utils_indicator_choices endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/imf_utils/indicator_choices?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@pytest.mark.parametrize(
"params",
[
{
"dataflow_group": "cpi",
"table": "cpi",
"country": "JPN",
"frequency": "A",
"dimension_values": None,
"limit": 1,
"raw": True,
}
],
)
@pytest.mark.integration
def test_imf_utils_presentation_table(params, headers):
"""Test imf_utils_presentation_table endpoint."""
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/imf_utils/presentation_table?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200

View File

@@ -0,0 +1,179 @@
"""IMF Utilities module integration tests."""
import pytest
from openbb_core.app.model.obbject import OBBject
# pylint: disable=redefined-outer-name
@pytest.fixture(scope="session")
def obb(pytestconfig): # pylint: disable=inconsistent-return-statements
"""Fixture to setup obb."""
if pytestconfig.getoption("markexpr") != "not integration":
import openbb # pylint: disable=import-outside-toplevel
return openbb.obb
@pytest.mark.parametrize(
"params",
[
{
"output_format": "json",
}
],
)
@pytest.mark.integration
def test_imf_utils_list_dataflows(params, obb):
"""Test imf_utils_list_dataflows endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.imf_utils.list_dataflows(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0 # type: ignore
@pytest.mark.parametrize(
"params",
[
{
"dataflow_id": "CPI",
"output_format": "json",
},
],
)
@pytest.mark.integration
def test_imf_utils_get_dataflow_dimensions(params, obb):
"""Test imf_utils_get_dataflow_dimensions endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.imf_utils.get_dataflow_dimensions(**params)
assert result
assert isinstance(result, OBBject)
assert result.results
assert isinstance(result.results, dict)
@pytest.mark.parametrize("params", [{}])
@pytest.mark.integration
def test_imf_utils_list_port_id_choices(params, obb):
"""Test imf_utils_list_port_id_choices endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.imf_utils.list_port_id_choices(**params)
assert result
assert isinstance(result, list)
@pytest.mark.parametrize("params", [{}])
@pytest.mark.integration
def test_imf_utils_list_tables(params, obb):
"""Test imf_utils_list_tables endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.imf_utils.list_tables(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0 # type: ignore
@pytest.mark.parametrize("params", [{}])
@pytest.mark.integration
def test_imf_utils_list_table_choices(params, obb):
"""Test imf_utils_list_table_choices endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.imf_utils.list_table_choices(**params)
assert result
assert isinstance(result, list)
assert len(result) > 0
@pytest.mark.parametrize("params", [{}])
@pytest.mark.integration
def test_imf_utils_list_dataflow_choices(params, obb):
"""Test imf_utils_list_dataflow_choices endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.imf_utils.list_dataflow_choices(**params)
assert result
assert isinstance(result, list)
assert len(result) > 0
@pytest.mark.parametrize(
"params",
[
{
"dataflow_group": None,
"table": None,
"country": None,
"frequency": None,
},
{
"dataflow_group": "cpi",
"table": None,
"country": None,
"frequency": None,
},
],
)
@pytest.mark.integration
def test_imf_utils_presentation_table_choices(params, obb):
"""Test imf_utils_presentation_table_choices endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.imf_utils.presentation_table_choices(**params)
assert result
assert isinstance(result, list)
@pytest.mark.parametrize(
"params",
[
{
"dataflow_group": "cpi",
"table": "cpi",
"country": "JPN",
"frequency": "A",
"dimension_values": None,
"limit": 1,
"raw": True,
}
],
)
@pytest.mark.integration
def test_imf_utils_presentation_table(params, obb):
"""Test imf_utils_presentation_table endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.imf_utils.presentation_table(**params)
assert result
assert isinstance(result, list)
assert len(result) > 1 # type: ignore
@pytest.mark.parametrize(
"params",
[
{
"symbol": "CPI::CPI__T",
"country": None,
"frequency": None,
"transform": None,
"dimension_values": None,
},
{
"symbol": "CPI::CPI__T",
"country": "JPN",
"frequency": None,
"transform": None,
"dimension_values": None,
},
],
)
@pytest.mark.integration
@pytest.mark.skip(reason="Not included in Python interface")
def test_imf_utils_indicator_choices(params, obb):
"""Test imf_utils_indicator_choices endpoint."""
params = {p: v for p, v in params.items() if v}
result = obb.imf_utils.indicator_choices(**params)
assert result
assert isinstance(result, list)
assert len(result) > 0

View File

@@ -2,6 +2,7 @@
from openbb_core.provider.abstract.provider import Provider
from openbb_imf.models.available_indicators import ImfAvailableIndicatorsFetcher
from openbb_imf.models.consumer_price_index import ImfConsumerPriceIndexFetcher
from openbb_imf.models.direction_of_trade import ImfDirectionOfTradeFetcher
from openbb_imf.models.economic_indicators import ImfEconomicIndicatorsFetcher
from openbb_imf.models.maritime_chokepoint_info import ImfMaritimeChokePointInfoFetcher
@@ -17,6 +18,7 @@ imf_provider = Provider(
description="Access International Monetary Fund (IMF) data APIs.",
fetcher_dict={
"AvailableIndicators": ImfAvailableIndicatorsFetcher,
"ConsumerPriceIndex": ImfConsumerPriceIndexFetcher,
"DirectionOfTrade": ImfDirectionOfTradeFetcher,
"EconomicIndicators": ImfEconomicIndicatorsFetcher,
"MaritimeChokePointInfo": ImfMaritimeChokePointInfoFetcher,

View File

@@ -0,0 +1,488 @@
[{
"name": "IMF Explorer",
"img": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3e/IMF-Seal_ENG_RGB.svg/500px-IMF-Seal_ENG_RGB.svg.png",
"img_dark": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3e/IMF-Seal_ENG_RGB.svg/500px-IMF-Seal_ENG_RGB.svg.png",
"img_light": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3e/IMF-Seal_ENG_RGB.svg/500px-IMF-Seal_ENG_RGB.svg.png",
"description": "Explore economic indicators and presentation tables published by the IMF. Disclaimer: This application is neither affiliated with nor endorsed by the IMF. See https://data.imf.org/ for terms and conditions.",
"allowCustomization": true,
"tabs": {
"dataflows": {
"id": "dataflows",
"name": "Dataflows",
"layout": [
{
"i": "imf_utils_get_dataflow_dimensions_custom_obb",
"x": 0,
"y": 2,
"w": 16,
"h": 29,
"state": {
"params": {
"dataflow_id": "CTOT"
}
},
"groups": []
},
{
"i": "imf_utils_list_dataflows_custom_obb",
"x": 16,
"y": 2,
"w": 24,
"h": 29,
"groups": []
}
]
},
"indicators": {
"id": "indicators",
"name": "Indicators",
"layout": [
{
"i": "economy_available_indicators_imf_obb",
"x": 0,
"y": 2,
"w": 40,
"h": 15,
"state": {
"params": {
"dataflows": "CTOT",
"symbol": "CTOT::CEMPI_CTOTNX_TT"
},
"chartView": {
"enabled": false,
"chartType": "line"
},
"columnState": {
"default": {
"columnPinning": {
"leftColIds": [
"symbol"
],
"rightColIds": []
},
"columnOrder": {
"orderedColIds": [
"symbol_root",
"symbol",
"description",
"agency_id",
"dataflow_id",
"dataflow_name",
"structure_id",
"dimension_id",
"long_description",
"member_of"
]
}
}
}
},
"groups": [
"Group 1"
]
},
{
"i": "economy_indicators_imf_obb",
"x": 0,
"y": 17,
"w": 40,
"h": 21,
"state": {
"params": {
"symbol": "CTOT::CEPI_CTOTX_GDP",
"country": [
"",
"AUS"
],
"frequency": "M",
"limit": "1",
"pivot": "false"
},
"chartModel": {
"modelType": "range",
"chartType": "customCombo",
"chartOptions": {
"line": {
"legend": {
"enabled": false
},
"axes": {
"category": {}
}
}
},
"cellRange": {
"columns": [
"date",
"value"
]
},
"suppressChartRanges": true,
"seriesChartTypes": [
{
"colId": "value",
"chartType": "groupedColumn",
"secondaryAxis": false
},
{
"colId": "unit_multiplier",
"chartType": "line",
"secondaryAxis": false
}
]
},
"chartView": {
"enabled": false,
"chartType": "customCombo"
},
"columnState": {
"default": {
"columnPinning": {
"leftColIds": [
"title",
"country",
"date",
"value"
],
"rightColIds": []
},
"columnVisibility": {
"hiddenColIds": [
"description"
]
},
"columnOrder": {
"orderedColIds": [
"title",
"country",
"unit",
"scale",
"date",
"symbol_root",
"symbol",
"value",
"unit_multiplier",
"order",
"level",
"indicator",
"description",
"country_code",
"wgt_type",
"wgt_type_code",
"frequency",
"frequency_code"
]
}
}
}
},
"groups": [
"Group 1"
]
}
]
},
"presentation-tables": {
"id": "presentation-tables",
"name": "Presentation Tables",
"layout": [
{
"i": "imf_utils_presentation_table_custom_obb",
"x": 0,
"y": 2,
"w": 40,
"h": 31,
"state": {
"params": {
"dataflow_group": "qgfs",
"table": "qgfs_balance",
"country": [
"BRA"
],
"frequency": "Q",
"limit": "3"
},
"columnState": {
"default": {
"columnOrder": {
"orderedColIds": [
"country",
"title",
"2024-12-31",
"2023-12-31",
"2022-12-31"
]
}
}
}
},
"groups": []
}
]
},
"direction-of-trade": {
"id": "direction-of-trade",
"name": "Direction of Trade",
"layout": [
{
"i": "economy_direction_of_trade_imf_obb",
"x": 0,
"y": 2,
"w": 40,
"h": 27,
"state": {
"params": {
"start_date": "2010-01-01",
"frequency": "quarter"
},
"chartModel": {
"modelType": "range",
"chartType": "line",
"chartOptions": {},
"suppressChartRanges": true
},
"chartView": {
"enabled": false,
"chartType": "line"
},
"columnState": {
"default": {
"columnPinning": {
"leftColIds": [
"date",
"symbol"
],
"rightColIds": []
},
"columnVisibility": {
"hiddenColIds": [
"symbol",
"country_code",
"counterpart_code",
"unit_multiplier"
]
},
"columnOrder": {
"orderedColIds": [
"date",
"symbol",
"country",
"counterpart",
"title",
"value",
"scale",
"unit",
"country_code",
"counterpart_code",
"unit_multiplier"
]
}
}
}
},
"groups": []
}
]
},
"port-watch": {
"id": "port-watch",
"name": "Port Watch",
"layout": [
{
"i": "economy_shipping_chokepoint_volume_imf_obb",
"x": 0,
"y": 2,
"w": 40,
"h": 16,
"state": {
"params": {
"start_date": "2024-01-01",
"chokepoint": [
"chokepoint2"
]
},
"chartView": {
"enabled": false,
"chartType": "line"
},
"columnState": {
"default": {
"columnPinning": {
"leftColIds": [
"date",
"chokepoint"
],
"rightColIds": []
},
"columnOrder": {
"orderedColIds": [
"date",
"chokepoint",
"vessels_total",
"vessels_cargo",
"vessels_tanker",
"vessels_container",
"vessels_general_cargo",
"vessels_dry_bulk",
"vessels_roro",
"capacity_total",
"capacity_cargo",
"capacity_tanker",
"capacity_container",
"capacity_general_cargo",
"capacity_dry_bulk",
"capacity_roro"
]
}
}
}
},
"groups": []
},
{
"i": "economy_shipping_port_info_imf_obb",
"x": 0,
"y": 18,
"w": 40,
"h": 14,
"state": {
"params": {
"country": "USA",
"port_code": "port786"
},
"chartView": {
"enabled": false,
"chartType": "line"
},
"columnState": {
"default": {
"columnPinning": {
"leftColIds": [
"port_code",
"continent",
"country",
"port_full_name"
],
"rightColIds": []
},
"columnVisibility": {
"hiddenColIds": [
"country_code",
"port_name"
]
},
"columnOrder": {
"orderedColIds": [
"port_code",
"continent",
"country",
"country_code",
"port_name",
"port_full_name",
"latitude",
"longitude",
"vessel_count_total",
"vessel_count_tanker",
"vessel_count_container",
"vessel_count_general_cargo",
"vessel_count_dry_bulk",
"vessel_count_roro",
"share_country_maritime_import",
"share_country_maritime_export",
"industry_top1",
"industry_top2",
"industry_top3"
]
}
}
}
},
"groups": [
"Group 2"
]
},
{
"i": "economy_shipping_port_volume_imf_obb",
"x": 0,
"y": 32,
"w": 40,
"h": 15,
"state": {
"params": {
"start_date": "2024-01-01",
"country": "DEU"
},
"chartView": {
"enabled": false,
"chartType": "line"
},
"columnState": {
"default": {
"columnPinning": {
"leftColIds": [
"date",
"port_code",
"port_name",
"country"
],
"rightColIds": []
},
"columnVisibility": {
"hiddenColIds": [
"country_code"
]
},
"columnOrder": {
"orderedColIds": [
"date",
"port_code",
"port_name",
"country",
"country_code",
"portcalls",
"portcalls_tanker",
"portcalls_container",
"portcalls_general_cargo",
"portcalls_dry_bulk",
"portcalls_roro",
"imports",
"imports_cargo",
"imports_tanker",
"imports_container",
"imports_general_cargo",
"imports_dry_bulk",
"imports_roro",
"exports",
"exports_cargo",
"exports_tanker",
"exports_container",
"exports_general_cargo",
"exports_dry_bulk",
"exports_roro"
]
}
}
}
},
"groups": [
"Group 2"
]
}
]
}
},
"groups": [
{
"name": "Group 1",
"type": "param",
"paramName": "symbol",
"defaultValue": "CTOT::CEPI_CTOTX_GDP"
},
{
"name": "Group 2",
"type": "param",
"paramName": "port_code",
"defaultValue": "port664"
}
]
}]

Binary file not shown.

View File

@@ -1,260 +0,0 @@
{
"AF": "Afghanistan",
"AL": "Albania",
"DZ": "Algeria",
"AS": "American Samoa",
"AO": "Angola",
"AI": "Anguilla",
"AG": "Antigua and Barbuda",
"AR": "Argentina",
"AM": "Armenia",
"AW": "Aruba",
"S19": "Asia not allocated",
"AU": "Australia",
"AT": "Austria",
"AZ": "Azerbaijan",
"BS": "Bahamas",
"BH": "Bahrain",
"BD": "Bangladesh",
"BB": "Barbados",
"BY": "Belarus",
"BE": "Belgium",
"R1": "Belgo-Luxembourg Economic Union",
"BZ": "Belize",
"BJ": "Benin",
"BM": "Bermuda",
"BT": "Bhutan",
"BO": "Bolivia",
"BA": "Bosnia and Herzegovina",
"BW": "Botswana",
"BR": "Brazil",
"BN": "Brunei Darussalam",
"BG": "Bulgaria",
"BF": "Burkina Faso",
"BI": "Burundi",
"CV": "Cabo Verde",
"KH": "Cambodia",
"CM": "Cameroon",
"CA": "Canada",
"5Y": "Eastern Caribbean Currency Union",
"CF": "Central African Republic",
"TD": "Chad",
"CL": "Chile",
"HK": "Hong Kong",
"MO": "Macao",
"CN": "China",
"CO": "Colombia",
"KM": "Comoros",
"CD": "Democratic Republic of the Congo",
"CG": "Congo",
"CR": "Costa Rica",
"CI": "Ivory Coast",
"HR": "Croatia",
"CU": "Cuba",
"CW": "Curacao",
"CY": "Cyprus",
"CZ": "Czech Republic",
"CSH": "Former Czechoslovakia",
"DK": "Denmark",
"DJ": "Djibouti",
"DM": "Dominica",
"DO": "Dominican Republic",
"DE2": "East Germany",
"EC": "Ecuador",
"EG": "Egypt",
"SV": "El Salvador",
"GQ": "Equatorial Guinea",
"ER": "Eritrea",
"EE": "Estonia",
"SZ": "Eswatini",
"ET": "Ethiopia",
"E19": "Europe not allocated",
"FK": "Falkland Islands (Malvinas)",
"FO": "Faroe Islands",
"FJ": "Fiji",
"FI": "Finland",
"FR": "France",
"PF": "French Polynesia",
"GA": "Gabon",
"GM": "Gambia",
"GE": "Georgia",
"DE": "Germany",
"GH": "Ghana",
"GI": "Gibraltar",
"GR": "Greece",
"GL": "Greenland",
"GD": "Grenada",
"GU": "Guam",
"GT": "Guatemala",
"GN": "Guinea",
"GW": "Guinea-Bissau",
"GY": "Guyana",
"HT": "Haiti",
"VA": "Vatican City State",
"HN": "Honduras",
"HU": "Hungary",
"IS": "Iceland",
"IN": "India",
"ID": "Indonesia",
"IR": "Iran",
"IQ": "Iraq",
"IE": "Ireland",
"IL": "Israel",
"IT": "Italy",
"JM": "Jamaica",
"JP": "Japan",
"JO": "Jordan",
"KZ": "Kazakhstan",
"KE": "Kenya",
"KI": "Kiribati",
"KP": "North Korea",
"KR": "South Korea",
"XK": "Kosovo",
"KW": "Kuwait",
"KG": "Kyrgyzstan",
"LA": "Lao",
"LV": "Latvia",
"LB": "Lebanon",
"LS": "Lesotho",
"LR": "Liberia",
"LY": "Libya",
"LT": "Lithuania",
"LU": "Luxembourg",
"MG": "Madagascar",
"MW": "Malawi",
"MY": "Malaysia",
"1C_554": "West Malaysia",
"MV": "Maldives",
"ML": "Mali",
"MT": "Malta",
"MH": "Marshall islands",
"MR": "Mauritania",
"MU": "Mauritius",
"MX": "Mexico",
"FM": "Micronesia",
"F979": "Middle East and Central Asia not specified",
"MD": "Moldova",
"MN": "Mongolia",
"ME": "Montenegro",
"MS": "Montserrat",
"MA": "Morocco",
"MZ": "Mozambique",
"MM": "Myanmar",
"NA": "Namibia",
"NR": "Nauru",
"NP": "Nepal",
"AN": "Antilles",
"NL": "Netherlands",
"NC": "New Caledonia",
"NZ": "New Zealand",
"NI": "Nicaragua",
"NE": "Niger",
"NG": "Nigeria",
"MK": "North Macedonia",
"1C_958": "North Vietnam",
"NO": "Norway",
"OM": "Oman",
"PK": "Pakistan",
"PW": "Palau",
"PA": "Panama",
"PG": "Papua New Guinea",
"PY": "Paraguay",
"PE": "Peru",
"PH": "Philippines",
"PL": "Poland",
"PT": "Portugal",
"QA": "Qatar",
"RO": "Romania",
"RU": "Russia",
"RW": "Rwanda",
"WS": "Samoa",
"SM": "San Marino",
"ST": "Sao Tome and Principe",
"SA": "Saudi Arabia",
"SN": "Senegal",
"CS": "Serbia and Montenegro",
"RS": "Serbia",
"SC": "Seychelles",
"SL": "Sierra Leone",
"SG": "Singapore",
"SX": "Sint Maarten (Dutch part)",
"SK": "Slovakia",
"SI": "Slovenia",
"SB": "Solomon Islands",
"SO": "Somalia",
"ZA": "South Africa",
"1C_198": "South African Common Customs Area",
"SS": "South Sudan",
"ES": "Spain",
"LK": "Sri Lanka",
"KN": "Saint Kitts and Nevis",
"LC": "Saint Lucia",
"VC": "Saint Vincent and the Grenadines",
"SD": "Sudan",
"SR": "Suriname",
"SE": "Sweden",
"CH": "Switzerland",
"SY": "Syria",
"TW": "Taiwan",
"TJ": "Tajikistan",
"TZ": "Tanzania",
"TH": "Thailand",
"TL": "Timor-Leste",
"TG": "Togo",
"TO": "Tonga",
"TT": "Trinidad and Tobago",
"TN": "Tunisia",
"TR": "Turkey",
"TM": "Turkmenistan",
"TV": "Tuvalu",
"UG": "Uganda",
"UA": "Ukraine",
"AE": "United Arab Emirates",
"GB": "United Kingdom",
"US": "United States",
"UY": "Uruguay",
"SUH": "Former USSR",
"UZ": "Uzbekistan",
"VU": "Vanuatu",
"VE": "Venezuela",
"VN": "Viet Nam",
"PS": "Palestine",
"YE": "Yemen",
"YUC": "Former Yugoslavia",
"ZM": "Zambia",
"ZW": "Zimbabwe",
"R14": "Community of Independent States",
"U2": "Euro Area",
"E1": "Europe",
"B0": "European Union",
"F97": "Middle East",
"XS5": "Middle East and Central Asia",
"X88": "Other Countries n.i.e. (IMF)",
"F1": "Africa",
"F6": "Sub-Saharan Africa",
"F19": "Africa not allocated",
"A10": "Western Hemisphere",
"A109": "Western Hemisphere not allocated",
"W00": "World",
"XR99": "Special Categories and Economic Zones",
"XS25": "Developing Asia (IMF)",
"XR43": "Emerging and Developing Countries",
"XR29": "Advanced Economies (IMF)",
"1C_ALLC": "All Countries",
"1C_ALL": "All Countries and Country Groups",
"1C_ALLG": "All Country Groups",
"1C_903": "Emerging and Developing Europe",
"1C_080": "Export Earnings: Fuel",
"1C_092": "Export Earnings: Nonfuel",
"1C_440": "Middle East, North Africa, Afghanistan, and Pakistan",
"1C_473": "Yemen Arab Rep",
"1C_459": "Yemen P.D. Rep",
"1C_All_Reporting_Countries_Data": "All Reporting Countries, Data",
"1C_All_Reporting_Countries_Metadata": "All Reporting Countries, Metadata",
"1C_Daily_Reporters": "Daily Reporters",
"1C_Dual_Reporters": "Dual Reporters",
"1C_Monthly_Reporters": "Monthly Reporters",
"1C_Single_Reporters": "Single Reporters",
"1C_Single_Reporters_USA_and_Peru": "Single Reporters - USA and Peru",
"_X": "Unspecified"
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,18 +10,57 @@ from openbb_core.provider.standard_models.available_indicators import (
AvailableIndicatorsData,
AvailableIndicesQueryParams,
)
from pydantic import Field
from pydantic import ConfigDict, Field
class ImfAvailableIndicatorsQueryParams(AvailableIndicesQueryParams):
"""IMF Available Indicators Query Parameters."""
__json_schema_extra__ = {"query": {"multiple_items_allowed": True}}
__json_schema_extra__ = {
"query": {"multiple_items_allowed": True},
"dataflows": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "endpoint",
"optionsEndpoint": "/api/v1/imf_utils/list_dataflow_choices",
"multiSelect": False,
"style": {"popupWidth": 950},
},
},
"symbol": {
"multiple_items_allowed": True,
"x-widget_config": {
"type": "text",
"multiSelect": False,
"style": {"width": 400},
"multiple": True,
},
},
"keywords": {"multiple_items_allowed": True},
}
query: str | None = Field(
default=None,
description="The query string to search through the available indicators."
+ " Use semicolons to separate multiple terms.",
description="The search query string. Multiple search phrases can be separated by semicolons."
+ " Each phrase can use AND (+) and OR (|) operators, as well as quoted phrases."
+ " Semicolon separation allows commas to be used within search phrases.",
)
dataflows: str | list[str] | None = Field(
default=None,
description="List of IMF dataflow IDs to filter the indicators."
+ " Use semicolons to separate multiple dataflow IDs.",
)
keywords: str | list[str] | None = Field(
default=None,
description="List of keywords to filter results. Each keyword is a single word that must"
+ " appear in the indicator's label or description. Keywords prefixed with 'not'"
+ " will exclude indicators containing that word (e.g., 'not USD' excludes indicators"
+ " with 'USD' in them).",
)
symbol: str | None = Field(
default=None,
exclude=True,
description="Dummy field to allow grouping by symbol.",
)
@@ -29,32 +68,46 @@ class ImfAvailableIndicatorsData(AvailableIndicatorsData):
"""IMF Available Indicators Data."""
__alias_dict__ = {
"symbol_root": "parent",
"description": "title",
"description": "label",
"symbol": "series_id",
"symbol_root": "indicator",
"long_description": "description",
}
dataset: str | None = Field(
default=None,
description="The IMF dataset associated with the symbol.",
model_config = ConfigDict(
extra="ignore",
json_schema_extra={
"symbol": {
"x-widget_config": {
"renderFn": "cellOnClick",
"renderFnParams": {
"actionType": "groupBy",
"groupByParamName": "symbol",
},
}
}
},
)
table: str | None = Field(
default=None,
description="The name of the table associated with the symbol.",
agency_id: str = Field(description="The agency ID responsible for the indicator.")
dataflow_id: str = Field(
description="The IMF dataflow ID associated with the indicator."
)
level: int | None = Field(
default=None,
description="The indentation level of the data, relative to the table and symbol_root",
dataflow_name: str = Field(
description="The name of the IMF dataflow (symbol root)."
)
order: int | float | None = Field(
default=None,
description="Order of the data, relative to the table.",
structure_id: str = Field(
description="The data structure ID associated with the indicator."
)
children: str | None = Field(
default=None,
description="The symbol of the child data, if any.",
dimension_id: str = Field(
description="The dimension ID of the indicator in the data structure."
)
unit: str | None = Field(
default=None,
description="The unit of the data.",
long_description: str | None = Field(
default=None, description="Detailed description of the indicator."
)
member_of: list[str] = Field(
default_factory=list,
description="List of table symbols (dataflow_id::table_id) this indicator belongs to.",
)
@@ -76,45 +129,38 @@ class ImfAvailableIndicatorsFetcher(
) -> list[dict]:
"""Fetch the data."""
# pylint: disable=import-outside-toplevel
from numpy import nan
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_imf.utils.constants import load_symbols
from pandas import DataFrame, Series
from openbb_imf.utils.metadata import ImfMetadata
metadata = ImfMetadata()
if isinstance(query.dataflows, str):
dataflows = query.dataflows.split(",")
elif isinstance(query.dataflows, list):
dataflows = query.dataflows
else:
dataflows = None
if isinstance(query.keywords, str):
keywords = query.keywords.split(",")
elif isinstance(query.keywords, list):
keywords = query.keywords
else:
keywords = None
try:
all_symbols = load_symbols("all")
except OpenBBError as e:
raise OpenBBError(f"Failed to load IMF symbols static file: {e}") from e
results = metadata.search_indicators(
query=query.query.replace(",", ", ") if query.query else "",
dataflows=dataflows,
keywords=keywords,
)
except Exception as e: # pylint: disable=broad-except
raise OpenBBError(e) from e
terms = [term.strip() for term in query.query.split(";")] if query.query else []
if not results:
raise EmptyDataError("No indicators found for the given query.")
df = (
DataFrame(all_symbols)
.T.reset_index()
.rename(columns={"index": "symbol"})
.replace({nan: None})
)
if not terms:
records = df.to_dict(orient="records")
else:
combined_mask = Series([True] * len(df))
for term in terms:
mask = df.apply(
lambda row, term=term: row.astype(str).str.contains(
term, case=False, regex=True, na=False
)
).any(axis=1)
combined_mask &= mask
matches = df[combined_mask]
if matches.empty:
raise EmptyDataError("No results found for the provided query.")
records = matches.to_dict(orient="records")
return records
return results
@staticmethod
def transform_data(
@@ -123,4 +169,11 @@ class ImfAvailableIndicatorsFetcher(
**kwargs: Any,
) -> list[ImfAvailableIndicatorsData]:
"""Transform the data."""
return [ImfAvailableIndicatorsData.model_validate(d) for d in data]
results = []
for d in data:
# Build the ready-to-use symbol: dataflow_id::indicator_code
dataflow_id = d.get("dataflow_id", "")
indicator_code = d.get("indicator", "")
d["symbol"] = f"{dataflow_id}::{indicator_code}"
results.append(ImfAvailableIndicatorsData.model_validate(d))
return results

View File

@@ -0,0 +1,588 @@
"""IMF CPI Data."""
# pylint: disable=unused-argument
from typing import Any
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.consumer_price_index import (
ConsumerPriceIndexData,
ConsumerPriceIndexQueryParams,
)
from openbb_imf.utils.helpers import normalize_country_label
from openbb_imf.utils.query_builder import ImfQueryBuilder
from pydantic import Field, field_validator
CpiCountries = [
{"label": "Afghanistan", "value": "AFG"},
{"label": "Albania", "value": "ALB"},
{"label": "Algeria", "value": "DZA"},
{"label": "Angola", "value": "AGO"},
{"label": "Anguilla, United Kingdom-British Overseas Territory", "value": "AIA"},
{"label": "Antigua and Barbuda", "value": "ATG"},
{"label": "Argentina", "value": "ARG"},
{"label": "Armenia, Republic of", "value": "ARM"},
{"label": "Aruba, Kingdom of the Netherlands", "value": "ABW"},
{"label": "Australia", "value": "AUS"},
{"label": "Austria", "value": "AUT"},
{"label": "Azerbaijan, Republic of", "value": "AZE"},
{"label": "Bahamas, The", "value": "BHS"},
{"label": "Bahrain", "value": "BHR"},
{"label": "Bangladesh", "value": "BGD"},
{"label": "Barbados", "value": "BRB"},
{"label": "Belarus, Republic of", "value": "BLR"},
{"label": "Belgium", "value": "BEL"},
{"label": "Belize", "value": "BLZ"},
{"label": "Benin", "value": "BEN"},
{"label": "Bhutan", "value": "BTN"},
{"label": "Bolivia", "value": "BOL"},
{"label": "Bosnia and Herzegovina", "value": "BIH"},
{"label": "Botswana", "value": "BWA"},
{"label": "Brazil", "value": "BRA"},
{"label": "British Virgin Islands", "value": "VGB"},
{"label": "Brunei Darussalam", "value": "BRN"},
{"label": "Bulgaria", "value": "BGR"},
{"label": "Burkina Faso", "value": "BFA"},
{"label": "Burundi", "value": "BDI"},
{"label": "Cabo Verde", "value": "CPV"},
{"label": "Cambodia", "value": "KHM"},
{"label": "Cameroon", "value": "CMR"},
{"label": "Canada", "value": "CAN"},
{"label": "Cayman Islands", "value": "CYM"},
{"label": "Central African Republic", "value": "CAF"},
{"label": "Chad", "value": "TCD"},
{"label": "Chile", "value": "CHL"},
{"label": "China", "value": "CHN"},
{"label": "Colombia", "value": "COL"},
{"label": "Comoros", "value": "COM"},
{"label": "Congo", "value": "COG"},
{"label": "Costa Rica", "value": "CRI"},
{"label": "Croatia, Republic of", "value": "HRV"},
{"label": "Curacao", "value": "CUW"},
{"label": "Cyprus", "value": "CYP"},
{"label": "Czech Republic", "value": "CZE"},
{"label": "Democratic Republic of the Congo", "value": "COD"},
{"label": "Denmark", "value": "DNK"},
{"label": "Djibouti", "value": "DJI"},
{"label": "Dominica", "value": "DMA"},
{"label": "Dominican Republic", "value": "DOM"},
{"label": "Ecuador", "value": "ECU"},
{"label": "Egypt", "value": "EGY"},
{"label": "El Salvador", "value": "SLV"},
{"label": "Equatorial Guinea, Republic of", "value": "GNQ"},
{"label": "Estonia, Republic of", "value": "EST"},
{"label": "Eswatini, Kingdom of", "value": "SWZ"},
{"label": "Ethiopia", "value": "ETH"},
{"label": "Euro Area (EA)", "value": "G163"},
{"label": "Fiji, Republic of", "value": "FJI"},
{"label": "Finland", "value": "FIN"},
{"label": "France", "value": "FRA"},
{"label": "Gabon", "value": "GAB"},
{"label": "Gambia", "value": "GMB"},
{"label": "Georgia", "value": "GEO"},
{"label": "Germany", "value": "DEU"},
{"label": "Ghana", "value": "GHA"},
{"label": "Greece", "value": "GRC"},
{"label": "Greenland", "value": "GRL"},
{"label": "Grenada", "value": "GRD"},
{"label": "Guadeloupe", "value": "GLP"},
{"label": "Guatemala", "value": "GTM"},
{"label": "Guinea", "value": "GIN"},
{"label": "Guinea-Bissau", "value": "GNB"},
{"label": "Guyana", "value": "GUY"},
{"label": "Haiti", "value": "HTI"},
{"label": "Honduras", "value": "HND"},
{"label": "Hong Kong", "value": "HKG"},
{"label": "Hungary", "value": "HUN"},
{"label": "Iceland", "value": "ISL"},
{"label": "India", "value": "IND"},
{"label": "Indonesia", "value": "IDN"},
{"label": "Iran", "value": "IRN"},
{"label": "Iraq", "value": "IRQ"},
{"label": "Ireland", "value": "IRL"},
{"label": "Israel", "value": "ISR"},
{"label": "Italy", "value": "ITA"},
{"label": "Ivory Coast", "value": "CIV"},
{"label": "Jamaica", "value": "JAM"},
{"label": "Japan", "value": "JPN"},
{"label": "Jordan", "value": "JOR"},
{"label": "Kazakhstan", "value": "KAZ"},
{"label": "Kenya", "value": "KEN"},
{"label": "Kiribati", "value": "KIR"},
{"label": "Kosovo", "value": "KOS"},
{"label": "Kuwait", "value": "KWT"},
{"label": "Kyrgyz Republic", "value": "KGZ"},
{"label": "Lao", "value": "LAO"},
{"label": "Latvia, Republic of", "value": "LVA"},
{"label": "Lebanon", "value": "LBN"},
{"label": "Lesotho", "value": "LSO"},
{"label": "Liberia", "value": "LBR"},
{"label": "Libya", "value": "LBY"},
{"label": "Lithuania, Republic of", "value": "LTU"},
{"label": "Luxembourg", "value": "LUX"},
{"label": "Macao", "value": "MAC"},
{"label": "Madagascar", "value": "MDG"},
{"label": "Malawi", "value": "MWI"},
{"label": "Malaysia", "value": "MYS"},
{"label": "Maldives", "value": "MDV"},
{"label": "Mali", "value": "MLI"},
{"label": "Malta", "value": "MLT"},
{"label": "Martinique", "value": "MTQ"},
{"label": "Mauritania", "value": "MRT"},
{"label": "Mauritius", "value": "MUS"},
{"label": "Mexico", "value": "MEX"},
{"label": "Micronesia, Federated States of", "value": "FSM"},
{"label": "Moldova, Republic of", "value": "MDA"},
{"label": "Mongolia", "value": "MNG"},
{"label": "Montenegro", "value": "MNE"},
{"label": "Montserrat", "value": "MSR"},
{"label": "Morocco", "value": "MAR"},
{"label": "Mozambique", "value": "MOZ"},
{"label": "Myanmar", "value": "MMR"},
{"label": "Namibia", "value": "NAM"},
{"label": "Nauru, Republic of", "value": "NRU"},
{"label": "Nepal", "value": "NPL"},
{"label": "Netherlands", "value": "NLD"},
{"label": "Netherlands Antilles", "value": "ANT"},
{"label": "New Caledonia", "value": "NCL"},
{"label": "New Zealand", "value": "NZL"},
{"label": "Nicaragua", "value": "NIC"},
{"label": "Niger", "value": "NER"},
{"label": "Nigeria", "value": "NGA"},
{"label": "North Macedonia", "value": "MKD"},
{"label": "Norway", "value": "NOR"},
{"label": "Oman", "value": "OMN"},
{"label": "Pakistan", "value": "PAK"},
{"label": "Palau", "value": "PLW"},
{"label": "Palestine", "value": "WBG"},
{"label": "Panama", "value": "PAN"},
{"label": "Papua New Guinea", "value": "PNG"},
{"label": "Paraguay", "value": "PRY"},
{"label": "Peru", "value": "PER"},
{"label": "Philippines", "value": "PHL"},
{"label": "Poland", "value": "POL"},
{"label": "Portugal", "value": "PRT"},
{"label": "Qatar", "value": "QAT"},
{"label": "Romania", "value": "ROU"},
{"label": "Russia", "value": "RUS"},
{"label": "Rwanda", "value": "RWA"},
{"label": "Saint Kitts and Nevis", "value": "KNA"},
{"label": "Saint Lucia", "value": "LCA"},
{"label": "Saint Vincent and the Grenadines", "value": "VCT"},
{"label": "Samoa", "value": "WSM"},
{"label": "San Marino", "value": "SMR"},
{"label": "Sao Tome and Principe", "value": "STP"},
{"label": "Saudi Arabia", "value": "SAU"},
{"label": "Senegal", "value": "SEN"},
{"label": "Serbia", "value": "SRB"},
{"label": "Seychelles", "value": "SYC"},
{"label": "Sierra Leone", "value": "SLE"},
{"label": "Singapore", "value": "SGP"},
{"label": "Sint Maarten (Dutch part)", "value": "SXM"},
{"label": "Slovak Republic", "value": "SVK"},
{"label": "Slovenia, Republic of", "value": "SVN"},
{"label": "Solomon Islands", "value": "SLB"},
{"label": "Somalia", "value": "SOM"},
{"label": "South Africa", "value": "ZAF"},
{"label": "South Korea", "value": "KOR"},
{"label": "South Sudan", "value": "SSD"},
{"label": "Spain", "value": "ESP"},
{"label": "Sri Lanka", "value": "LKA"},
{"label": "Sudan", "value": "SDN"},
{"label": "Suriname", "value": "SUR"},
{"label": "Sweden", "value": "SWE"},
{"label": "Switzerland", "value": "CHE"},
{"label": "Syria", "value": "SYR"},
{"label": "Tajikistan", "value": "TJK"},
{"label": "Tanzania", "value": "TZA"},
{"label": "Thailand", "value": "THA"},
{"label": "Timor-Leste", "value": "TLS"},
{"label": "Togo", "value": "TGO"},
{"label": "Tonga", "value": "TON"},
{"label": "Trinidad and Tobago", "value": "TTO"},
{"label": "Tunisia", "value": "TUN"},
{"label": "Turkey", "value": "TUR"},
{"label": "Tuvalu", "value": "TUV"},
{"label": "Uganda", "value": "UGA"},
{"label": "Ukraine", "value": "UKR"},
{"label": "United Arab Emirates", "value": "ARE"},
{"label": "United Kingdom", "value": "GBR"},
{"label": "United States", "value": "USA"},
{"label": "Uruguay", "value": "URY"},
{"label": "Uzbekistan", "value": "UZB"},
{"label": "Vanuatu", "value": "VUT"},
{"label": "Venezuela", "value": "VEN"},
{"label": "Viet Nam", "value": "VNM"},
{"label": "Yemen", "value": "YEM"},
{"label": "Zambia", "value": "ZMB"},
{"label": "Zimbabwe", "value": "ZWE"},
{"label": "All", "value": "*"},
]
CPI_LABEL_TO_CODE: dict[str, str] = {
normalize_country_label(item["label"]): item["value"] for item in CpiCountries
}
CPI_CODE_TO_LABEL: dict[str, str] = {
item["value"]: normalize_country_label(item["label"]) for item in CpiCountries
}
CPI_CODE_SET: set[str] = {item["value"] for item in CpiCountries}
transformation_map: dict = {
"index": "IX",
"period": "POP_PCH_PA_PT",
"yoy": "YOY_PCH_PA_PT",
"ref_index": "SRP_IX",
"ref_period": "SRP_POP_PCH_PA_PT",
"ref_yoy": "SRP_YOY_PCH_PA_PT",
"weight": "WGT",
"weight_percent": "WGT_PT",
}
transformation_choices = [
"index",
"period",
"yoy",
"ref_index",
"ref_period",
"ref_yoy",
"weight",
"weight_percent",
]
expenditure_dict_rev: dict = {
"_T": "total",
"CP01": "food_non_alcoholic_beverages",
"CP02": "alcoholic_beverages_tobacco_narcotics",
"CP03": "clothing_footwear",
"CP04": "housing_water_electricity_gas",
"CP05": "furniture_household_equipment",
"CP06": "health",
"CP07": "transport",
"CP08": "communication",
"CP09": "recreation_culture",
"CP10": "education",
"CP11": "restaurants_hotels",
"CP12": "miscellaneous_goods_services",
"*": "all",
}
expenditure_dict: dict = {v: k for k, v in expenditure_dict_rev.items()}
# Order mapping for sorting expenditure categories (COICOP codes)
# _T (total) comes first, then CP01-CP12 in numerical order
expenditure_order: dict[str, int] = {
"_T": 0,
"CP01": 1,
"CP02": 2,
"CP03": 3,
"CP04": 4,
"CP05": 5,
"CP06": 6,
"CP07": 7,
"CP08": 8,
"CP09": 9,
"CP10": 10,
"CP11": 11,
"CP12": 12,
"CP13": 13,
"CP14": 14,
}
expenditure_choices = [
"total",
"all",
"food_non_alcoholic_beverages",
"alcoholic_beverages_tobacco_narcotics",
"clothing_footwear",
"housing_water_electricity_gas",
"furniture_household_equipment",
"health",
"transport",
"communication",
"recreation_culture",
"education",
"restaurants_hotels",
"miscellaneous_goods_services",
]
class ImfConsumerPriceIndexQueryParams(ConsumerPriceIndexQueryParams):
"""IMF CPI Query Params Model."""
__json_schema_extra__ = {
"country": {
"multiple_items_allowed": True,
"choices": list(CPI_LABEL_TO_CODE),
},
"transform": {
"multiple_items_allowed": False,
"choices": transformation_choices,
},
"expenditure": {
"multiple_items_allowed": True,
"choices": expenditure_choices,
},
}
expenditure: str = Field(
default="total", description="Expenditure component of CPI."
)
limit: int | None = Field(
default=None,
description="Maximum number of records to retrieve per series and country."
+ " If None, retrieves all available records.",
)
@field_validator("country", mode="before", check_fields=False)
@classmethod
def validate_country(cls, c):
"""Validate country.
Accepts both ISO3 codes (e.g., "USA") and snake_case country names
(e.g., "united_states"). Converts names to ISO3 codes.
"""
result: list = []
values = c.replace(" ", "_").split(",")
for v in values:
v_upper = v.upper()
v_lower = v.lower()
# Check if it's a valid ISO3 code
if v_upper in CPI_CODE_SET:
result.append(v_upper)
# Check if it's a valid snake_case country name
elif v_lower in CPI_LABEL_TO_CODE:
result.append(CPI_LABEL_TO_CODE[v_lower])
else:
raise ValueError(
f"Country '{v}' is not a valid IMF country code (ISO3) or country name."
)
return ",".join(result)
@field_validator("expenditure", mode="before", check_fields=False)
@classmethod
def validate_expenditure(cls, v):
"""Validate expenditure."""
expenditures = v.replace(" ", "_").split(",")
new_expenditures: list[str] = []
for e in expenditures:
if (
e.lower() not in expenditure_choices
and e.upper() not in expenditure_dict_rev
):
raise ValueError(
f"Expenditure '{e}' is not a valid choice. Valid choices:\n\n{expenditure_choices}"
)
new_expenditures.append(e.lower())
return ",".join(new_expenditures)
class ImfConsumerPriceIndexData(ConsumerPriceIndexData):
"""IMF CPI Data Model."""
unit: str = Field(
description="Unit of measurement.",
)
unit_multiplier: int | float = Field(
description="Unit multiplier for the observation value.",
)
country_code: str = Field(
description="ISO3 country code.",
)
series_id: str = Field(
description="IMF series identifier.",
)
expenditure: str = Field(
description="Expenditure category.",
)
title: str = Field(
description="Complete reference title for the series.",
)
order: int | None = Field(
default=None,
description="Sort order for expenditure categories and table presentations.",
)
class ImfConsumerPriceIndexFetcher(
Fetcher[ImfConsumerPriceIndexQueryParams, list[ImfConsumerPriceIndexData]]
):
"""IMF CPI Fetcher."""
@staticmethod
def transform_query(
params: dict[str, Any],
) -> ImfConsumerPriceIndexQueryParams:
"""Transform query."""
return ImfConsumerPriceIndexQueryParams(**params)
@staticmethod
async def aextract_data(
query: ImfConsumerPriceIndexQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> dict:
"""Extract data."""
countries = query.country.split(",")
countries_str = (
"*" if "*" in countries else "+".join([c.upper() for c in countries])
)
index_type = "HICP" if query.harmonized is True else "CPI"
expenditures = query.expenditure.split(",") if query.expenditure else ["total"]
expenditures_str = (
"*"
if "all" in expenditures
else "+".join(
[
(
e.upper()
if e.upper() in expenditure_dict_rev
else expenditure_dict[e]
)
for e in expenditures
]
)
)
parameters: dict = {
"COUNTRY": countries_str,
"INDEX_TYPE": index_type,
"COICOP_1999": expenditures_str,
"TYPE_OF_TRANSFORMATION": transformation_map[query.transform],
"FREQUENCY": query.frequency[0].upper(),
}
query_builder = ImfQueryBuilder()
# Mappings from IMF dimension codes to user-friendly parameter names
dim_to_param = {
"COUNTRY": "country",
"INDEX_TYPE": "harmonized",
"COICOP_1999": "expenditure",
"TYPE_OF_TRANSFORMATION": "transform",
"FREQUENCY": "frequency",
}
# Reverse mappings for values
transformation_rev = {v: k for k, v in transformation_map.items()}
frequency_map = {"A": "annual", "Q": "quarter", "M": "monthly"}
if query.limit is not None:
parameters["lastNObservations"] = query.limit
try:
data = query_builder.fetch_data(
dataflow="CPI",
start_date=(
query.start_date.strftime("%Y-%m-%d") if query.start_date else None
),
end_date=(
query.end_date.strftime("%Y-%m-%d") if query.end_date else None
),
**parameters,
)
except ValueError as e:
# Translate dimension codes to user-friendly parameter names in error message
error_msg = str(e)
for dim_code, param_name in dim_to_param.items():
error_msg = error_msg.replace(f"'{dim_code}'", f"'{param_name}'")
error_msg = error_msg.replace(f'"{dim_code}"', f'"{param_name}"')
# Translate transformation values
for api_val, user_val in transformation_rev.items():
error_msg = error_msg.replace(f"'{api_val}'", f"'{user_val}'")
# Translate frequency values
for api_val, user_val in frequency_map.items():
error_msg = error_msg.replace(f"'{api_val}'", f"'{user_val}'")
# Translate expenditure values
for api_val, user_val in expenditure_dict_rev.items():
error_msg = error_msg.replace(f"'{api_val}'", f"'{user_val}'")
# Translate INDEX_TYPE values
error_msg = error_msg.replace("'CPI'", "'False'")
error_msg = error_msg.replace("'HICP'", "'True'")
# Translate country codes back to user-friendly names
for code, label in CPI_CODE_TO_LABEL.items():
error_msg = error_msg.replace(f"'{code}'", f"'{label}'")
raise OpenBBError(error_msg) from e
except OpenBBError as e:
raise OpenBBError(e) from e
return data
@staticmethod
def transform_data(
query: ImfConsumerPriceIndexQueryParams,
data: dict,
**kwargs: Any,
) -> AnnotatedResult[list[ImfConsumerPriceIndexData]]:
"""Transform data and validate the model."""
row_data = data.get("data", [])
result: list[ImfConsumerPriceIndexData] = []
metadata: dict = data.get("metadata", {})
dataset_info: dict = metadata.pop("dataset", {})
table_info: dict = (
metadata.pop("IMF_STA_CPI_CPI", {})
or metadata.pop("IMF_STA_CPI_HICP", {})
or {}
)
dataset_info["index_type"] = table_info.get("indicator", "")
dataset_info["index_description"] = table_info.get("description", "")
if not row_data:
raise OpenBBError("No data returned for the given query parameters.")
for item in row_data:
# Filter by date range here because IMF API date filtering can be inconsistent
item_date = item.get("TIME_PERIOD", None)
if (
query.start_date
and item_date
and item_date < query.start_date.strftime("%Y-%m-%d")
):
continue
if (
query.end_date
and item_date
and item_date > query.end_date.strftime("%Y-%m-%d")
):
continue
# Get translated labels (these are now human-readable)
frequency = (item.get("FREQUENCY") or "").strip()
index_type = (item.get("INDEX_TYPE") or "").strip()
expenditure = (item.get("COICOP_1999") or item.get("title") or "").strip()
expenditure_code = (item.get("COICOP_1999_code") or "").strip()
transformation = (item.get("TYPE_OF_TRANSFORMATION") or "").strip()
# Build title from translated values
title = f"{frequency} {index_type} - {expenditure} - {transformation}"
# Get unit from transformation (use last part if comma-separated)
unit = (transformation.rsplit(", ", maxsplit=1)[-1] or "").strip()
# Get sort order from expenditure code
order = expenditure_order.get(expenditure_code, 99)
obs_value = item.get("OBS_VALUE", None)
multiplier = item.get("UNIT_MULT", 1)
if "percent" in unit.lower() and obs_value is not None:
obs_value = obs_value / 100.0
multiplier = 100
symbol = item.get("series_id", "").strip().split("IMF_STA_CPI_")[-1]
symbol = f"CPI::{symbol}"
new_row = {
"date": item_date,
"country": (item.get("COUNTRY") or "").strip() or None,
"country_code": (item.get("country_code") or "").strip() or None,
"series_id": symbol,
"expenditure": expenditure or None,
"title": title.strip(),
"unit": unit,
"unit_multiplier": multiplier,
"value": obs_value,
"order": order,
}
result.append(ImfConsumerPriceIndexData.model_validate(new_row))
# Sort by date, then country, then order (expenditure)
result.sort(
key=lambda x: (
x.date,
x.country or "",
x.order if x.order is not None else 99,
)
)
return AnnotatedResult(
result=result, metadata={"dataset": dataset_info, "series": metadata}
)

View File

@@ -2,10 +2,11 @@
# pylint: disable=unused-argument
from datetime import datetime
from datetime import date as dateType
from typing import Any
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.direction_of_trade import (
DirectionOfTradeData,
@@ -13,22 +14,16 @@ from openbb_core.provider.standard_models.direction_of_trade import (
)
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_imf.utils.dot_helpers import (
load_country_map,
load_country_to_code_map,
validate_countries,
get_label_to_code_map,
resolve_country_input,
)
from pydantic import ConfigDict, Field, field_validator
dot_indicators_dict = {
"exports": "TXG_FOB_USD",
"imports": "TMG_CIF_USD",
"exports": "XG_FOB_USD",
"imports": "MG_CIF_USD",
"balance": "TBG_USD",
"all": "TXG_FOB_USD+TMG_CIF_USD+TBG_USD",
}
dot_titles_map = {
"TXG_FOB_USD": "Goods, Value of Exports, Free on board (FOB), US Dollars",
"TMG_CIF_USD": "Goods, Value of Imports, Cost, Insurance, Freight (CIF), US Dollars",
"TBG_USD": "Goods, Value of Trade Balance, US Dollars",
"all": "*",
}
@@ -38,18 +33,99 @@ class ImfDirectionOfTradeQueryParams(DirectionOfTradeQueryParams):
__json_schema_extra__ = {
"country": {
"multiple_items_allowed": True,
"choices": ["all"] + sorted(list(load_country_to_code_map())),
"choices": list(get_label_to_code_map()),
"x-widget_config": {"value": "united_states", "style": {"popupWidth": 600}},
},
"counterpart": {
"multiple_items_allowed": True,
"choices": ["all"] + sorted(list(load_country_to_code_map())),
"choices": ["all"] + list(get_label_to_code_map()),
"x-widget_config": {"value": "world", "style": {"popupWidth": 600}},
},
}
limit: int | None = Field(
default=None,
description="Limit the number of results returned, the most recent data points first.",
)
@field_validator("country", "counterpart", mode="before")
@classmethod
def _validate_country_fields(cls, v):
"""Validate country and counterpart fields.
Accepts both ISO3 codes (e.g., 'USA') and snake_case country names
(e.g., 'united_states'). Converts names to ISO3 codes.
"""
if not v:
raise ValueError("Required parameter for IMF provider not supplied.")
if isinstance(v, str) and v.lower() in ["all", "*"]:
return "*"
# Split by comma if string
values = (
v.split(",")
if isinstance(v, str) and "," in v
else [v] if isinstance(v, str) else v
)
result: list[str] = []
for item in values:
item_stripped = item.strip()
if item_stripped.lower() in ["all", "*"]:
if len(values) > 1:
raise ValueError(
"'all' cannot be used with other country codes in a list."
)
return "*"
# Resolve the country input (handles both codes and names)
resolved = resolve_country_input(item_stripped)
result.append(resolved)
return ",".join(result)
class ImfDirectionOfTradeData(DirectionOfTradeData):
"""IMF Direction Of Trade Data."""
model_config = ConfigDict(extra="ignore")
__alias_dict__ = {
"date": "TIME_PERIOD",
"symbol": "series_id",
"country": "COUNTRY",
"counterpart": "COUNTERPART_COUNTRY",
"counterpart_code": "counterpart_country_code",
"value": "OBS_VALUE",
"scale": "SCALE",
"frequency": "FREQUENCY",
}
date: dateType | int = Field(description="The date of the data.")
country: str = Field(description="The country or region to the trade.")
unit: str | None = Field(default=None, description="Unit of the value.")
country_code: str = Field(description="IMF country code.")
counterpart: str = Field(description="Counterpart country or region to the trade.")
counterpart_code: str = Field(description="IMF counterpart country code.")
symbol: str | None = Field(
default=None,
description="Symbol representing the entity requested in the data. Concatenated series identifier.",
)
title: str | None = Field(
default=None, description="Title corresponding to the symbol."
)
value: float = Field(description="Trade value.")
scale: str | None = Field(default=None, description="Scale of the value.")
unit_multiplier: int | None = Field(
default=None, description="Unit multiplier of the value."
)
@field_validator("symbol", mode="before")
@classmethod
def _validate_symbol(cls, v):
"""Format symbol to indicators format."""
symbol = v.split("IMTS_")[-1]
return f"IMTS::{symbol}"
class ImfDirectionOfTradeFetcher(
Fetcher[ImfDirectionOfTradeQueryParams, list[ImfDirectionOfTradeData]]
@@ -59,35 +135,6 @@ class ImfDirectionOfTradeFetcher(
@staticmethod
def transform_query(params: dict[str, Any]) -> ImfDirectionOfTradeQueryParams:
"""Transform query parameters."""
countries = params.get("country", "")
countries = countries.split(",") if countries else "all"
if countries != "all":
countries = validate_countries(countries)
counterparts = params.get("counterpart", "")
counterparts = counterparts.split(",") if params.get("counterpart") else "all"
if counterparts != "all":
counterparts = validate_countries(counterparts)
now = datetime.now().date()
if countries == "all" and counterparts == "all":
raise OpenBBError(
"Both 'country' and 'counterpart' cannot be None, or 'all'."
+ " Please supply lowercase country names or two-letter ISO codes."
)
if countries == counterparts:
raise OpenBBError("The 'country' and 'counterpart' cannot be the same.")
params["country"] = countries
params["counterpart"] = counterparts
if not params.get("end_date"):
params["end_date"] = now.replace(month=12, day=31).strftime("%Y-%m-%d")
if (countries == "all" or counterparts == "all") and not params.get(
"start_date"
):
params["start_date"] = now.replace(year=now.year - 1).strftime("%Y-%m-%d")
return ImfDirectionOfTradeQueryParams(**params)
@staticmethod
@@ -95,180 +142,47 @@ class ImfDirectionOfTradeFetcher(
query: ImfDirectionOfTradeQueryParams,
credentials: dict[str, str] | None,
**kwargs: Any,
) -> list[dict]:
) -> dict:
"""Extract the data from the IMF API."""
# pylint: disable=import-outside-toplevel
from aiohttp.client_exceptions import ContentTypeError # noqa
from json import JSONDecodeError
from openbb_core.provider.utils.helpers import amake_request
from pandas import to_datetime
from pandas.tseries import offsets
from openbb_imf.utils.dot_helpers import imts_query
start_date = query.start_date
end_date = query.end_date
frequency = query.frequency[0].upper()
country = query.country if query.country != "all" else ""
counterpart = query.counterpart if query.counterpart != "all" else ""
indicator = dot_indicators_dict[query.direction]
# Adjust the dates to the date relative to frequency.
# The API does not accept arbitrary dates, so we need to adjust them.
if start_date:
start_date = to_datetime(start_date)
if frequency == "Q":
start_date = offsets.QuarterBegin(startingMonth=1).rollback(start_date)
elif frequency == "A":
start_date = offsets.YearBegin().rollback(start_date)
else:
start_date = offsets.MonthBegin().rollback(start_date)
start_date = start_date.strftime("%Y-%m-%d") # type: ignore
if end_date:
end_date = to_datetime(end_date)
if frequency == "Q":
end_date = offsets.QuarterEnd().rollforward(end_date)
elif frequency == "A":
end_date = offsets.YearEnd().rollforward(end_date)
else:
end_date = offsets.MonthEnd().rollforward(end_date)
end_date = end_date.strftime("%Y-%m-%d") # type: ignore
date_range = ( # type: ignore
f"?startPeriod={start_date}&endPeriod={end_date}"
if start_date and end_date
else ""
)
base_url = "http://dataservices.imf.org/REST/SDMX_JSON.svc/"
key = f"CompactData/DOT/{frequency}.{country}.{indicator}.{counterpart}"
url = f"{base_url}{key}{date_range}"
if query.limit:
kwargs = {"lastNObservations": query.limit}
try:
response = await amake_request(url, timeout=20)
except (JSONDecodeError, ContentTypeError) as e:
raise OpenBBError(
"Error fetching data; This might be rate-limiting. Try again later."
) from e
if "ErrorDetails" in response:
raise OpenBBError(
f"{response['ErrorDetails'].get('Code')} -> {response['ErrorDetails'].get('Message')}" # type: ignore
return imts_query(
country=query.country or "",
counterpart=query.counterpart or "",
indicator=dot_indicators_dict.get(query.direction, "*"),
freq=query.frequency[0].upper(),
start_date=(
query.start_date.strftime("%Y-%m-%d") if query.start_date else None
),
end_date=(
query.end_date.strftime("%Y-%m-%d") if query.end_date else None
),
**kwargs,
)
series = response.get("CompactData", {}).get("DataSet", {}).pop("Series", {}) # type: ignore
if not series:
raise OpenBBError(f"No time series data found -> {url} -> {response}")
# If there is only one series, they ruturn a dict instead of a list.
if series and isinstance(series, dict):
series = [series]
return series
except (ValueError, OpenBBError) as e:
raise OpenBBError(e) from e
@staticmethod
def transform_data(
query: ImfDirectionOfTradeQueryParams,
data: list[dict],
data: dict,
**kwargs: Any,
) -> list[ImfDirectionOfTradeData]:
) -> AnnotatedResult[list[ImfDirectionOfTradeData]]:
"""Transform the data."""
# pylint: disable=import-outside-toplevel
from openbb_imf.utils.constants import UNIT_MULTIPLIERS_MAP # noqa
from pandas import Categorical, DataFrame, to_datetime
from pandas.tseries import offsets
meta = data.get("metadata", {})
records = data.get("data", [])
if not data:
raise EmptyDataError()
if not records:
raise EmptyDataError("No data found for the given query parameters.")
dot_code_to_country = load_country_map()
series = data
results: list = []
for s in series:
if "Obs" not in s:
continue
meta = {
k.replace("@", "").lower(): (
UNIT_MULTIPLIERS_MAP.get(str(v), v) if k == "@UNIT_MULT" else v
)
for k, v in s.items()
if k != "Obs"
}
_symbol = meta.get("indicator", "")
_title = None
_data = s.pop("Obs", [])
if isinstance(_data, dict):
_data = [_data]
for d in _data:
_date = d.pop("@TIME_PERIOD", None)
val: float | None = d.pop("@OBS_VALUE", None)
_ = d.pop("@OBS_STATUS", None)
val = float(val) if val else None
if not val:
continue
if _date:
offset = (
offsets.QuarterEnd
if "Q" in _date
else (
offsets.YearEnd
if len(str(_date)) == 4
else offsets.MonthEnd
)
)
_date = to_datetime(_date)
_date = _date + offset(0)
_date = _date.strftime("%Y-%m-%d")
vals = {
k: v
for k, v in {
"date": _date,
"symbol": _symbol,
"country": dot_code_to_country.get(
meta.get("ref_area"), meta.get("ref_area")
),
"counterpart": dot_code_to_country.get(
meta.get("counterpart_area"), meta.get("counterpart_area")
),
"title": dot_titles_map.get(_symbol),
"scale": meta.get("unit_mult"),
"value": val,
}.items()
if v
}
if (
vals.get("value")
and vals.get("date")
and vals.get("country") != vals.get("counterpart")
):
d.update(vals)
if _data:
results.extend([d for d in _data if d])
df = DataFrame(results)
df["symbol"] = Categorical(
df["symbol"],
categories=list(dot_titles_map),
ordered=True,
return AnnotatedResult(
result=[
ImfDirectionOfTradeData.model_validate(record) for record in records
],
metadata=meta,
)
df["country"] = Categorical(
df["country"],
categories=sorted(df.country.unique().tolist()),
ordered=True,
)
df["counterpart"] = Categorical(
df["counterpart"],
categories=sorted(df.counterpart.unique().tolist()),
ordered=True,
)
df = df.sort_values(by=["date", "country", "counterpart"])
return [
ImfDirectionOfTradeData.model_validate(r)
for r in df.to_dict(orient="records")
]

View File

@@ -0,0 +1,82 @@
"""Miscellaneous metadata models for IMF provider."""
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.query_params import QueryParams
from pydantic import Field
class ImfIndicatorMetadata(Data):
"""Model representing an IMF Indicator."""
symbol: str = Field(
description="Concatenated series identifier for the indicator.",
validation_alias="series_id",
serialization_alias="symbol",
json_schema_extra={
"x-widget_config": {
"renderFn": "cellOnClick",
"renderFnParams": {
"actionType": "groupBy",
"groupBy": {
"paramName": "symbol",
"valueField": "symbol",
},
},
}
},
)
label: str = Field(description="Human-readable label for the indicator.")
description: str | None = Field(
default=None, description="Detailed description of the indicator."
)
dimension_id: str = Field(
description="The dimension ID of the indicator in the data structure."
)
indicator: str = Field(description="Indicator code.")
agency_id: str = Field(description="The agency ID responsible for the indicator.")
structure_id: str = Field(
description="The data structure ID associated with the indicator."
)
dataflow_id: str = Field(
description="The IMF dataflow ID the indicator belongs to."
)
dataflow_name: str = Field(description="The name of the IMF dataflow.")
class ImfTableMetadata(Data):
"""Model representing an IMF Table."""
name: str = Field(description="The name of the IMF table.")
symbol: str = Field(description="The table symbol.")
description: str = Field(description="Description of the IMF table.")
agency_id: str = Field(description="The agency ID responsible for the table.")
dataflow_id: str = Field(description="The IMF dataflow ID the table belongs to.")
codelist_id: str = Field(description="The codelist ID associated with the table.")
class ImfPresentationTableQuery(QueryParams):
"""Query parameters for IMF presentation table metadata."""
dataflow: str | None = Field(
default=None,
description="The IMF dataflow ID. See list_dataflows() for options.",
)
table: str | None = Field(
default=None, description="The IMF table ID. See list_tables() for options."
)
country: str | None = Field(
default=None, description="Country code to filter the data."
)
frequency: str | None = Field(default=None, description="Frequency of the data.")
dimension_values: str | None = Field(
default=None,
description="Dimension selection for filtering. Format: 'DIM_ID1:VAL1+VAL2.'"
+ " See presentation_table_choices() and list_dataflow_choices() for available dimensions and values.",
)
limit: int = Field(
default=4, description="Maximum number of records to retrieve per series."
)
raw: bool = Field(
default=False,
description="Return presentation table as raw JSON data if True.",
)

View File

@@ -38,19 +38,27 @@ class ImfPortInfoQueryParams(PortInfoQueryParams):
"style": {"popupWidth": 350},
}
},
"port_code": {
"x-widget_config": {
"description": "Dummy parameter for widget grouping.",
}
},
}
continent: PortContinents | None = Field(
default=None,
description="Filter by continent. This parameter is ignored when a `country` is provided.",
)
country: PortCountries | None = Field(
default=None,
description="Country to focus on. Enter as a 3-letter ISO country code."
+ " This parameter supersedes `continent` if both are provided.",
)
port_code: str | None = Field(
default=None,
exclude=True,
description="This is a dummy parameter to allow grouping in OpenBB Workspace widgets.",
)
limit: int | None = Field(
default=None,
description="Limit the number of results returned."
@@ -72,7 +80,8 @@ class ImfPortInfoData(PortInfoData):
"$.gridData": {
"h": 25,
"w": 25,
}
},
"$.description": "General information and statistics about global ports.",
}
},
)
@@ -91,6 +100,16 @@ class ImfPortInfoData(PortInfoData):
port_code: str = Field(
description="Unique ID assigned to the port.",
title="Port ID",
json_schema_extra={
"x-widget_config": {
"description": "Unique ID assigned to the port. Click on a value to change the widget group value.",
"renderFn": "cellOnClick",
"renderFnParams": {
"actionType": "groupBy",
"groupByParamName": "port_code",
},
}
},
)
continent: str = Field(
description="Continent where the port is located.",

View File

@@ -6,6 +6,7 @@ from datetime import date as dateType
from typing import Any
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.app.service.system_service import SystemService
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.port_volume import (
PortVolumeData,
@@ -22,6 +23,8 @@ from openbb_imf.utils.port_watch_helpers import (
)
from pydantic import ConfigDict, Field, field_validator, model_validator
api_prefix = SystemService().system_settings.api_settings.prefix
class ImfPortVolumeQueryParams(PortVolumeQueryParams):
"""IMF Port Volume Query Parameters.
@@ -37,7 +40,7 @@ class ImfPortVolumeQueryParams(PortVolumeQueryParams):
"port_code": {
"multiple_items_allowed": True,
"x-widget_config": {
"options": get_port_id_choices(),
"optionsEndpoint": f"{api_prefix}/imf_utils/list_port_id_choices",
"style": {"popupWidth": 350},
},
},

View File

@@ -455,39 +455,6 @@ PortCountries = Literal[
]
FSI_PRESETS = [
"fsi_core",
"fsi_core_underlying",
"fsi_other",
"fsi_encouraged_set",
"fsi_balance_sheets",
"fsi_all",
]
IRFCL_HEADLINE = "RAF_USD,RAFA_USD,RAFAFX_USD,RAOFA_USD,RAPFA_USD,RAFAIMF_USD,RAFASDR_USD,RAFAGOLD_USD,RACFA_USD,RAMDCD_USD,RAMFIFC_USD,RAMSR_USD"
RESERVE_ASSETS_AND_OTHER_FX_ASSETS = "RAF_USD,RAFA_USD,RAFAFX_USD,RAFAFXS_USD,RAFAFXSI_USD,RAFAFXCD_USD,RAFAFXCDN_USD,RAFAFXCDBI_USD,RAFAFXCDBIA_USD,RAFAFXCDBO_USD,RAFAFXCDBOA_USD,RAFAIMF_USD,RAFASDR_USD,RAFAGOLD_USD,RAFAGOLDV_OZT,RAFAO_USD,RAFAOF_USD,RAFAOL_USD,RAFAOO_USD,RAOFA_USD,RAOFAS_USD,RAOFAD_USD,RAOFAL_USD,RAOFAF_USD,RAOFAG_USD"
PREDETERMINED_DRAINS_ON_FX_ASSETS = "RAPFA_USD,RAPFALSD_USD,RAPFALSD_1M_USD,RAPFALSD_1M_3M_USD,RAPFALSD_3M_1Y_USD,RAPFALSDOP_USD,RAPFALSDOP_1M_USD,RAPFALSDOP_1M_3M_USD,RAPFALSDOP_3M_1Y_USD,RAPFALSDOI_USD,RAPFALSDOI_1M_USD,RAPFALSDOI_1M_3M_USD,RAPFALSDOI_3M_1Y_USD,RAPFALSDIP_USD,RAPFALSDIP_1M_USD,RAPFALSDIP_1M_3M_USD,RAPFALSDIP_3M_1Y_USD,RAPFALSDII_USD,RAPFALSDII_1M_USD,RAPFALSDII_1M_3M_USD,RAPFALSDII_3M_1Y_USD,RAPFAFFS_USD,RAPFAFFS_1M_USD,RAPFAFFS_1M_3M_USD,RAPFAFFS_3M_1Y_USD,RAPFAFFL_USD,RAPFAFFL_1M_USD,RAPFAFFL_1M_3M_USD,RAPFAFFL_3M_1Y_USD,RAPFAO_USD,RAPFAO_1M_USD,RAPFAO_1M_3M_USD,RAPFAO_3M_1Y_USD,RAPFAOOR_USD,RAPFAOOR_1M_USD,RAPFAOOR_1M_3M_USD,RAPFAOOR_3M_1Y_USD,RAPFAOIRR_USD,RAPFAOIRR_1M_USD,RAPFAOIRR_1M_3M_USD,RAPFAOIRR_3M_1Y_USD,RAPFAOOC_USD,RAPFAOOC_1M_USD,RAPFAOOC_1M_3M_USD,RAPFAOOC_3M_1Y_USD,RAPFAOIC_USD,RAPFAOIC_1M_USD,RAPFAOIC_1M_3M_USD,RAPFAOIC_3M_1Y_USD,RAPFAOOP_USD,RAPFAOOP_1M_USD,RAPFAOOP_1M_3M_USD,RAPFAOOP_3M_1Y_USD,RAPFAOIR_USD,RAPFAOIR_1M_USD,RAPFAOIR_1M_3M_USD,RAPFAOIR_3M_1Y_USD,RAFA_RAPFA_RO"
CONTINGENT_DRAINS_FX_ASSETS = "RACFA_USD,RACFAL_USD,RACFAL_1M_USD,RACFAL_1M_3M_USD,RACFAL_3M_1Y_USD,RACFALG_USD,RACFALG_1M_USD,RACFALG_1M_3M_USD,RACFALO_USD,RACFALO_1M_USD,RACFALO_1M_3M_USD,RACFALO_3M_1Y_USD,RACFAS_USD,RACFACB_USD,RACFACB_1M_USD,RACFACB_1M_3M_USD,RACFACB_3M_1Y_USD,RACFACBA_USD,RACFACBA_1M_USD,RACFACBA_1M_3M_USD,RACFACBA_3M_1Y_USD,RACFACBAOI_USD,RACFACBAOI_1M_USD,RACFACBAOI_1M_3M_USD,RACFACBAON_USD,RACFACBAON_1M_USD,RACFACBAON_1M_3M_USD,RACFACBAON_3M_1Y_USD,RACFACBABIS_USD,RACFACBAIMF_1M_USD,RACFACBAIMF_1M_3M_USD,RACFACBAIMF_3M_1Y_USD,RACFACBAIMF_USD,RACFACBFIR_USD,RACFACBFIR_1M_USD,RACFACBFIR_1M_3M_USD,RACFACBFIR_3M_1Y_USD,RACFACBFIO_USD,RACFACBFIO_1M_USD,RACFACBFIO_1M_3M_USD,RACFACBFIO_3M_1Y_USD,RACFACT_USD,RACFACT_1M_USD,RACFACT_1M_3M_USD,RACFACT_3M_1Y_USD,RACFACTA_USD,RACFACTA_1M_USD,RACFACTA_1M_3M_USD,RACFACTA_3M_1Y_USD,RACFACTAOI_USD,RACFACTAOI_1M_USD,RACFACTAOI_1M_3M_USD,RACFACTAOI_3M_1Y_USD,RACFACTAON_USD,RACFACTAON_1M_USD,RACFACTAON_1M_3M_USD,RACFACTAON_3M_1Y_USD,RACFACTABIS_USD,RACFACTABIS_1M_USD,RACFACTABIS_1M_3M_USD,RACFACTABIS_3M_1Y_USD,RACFACTAIMF_USD,RACFACTAIMF_1M_USD,RACFACTFIR_USD,RACFACTFIR_1M_USD,RACFACTFIR_1M_3M_USD,RACFACTFIR_3M_1Y_USD,RACFACTFIO_USD,RACFACTFIO_1M_USD,RACFACTFIO_1M_3M_USD,RACFACTFIO_3M_1Y_USD,RACFAPPS_USD,RACFAPPS_1M_USD,RACFAPPS_1M_3M_USD,RACFAPPS_3M_1Y_USD,RACFAPPSBP_USD,RACFAPPSBP_1M_USD,RACFAPPSBP_1M_3M_USD,RACFAPPSBP_3M_1Y_USD,RACFAPPSWC_USD,RACFAPPSWC_1M_USD,RACFAPPSWC_1M_3M_USD,RACFAPPSWC_3M_1Y_USD,RACFAPPL_USD,RACFAPPL_1M_USD,RACFAPPL_1M_3M_USD,RACFAPPL_3M_1Y_USD,RACFAPPLBC_USD,RACFAPPLBC_1M_USD,RACFAPPLBC_1M_3M_USD,RACFAPPLBC_3M_1Y_USD,RACFAPPLWP_USD,RACFAPPLWP_1M_USD,RACFAPPLWP_1M_3M_USD,RACFAPPLWP_3M_1Y_USD,RACFAMPAS_USD,RACFAMPAS_1M_USD,RACFAMPAS_1M_3M_USD,RACFAMPAS_3M_1Y_USD,RACFAMPAL_USD,RACFAMPAL_1M_USD,RACFAMPAL_1M_3M_USD,RACFAMPAL_3M_1Y_USD,RACFAMPBS_USD,RACFAMPBS_1M_USD,RACFAMPBS_1M_3M_USD,RACFAMPBS_3M_1Y_USD,RACFAMPBL_USD,RACFAMPBL_1M_USD,RACFAMPBL_1M_3M_USD,RACFAMPBL_3M_1Y_USD,RACFAMPCS_USD,RACFAMPCS_1M_USD,RACFAMPCS_1M_3M_USD,RACFAMPCS_3M_1Y_USD,RACFAMPCL_USD,RACFAMPCL_1M_USD,RACFAMPCL_1M_3M_USD,RACFAMPCL_3M_1Y_USD,RACFAMPDS_USD,RACFAMPDS_1M_USD,RACFAMPDS_1M_3M_USD,RACFAMPDS_3M_1Y_USD,RACFAMPDL_USD,RACFAMPDL_1M_USD,RACFAMPDL_1M_3M_USD,RACFAMPDL_3M_1Y_USD,RACFAMPES_USD,RACFAMPES_1M_USD,RACFAMPES_1M_3M_USD,RACFAMPES_3M_1Y_USD,RACFAMPEL_USD,RACFAMPEL_1M_USD,RACFAMPEL_1M_3M_USD,RACFAMPEL_3M_1Y_USD,RACFAMPFS_USD,RACFAMPFS_1M_USD,RACFAMPFS_1M_3M_USD,RACFAMPFS_3M_1Y_USD,RACFAMPFL_USD,RACFAMPFL_1M_USD,RACFAMPFL_1M_3M_USD,RACFAMPFL_3M_1Y_USD"
IRFCL_MEMORANDUM_ITEMS = "RAMDCD_USD,RAMFIFC_USD,RAMPA_USD,RAMFFS_USD,RAMPAOA_USD,RAMSR_USD,RAMSRLRI_USD,RAMSRLRN_USD,RAMSRBRI_USD,RAMSRBAN_USD,RAMFDA_USD,RAMFDAF_USD,RAMFDAU_USD,RAMFDAW_USD,RAMFDAP_USD,RAMFDAO_USD,RAMFFL_USD,RAMPPS_USD,RAMPPSBP_USD,RAMPPSWC_USD,RAMPPL_USD,RAMPPLBP_USD,RAMPPLWC_USD,RAMCR_USD,RAMCRISDR_USD,RAMCRIC_USD_USD,RAMCRIC_EUR_USD,RAMCRIC_CNY_USD,RAMCRIC_JPY_USD,RAMCRIC_GBP_USD,RAMCROSDR_USD"
IRFCL_TABLES = {
"reserve_assets_and_other_fx_assets": RESERVE_ASSETS_AND_OTHER_FX_ASSETS,
"predetermined_drains_on_fx_assets": PREDETERMINED_DRAINS_ON_FX_ASSETS,
"contingent_drains_fx_assets": CONTINGENT_DRAINS_FX_ASSETS,
"memorandum_items": IRFCL_MEMORANDUM_ITEMS,
}
IRFCL_PRESET = {
"irfcl_top_lines": IRFCL_HEADLINE,
**IRFCL_TABLES,
"gold_reserves": "RAFAGOLD_USD,RAFAGOLDV_OZT",
"derivative_assets": "RAMFDA_USD",
}
FREQUENCY_DICT = {
"month": "M",
"quarter": "Q",
@@ -553,25 +520,143 @@ REF_SECTOR_MAP = {
"AllSectorsIncludingAllSectors": "All Sectors Including All Sectors",
}
PRESENTATION_TABLES: dict[str, str] = {
"bop_standard": "BOP::H_BOP_BOP_AGG_STANDARD_PRESENTATION",
"bop_analytic": "BOP::H_BOP_BOP_AGG_ANALYTIC_PRESENTATION",
"bop_international_transactions": "BOP_AGG::H_BOP_AGG_SUMMARY_OF_INTERNATIONAL_TRANSACTIONS",
"bop_global_discrepancies": "BOP_AGG::H_BOP_AGG_GLOBAL_DISCREPANCIES",
"bop_global_discrepancies_percent": "BOP_AGG::H_BOP_AGG_GLOBAL_DISCREPANCIES_PERCENTAGE",
"dip": "DIP::H_DIP_INDICATOR",
"iip_aggregated": "IIP::H_BOP_IIP_AGG",
"iip_currency_composition": "IIPCC::H_BOP_IIPCC_CURRENCY_COMPOSITION",
"eer": "EER::H_EER_INDICATOR_HIERARCHY",
"irfcl_reserve_assets": "IRFCL::H_CL_IRFCL_INDICATOR_DEFAULT_PUB:CL_IRFCL_DEFAULT_INDICATOR_PUB2_1",
"irfcl_predetermined_drains": "IRFCL::H_CL_IRFCL_INDICATOR_DEFAULT_PUB:CL_IRFCL_DEFAULT_INDICATOR_PUB2_29",
"irfcl_contingent_drains": "IRFCL::H_CL_IRFCL_INDICATOR_DEFAULT_PUB:CL_IRFCL_DEFAULT_INDICATOR_PUB2_91",
"irfcl_memorandum": "IRFCL::H_CL_IRFCL_INDICATOR_DEFAULT_PUB:CL_IRFCL_DEFAULT_INDICATOR_PUB2_273",
"fsi_core_and_additional": "FSIC::H_FSIC_INDICATOR",
"fsi_indicators_and_underlying": "FSIC::H_FSIC_FSD",
"fsi_concentration_distribution_measures": "FSICDM::H_FSI_FSICDM_CDM",
"mfs_monetary_aggs": "MFS_MA::H_CL_MFS_MA_INDICATOR_5SR",
"mfs_central_bank": "MFS_CBS::H_CL_MFS_CBS_SRFS_1SG_VIEW",
"mfs_other_depository_corps": "MFS_ODC::H_CL_MFS_ODC_SRFS_2SG_VIEW",
"mfs_depository_corps": "MFS_DC::H_CL_MFS_DCS_SRFS_3SG_VIEW",
"mfs_other_financial_corps": "MFS_OFC::H_CL_MFS_OFC_SRFS_4SG_VIEW",
"mfs_financial_corps": "MFS_FC::H_CL_MFS_FCS_SRFS_5SG_VIEW",
"gfs_balance": "GFS_BS::H_GFS_BS",
"gfs_operations": "GFS_SOO::H_GFS_SOO",
"gfs_expenditures": "GFS_COFOG::H_GFS_COFOG",
"gfs_stocks_and_flows": "GFS_SFCP::H_GFS_SFCP",
"gfs_sources_and_uses": "GFS_SSUC::H_GFS_SSUC",
"qgfs_balance": "QGFS::H_QGFS_BALANCE",
"qgfs_operations": "QGFS::H_QGFS_STATGO",
"qgfs_sources_and_uses": "QGFS::H_QGFS_GSUC",
"gdp_annual_expenditure": "ANEA::H_NEA_GDP_BY_EXPENDITURE",
"gdp_quarterly_expenditure": "QNEA::H_NEA_GDP_BY_EXPENDITURE",
"cpi": "CPI::H_CPI_BY_COMPONENT",
"fas_indicator_by_country": "FAS::H_FAS_INDICATOR_BY_COUNTRY",
"fas_data_by_country": "FAS::H_FAS_BY_COUNTRY",
"isora_indicators_by_topic": "ISORA_LATEST_DATA_PUB::H_CL_INDICATORS_BY_TOPIC",
}
def load_symbols(dataset: str) -> dict:
"""Load IMF symbol list."""
# pylint: disable=import-outside-toplevel
import json # noqa
from json.decoder import JSONDecodeError
from pathlib import Path
from openbb_core.app.model.abstract.error import OpenBBError
table_dataflow_map: dict[str, list[str]] = {
"bop": [
"bop_standard",
"bop_analytic",
"bop_international_transactions",
"bop_global_discrepancies",
"bop_global_discrepancies_percent",
],
"dip": ["dip"],
"iip": ["iip_aggregated", "iip_currency_composition"],
"eer": ["eer"],
"irfcl": [
"irfcl_reserve_assets",
"irfcl_predetermined_drains",
"irfcl_contingent_drains",
"irfcl_memorandum",
],
"fsi": [
"fsi_core_and_additional",
"fsi_indicators_and_underlying",
"fsi_concentration_distribution_measures",
],
"mfs": [
"mfs_monetary_aggs",
"mfs_central_bank",
"mfs_other_depository_corps",
"mfs_depository_corps",
"mfs_other_financial_corps",
"mfs_financial_corps",
],
"gfs": [
"gfs_balance",
"gfs_operations",
"gfs_expenditures",
"gfs_stocks_and_flows",
"gfs_sources_and_uses",
],
"qgfs": ["qgfs_balance", "qgfs_operations", "qgfs_sources_and_uses"],
"gdp": ["gdp_annual_expenditure", "gdp_quarterly_expenditure"],
"cpi": ["cpi"],
"fas": ["fas_indicator_by_country", "fas_data_by_country"],
"isora": ["isora_indicators_by_topic"],
}
try:
symbols_file = Path(__file__).parents[1].joinpath("assets", "imf_symbols.json")
with symbols_file.open(encoding="utf-8") as file:
symbols = json.load(file)
except (FileNotFoundError, JSONDecodeError) as e:
raise OpenBBError(
f"Failed to load IMF symbols from the static file: {e}"
) from e
table_dataflow_choices: list[dict[str, str]] = [
{"label": "Balance of Payments", "value": "bop"},
{"label": "Direct Investment Position", "value": "dip"},
{"label": "Effective Exchange Rates", "value": "eer"},
{"label": "International Investment Position", "value": "iip"},
{
"label": "International Reserves and Foreign Currency Liquidity",
"value": "irfcl",
},
{"label": "Financial Soundness Indicators", "value": "fsi"},
{"label": "Monetary and Financial Statistics", "value": "mfs"},
{"label": "Government Finance Statistics", "value": "gfs"},
{"label": "Quarterly Government Finance Statistics", "value": "qgfs"},
{"label": "Gross Domestic Product", "value": "gdp"},
{"label": "Consumer Price Index", "value": "cpi"},
{"label": "Financial Access Survey", "value": "fas"},
{"label": "ISORA Indicators", "value": "isora"},
]
if dataset == "all":
return symbols
return {k: v for k, v in symbols.items() if v["dataset"] == dataset}
table_name_map: dict[str, str] = {
"bop_standard": "Standard Presentation",
"bop_analytic": "Analytic Presentation",
"bop_international_transactions": "Summary of International Transactions",
"bop_global_discrepancies": "Global Discrepancies",
"bop_global_discrepancies_percent": "Global Discrepancies Percentage",
"dip": "Direct Investment Position",
"iip_aggregated": "Aggregated International Investment Position",
"iip_currency_composition": "Currency Composition of International Investment Position",
"eer": "Effective Exchange Rates",
"irfcl_reserve_assets": "Reserve Assets",
"irfcl_predetermined_drains": "Predetermined Drains",
"irfcl_contingent_drains": "Contingent Drains",
"irfcl_memorandum": "Memorandum Items",
"fsi_core_and_additional": "Core and Additional Financial Soundness Indicators",
"fsi_indicators_and_underlying": "Financial Soundness Indicators and Underlying Data",
"fsi_concentration_distribution_measures": "Concentration and Distribution Measures",
"mfs_monetary_aggs": "Monetary Aggregates",
"mfs_central_bank": "Central Bank",
"mfs_other_depository_corps": "Other Depository Corporations",
"mfs_depository_corps": "Depository Corporations",
"mfs_financial_corps": "Financial Corporations",
"mfs_other_financial_corps": "Other Financial Corporations",
"gfs_balance": "Government Balance Sheet",
"gfs_operations": "Statement of Government Operations",
"gfs_expenditures": "Expenditures by Function (COFOG)",
"gfs_stocks_and_flows": "Stocks and Flows of Central Government",
"gfs_sources_and_uses": "Sources and Uses of Central Government Funds",
"qgfs_balance": "Quarterly Government Finance Statistics Balance Sheet",
"qgfs_operations": "Quarterly Government Finance Statistics Statement of Operations",
"qgfs_sources_and_uses": "Quarterly Government Finance Statistics Sources and Uses of Funds",
"gdp_annual_expenditure": "Annual GDP by Expenditure",
"gdp_quarterly_expenditure": "Quarterly GDP by Expenditure",
"cpi": "Consumer Price Index by Component",
"fas_indicator_by_country": "Financial Access Survey Indicators by Country",
"fas_data_by_country": "Financial Access Survey Data by Country",
"isora_indicators_by_topic": "ISORA Indicators by Topic",
}

View File

@@ -1,87 +1,230 @@
"""Direction Of Trade Utilities."""
from openbb_core.app.model.abstract.error import OpenBBError
# pylint: disable=R0917,R0913,R0914,R0801
def load_country_map() -> dict:
def load_country_choices() -> list[dict[str, str]]:
"""Load IMF IRFCL country map."""
# pylint: disable=import-outside-toplevel
import json # noqa
from json.decoder import JSONDecodeError
from pathlib import Path
from openbb_imf.utils.metadata import ImfMetadata
try:
country_map_file = (
Path(__file__).parents[1].joinpath("assets", "imf_country_map.json")
)
with country_map_file.open(encoding="utf-8") as file:
country_map_dict = json.load(file)
except (FileNotFoundError, JSONDecodeError) as e:
raise OpenBBError(f"Failed to load IMF DOT country map: {e}") from e
data = ImfMetadata().get_dataflow_parameters("IMTS")["COUNTRY"]
countries_list: list = []
g_regions: list = []
tx_groups: list = []
return country_map_dict
def load_country_to_code_map() -> dict:
"""Load a map of lowercase country name to 2-letter ISO symbol."""
# pylint: disable=import-outside-toplevel
from warnings import warn # noqa
try:
return {
(
"euro_area"
if k == "U2"
else v.lower()
.replace(".", "")
.replace(",", "")
.replace(":", "")
.split("(")[0]
.strip()
.replace(" ", "_")
): k
for k, v in load_country_map().items()
if not k.startswith("1C_ALL")
and "Report" not in v
and k not in ("GW", "_X")
}
except OpenBBError:
warn(f"Failed to load country to code map. -> {OpenBBError}")
return {}
def validate_countries(countries: list[str]) -> str:
"""Validate the list of countries."""
# pylint: disable=import-outside-toplevel
from warnings import warn
try:
country_to_code_map = load_country_to_code_map()
except OpenBBError as e:
raise OpenBBError(f"Failed to load country to code map: {e}") from e
if not country_to_code_map:
raise OpenBBError("Failed to load country to code map.")
new_countries: list = []
for country in countries:
if country == "all":
return country
if country in country_to_code_map:
new_countries.append(country_to_code_map.get(country))
elif country.upper() in country_to_code_map.values():
new_countries.append(country.upper())
elif country.lower() == "eu":
new_countries.append(country_to_code_map.get("european_union"))
elif country.lower() == "ea":
new_countries.append(country_to_code_map.get("euro_area"))
for item in data:
if item["value"].startswith("G") and (
item["value"][1:].isdigit()
or (item["value"][1] == "X" and item["value"][2:].isdigit())
):
g_regions.append(item)
elif item["value"].startswith("TX"):
tx_groups.append(item)
else:
warn(f"Invalid country {country}")
continue
countries_list.append(item)
if not new_countries:
raise OpenBBError(
f"No valid countries found. Please choose from {list(country_to_code_map)}"
)
# Sort each group alphabetically by label
countries_sorted = sorted(countries_list, key=lambda x: x["label"])
g_regions_sorted = sorted(g_regions, key=lambda x: x["label"])
tx_groups_sorted = sorted(tx_groups, key=lambda x: x["label"])
return "+".join(new_countries)
return countries_sorted + g_regions_sorted + tx_groups_sorted
def list_country_choices() -> list[str]:
"""List IMF IRFCL country choices."""
countries = load_country_choices()
return [item["value"] for item in countries]
def get_label_to_code_map() -> dict[str, str]:
"""Get a mapping from normalized labels to country codes.
Normalizes labels to lower_snake_case for flexible matching.
"""
# pylint: disable=import-outside-toplevel
from openbb_imf.utils.helpers import normalize_country_label
countries = load_country_choices()
return {normalize_country_label(item["label"]): item["value"] for item in countries}
def get_code_to_label_map() -> dict[str, str]:
"""Get a mapping from country codes to normalized labels."""
# pylint: disable=import-outside-toplevel
from openbb_imf.utils.helpers import normalize_country_label
countries = load_country_choices()
return {item["value"]: normalize_country_label(item["label"]) for item in countries}
def resolve_country_input(value: str) -> str:
"""Resolve a country input to its ISO code.
Accepts both ISO3 codes (e.g., 'USA') and snake_case country names
(e.g., 'united_states'). Returns the ISO code.
Parameters
----------
value : str
Country code or name to resolve.
Returns
-------
str
The resolved ISO country code.
Raises
------
ValueError
If the input cannot be resolved to a valid country code.
"""
if not value:
raise ValueError("Country value cannot be empty.")
# Handle wildcards
if value.lower() in ["all", "*"]:
return "*"
# Common aliases for frequently used regions
common_aliases = {
"world": "G001",
"euro_area": "G163",
"eurozone": "G163",
"eu": "G998",
"european_union": "G998",
"europe": "GX170",
}
v_lower = value.lower().replace(" ", "_")
# Check common aliases first
if v_lower in common_aliases:
return common_aliases[v_lower]
code_set = set(list_country_choices())
label_to_code = get_label_to_code_map()
v_upper = value.upper()
# Check if it's a valid ISO code
if v_upper in code_set:
return v_upper
# Check if it's a valid snake_case country name
if v_lower in label_to_code:
return label_to_code[v_lower]
# Not found - raise error with helpful message
raise ValueError(
f"Country '{value}' is not a valid IMF country code or country name. "
f"Use ISO3 codes (e.g., 'USA', 'DEU') or snake_case names (e.g., 'united_states', 'germany')."
)
def imts_query(
country: str | list[str],
counterpart: str | list[str],
indicator: str | list[str],
freq: str = "A",
start_date: str | None = None,
end_date: str | None = None,
**kwargs,
) -> dict:
"""Query the Direction of Trade (IMTS) dataset.
This function handles input validation for countries and counterparts.
Parameters
----------
country : str | list[str]
The country or countries to fetch data for.
counterpart : str | list[str]
The counterpart country or countries. Use "*" for all.
indicator : str | list[str]
The indicator or indicators to fetch. Use "*" for all.
freq : str | None
The frequency of the data, by default "A" (annual).
start_date : str | None
The start date of the data, by default None.
end_date : str | None
The end date of the data, by default None.
**kwargs : dict
Additional query parameters to pass to the API.
Returns
-------
dict
A dictionary with keys: 'data' containing the fetched data,
and 'metadata' containing the related metadata.
"""
# pylint: disable=import-outside-toplevel
from openbb_imf.utils.query_builder import ImfQueryBuilder
if not country or not counterpart:
raise ValueError("Country and counterpart parameters cannot be empty.")
freq = freq[0].upper()
if freq and freq not in ["A", "Q", "M"]:
raise ValueError("Frequency must be one of 'A', 'Q', or 'M'.")
query_builder = ImfQueryBuilder()
dataflow_id = "IMTS"
params = query_builder.metadata.get_dataflow_parameters(dataflow_id)
country_values = {item["value"] for item in params.get("COUNTRY", [])}
counterpart_values = {
item["value"]
for item in params.get("COUNTERPART_COUNTRY", params.get("COUNTRY", []))
}
def _validate_selection(selection, valid_values, name):
"""Validate country or counterpart selection."""
if not valid_values:
return selection
# Handle wildcards - return "*" as-is
if selection == "*":
return "*"
# Parse the selection into a list
if isinstance(selection, str):
# Check if it contains commas (comma-separated list)
selection_list = (
[item.strip() for item in selection.split(",")]
if "," in selection
else [selection]
)
else:
selection_list = selection
# Check if any item is a wildcard
if "*" in selection_list:
return "*"
invalid = [item for item in selection_list if item not in valid_values]
if invalid:
raise ValueError(f"Invalid {name}(s): {', '.join(invalid)}")
return selection_list if len(selection_list) > 1 else selection_list[0]
validated_country = _validate_selection(country, country_values, "country")
validated_counterpart = _validate_selection(
counterpart, counterpart_values, "counterpart"
)
# For indicator, handle wildcards and comma-separated values the same way
if isinstance(indicator, str) and "," in indicator:
validated_indicator = [item.strip() for item in indicator.split(",")]
else:
validated_indicator = indicator if indicator != "*" else "*" # type: ignore
return query_builder.fetch_data(
dataflow=dataflow_id,
start_date=start_date,
end_date=end_date,
FREQUENCY=freq,
COUNTRY=validated_country,
COUNTERPART_COUNTRY=validated_counterpart,
INDICATOR=validated_indicator,
**kwargs,
)

View File

@@ -1,245 +0,0 @@
"""IMF FSI Data Set Helpers."""
def load_fsi_symbols() -> dict:
"""Load IMF FSI symbols."""
# pylint: disable=import-outside-toplevel
from openbb_imf.utils.constants import load_symbols
return load_symbols("FSI")
def validate_symbols(symbols) -> str:
"""Validate the IMF FSI symbols."""
# pylint: disable=import-outside-toplevel
from warnings import warn # noqa
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_imf.utils.constants import FSI_PRESETS
fsi_symbols = load_fsi_symbols()
if isinstance(symbols, str):
symbols = symbols.split(",")
elif not isinstance(symbols, list):
raise OpenBBError("Invalid symbols list.")
new_symbols: list = []
for symbol in symbols:
if symbol in FSI_PRESETS:
return symbol
if symbol.upper() not in fsi_symbols:
warn(f"Unsupported IMF FSI symbol: {symbol}")
new_symbols.append(symbol.upper())
return "+".join(new_symbols) if len(new_symbols) > 1 else new_symbols[0]
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
async def _get_fsi_data(**kwargs) -> list[dict]: # noqa:PLR0912
"""Get IMF FSI data.
This function is not intended to be called directly,
but through the `ImfEconomicIndicatorsFetcher` class.
"""
# pylint: disable=import-outside-toplevel
from datetime import date # noqa
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_imf.utils.helpers import get_data
from openbb_imf.utils.irfcl_helpers import load_country_to_code_map
from openbb_imf.utils import constants
from pandas import to_datetime
from pandas.tseries import offsets
countries = kwargs.get("country", "")
countries = (
"" if countries == "all" else countries.replace(",", "+") if countries else ""
)
frequency = constants.FREQUENCY_DICT.get(kwargs.get("frequency", "quarter"), "Q")
start_date = kwargs.get("start_date", "")
end_date = kwargs.get("end_date", "")
all_symbols = load_fsi_symbols()
core_only = [k for k, v in all_symbols.items() if v.get("table") == "fsi_core"]
encouraged_set = [
k for k, v in all_symbols.items() if v.get("table") == "fsi_encouraged_set"
]
indicator = kwargs.get("symbol")
indicators = (
"+".join(core_only if indicator == "fsi_core" else encouraged_set)
if indicator in ["fsi_core", "fsi_encouraged_set"]
else (
""
if indicator
in ["fsi_other", "fsi_all", "fsi_core_underlying", "fsi_balance_sheets"]
else validate_symbols(indicator)
)
)
if not indicators and not countries:
raise OpenBBError(
"All countries not supported for this group of indicators. Please supply a country."
)
if not start_date and not end_date and not countries:
start_date = (
date.today().replace(year=date.today().year - 1).strftime("%Y-%m-%d")
)
if start_date and not end_date:
end_date = date.today().strftime("%Y-%m-%d")
# Adjust the dates to the date relative to frequency.
# The API does not accept arbitrary dates, so we need to adjust them.
if start_date:
start_date = to_datetime(start_date)
if frequency == "Q":
start_date = offsets.QuarterBegin(startingMonth=1).rollback(start_date)
elif frequency == "A":
start_date = offsets.YearBegin().rollback(start_date)
else:
start_date = offsets.MonthBegin().rollback(start_date)
start_date = start_date.strftime("%Y-%m-%d")
if end_date:
end_date = to_datetime(end_date)
if frequency == "Q":
end_date = offsets.QuarterEnd().rollforward(end_date)
elif frequency == "A":
end_date = offsets.YearEnd().rollforward(end_date)
else:
end_date = offsets.MonthEnd().rollforward(end_date)
end_date = end_date.strftime("%Y-%m-%d")
date_range = (
f"?startPeriod={start_date}&endPeriod={end_date}"
if start_date and end_date
else ""
)
base_url = "http://dataservices.imf.org/REST/SDMX_JSON.svc/"
key = f"CompactData/FSI/{frequency}.{countries}.{indicators}"
url = f"{base_url}{key}{date_range}"
series = await get_data(url)
data: list = []
all_symbols = load_fsi_symbols()
if indicator in ["fsi_core", "fsi_encouraged_set"]:
all_symbols = {
k: v for k, v in all_symbols.items() if v.get("table") == indicator
}
elif indicator == "fsi_core_underlying":
all_symbols = {
k: v
for k, v in all_symbols.items()
if "core set" in v.get("title", "").lower() and v.get("unit") != "Percent"
}
elif indicator == "fsi_balance_sheets":
all_symbols = {
k: v
for k, v in all_symbols.items()
if "balance sheets" in v.get("title", "").lower()
}
elif indicator == "fsi_other":
all_symbols = {
k: v
for k, v in all_symbols.items()
if "Additional FSIs" in v.get("title", "")
}
country_map_dict = {
v: k.replace("_", " ").title().replace("Ecb", "ECB")
for k, v in load_country_to_code_map().items()
}
# Iterate over the series to extract observations and map the metadata.
for s in series:
if "Obs" not in s:
continue
meta = {
k.replace("@", "").lower(): (
constants.UNIT_MULTIPLIERS_MAP.get(str(v), v)
if k == "@UNIT_MULT"
else v
)
for k, v in s.items()
if k != "Obs"
}
_symbol = meta.get("indicator")
_parent: str | None = None
_order: str | None = None
_level: str | None = None
_table: str | None = None
_title: str | None = None
_unit: str | None = None
if _symbol not in all_symbols:
continue
_table = all_symbols.get(_symbol, {}).get("table", "")
if _table and indicator == "fsi_core_underlying":
_table = _table.replace("fsi_other", "fsi_core_underlying") # type: ignore
_parent = all_symbols.get(_symbol, {}).get("parent", "")
_order = all_symbols.get(_symbol, {}).get("order", "")
_level = all_symbols.get(_symbol, {}).get("level", "")
_title = all_symbols.get(_symbol, {}).get("title", "")
if _title:
_title = " - ".join(_title.split(", ")[1:-1]).replace(
"Financial Soundness Indicators - ", ""
)
_unit = all_symbols.get(_symbol, {}).get("unit", "")
_data = s.pop("Obs", [])
if isinstance(_data, dict):
_data = [_data]
for d in _data:
_date = d.pop("@TIME_PERIOD", None)
val = d.pop("@OBS_VALUE", None)
_ = d.pop("@OBS_STATUS", None)
if not val:
continue
val = float(val)
if _unit and _unit.lower() == "percent":
val = val / 100
if _date:
offset = (
offsets.QuarterEnd
if "Q" in _date
else offsets.YearEnd if len(str(_date)) == 4 else offsets.MonthEnd
)
_date = to_datetime(_date)
_date = _date + offset(0)
_date = _date.strftime("%Y-%m-%d")
vals = {
k: v
for k, v in {
"date": _date,
"table": _table,
"symbol": _symbol,
"parent": _parent if _parent else _symbol,
"level": _level,
"order": _order,
"country": country_map_dict.get(
meta.get("ref_area"), meta.get("ref_area")
),
"reference_sector": None,
"title": _title,
"value": val,
"unit": (
_unit.upper() if _unit and _unit in ["usd", "eur"] else _unit
),
"scale": (
"Basis Points"
if _unit and _unit.lower() == "percent"
else meta.get("unit_mult")
),
}.items()
if v
}
if vals.get("value") and vals.get("date"):
d.update(vals)
data.append(d)
if not data:
raise OpenBBError(f"No data found for, '{indicator}', in, '{countries}'.")
return data

View File

@@ -1,34 +1,892 @@
"""IMF Helper Utilities."""
# pylint: disable=R0912,W0212,R0914,R0915,R0916,R1702,W0718
# flake8: noqa: PLR0912
async def get_data(url: str) -> list[dict]:
"""Get data from the IMF API."""
from typing import Any
from openbb_core.app.model.abstract.error import OpenBBError
def normalize_country_label(label: str) -> str:
"""Normalize country label to lower_snake_case.
Examples
--------
>>> _normalize_country_label("United States")
'united_states'
>>> _normalize_country_label("Armenia, Republic of")
'armenia'
>>> _normalize_country_label("Euro Area (EA)")
'euro_area'
>>> _normalize_country_label("Guinea-Bissau")
'guinea_bissau'
"""
# pylint: disable=import-outside-toplevel
import re
from aiohttp.client_exceptions import ContentTypeError # noqa
from json.decoder import JSONDecodeError
from openbb_core.provider.utils.helpers import amake_request
from openbb_core.app.model.abstract.error import OpenBBError
# Remove all parenthetical content (handles nested parens by repeating)
prev_label = None
while prev_label != label:
prev_label = label
label = re.sub(r"\s*\([^()]*\)", "", label)
# Remove everything after comma (handles ", Republic of", ", The", etc.)
label = label.split(",")[0].strip()
# Replace hyphens and spaces with underscores, convert to lowercase
return label.replace("-", "_").replace(" ", "_").lower()
def resolve_country_code(country: str, metadata) -> str:
"""
Resolve a country name or code to an ISO3 country code.
Uses the CL_COUNTRY codelist to resolve names like "Japan" to "JPN".
Returns the input unchanged if it's already an ISO3 code or can't be resolved.
"""
country_upper = country.upper().strip()
country_lower = country.lower().strip()
# Check if it's already a valid code
if "CL_COUNTRY" in metadata._codelist_cache:
codes = metadata._codelist_cache["CL_COUNTRY"]
if country_upper in codes:
return country_upper
# Try to resolve by name
for code, name in codes.items():
if name.lower() == country_lower:
return code
return country_upper # Return as-is (uppercase)
def _parse_compound_code(
code: str, code_to_dimension: dict[str, str]
) -> tuple[list[tuple[str, str]], list[str]]:
"""
Parse a compound code like 'S13_G1_G23_T' into dimension matches.
Uses greedy matching to find longest combinations first.
ALL parts must match a dimension, otherwise returns unmatched parts.
Returns tuple of:
- list of (dimension_id, matched_code) tuples
- list of unmatched parts (empty if all matched)
"""
parts = code.split("_")
matched_parts: list[tuple[str, str]] = []
unmatched_parts: list[str] = []
# Try to match longest combinations first (greedy matching)
i = 0
while i < len(parts):
matched = False
# Try longest possible combination first
for j in range(len(parts), i, -1):
combined = "_".join(parts[i:j])
if combined in code_to_dimension:
dim_id = code_to_dimension[combined]
# Don't match same dimension twice
if not any(m[0] == dim_id for m in matched_parts):
matched_parts.append((dim_id, combined))
i = j
matched = True
break
if not matched:
# Track unmatched part
unmatched_parts.append(parts[i])
i += 1
return matched_parts, unmatched_parts
def _build_dimension_lookups(
dataflow: str, metadata
) -> tuple[dict[str, str], dict[str, set[str]], list[str]]:
"""
Build lookups for mapping codes to dimensions.
Returns (code_to_dimension, codes_by_dimension, dimension_order) tuple.
- code_to_dimension: maps any valid code to its dimension ID (for parsing)
- codes_by_dimension: maps dimension ID to valid individual codes (for validation messages)
- dimension_order: list of dimension IDs in their proper order from DSD
"""
# pylint: disable=import-outside-toplevel
from collections import defaultdict
code_to_dimension: dict[str, str] = {}
codes_by_dimension: dict[str, set[str]] = defaultdict(set)
dimension_order: list[str] = []
# Get ALL dimension codes from parameters API (authoritative source)
# This provides the actual valid values for each dimension
try:
all_params = metadata.get_dataflow_parameters(dataflow)
# Exclude time-related dimensions and country/area dimensions from indicator lookup
# Country is handled separately via the 'country' parameter
exclude_dims = {"FREQUENCY", "TIME_PERIOD", "COUNTRY", "REF_AREA"}
for dim_id, values in all_params.items():
if dim_id in exclude_dims:
continue
for v in values:
code = v.get("value")
if code:
# Add to codes_by_dimension (authoritative source for validation)
codes_by_dimension[dim_id].add(code)
# Add to code_to_dimension for matching
if code not in code_to_dimension:
code_to_dimension[code] = dim_id
except Exception: # noqa
pass # Continue with indicator-only lookup if params unavailable
# Get dimension order from DSD
# Keep COUNTRY/REF_AREA in order (they define the first segment position)
# Exclude trailing dimensions that are always separate parameters
try:
df_obj = metadata.dataflows.get(dataflow, {})
dsd_id = df_obj.get("structureRef", {}).get("id")
if dsd_id and dsd_id in metadata.datastructures:
dsd = metadata.datastructures[dsd_id]
# Only exclude trailing parameter dimensions
trailing_dims = {
"FREQUENCY",
"TIME_PERIOD",
"TYPE_OF_TRANSFORMATION",
"TRANSFORMATION",
}
for dim in dsd.get("dimensions", []):
dim_id = dim.get("id", "")
if (
dim_id
and dim_id.upper() not in trailing_dims
and "TRANSFORM" not in dim_id.upper()
):
dimension_order.append(dim_id)
except Exception: # noqa
pass
return code_to_dimension, dict(codes_by_dimension), dimension_order
def detect_indicator_dimensions(
dataflow: str, indicator_codes: list[str], metadata
) -> dict[str, list[str]]:
"""
Detect which dimension each indicator code belongs to.
Different dataflows use different dimension names for indicators:
- INDICATOR (most common)
- BOP_ACCOUNTING_ENTRY (Balance of Payments)
- SERIES (some datasets)
- ITEM (some datasets)
etc.
Supports compound codes like 'HICP_CP01' which are split into multiple
dimensions (e.g., INDEX_TYPE=HICP, COICOP_1999=CP01).
Also supports compound codes with non-indicator dimensions like 'S13_G1_G23_T'
which combines SECTOR + GFS_GRP + INDICATOR.
Returns a dict mapping dimension_id -> list of indicator codes.
Raises
------
OpenBBError
If any indicator code is not valid for the dataflow.
"""
# pylint: disable=import-outside-toplevel
from collections import defaultdict
dimension_codes: dict[str, list[str]] = defaultdict(list)
try:
response = await amake_request(url, timeout=20)
except (JSONDecodeError, ContentTypeError) as e:
raise OpenBBError(
"Error fetching data; This might be rate-limiting. Try again later."
) from e
if "ErrorDetails" in response:
raise OpenBBError(
f"{response['ErrorDetails'].get('Code')} -> {response['ErrorDetails'].get('Message')}" # type: ignore
code_to_dimension, codes_by_dimension, dimension_order = (
_build_dimension_lookups(dataflow, metadata)
)
series = response.get("CompactData", {}).get("DataSet", {}).pop("Series", {}) # type: ignore
invalid_codes: list[tuple[str, list[str]]] = [] # (code, unmatched_parts)
for code in indicator_codes:
if code == "*":
# Handle wildcard - prefer INDICATOR dimension if available
if "INDICATOR" in codes_by_dimension:
dimension_codes["INDICATOR"].append(code)
elif code_to_dimension:
primary_dim = next(iter(code_to_dimension.values()))
dimension_codes[primary_dim].append(code)
else:
dimension_codes["INDICATOR"].append(code)
elif code in code_to_dimension:
dimension_codes[code_to_dimension[code]].append(code)
else:
# Try to parse compound codes
matched_parts, unmatched = _parse_compound_code(code, code_to_dimension)
if matched_parts and not unmatched:
# All parts matched - valid compound code
for dim_id, code_part in matched_parts:
if code_part not in dimension_codes[dim_id]:
dimension_codes[dim_id].append(code_part)
else:
# Either no matches or some parts didn't match
invalid_codes.append((code, unmatched))
if not series:
raise OpenBBError(f"No time series data found -> {response}")
if invalid_codes:
# Build detailed error message using dimension order
error_parts: list[str] = []
# If there is only one series, they ruturn a dict instead of a list.
if series and isinstance(series, dict):
series = [series]
# Country-like dimensions to exclude unless explicitly matched
country_dims = {"COUNTRY", "REF_AREA"}
return series
for code, unmatched in invalid_codes:
if unmatched:
# First pass: identify all segments and their matches
parts = code.split("_")
segments: list[tuple[str, str | None]] = (
[]
) # (segment, dim_id or None)
i = 0
while i < len(parts):
# Try greedy matching (longest first)
matched = False
for j in range(len(parts), i, -1):
combined = "_".join(parts[i:j])
if combined in code_to_dimension:
segments.append((combined, code_to_dimension[combined]))
i = j
matched = True
break
if not matched:
segments.append((parts[i], None))
i += 1
# Check if any segment matched a country dimension
has_country_match = any(
dim_id in country_dims for _, dim_id in segments if dim_id
)
# Build effective dimension order - exclude country if not matched
effective_dim_order = (
dimension_order
if has_country_match
else [d for d in dimension_order if d not in country_dims]
)
# Find the first matched dimension to anchor our position
first_matched_idx: Any = None
first_matched_dim_pos: Any = None
for idx, (seg, dim_id) in enumerate(segments): # type: ignore
if dim_id and dim_id in effective_dim_order:
first_matched_idx = idx
first_matched_dim_pos = effective_dim_order.index(dim_id)
break
# Build error messages
segment_errors: list = []
for idx, (seg, dim_id) in enumerate(segments): # type: ignore
if dim_id is None:
# Calculate expected dimension based on position relative to first match
if (
first_matched_idx is not None
and first_matched_dim_pos is not None
):
expected_pos = first_matched_dim_pos - (
first_matched_idx - idx
)
else:
expected_pos = idx
if 0 <= expected_pos < len(effective_dim_order):
expected_dim = effective_dim_order[expected_pos]
sample = sorted(
codes_by_dimension.get(expected_dim, set())
)[:5]
segment_errors.append(
f"'{seg}' is invalid for {expected_dim} (valid: {', '.join(sample)})"
)
else:
segment_errors.append(f"'{seg}' is unrecognized")
error_parts.append(f"'{code}': {'; '.join(segment_errors)}")
else:
error_parts.append(f"'{code}'")
raise OpenBBError(
f"Invalid indicator code(s) for dataflow '{dataflow}': "
f"{'; '.join(error_parts)}. "
f"Use `obb.economy.available_indicators(provider='imf', dataflows='{dataflow}')` to see all valid codes."
)
except OpenBBError:
raise
except Exception:
# Fallback: put all codes in INDICATOR dimension (can't validate)
dimension_codes["INDICATOR"] = indicator_codes
return dict(dimension_codes)
def detect_transform_dimension(
dataflow: str,
) -> tuple[str | None, str | None, dict[str, str], dict[str, str]]:
"""
Detect transformation and unit dimensions for a dataflow.
Dynamically finds dimensions containing 'TRANSFORM' or 'UNIT' in their names.
Returns:
tuple of (transform_dim, unit_dim, transform_lookup, unit_lookup) where:
- transform_dim: name of transform dimension (or None)
- unit_dim: name of unit dimension (or None)
- transform_lookup: maps user-friendly names (index, yoy, period) to IMF codes
- unit_lookup: maps user-friendly names (usd, eur, index, local) to IMF codes
"""
# pylint: disable=import-outside-toplevel
from openbb_imf.utils.metadata import ImfMetadata
transform_dim: str | None = None
unit_dim: str | None = None
transform_lookup: dict[str, str] = {}
unit_lookup: dict[str, str] = {}
try:
m = ImfMetadata()
params = m.get_dataflow_parameters(dataflow)
for dim, values in params.items():
dim_upper = dim.upper()
# Handle TRANSFORM dimension
if "TRANSFORM" in dim_upper:
transform_dim = dim
for v in values:
code = v.get("value", "")
label = v.get("label", "").lower()
# Prefer simpler codes (shorter, no prefix like SRP_, WGT, SA_)
is_simple = (
not code.startswith("SRP_")
and not code.startswith("WGT")
and not code.startswith("SA_")
)
# Index: just "Index" without change/percent
if (
label == "index"
or (
"index" in label
and "change" not in label
and "percent" not in label
)
) and ("index" not in transform_lookup or is_simple):
transform_lookup["index"] = code
# YOY: year-over-year or year ago
if (
"year-over-year" in label
or "yoy" in label
or "year ago" in label
) and ("yoy" not in transform_lookup or is_simple):
transform_lookup["yoy"] = code
# Period: period-over-period (not year-over-year)
if (
"period-over-period" in label
or (
"period" in label
and "change" in label
and "year" not in label
)
) and ("period" not in transform_lookup or is_simple):
transform_lookup["period"] = code
if ("percent of gdp" in label or "% of gdp" in label) and (
"percent_gdp" not in transform_lookup or is_simple
):
transform_lookup["percent_gdp"] = code
# Currency / Domestic currency (for GFS dataflows)
if ("domestic currency" in label or label == "currency") and (
"currency" not in transform_lookup or is_simple
):
transform_lookup["currency"] = code
# Also allow direct code access (case-insensitive)
transform_lookup[code.lower()] = code
# Handle UNIT dimension
elif dim_upper == "UNIT":
unit_dim = dim
for v in values:
code = v.get("value", "")
label = v.get("label", "").lower()
# Map common unit names
if "us dollar" in label or label == "usd":
unit_lookup["usd"] = code
if "euro" in label or label == "eur":
unit_lookup["eur"] = code
if label == "index" or "index" in label:
unit_lookup["index"] = code
if "local" in label or "national" in label or "domestic" in label:
unit_lookup["local"] = code
if "percent" in label or "%" in label:
unit_lookup["percent"] = code
# Also allow direct code access (case-insensitive)
unit_lookup[code.lower()] = code
except (KeyError, ValueError):
pass
return transform_dim, unit_dim, transform_lookup, unit_lookup
def parse_time_period(time_str: str) -> str:
"""Convert IMF time period formats to valid date strings (period ending).
Examples:
'2025-M03' -> '2025-03-31'
'2025-Q1' -> '2025-03-31'
'2025' -> '2025-12-31'
"""
# pylint: disable=import-outside-toplevel
import calendar
from datetime import datetime
if not time_str:
return time_str
try:
# Handle monthly format (YYYY-MXX)
if "-M" in time_str:
parts = time_str.split("-M")
if len(parts) == 2:
year = int(parts[0])
month = int(parts[1])
# Get the last day of the month using calendar module
last_day = calendar.monthrange(year, month)[1]
# Create date object and format it
date_obj = datetime(year, month, last_day)
return date_obj.strftime("%Y-%m-%d")
# Handle quarterly format (YYYY-QX)
elif "-Q" in time_str:
parts = time_str.split("-Q")
if len(parts) == 2:
year = int(parts[0])
quarter = int(parts[1])
# Map quarters to their last month
quarter_last_month = {1: 3, 2: 6, 3: 9, 4: 12}
month = quarter_last_month.get(quarter, 12)
# Get the last day of the quarter's last month
last_day = calendar.monthrange(year, month)[1]
# Create date object and format it
date_obj = datetime(year, month, last_day)
return date_obj.strftime("%Y-%m-%d")
# Handle yearly format (YYYY)
elif len(time_str) == 4 and time_str.isdigit():
year = int(time_str)
# Last day of the year is always December 31
date_obj = datetime(year, 12, 31)
return date_obj.strftime("%Y-%m-%d")
# Return as-is if it's already in a valid format or unrecognized
return time_str
except (ValueError, KeyError):
# If parsing fails, return the original string
return time_str
def parse_agency_from_urn(code_urn: str) -> str | None:
"""
Parse agency ID from hierarchicalCode's code URN.
Examples
--------
>>> parse_agency_from_urn("urn:sdmx:org.sdmx.infomodel.codelist.Code=ISORA:CL_RAFIT_LABELS(1.0+.0).CL_TOPIC_1")
'ISORA'
>>> parse_agency_from_urn("urn:sdmx:org.sdmx.infomodel.codelist.Code=IMF.STA:CL_BOP_INDICATOR(10.0+.0).CAB")
'IMF.STA'
"""
if not code_urn or "=" not in code_urn or ":" not in code_urn:
return None
try:
# Extract everything after the "=" and before the last ":"
# Example: "ISORA:CL_RAFIT_LABELS(1.0+.0).CL_TOPIC_1"
after_equals = code_urn.split("=")[-1]
# Split by ":" to get agency (first part) and codelist info (second part)
parts = after_equals.split(":")
if len(parts) >= 2:
return parts[0]
return None
except Exception:
return None
def parse_indicator_code_from_urn(code_urn: str) -> str | None:
"""
Parse indicator code from hierarchicalCode's code URN.
Examples
--------
>>> parse_indicator_code_from_urn(
... "urn:sdmx:org.sdmx.infomodel.codelist.Code=IMF.STA:CL_FSIBSIS_INDICATOR(4.0+.0).INTINC"
... )
'INTINC'
"""
if not code_urn or "." not in code_urn:
return None
return code_urn.split(".")[-1]
def parse_codelist_id_from_urn(code_urn: str) -> str | None:
"""
Parse codelist ID from hierarchicalCode's code URN.
Examples
--------
>>> parse_codelist_id_from_urn("urn:sdmx:org.sdmx.infomodel.codelist.Code=IMF.STA:CL_BOP_INDICATOR(10.0+.0).CAB")
'CL_BOP_INDICATOR'
"""
if not code_urn or ":" not in code_urn or "(" not in code_urn:
return None
try:
# Extract everything after the last colon
after_colon = code_urn.split(":")[-1]
# Extract everything before the opening parenthesis
codelist_id = after_colon.split("(")[0]
return codelist_id
except Exception:
return None
def parse_search_query(query: str) -> list[list[str]]:
"""
Parse a search query string into OR-groups of AND-terms.
Supports:
- Quoted phrases: "exact phrase"
- OR operator: term1 | term2
- AND operator (implicit or +): term1 term2, term1+term2
- Stop words are filtered out (of, the, a, an, is, are, in, on, for, with, and, or)
Examples
--------
>>> parse_search_query('inflation | "consumer price"')
[['inflation'], ['consumer price']]
>>> parse_search_query('gdp growth')
[['gdp', 'growth']]
"""
# pylint: disable=import-outside-toplevel
import string as string_module
STOP_WORDS = { # pylint: disable=C0103
"of",
"the",
"a",
"an",
"is",
"are",
"in",
"on",
"for",
"with",
"and",
"or",
}
or_groups: list = []
parts_by_or = [p.strip() for p in query.split("|")]
for or_part in parts_by_or:
if not or_part:
continue
current_and_group: list = []
in_quote = False
current_term: list = []
# Add a space at the end to ensure the last term is processed
for char in or_part + " ":
if char == '"':
if in_quote:
# End of a quoted term
if current_term:
term = "".join(current_term).lower()
current_and_group.append(term)
current_term = []
in_quote = False
else:
# Start of a quoted term
if current_term: # process term before quote
term = (
"".join(current_term)
.lower()
.strip(string_module.punctuation)
)
if term and term not in STOP_WORDS:
current_and_group.append(term)
current_term = []
in_quote = True
elif (char == "+" or char.isspace()) and not in_quote:
# End of a non-quoted term
if current_term:
term = (
"".join(current_term).lower().strip(string_module.punctuation)
)
if term and term not in STOP_WORDS:
current_and_group.append(term)
current_term = []
else:
current_term.append(char)
if current_and_group:
or_groups.append(current_and_group)
return or_groups
def build_time_period_params(
constraints_response: dict | None,
) -> tuple[list[dict], str | None]:
"""
Build time period parameters from a constraints API response.
Extracts start date, end date, and series count from annotations.
Parameters
----------
constraints_response : dict | None
The response from get_available_constraints
Returns
-------
tuple[list[dict], str | None]
A tuple of (options list, series_count) where options contains
start/end date dicts with 'label' and 'value' keys.
"""
if not constraints_response:
return [], None
full_response = constraints_response.get("full_response", {})
content_constraints = full_response.get("data", {}).get("contentConstraints", [])
annotations = (
content_constraints[0].get("annotations", []) if content_constraints else []
)
start = end = series_count = None
for annotation in annotations:
ann_id = annotation.get("id")
if ann_id == "time_period_start":
start = annotation.get("title")
elif ann_id == "time_period_end":
end = annotation.get("title")
elif ann_id == "series_count":
series_count = annotation.get("title")
options: list[dict] = []
if start:
options.append({"label": f"Start Date: {start}", "value": start})
if end:
options.append({"label": f"End Date: {end}", "value": end})
return options, series_count
def parse_codelist_urn(urn: str) -> str | None:
"""
Parse codelist ID from owningCodelistUrn.
Examples
--------
>>> parse_codelist_urn(
... "urn:sdmx:com.epam.quanthub.sdmxplus.infomodel.Glossary=IMF.STA:CL_FSIBSIS_INDICATOR(4.0+.0)"
... )
'CL_FSIBSIS_INDICATOR'
"""
if not urn or "=" not in urn:
return None
try:
parts = urn.split("=")[-1]
codelist_with_version = parts.split(":")[-1]
codelist_id = codelist_with_version.split("(")[0]
return codelist_id
except Exception:
return None
def extract_all_codelists_from_hierarchy(hierarchy: dict) -> set[str]:
"""
Recursively extract ALL codelist IDs from a hierarchy's hierarchicalCodes.
This ensures that hierarchies containing multiple codelists (e.g., labels,
table levels, AND indicator codelists) are properly indexed for reverse lookup.
Parameters
----------
hierarchy : dict
The hierarchy object containing hierarchicalCodes.
Returns
-------
set[str]
Set of all codelist IDs found in the hierarchy.
"""
codelists: set[str] = set()
def scan_codes(codes: list) -> None:
for code in codes:
code_urn = code.get("code", "")
if code_urn:
codelist_id = parse_codelist_id_from_urn(code_urn)
if codelist_id:
codelists.add(codelist_id)
# Recurse into nested hierarchicalCodes
nested = code.get("hierarchicalCodes", [])
if nested:
scan_codes(nested)
hcodes = hierarchy.get("hierarchicalCodes", [])
if hcodes and isinstance(hcodes, list):
scan_codes(hcodes)
return codelists
def build_hierarchy_to_codelist_map(hierarchies: dict) -> dict[str, str]:
"""
Build mapping from hierarchy ID to codelist ID.
Parameters
----------
hierarchies : dict
Dictionary of hierarchy_id -> hierarchy data.
Returns
-------
dict[str, str]
Mapping from hierarchy ID to its primary codelist ID.
"""
mapping: dict[str, str] = {}
for hierarchy_id, hierarchy in hierarchies.items():
codelist_id = None
# Method 1: Check owningCodelistUrn annotation
annotations = hierarchy.get("annotations", [])
for annotation in annotations:
if annotation.get("id") == "owningCodelistUrn":
urn = annotation.get("text", "")
codelist_id = parse_codelist_urn(urn)
if codelist_id:
break
# Method 2: Infer from first hierarchicalCode's code URN
# Handles hierarchies like H_QGFS_* that don't have owningCodelistUrn
if not codelist_id:
hcodes = hierarchy.get("hierarchicalCodes", [])
if hcodes and isinstance(hcodes, list) and hcodes:
first_code = hcodes[0]
code_urn = first_code.get("code", "")
if code_urn:
codelist_id = parse_codelist_id_from_urn(code_urn)
if codelist_id:
mapping[hierarchy_id] = codelist_id
return mapping
def build_codelist_to_hierarchies_map(hierarchies: dict) -> dict[str, list[str]]:
"""
Build reverse mapping from codelist ID to list of hierarchy IDs.
Unlike build_hierarchy_to_codelist_map (which maps to the primary codelist),
this function indexes ALL codelists contained in each hierarchy. This ensures
that hierarchies with nested codelists (e.g., H_FSIC_INDICATOR containing
CL_FSI_LABELS, CL_FSIC_INDICATOR_LABELS, AND CL_FSIC_INDICATOR) are found
when looking up any of their contained codelists.
Parameters
----------
hierarchies : dict
Dictionary of hierarchy_id -> hierarchy data.
Returns
-------
dict[str, list[str]]
Mapping from codelist ID to list of hierarchy IDs containing it.
"""
mapping: dict[str, list[str]] = {}
for hierarchy_id, hierarchy in hierarchies.items():
# Extract ALL codelists from this hierarchy (not just the first one)
all_codelists = extract_all_codelists_from_hierarchy(hierarchy)
for codelist_id in all_codelists:
if codelist_id not in mapping:
mapping[codelist_id] = []
if hierarchy_id not in mapping[codelist_id]:
mapping[codelist_id].append(hierarchy_id)
return mapping
def translate_error_message(error_msg: str) -> str:
"""Translate IMF dimension codes to user-friendly parameter names in error messages.
Maps internal IMF dimension names like COUNTRY, FREQUENCY, INDICATOR
to user-facing parameter names like country, frequency, symbol/indicator.
Also translates common IMF code values to user-friendly equivalents.
"""
# Dimension code to parameter name mappings
dim_to_param = {
"COUNTRY": "country",
"REF_AREA": "country",
"JURISDICTION": "country",
"COUNTERPART_AREA": "counterpart_country",
"FREQUENCY": "frequency",
"INDICATOR": "indicator",
"CLASSIFICATION": "indicator",
"SERIES": "indicator",
"ITEM": "indicator",
"SECTOR": "sector",
"BOP_ACCOUNTING_ENTRY": "accounting_entry",
"ACCOUNTING_ENTRY": "accounting_entry",
"TYPE_OF_TRANSFORMATION": "transform",
"PRICE_TYPE": "price_type",
"S_ADJUSTMENT": "seasonal_adjustment",
"UNIT_MEASURE": "unit",
"UNIT_MULT": "unit_multiplier",
"TIME_PERIOD": "time_period",
}
# Frequency value mappings
frequency_map = {
"'A'": "'annual'",
"'Q'": "'quarter'",
"'M'": "'month'",
'"A"': '"annual"',
'"Q"': '"quarter"',
'"M"': '"month"',
}
# Apply dimension translations
for dim_code, param_name in dim_to_param.items():
error_msg = error_msg.replace(f"'{dim_code}'", f"'{param_name}'")
error_msg = error_msg.replace(f'"{dim_code}"', f'"{param_name}"')
# Also handle "dimension 'X'" pattern without quotes around dimension
error_msg = error_msg.replace(
f"dimension '{dim_code}'", f"'{param_name}' parameter"
)
# Handle dimension X: pattern
error_msg = error_msg.replace(f"{dim_code} codes", f"{param_name} codes")
# Apply frequency value translations
for api_val, user_val in frequency_map.items():
error_msg = error_msg.replace(api_val, user_val)
return error_msg

View File

@@ -1,281 +0,0 @@
"""IMF IRFCL Data Set Helpers."""
def load_irfcl_symbols() -> dict:
"""Load IMF IRFCL symbols."""
# pylint: disable=import-outside-toplevel
from openbb_imf.utils.constants import load_symbols
return load_symbols("IRFCL")
def load_country_map() -> dict:
"""Load IMF IRFCL country map."""
# pylint: disable=import-outside-toplevel
import json # noqa
from json.decoder import JSONDecodeError
from pathlib import Path
from openbb_core.app.model.abstract.error import OpenBBError
try:
country_map_file = (
Path(__file__).parents[1].joinpath("assets", "imf_country_map.json")
)
with country_map_file.open(encoding="utf-8") as file:
country_map_dict = json.load(file)
except (FileNotFoundError, JSONDecodeError) as e:
raise OpenBBError(f"Failed to load IMF IRFCL country map: {e}") from e
return {
k: v.split(",")[0].split("_(")[0]
for k, v in country_map_dict.items()
if len(k) == 2
and k[0] not in ("5", "1", "7")
and k not in ("X0", "R1", "GW", "F1", "F6")
}
def load_country_to_code_map() -> dict:
"""Load a map of lowercase country name to 2-letter ISO symbol."""
return {
(
"euro_area"
if k == "U2"
else v.lower()
.replace(" ", "_")
.replace("`", "")
.split(",")[0]
.split("_(")[0]
): k
for k, v in load_country_map().items()
}
def validate_countries(countries) -> str:
"""Validate the country and convert to a 2-letter ISO country code."""
# pylint: disable=import-outside-toplevel
from warnings import warn # noqa
from openbb_core.app.model.abstract.error import OpenBBError
country_map_dict = load_country_to_code_map()
if isinstance(countries, str):
countries = countries.split(",")
elif not isinstance(countries, list):
raise OpenBBError("Invalid countries list.")
new_countries: list = []
if "all" in countries or "ALL" in countries:
return "all"
for country in countries:
if country.lower() not in country_map_dict and country.upper() not in list(
country_map_dict.values()
):
warn(f"Invalid IMF IRFCL country: {country}")
continue
if country.upper() in list(country_map_dict.values()):
new_countries.append(country.upper())
else:
new_countries.append(country_map_dict.get(country, country).upper())
new_countries = [c for c in new_countries if c]
if not new_countries:
raise OpenBBError("No valid countries found in the supplied list.")
return ",".join(new_countries)
def validate_symbols(symbols) -> str:
"""Validate the IMF IRFCL symbols."""
# pylint: disable=import-outside-toplevel
from warnings import warn # noqa
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_imf.utils.constants import IRFCL_PRESET
irfcl_symbols = load_irfcl_symbols()
if isinstance(symbols, str):
symbols = symbols.split(",")
elif not isinstance(symbols, list):
raise OpenBBError("Invalid symbols list.")
if "IRFCL" in symbols or "all" in symbols:
return "all"
new_symbols: list = []
for symbol in symbols:
if symbol in IRFCL_PRESET:
return IRFCL_PRESET[symbol].replace(",", "+")
if symbol.upper() not in irfcl_symbols:
warn(f"Invalid IMF IRFCL symbol: {symbol}")
new_symbols.append(symbol.upper())
return "+".join(new_symbols) if len(new_symbols) > 1 else new_symbols[0]
# We use this as a helper to allow future expansion of the supported IMF indicators.
# Each database has its own nuances with URL construction and schemas.
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
async def _get_irfcl_data(**kwargs) -> list[dict]:
"""Get IMF IRFCL data.
This function is not intended to be called directly,
but through the `ImfEconomicIndicatorsFetcher` class.
"""
# pylint: disable=import-outside-toplevel
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_imf.utils import constants
from openbb_imf.utils.helpers import get_data
from pandas import to_datetime
from pandas.tseries import offsets
countries = kwargs.get("country", "")
countries = (
"" if countries == "all" else countries.replace(",", "+") if countries else ""
)
frequency = constants.FREQUENCY_DICT.get(kwargs.get("frequency", "quarter"), "Q")
sector = kwargs.get("sector", "monetary_authorities")
sector = constants.REF_SECTORS_DICT.get(sector, "")
start_date = kwargs.get("start_date", "")
end_date = kwargs.get("end_date", "")
# Adjust the dates to the date relative to frequency.
# The API does not accept arbitrary dates, so we need to adjust them.
if start_date:
start_date = to_datetime(start_date)
if frequency == "Q":
start_date = offsets.QuarterBegin(startingMonth=1).rollback(start_date)
elif frequency == "A":
start_date = offsets.YearBegin().rollback(start_date)
else:
start_date = offsets.MonthBegin().rollback(start_date)
start_date = start_date.strftime("%Y-%m-%d")
if end_date:
end_date = to_datetime(end_date)
if frequency == "Q":
end_date = offsets.QuarterEnd().rollforward(end_date)
elif frequency == "A":
end_date = offsets.YearEnd().rollforward(end_date)
else:
end_date = offsets.MonthEnd().rollforward(end_date)
end_date = end_date.strftime("%Y-%m-%d")
indicator = kwargs.get("symbol")
indicators = validate_symbols(indicator) if indicator else ""
indicators = "" if indicators == "all" else indicators
if not indicators and not countries:
raise OpenBBError("Country is required when returning the complete dataset.")
date_range = (
f"?startPeriod={start_date}&endPeriod={end_date}"
if start_date and end_date
else ""
)
base_url = "http://dataservices.imf.org/REST/SDMX_JSON.svc/"
key = f"CompactData/IRFCL/{frequency}.{countries}.{indicators}.{sector}"
url = f"{base_url}{key}{date_range}"
series = await get_data(url)
data: list = []
all_symbols = load_irfcl_symbols()
country_map_dict = {
v: k.replace("_", " ").title().replace("Ecb", "ECB")
for k, v in load_country_to_code_map().items()
}
# Iterate over the series to extract observations and map the metadata.
for s in series:
if "Obs" not in s:
continue
meta = {
k.replace("@", "").lower(): (
constants.UNIT_MULTIPLIERS_MAP.get(str(v), v)
if k == "@UNIT_MULT"
else v
)
for k, v in s.items()
if k != "Obs"
}
_symbol = meta.get("indicator")
_parent: str | None = None
_order: str | None = None
_level: str | None = None
_table: str | None = None
_title: str | None = None
_unit: str | None = None
if _symbol not in all_symbols:
continue
_table = all_symbols.get(_symbol, {}).get("table")
_parent = all_symbols.get(_symbol, {}).get("parent", "")
_order = all_symbols.get(_symbol, {}).get("order", "")
_level = all_symbols.get(_symbol, {}).get("level", "")
_title = all_symbols.get(_symbol, {}).get("title", "").replace(", ", " - ")
_unit = all_symbols.get(_symbol, {}).get("unit", "")
if _title:
_title = " - ".join(_title.split(", ")[:-1])
_data = s.pop("Obs", [])
if isinstance(_data, dict):
_data = [_data]
for d in _data:
_date = d.pop("@TIME_PERIOD", None)
val = d.pop("@OBS_VALUE", None)
_ = d.pop("@OBS_STATUS", None)
if not val:
continue
if _date:
offset = (
offsets.QuarterEnd
if "Q" in _date
else offsets.YearEnd if len(str(_date)) == 4 else offsets.MonthEnd
)
_date = to_datetime(_date)
_date = _date + offset(0)
_date = _date.strftime("%Y-%m-%d")
vals = {
k: v
for k, v in {
"date": _date,
"table": _table,
"symbol": _symbol,
"parent": _parent,
"order": _order,
"level": _level,
"country": country_map_dict.get(
meta.get("ref_area"), meta.get("ref_area")
),
"reference_sector": constants.SECTOR_MAP.get(
meta.get("ref_sector"), meta.get("ref_sector")
),
"title": _title,
"value": float(val) if val else None,
"unit": _unit,
"scale": meta.get("unit_mult"),
}.items()
if v
}
if vals.get("value") and vals.get("date"):
d.update(vals)
data.append(d)
if not data:
raise OpenBBError(f"No data found for '{indicator}' in '{countries}'.")
return data

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,42 @@
"""IMF Port Watch helpers."""
# pylint: disable=R0914
from typing import Any
from async_lru import alru_cache
from openbb_imf.utils.constants import (
CHOKEPOINTS_BASE_URL,
DAILY_TRADE_BASE_URL,
PORT_COUNTRIES,
)
def list_countries() -> list[dict[str, str]]:
"""List available countries for IMF Port Watch.
Returns
-------
list of dict
A list of dictionaries with 'label' and 'value' for each country.
"""
choices: list = []
ports = get_ports()
seen: set = set()
for port in ports:
if port["ISO3"] in seen:
continue
seen.add(port["ISO3"])
choices.append(
{
"label": port["countrynoaccents"],
"value": port["ISO3"],
}
)
return choices
def map_port_country_code(country_code: str) -> str:
"""Map the 3-letter country code to the full country name.
@@ -21,11 +50,13 @@ def map_port_country_code(country_code: str) -> str:
str
The full country name, without accents, corresponding to the provided country code.
"""
COUNTRY_CODE_TO_PORT = {v: k for k, v in PORT_COUNTRIES.items()}
if country_code not in COUNTRY_CODE_TO_PORT:
cc = country_code.upper()
countries = list_countries()
code_to_country = {country["value"]: country["label"] for country in countries}
if cc not in code_to_country:
raise ValueError("Country code is not supported by IMF Port Watch.")
return COUNTRY_CODE_TO_PORT.get(country_code.upper(), country_code.upper())
return code_to_country.get(cc, cc)
def get_port_ids_by_country(country_code: str) -> str:
@@ -41,28 +72,13 @@ def get_port_ids_by_country(country_code: str) -> str:
str
A list of port IDs as a comma-separated string.
"""
# pylint: disable=import-outside-toplevel
import importlib.resources
import json
ports = get_ports()
ports_ids: list = []
for port in ports:
if port["ISO3"] == country_code.upper():
ports_ids.append(port["portid"])
json_path = (
importlib.resources.files("openbb_imf") / "assets" / "imf_ports_by_country.json"
)
if not json_path.exists(): # type: ignore
raise FileNotFoundError(f"Port IDs JSON file not found at {json_path}")
port_ids_by_country: dict = {}
with open(str(json_path), encoding="utf-8") as file:
port_ids_by_country = json.load(file)
if country_code.upper() not in port_ids_by_country:
raise ValueError(
f"Country code '{country_code}' is not supported by IMF Port Watch."
)
return port_ids_by_country.get(country_code.upper(), "")
return ",".join(ports_ids)
def get_port_id_choices() -> list:
@@ -73,31 +89,16 @@ def get_port_id_choices() -> list:
list
A list of dictionaries, with labels and values for each port ID.
"""
# pylint: disable=import-outside-toplevel
import json # noqa
import importlib.resources
json_path = (
importlib.resources.files("openbb_imf") / "assets" / "imf_portid_map.json"
)
if not json_path.exists(): # type: ignore
raise FileNotFoundError(f"imf_portid_map.json not found at: {json_path}")
choices: list = []
portids: dict = {}
ports = get_ports()
with open(str(json_path), encoding="utf-8") as file:
portids = json.load(file)
for portid, portname in portids.items():
for port in ports:
choices.append(
{
"label": portname,
"value": portid,
"label": port["portname"],
"value": port["portid"],
}
)
return choices
@@ -387,3 +388,65 @@ async def get_daily_port_activity_data(
)
return final_output
@alru_cache(maxsize=1)
async def list_ports() -> list[dict[str, Any]]:
"""List all available ports from the IMF Port Watch dataset.
Returns
-------
list[dict]
A list of dictionaries, each representing a port with its details.
"""
# pylint: disable=import-outside-toplevel
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.utils.helpers import get_async_requests_session
url = (
"https://services9.arcgis.com/weJ1QsnbMYJlCHdG/arcgis/rest/services/PortWatch_ports_database/"
+ "FeatureServer/0/query?where=1%3D1&outFields=countrynoaccents,portid,lon,lat,portname,ISO3,continent,fullname"
+ "+&returnGeometry=false&orderByFields=vessel_count_total%20DESC&outSR=&f=json"
)
ports: list[dict] = []
try:
async with await get_async_requests_session() as session, await session.get(
url
) as response:
if response.status != 200:
raise OpenBBError(
f"Failed to fetch ports data: {response.status} - {response.reason}"
)
data = await response.json()
for feature in data.get("features", []):
ports.append(feature.get("attributes", {}))
return ports
except Exception as e:
raise OpenBBError(f"Error fetching ports data: {e} -> {e.args}") from e
def get_ports() -> list[dict[str, Any]]:
"""Get the list of all ports synchronously."""
# pylint: disable=import-outside-toplevel
import asyncio
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop is not None:
# Already in an async context
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(asyncio.run, list_ports())
ports = future.result()
else:
ports = asyncio.run(list_ports())
return ports

View File

@@ -0,0 +1,242 @@
"""IMF Progressive Query Helper."""
# pylint: disable=W0212
from openbb_imf.utils.query_builder import ImfQueryBuilder
class ImfParamsBuilder:
"""A helper class to build IMF queries progressively by making sequential dimension selections,
for each dimension of a dataflow, filtering the available options at each step based on previous selections.
"""
def __init__(self, dataflow_id: str):
"""Initialize the ImfParamsBuilder object.
Parameters
----------
dataflow_id : str
The ID of the dataflow to build a query for.
"""
self._builder = ImfQueryBuilder()
if dataflow_id not in self._builder.metadata.dataflows:
raise KeyError(
f"Dataflow '{dataflow_id}' not found."
f" Available dataflows: {list(self._builder.metadata.dataflows.keys())}"
)
self.dataflow_id = dataflow_id
self.dsd = self._get_dsd()
self._dimensions = self._get_dimensions_in_order()
self.current_dimension = self._dimensions[0] if self._dimensions else None
self._selections: dict = {dim: None for dim in self._dimensions}
self._last_constraints_response: dict = {}
def _get_dsd(self):
"""Get the Data Structure Definition (DSD) for the current dataflow."""
df_obj = self._builder.metadata.dataflows[self.dataflow_id]
dsd_id = df_obj.get("structureRef", {}).get("id")
return self._builder.metadata.datastructures.get(dsd_id, {})
def _get_dimensions_in_order(self) -> list[str]:
"""Get the list of dimension IDs in their specified order."""
dimensions_metadata = self.dsd.get("dimensions", [])
# Sort by position if available, otherwise keep original order
if dimensions_metadata and all(
d.get("position") is not None for d in dimensions_metadata
):
dimensions_metadata = sorted(
dimensions_metadata, key=lambda x: int(x.get("position")) # type: ignore
)
return [
d["id"]
for d in dimensions_metadata
if d.get("id") and d.get("id") != "TIME_PERIOD"
]
def get_next_dimension_to_select(self) -> str | None:
"""
Get the ID of the next dimension that needs a selection.
Returns
-------
str or None
The ID of the next dimension, or None if all dimensions have been selected.
"""
for dim in self._dimensions:
if self._selections[dim] is None:
return dim
return None
def get_options_for_dimension(
self, dimension_id: str | None = None
) -> list[dict[str, str]]:
"""Get the available options for a given dimension, based on the current selections.
Parameters
----------
dimension_id : str
The ID of the dimension to get options for.
Returns
-------
list[dict]
A list of available options, where each option is a dictionary with
'label' and 'value' keys.
"""
dimension_id = dimension_id or self.get_next_dimension_to_select()
if not dimension_id:
return []
if dimension_id not in self._dimensions:
raise ValueError(
f"Dimension '{dimension_id}' not found for dataflow '{self.dataflow_id}'."
)
key_parts: list = []
for dim in self._dimensions:
if self._selections[dim] is not None:
key_parts.append(self._selections[dim])
else:
# Use wildcard '*' for unselected dimensions instead of empty string
# Empty string creates malformed URLs like '../'
key_parts.append("*")
key = ".".join(key_parts)
constraints = self._builder.metadata.get_available_constraints(
dataflow_id=self.dataflow_id,
key=key,
component_id=dimension_id,
)
# Store the last constraints response for time period validation
self._last_constraints_response = constraints
options: list[dict[str, str]] = []
key_values = constraints.get("key_values", [])
for kv in key_values:
if kv.get("id") == dimension_id:
codelist_map = self._get_codelist_for_dim(dimension_id)
for value_id in kv.get("values", []):
options.append(
{
"label": codelist_map.get(value_id, value_id),
"value": value_id,
}
)
return options
def _get_codelist_for_dim(self, dimension_id: str) -> dict:
"""Get the codelist map for a given dimension."""
df_obj = self._builder.metadata.dataflows[self.dataflow_id]
agency_id = df_obj.get("agencyID")
if not agency_id:
return {}
dimensions_metadata = {
d["id"]: d for d in self.dsd.get("dimensions", []) if d.get("id")
}
dim_meta = dimensions_metadata.get(dimension_id)
if not dim_meta:
return {}
dsd_id = self.dsd.get("id")
codelist_id = self._builder.metadata._resolve_codelist_id(
self.dataflow_id, dsd_id, dimension_id, dim_meta
)
if codelist_id:
return self._builder.metadata._get_codelist_map(
codelist_id, agency_id, self.dataflow_id
)
return {}
def set_dimension(self, dimension: tuple[str, str]) -> dict:
"""Set a value for a dimension and clear downstream selections.
Parameters
----------
dimension : tuple
A tuple of (dimension_id, value) to set.
Returns
-------
dict
The updated selections after setting the dimension.
"""
if dimension[0] not in self._dimensions:
raise KeyError(
f"Dimension '{dimension[0]}' not valid for this dataflow."
f" Valid dimensions: {list(self._selections.keys())}"
)
self._selections[dimension[0]] = dimension[1]
# When a selection is made, we clear selections for downstream dimensions
# as they might now be invalid.
found_dim = False
for dim in self._dimensions:
if found_dim:
self._selections[dim] = None
if dim == dimension[0]:
found_dim = True
self.current_dimension = self.get_next_dimension_to_select()
return self._selections.copy()
def get_dimensions(self) -> dict[str, str | None]:
"""Get the current selections for the dimension_id.
Returns
-------
dict
A dictionary of the current dimension selections.
"""
return self._selections.copy()
def build_url(
self, start_date: str | None = None, end_date: str | None = None
) -> str:
"""
Build the final API URL based on the current selections.
Parameters
----------
start_date : str, optional
The start date for the data query.
end_date : str, optional
The end date for the data query.
Returns
-------
str
The constructed IMF API URL.
"""
return self._builder.build_url(
dataflow=self.dataflow_id,
start_date=start_date,
end_date=end_date,
**self._selections,
)
def fetch(self, start_date: str | None = None, end_date: str | None = None) -> dict:
"""
Build the URL and fetch the data based on the current selections.
Parameters
----------
start_date : str, optional
The start date for the data query.
end_date : str, optional
The end date for the data query.
Returns
-------
dict
The fetched data and metadata.
"""
return self._builder.fetch_data(
dataflow=self.dataflow_id,
start_date=start_date,
end_date=end_date,
**self._selections,
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -157,7 +157,7 @@ def plot_port_info_map(data: list[ImfPortInfoData]):
custom_data=df[["hover_html"]],
size="marker_size",
size_max=max_size,
color_discrete_sequence=["fuchsia"],
color_discrete_sequence=["#7F7F7F"],
opacity=0.4,
height=600,
width=600,

View File

@@ -418,6 +418,42 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "deepdiff"
version = "8.6.1"
description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "deepdiff-8.6.1-py3-none-any.whl", hash = "sha256:ee8708a7f7d37fb273a541fa24ad010ed484192cd0c4ffc0fa0ed5e2d4b9e78b"},
{file = "deepdiff-8.6.1.tar.gz", hash = "sha256:ec56d7a769ca80891b5200ec7bd41eec300ced91ebcc7797b41eb2b3f3ff643a"},
]
[package.dependencies]
orderly-set = ">=5.4.1,<6"
[package.extras]
cli = ["click (>=8.1.0,<8.2.0)", "pyyaml (>=6.0.0,<6.1.0)"]
coverage = ["coverage (>=7.6.0,<7.7.0)"]
dev = ["bump2version (>=1.0.0,<1.1.0)", "ipdb (>=0.13.0,<0.14.0)", "jsonpickle (>=4.0.0,<4.1.0)", "nox (==2025.5.1)", "numpy (>=2.0,<3.0) ; python_version < \"3.10\"", "numpy (>=2.2.0,<2.3.0) ; python_version >= \"3.10\"", "orjson (>=3.10.0,<3.11.0)", "pandas (>=2.2.0,<2.3.0)", "polars (>=1.21.0,<1.22.0)", "python-dateutil (>=2.9.0,<2.10.0)", "tomli (>=2.2.0,<2.3.0)", "tomli-w (>=1.2.0,<1.3.0)", "uuid6 (==2025.0.1)"]
docs = ["Sphinx (>=6.2.0,<6.3.0)", "sphinx-sitemap (>=2.6.0,<2.7.0)", "sphinxemoji (>=0.3.0,<0.4.0)"]
optimize = ["orjson"]
static = ["flake8 (>=7.1.0,<7.2.0)", "flake8-pyproject (>=1.2.3,<1.3.0)", "pydantic (>=2.10.0,<2.11.0)"]
test = ["pytest (>=8.3.0,<8.4.0)", "pytest-benchmark (>=5.1.0,<5.2.0)", "pytest-cov (>=6.0.0,<6.1.0)", "python-dotenv (>=1.0.0,<1.1.0)"]
[[package]]
name = "defusedxml"
version = "0.7.1"
description = "XML bomb protection for Python stdlib modules"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
groups = ["main"]
files = [
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
]
[[package]]
name = "exceptiongroup"
version = "1.3.1"
@@ -460,6 +496,22 @@ all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "fastjsonschema"
version = "2.21.2"
description = "Fastest Python implementation of JSON schema"
optional = true
python-versions = "*"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463"},
{file = "fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de"},
]
[package.extras]
devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"]
[[package]]
name = "frozenlist"
version = "1.8.0"
@@ -651,14 +703,14 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2
[[package]]
name = "importlib-metadata"
version = "8.7.0"
version = "8.7.1"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"},
{file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"},
{file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"},
{file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"},
]
[package.dependencies]
@@ -668,10 +720,102 @@ zipp = ">=3.20"
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
enabler = ["pytest-enabler (>=2.2)"]
enabler = ["pytest-enabler (>=3.4)"]
perf = ["ipython"]
test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"]
type = ["pytest-mypy"]
test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"]
type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"]
[[package]]
name = "jsonschema"
version = "4.25.1"
description = "An implementation of JSON Schema validation for Python"
optional = true
python-versions = ">=3.9"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63"},
{file = "jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85"},
]
[package.dependencies]
attrs = ">=22.2.0"
jsonschema-specifications = ">=2023.03.6"
referencing = ">=0.28.4"
rpds-py = ">=0.7.1"
[package.extras]
format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"]
format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"]
[[package]]
name = "jsonschema-specifications"
version = "2025.9.1"
description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry"
optional = true
python-versions = ">=3.9"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"},
{file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"},
]
[package.dependencies]
referencing = ">=0.31.0"
[[package]]
name = "jupyter-core"
version = "5.9.1"
description = "Jupyter core package. A base package on which Jupyter projects rely."
optional = true
python-versions = ">=3.10"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407"},
{file = "jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508"},
]
[package.dependencies]
platformdirs = ">=2.5"
traitlets = ">=5.3"
[package.extras]
docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-spelling", "traitlets"]
test = ["ipykernel", "pre-commit", "pytest (<9)", "pytest-cov", "pytest-timeout"]
[[package]]
name = "llvmlite"
version = "0.46.0"
description = "lightweight wrapper around basic LLVM functionality"
optional = true
python-versions = ">=3.10"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "llvmlite-0.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4323177e936d61ae0f73e653e2e614284d97d14d5dd12579adc92b6c2b0597b0"},
{file = "llvmlite-0.46.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a2d461cb89537b7c20feb04c46c32e12d5ad4f0896c9dfc0f60336219ff248e"},
{file = "llvmlite-0.46.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1f6595a35b7b39c3518b85a28bf18f45e075264e4b2dce3f0c2a4f232b4a910"},
{file = "llvmlite-0.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:e7a34d4aa6f9a97ee006b504be6d2b8cb7f755b80ab2f344dda1ef992f828559"},
{file = "llvmlite-0.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82f3d39b16f19aa1a56d5fe625883a6ab600d5cc9ea8906cca70ce94cabba067"},
{file = "llvmlite-0.46.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3df43900119803bbc52720e758c76f316a9a0f34612a886862dfe0a5591a17e"},
{file = "llvmlite-0.46.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de183fefc8022d21b0aa37fc3e90410bc3524aed8617f0ff76732fc6c3af5361"},
{file = "llvmlite-0.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:e8b10bc585c58bdffec9e0c309bb7d51be1f2f15e169a4b4d42f2389e431eb93"},
{file = "llvmlite-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9588ad4c63b4f0175a3984b85494f0c927c6b001e3a246a3a7fb3920d9a137"},
{file = "llvmlite-0.46.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3535bd2bb6a2d7ae4012681ac228e5132cdb75fefb1bcb24e33f2f3e0c865ed4"},
{file = "llvmlite-0.46.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cbfd366e60ff87ea6cc62f50bc4cd800ebb13ed4c149466f50cf2163a473d1e"},
{file = "llvmlite-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:398b39db462c39563a97b912d4f2866cd37cba60537975a09679b28fbbc0fb38"},
{file = "llvmlite-0.46.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:30b60892d034bc560e0ec6654737aaa74e5ca327bd8114d82136aa071d611172"},
{file = "llvmlite-0.46.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6cc19b051753368a9c9f31dc041299059ee91aceec81bd57b0e385e5d5bf1a54"},
{file = "llvmlite-0.46.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bca185892908f9ede48c0acd547fe4dc1bafefb8a4967d47db6cf664f9332d12"},
{file = "llvmlite-0.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:67438fd30e12349ebb054d86a5a1a57fd5e87d264d2451bcfafbbbaa25b82a35"},
{file = "llvmlite-0.46.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:d252edfb9f4ac1fcf20652258e3f102b26b03eef738dc8a6ffdab7d7d341d547"},
{file = "llvmlite-0.46.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:379fdd1c59badeff8982cb47e4694a6143bec3bb49aa10a466e095410522064d"},
{file = "llvmlite-0.46.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e8cbfff7f6db0fa2c771ad24154e2a7e457c2444d7673e6de06b8b698c3b269"},
{file = "llvmlite-0.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:7821eda3ec1f18050f981819756631d60b6d7ab1a6cf806d9efefbe3f4082d61"},
{file = "llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb"},
]
[[package]]
name = "multidict"
@@ -832,6 +976,91 @@ files = [
[package.dependencies]
typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""}
[[package]]
name = "narwhals"
version = "2.14.0"
description = "Extremely lightweight compatibility layer between dataframe libraries"
optional = true
python-versions = ">=3.9"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "narwhals-2.14.0-py3-none-any.whl", hash = "sha256:b56796c9a00179bd757d15282c540024e1d5c910b19b8c9944d836566c030acf"},
{file = "narwhals-2.14.0.tar.gz", hash = "sha256:98be155c3599db4d5c211e565c3190c398c87e7bf5b3cdb157dece67641946e0"},
]
[package.extras]
cudf = ["cudf (>=24.10.0)"]
dask = ["dask[dataframe] (>=2024.8)"]
duckdb = ["duckdb (>=1.1)"]
ibis = ["ibis-framework (>=6.0.0)", "packaging", "pyarrow-hotfix", "rich"]
modin = ["modin"]
pandas = ["pandas (>=1.1.3)"]
polars = ["polars (>=0.20.4)"]
pyarrow = ["pyarrow (>=13.0.0)"]
pyspark = ["pyspark (>=3.5.0)"]
pyspark-connect = ["pyspark[connect] (>=3.5.0)"]
sqlframe = ["sqlframe (>=3.22.0,!=3.39.3)"]
[[package]]
name = "nbformat"
version = "5.10.4"
description = "The Jupyter Notebook format"
optional = true
python-versions = ">=3.8"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"},
{file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"},
]
[package.dependencies]
fastjsonschema = ">=2.15"
jsonschema = ">=2.6"
jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
traitlets = ">=5.1"
[package.extras]
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"]
test = ["pep440", "pre-commit", "pytest", "testpath"]
[[package]]
name = "numba"
version = "0.63.1"
description = "compiling Python code using LLVM"
optional = true
python-versions = ">=3.10"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "numba-0.63.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6d6bf5bf00f7db629305caaec82a2ffb8abe2bf45eaad0d0738dc7de4113779"},
{file = "numba-0.63.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08653d0dfc9cc9c4c9a8fba29ceb1f2d5340c3b86c4a7e5e07e42b643bc6a2f4"},
{file = "numba-0.63.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09eebf5650246ce2a4e9a8d38270e2d4b0b0ae978103bafb38ed7adc5ea906e"},
{file = "numba-0.63.1-cp310-cp310-win_amd64.whl", hash = "sha256:f8bba17421d865d8c0f7be2142754ebce53e009daba41c44cf6909207d1a8d7d"},
{file = "numba-0.63.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b33db00f18ccc790ee9911ce03fcdfe9d5124637d1ecc266f5ae0df06e02fec3"},
{file = "numba-0.63.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d31ea186a78a7c0f6b1b2a3fe68057fdb291b045c52d86232b5383b6cf4fc25"},
{file = "numba-0.63.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed3bb2fbdb651d6aac394388130a7001aab6f4541837123a4b4ab8b02716530c"},
{file = "numba-0.63.1-cp311-cp311-win_amd64.whl", hash = "sha256:1ecbff7688f044b1601be70113e2fb1835367ee0b28ffa8f3adf3a05418c5c87"},
{file = "numba-0.63.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2819cd52afa5d8d04e057bdfd54367575105f8829350d8fb5e4066fb7591cc71"},
{file = "numba-0.63.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5cfd45dbd3d409e713b1ccfdc2ee72ca82006860254429f4ef01867fdba5845f"},
{file = "numba-0.63.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a599df6976c03b7ecf15d05302696f79f7e6d10d620367407517943355bcb0"},
{file = "numba-0.63.1-cp312-cp312-win_amd64.whl", hash = "sha256:bbad8c63e4fc7eb3cdb2c2da52178e180419f7969f9a685f283b313a70b92af3"},
{file = "numba-0.63.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:0bd4fd820ef7442dcc07da184c3f54bb41d2bdb7b35bacf3448e73d081f730dc"},
{file = "numba-0.63.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53de693abe4be3bd4dee38e1c55f01c55ff644a6a3696a3670589e6e4c39cde2"},
{file = "numba-0.63.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81227821a72a763c3d4ac290abbb4371d855b59fdf85d5af22a47c0e86bf8c7e"},
{file = "numba-0.63.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb227b07c2ac37b09432a9bda5142047a2d1055646e089d4a240a2643e508102"},
{file = "numba-0.63.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f180883e5508940cc83de8a8bea37fc6dd20fbe4e5558d4659b8b9bef5ff4731"},
{file = "numba-0.63.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0938764afa82a47c0e895637a6c55547a42c9e1d35cac42285b1fa60a8b02bb"},
{file = "numba-0.63.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f90a929fa5094e062d4e0368ede1f4497d5e40f800e80aa5222c4734236a2894"},
{file = "numba-0.63.1-cp314-cp314-win_amd64.whl", hash = "sha256:8d6d5ce85f572ed4e1a135dbb8c0114538f9dd0e3657eeb0bb64ab204cbe2a8f"},
{file = "numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b"},
]
[package.dependencies]
llvmlite = "==0.46.*"
numpy = ">=1.22,<2.4"
[[package]]
name = "numpy"
version = "2.2.6"
@@ -983,6 +1212,28 @@ files = [
{file = "numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0"},
]
[[package]]
name = "openbb-charting"
version = "2.4.0"
description = "Charting extension for OpenBB"
optional = true
python-versions = "<3.14,>=3.9.21"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "openbb_charting-2.4.0-py3-none-any.whl", hash = "sha256:137f130094bd20b822a8af8355b7e9d2c3f9a1373d5495800f8e26b6bbf33120"},
{file = "openbb_charting-2.4.0.tar.gz", hash = "sha256:522c8f8a039562dd7d25c675a399956969959e2e157cab7ba9e720e6431ab4ec"},
]
[package.dependencies]
nbformat = ">=5.10.0,<6.0.0"
openbb-core = ">=1.5.1,<2.0.0"
pandas-ta-openbb = ">=0.4.22,<0.5.0"
plotly = ">=6.3.1,<7.0.0"
[package.extras]
pywry = ["pywry (>=0.6.2,<0.7.0)"]
[[package]]
name = "openbb-core"
version = "1.5.6"
@@ -1011,6 +1262,69 @@ uuid7 = ">=0.1.0,<0.2.0"
uvicorn = ">=0.35.0,<0.36.0"
websockets = ">=15.0,<16.0"
[[package]]
name = "openbb-economy"
version = "1.5.0"
description = "Economy extension for OpenBB"
optional = false
python-versions = "<3.14,>=3.9.21"
groups = ["main"]
files = [
{file = "openbb_economy-1.5.0-py3-none-any.whl", hash = "sha256:cad012ed440faa8d286575dc846444eaf54f5bf95505cc4617f88d46b40d4bb4"},
{file = "openbb_economy-1.5.0.tar.gz", hash = "sha256:1b17c0316079ab3f3303979ecb26064bc44527cf9a2bb40c401d94d6081c165c"},
]
[package.dependencies]
openbb-core = ">=1.5.1,<2.0.0"
[[package]]
name = "openbb-platform-api"
version = "1.2.2"
description = "OpenBB Platform API: Launch script and widgets builder for the Open Data Platform REST API and Workspace Backend Connector."
optional = false
python-versions = "<3.14,>=3.10"
groups = ["main"]
files = [
{file = "openbb_platform_api-1.2.2-py3-none-any.whl", hash = "sha256:df5d8db50794d2d6e0d58f3496075df28e5280e8dace55704927eec86196d6bc"},
{file = "openbb_platform_api-1.2.2.tar.gz", hash = "sha256:a2c50a650ac47ace5db6c4b1e1a7de93dce49cffc2898e3cb8d8e6503ce8abb6"},
]
[package.dependencies]
deepdiff = ">=8.6.1"
openbb-core = ">=1.5.6,<2.0.0"
[[package]]
name = "orderly-set"
version = "5.5.0"
description = "Orderly set"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7"},
{file = "orderly_set-5.5.0.tar.gz", hash = "sha256:e87185c8e4d8afa64e7f8160ee2c542a475b738bc891dc3f58102e654125e6ce"},
]
[package.extras]
coverage = ["coverage (>=7.6.0,<7.7.0)"]
dev = ["bump2version (>=1.0.0,<1.1.0)", "ipdb (>=0.13.0,<0.14.0)"]
optimize = ["orjson"]
static = ["flake8 (>=7.1.0,<7.2.0)", "flake8-pyproject (>=1.2.3,<1.3.0)"]
test = ["pytest (>=8.3.0,<8.4.0)", "pytest-benchmark (>=5.1.0,<5.2.0)", "pytest-cov (>=6.0.0,<6.1.0)", "python-dotenv (>=1.0.0,<1.1.0)"]
[[package]]
name = "packaging"
version = "25.0"
description = "Core utilities for Python packages"
optional = true
python-versions = ">=3.8"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
]
[[package]]
name = "pandas"
version = "2.3.3"
@@ -1111,6 +1425,88 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d
test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"]
xml = ["lxml (>=4.9.2)"]
[[package]]
name = "pandas-ta-openbb"
version = "0.4.22"
description = "Fork of pandas-ta for use with NumPy 2 and OpenBB."
optional = true
python-versions = "<3.14,>=3.9.21"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "pandas_ta_openbb-0.4.22-py3-none-any.whl", hash = "sha256:ea357cc014952fbb0a2d95edfceffec0a5d8659c02eb2468ab6e11bacfbd5706"},
{file = "pandas_ta_openbb-0.4.22.tar.gz", hash = "sha256:fdea8000b54c929c904e09ed703171e597d0d57af1cc7318afcff9cb717e8745"},
]
[package.dependencies]
numba = {version = ">=0.61.0", markers = "python_version >= \"3.10\""}
numpy = ">=1.26.4"
pandas = ">=2.2.0"
scipy = "<=1.15.3"
statsmodels = ">=0.14.0,<0.15.0"
[[package]]
name = "patsy"
version = "1.0.2"
description = "A Python package for describing statistical models and for building design matrices."
optional = true
python-versions = ">=3.6"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "patsy-1.0.2-py2.py3-none-any.whl", hash = "sha256:37bfddbc58fcf0362febb5f54f10743f8b21dd2aa73dec7e7ef59d1b02ae668a"},
{file = "patsy-1.0.2.tar.gz", hash = "sha256:cdc995455f6233e90e22de72c37fcadb344e7586fb83f06696f54d92f8ce74c0"},
]
[package.dependencies]
numpy = ">=1.4"
[package.extras]
test = ["pytest", "pytest-cov", "scipy"]
[[package]]
name = "platformdirs"
version = "4.5.1"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = true
python-versions = ">=3.10"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"},
{file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"},
]
[package.extras]
docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"]
type = ["mypy (>=1.18.2)"]
[[package]]
name = "plotly"
version = "6.5.0"
description = "An open-source interactive data visualization library for Python"
optional = true
python-versions = ">=3.8"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "plotly-6.5.0-py3-none-any.whl", hash = "sha256:5ac851e100367735250206788a2b1325412aa4a4917a4fe3e6f0bc5aa6f3d90a"},
{file = "plotly-6.5.0.tar.gz", hash = "sha256:d5d38224883fd38c1409bef7d6a8dc32b74348d39313f3c52ca998b8e447f5c8"},
]
[package.dependencies]
narwhals = ">=1.15.1"
packaging = "*"
[package.extras]
dev = ["plotly[dev-optional]"]
dev-build = ["build", "jupyter", "plotly[dev-core]"]
dev-core = ["pytest", "requests", "ruff (==0.11.12)"]
dev-optional = ["anywidget", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "numpy", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "plotly[dev-build]", "plotly[kaleido]", "polars[timezone]", "pyarrow", "pyshp", "pytz", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"]
express = ["numpy"]
kaleido = ["kaleido (>=1.1.0)"]
[[package]]
name = "propcache"
version = "0.4.1"
@@ -1471,6 +1867,24 @@ files = [
{file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"},
]
[[package]]
name = "referencing"
version = "0.37.0"
description = "JSON Referencing + Python"
optional = true
python-versions = ">=3.10"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"},
{file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"},
]
[package.dependencies]
attrs = ">=22.2.0"
rpds-py = ">=0.7.0"
typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""}
[[package]]
name = "requests"
version = "2.32.5"
@@ -1493,6 +1907,132 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "rpds-py"
version = "0.30.0"
description = "Python bindings to Rust's persistent data structures (rpds)"
optional = true
python-versions = ">=3.10"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"},
{file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"},
{file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6"},
{file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7"},
{file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324"},
{file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df"},
{file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3"},
{file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221"},
{file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7"},
{file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff"},
{file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7"},
{file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139"},
{file = "rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464"},
{file = "rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169"},
{file = "rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425"},
{file = "rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d"},
{file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4"},
{file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f"},
{file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4"},
{file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97"},
{file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89"},
{file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d"},
{file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038"},
{file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7"},
{file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed"},
{file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85"},
{file = "rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c"},
{file = "rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825"},
{file = "rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229"},
{file = "rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad"},
{file = "rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05"},
{file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28"},
{file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd"},
{file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f"},
{file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1"},
{file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23"},
{file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6"},
{file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51"},
{file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5"},
{file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e"},
{file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394"},
{file = "rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf"},
{file = "rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b"},
{file = "rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e"},
{file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"},
{file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"},
{file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"},
{file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"},
{file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"},
{file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"},
{file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"},
{file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"},
{file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"},
{file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"},
{file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"},
{file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"},
{file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"},
{file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"},
{file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"},
{file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"},
{file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"},
{file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"},
{file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"},
{file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"},
{file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"},
{file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"},
{file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"},
{file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"},
{file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"},
{file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"},
{file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"},
{file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"},
{file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"},
{file = "rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0"},
{file = "rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be"},
{file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f"},
{file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f"},
{file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87"},
{file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18"},
{file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad"},
{file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07"},
{file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f"},
{file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65"},
{file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f"},
{file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53"},
{file = "rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed"},
{file = "rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950"},
{file = "rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6"},
{file = "rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb"},
{file = "rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8"},
{file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7"},
{file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898"},
{file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e"},
{file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419"},
{file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551"},
{file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8"},
{file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5"},
{file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404"},
{file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856"},
{file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40"},
{file = "rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0"},
{file = "rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3"},
{file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58"},
{file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a"},
{file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb"},
{file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c"},
{file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3"},
{file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5"},
{file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738"},
{file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f"},
{file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877"},
{file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a"},
{file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4"},
{file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"},
{file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"},
]
[[package]]
name = "ruff"
version = "0.13.3"
@@ -1522,6 +2062,71 @@ files = [
{file = "ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e"},
]
[[package]]
name = "scipy"
version = "1.15.3"
description = "Fundamental algorithms for scientific computing in Python"
optional = true
python-versions = ">=3.10"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c"},
{file = "scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253"},
{file = "scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f"},
{file = "scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92"},
{file = "scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82"},
{file = "scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40"},
{file = "scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e"},
{file = "scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c"},
{file = "scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13"},
{file = "scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b"},
{file = "scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba"},
{file = "scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65"},
{file = "scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1"},
{file = "scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889"},
{file = "scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982"},
{file = "scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9"},
{file = "scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594"},
{file = "scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb"},
{file = "scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019"},
{file = "scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6"},
{file = "scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477"},
{file = "scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c"},
{file = "scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45"},
{file = "scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49"},
{file = "scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e"},
{file = "scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539"},
{file = "scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed"},
{file = "scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759"},
{file = "scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62"},
{file = "scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb"},
{file = "scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730"},
{file = "scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825"},
{file = "scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7"},
{file = "scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11"},
{file = "scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126"},
{file = "scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163"},
{file = "scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8"},
{file = "scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5"},
{file = "scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e"},
{file = "scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb"},
{file = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723"},
{file = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb"},
{file = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4"},
{file = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5"},
{file = "scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca"},
{file = "scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf"},
]
[package.dependencies]
numpy = ">=1.23.5,<2.5"
[package.extras]
dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"]
doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"]
test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja ; sys_platform != \"emscripten\"", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
[[package]]
name = "six"
version = "1.17.0"
@@ -1553,6 +2158,82 @@ typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""
[package.extras]
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
[[package]]
name = "statsmodels"
version = "0.14.6"
description = "Statistical computations and models for Python"
optional = true
python-versions = ">=3.9"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "statsmodels-0.14.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4ff0649a2df674c7ffb6fa1a06bffdb82a6adf09a48e90e000a15a6aaa734b0"},
{file = "statsmodels-0.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:109012088b3e370080846ab053c76d125268631410142daad2f8c10770e8e8d9"},
{file = "statsmodels-0.14.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93bd5d220f3cb6fc5fc1bffd5b094966cab8ee99f6c57c02e95710513d6ac3f"},
{file = "statsmodels-0.14.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06eec42d682fdb09fe5d70a05930857efb141754ec5a5056a03304c1b5e32fd9"},
{file = "statsmodels-0.14.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0444e88557df735eda7db330806fe09d51c9f888bb1f5906cb3a61fb1a3ed4a8"},
{file = "statsmodels-0.14.6-cp310-cp310-win_amd64.whl", hash = "sha256:e83a9abe653835da3b37fb6ae04b45480c1de11b3134bd40b09717192a1456ea"},
{file = "statsmodels-0.14.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ad5c2810fc6c684254a7792bf1cbaf1606cdee2a253f8bd259c43135d87cfb4"},
{file = "statsmodels-0.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:341fa68a7403e10a95c7b6e41134b0da3a7b835ecff1eb266294408535a06eb6"},
{file = "statsmodels-0.14.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdf1dfe2a3ca56f5529118baf33a13efed2783c528f4a36409b46bbd2d9d48eb"},
{file = "statsmodels-0.14.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3764ba8195c9baf0925a96da0743ff218067a269f01d155ca3558deed2658ca"},
{file = "statsmodels-0.14.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e8d2e519852adb1b420e018f5ac6e6684b2b877478adf7fda2cfdb58f5acb5d"},
{file = "statsmodels-0.14.6-cp311-cp311-win_amd64.whl", hash = "sha256:2738a00fca51196f5a7d44b06970ace6b8b30289839e4808d656f8a98e35faa7"},
{file = "statsmodels-0.14.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe76140ae7adc5ff0e60a3f0d56f4fffef484efa803c3efebf2fcd734d72ecb5"},
{file = "statsmodels-0.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26d4f0ed3b31f3c86f83a92f5c1f5cbe63fc992cd8915daf28ca49be14463a1c"},
{file = "statsmodels-0.14.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c00a42863e4f4733ac9d078bbfad816249c01451740e6f5053ecc7db6d6368"},
{file = "statsmodels-0.14.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b58cf7474aa9e7e3b0771a66537148b2df9b5884fbf156096c0e6c1ff0469d"},
{file = "statsmodels-0.14.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81e7dcc5e9587f2567e52deaff5220b175bf2f648951549eae5fc9383b62bc37"},
{file = "statsmodels-0.14.6-cp312-cp312-win_amd64.whl", hash = "sha256:b5eb07acd115aa6208b4058211138393a7e6c2cf12b6f213ede10f658f6a714f"},
{file = "statsmodels-0.14.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47ee7af083623d2091954fa71c7549b8443168f41b7c5dce66510274c50fd73e"},
{file = "statsmodels-0.14.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa60d82e29fcd0a736e86feb63a11d2380322d77a9369a54be8b0965a3985f71"},
{file = "statsmodels-0.14.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89ee7d595f5939cc20bf946faedcb5137d975f03ae080f300ebb4398f16a5bd4"},
{file = "statsmodels-0.14.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:730f3297b26749b216a06e4327fe0be59b8d05f7d594fb6caff4287b69654589"},
{file = "statsmodels-0.14.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f1c08befa85e93acc992b72a390ddb7bd876190f1360e61d10cf43833463bc9c"},
{file = "statsmodels-0.14.6-cp313-cp313-win_amd64.whl", hash = "sha256:8021271a79f35b842c02a1794465a651a9d06ec2080f76ebc3b7adce77d08233"},
{file = "statsmodels-0.14.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:00781869991f8f02ad3610da6627fd26ebe262210287beb59761982a8fa88cae"},
{file = "statsmodels-0.14.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:73f305fbf31607b35ce919fae636ab8b80d175328ed38fdc6f354e813b86ee37"},
{file = "statsmodels-0.14.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e443e7077a6e2d3faeea72f5a92c9f12c63722686eb80bb40a0f04e4a7e267ad"},
{file = "statsmodels-0.14.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3414e40c073d725007a6603a18247ab7af3467e1af4a5e5a24e4c27bc26673b4"},
{file = "statsmodels-0.14.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a518d3f9889ef920116f9fa56d0338069e110f823926356946dae83bc9e33e19"},
{file = "statsmodels-0.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:151b73e29f01fe619dbce7f66d61a356e9d1fe5e906529b78807df9189c37721"},
{file = "statsmodels-0.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4d0c1b0f9f6915619e2a0d3853e5763d4d66876892ad352e7d7b93a737556978"},
{file = "statsmodels-0.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e0fc891d6358bf376cc0ae1fee10a650478172ae9ba359daba1785fc496cd1a"},
{file = "statsmodels-0.14.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f52ef0f0b63b8fd11e1ef1c2a1e73a410720b8715c9a83a26d733b6815597fe"},
{file = "statsmodels-0.14.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b328eafa86a2a67303fdb1d25677d15b70cd2a5229aabec7670ec5ea840f1375"},
{file = "statsmodels-0.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:3bef39f8587754f2d644b2e831e102fa08ace9a5a1af4b583b122e6fd3e083ab"},
{file = "statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a"},
]
[package.dependencies]
numpy = ">=1.22.3,<3"
packaging = ">=21.3"
pandas = ">=1.4,<2.1.0 || >2.1.0"
patsy = ">=0.5.6"
scipy = ">=1.8,<1.9.2 || >1.9.2"
[package.extras]
build = ["cython (>=3.0.10)"]
develop = ["colorama", "cython (>=3.0.10)", "cython (>=3.0.10,<4)", "flake8", "isort", "jinja2", "joblib", "matplotlib (>=3)", "pytest (>=7.3.0,<8)", "pytest-cov", "pytest-randomly", "pytest-xdist", "pywinpty ; os_name == \"nt\"", "setuptools_scm[toml] (>=8.0,<9.0)"]
docs = ["ipykernel", "jupyter_client", "matplotlib", "nbconvert", "nbformat", "numpydoc", "pandas-datareader", "sphinx"]
[[package]]
name = "traitlets"
version = "5.14.3"
description = "Traitlets Python configuration system"
optional = true
python-versions = ">=3.8"
groups = ["main"]
markers = "extra == \"charting\""
files = [
{file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"},
{file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"},
]
[package.extras]
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"]
[[package]]
name = "typing-extensions"
version = "4.15.0"
@@ -1582,26 +2263,26 @@ typing-extensions = ">=4.12.0"
[[package]]
name = "tzdata"
version = "2025.2"
version = "2025.3"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
groups = ["main"]
files = [
{file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"},
{file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"},
{file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"},
{file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"},
]
[[package]]
name = "urllib3"
version = "2.6.1"
version = "2.6.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b"},
{file = "urllib3-2.6.1.tar.gz", hash = "sha256:5379eb6e1aba4088bae84f8242960017ec8d8e3decf30480b3a1abdaa9671a3f"},
{file = "urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd"},
{file = "urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797"},
]
[package.extras]
@@ -1898,7 +2579,10 @@ enabler = ["pytest-enabler (>=2.2)"]
test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
type = ["pytest-mypy"]
[extras]
charting = ["openbb-charting"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<3.14"
content-hash = "019b3af81f6f82b3a0d73b0347724270aff7e2ad5227a044158e1c6fca53d6f1"
content-hash = "d35085892579a6561a545db388f78970463f6f9dc71d8fedcd91fe045cfa0545"

View File

@@ -9,8 +9,15 @@ packages = [{ include = "openbb_imf" }]
[tool.poetry.dependencies]
python = ">=3.10,<3.14"
openbb-core = "^1.5.1"
openbb-core = "^1.5.6"
openbb-platform-api = "^1.2.2"
openbb-economy = "^1.5.0"
openbb-charting = { version = "^2.4.0", optional = true }
async-lru = "^2"
defusedxml = ">=0.7.1"
[tool.poetry.extras]
charting = ["openbb-charting"]
[build-system]
requires = ["poetry-core"]
@@ -18,3 +25,6 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry.plugins."openbb_provider_extension"]
imf = "openbb_imf:imf_provider"
[tool.poetry.plugins."openbb_core_extension"]
imf_utils = "openbb_imf.imf_router:router"

View File

@@ -0,0 +1,351 @@
interactions:
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate, br, zstd
Connection:
- keep-alive
method: GET
uri: https://api.imf.org/external/sdmx/3.0/availability/dataflow/IMF.STA/CPI/%2B/*.*.*.*.*/COUNTRY
response:
body:
string: '{"data":{"dataConstraints":[{"links":[{"urn":"urn:sdmx:org.sdmx.infomodel.registry.DataConstraint=IMF.STA:CPI(5.0.0)","type":"dataconstraint","rel":"self"}],"annotations":[{"id":"series_count","title":"31587","type":"sdmx_metrics"},{"id":"time_period_start","title":"1900-01-01","type":"sdmx_metrics"},{"id":"time_period_end","title":"2026-01-01","type":"sdmx_metrics"}],"id":"CPI","name":"Data
Constraint","names":{"en":"Data Constraint"},"description":"Data Availability
Constraint","descriptions":{"en":"Data Availability Constraint"},"version":"5.0.0","agencyID":"IMF.STA","role":"Actual","constraintAttachment":{"dataStructures":["urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_CPI(5.0.0)"]},"cubeRegions":[{"include":true,"components":[{"id":"COUNTRY","include":false,"removePrefix":false,"values":[{"value":"ABW"},{"value":"AFG"},{"value":"AGO"},{"value":"AIA"},{"value":"ALB"},{"value":"ANT"},{"value":"ARE"},{"value":"ARG"},{"value":"ARM"},{"value":"ATG"},{"value":"AUS"},{"value":"AUT"},{"value":"AZE"},{"value":"BDI"},{"value":"BEL"},{"value":"BEN"},{"value":"BFA"},{"value":"BGD"},{"value":"BGR"},{"value":"BHR"},{"value":"BHS"},{"value":"BIH"},{"value":"BLR"},{"value":"BLZ"},{"value":"BOL"},{"value":"BRA"},{"value":"BRB"},{"value":"BRN"},{"value":"BTN"},{"value":"BWA"},{"value":"CAF"},{"value":"CAN"},{"value":"CHE"},{"value":"CHL"},{"value":"CHN"},{"value":"CIV"},{"value":"CMR"},{"value":"COD"},{"value":"COG"},{"value":"COL"},{"value":"COM"},{"value":"CPV"},{"value":"CRI"},{"value":"CUW"},{"value":"CYM"},{"value":"CYP"},{"value":"CZE"},{"value":"DEU"},{"value":"DJI"},{"value":"DMA"},{"value":"DNK"},{"value":"DOM"},{"value":"DZA"},{"value":"ECU"},{"value":"EGY"},{"value":"ESP"},{"value":"EST"},{"value":"ETH"},{"value":"FIN"},{"value":"FJI"},{"value":"FRA"},{"value":"FSM"},{"value":"G163"},{"value":"GAB"},{"value":"GBR"},{"value":"GEO"},{"value":"GHA"},{"value":"GIN"},{"value":"GLP"},{"value":"GMB"},{"value":"GNB"},{"value":"GNQ"},{"value":"GRC"},{"value":"GRD"},{"value":"GRL"},{"value":"GTM"},{"value":"GUY"},{"value":"HKG"},{"value":"HND"},{"value":"HRV"},{"value":"HTI"},{"value":"HUN"},{"value":"IDN"},{"value":"IND"},{"value":"IRL"},{"value":"IRN"},{"value":"IRQ"},{"value":"ISL"},{"value":"ISR"},{"value":"ITA"},{"value":"JAM"},{"value":"JOR"},{"value":"JPN"},{"value":"KAZ"},{"value":"KEN"},{"value":"KGZ"},{"value":"KHM"},{"value":"KIR"},{"value":"KNA"},{"value":"KOR"},{"value":"KOS"},{"value":"KWT"},{"value":"LAO"},{"value":"LBN"},{"value":"LBR"},{"value":"LBY"},{"value":"LCA"},{"value":"LKA"},{"value":"LSO"},{"value":"LTU"},{"value":"LUX"},{"value":"LVA"},{"value":"MAC"},{"value":"MAR"},{"value":"MDA"},{"value":"MDG"},{"value":"MDV"},{"value":"MEX"},{"value":"MKD"},{"value":"MLI"},{"value":"MLT"},{"value":"MMR"},{"value":"MNE"},{"value":"MNG"},{"value":"MOZ"},{"value":"MRT"},{"value":"MSR"},{"value":"MTQ"},{"value":"MUS"},{"value":"MWI"},{"value":"MYS"},{"value":"NAM"},{"value":"NCL"},{"value":"NER"},{"value":"NGA"},{"value":"NIC"},{"value":"NLD"},{"value":"NOR"},{"value":"NPL"},{"value":"NRU"},{"value":"NZL"},{"value":"OMN"},{"value":"PAK"},{"value":"PAN"},{"value":"PER"},{"value":"PHL"},{"value":"PLW"},{"value":"PNG"},{"value":"POL"},{"value":"PRT"},{"value":"PRY"},{"value":"QAT"},{"value":"ROU"},{"value":"RUS"},{"value":"RWA"},{"value":"SAU"},{"value":"SDN"},{"value":"SEN"},{"value":"SGP"},{"value":"SLB"},{"value":"SLE"},{"value":"SLV"},{"value":"SMR"},{"value":"SOM"},{"value":"SRB"},{"value":"SSD"},{"value":"STP"},{"value":"SUR"},{"value":"SVK"},{"value":"SVN"},{"value":"SWE"},{"value":"SWZ"},{"value":"SXM"},{"value":"SYC"},{"value":"SYR"},{"value":"TCD"},{"value":"TGO"},{"value":"THA"},{"value":"TJK"},{"value":"TLS"},{"value":"TON"},{"value":"TTO"},{"value":"TUN"},{"value":"TUR"},{"value":"TUV"},{"value":"TZA"},{"value":"UGA"},{"value":"UKR"},{"value":"URY"},{"value":"USA"},{"value":"UZB"},{"value":"VCT"},{"value":"VEN"},{"value":"VGB"},{"value":"VNM"},{"value":"VUT"},{"value":"WBG"},{"value":"WSM"},{"value":"YEM"},{"value":"ZAF"},{"value":"ZMB"},{"value":"ZWE"}]}],"keyValues":[],"annotations":[]}]}]},"meta":{"schema":"https://raw.githubusercontent.com/sdmx-twg/sdmx-json/master/metadata-message/tools/schemas/2.0.0/sdmx-json-metadata-schema.json","id":"IDREF5550","test":false,"prepared":"2025-12-16T05:14:38.550091525Z","contentLanguages":["en"],"sender":{"id":"unknown"}}}'
headers:
Connection:
- keep-alive
Content-Type:
- application/json
Date:
- Tue, 16 Dec 2025 05:14:38 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
Transfer-Encoding:
- chunked
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
content-encoding:
- gzip
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
vary:
- accept-encoding
x-content-type-options:
- nosniff
x-correlation-id:
- b2dfcb0d3595626dc6220e68ec7f1319
x-envoy-upstream-service-time:
- '61'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate, br, zstd
Connection:
- keep-alive
method: GET
uri: https://api.imf.org/external/sdmx/3.0/availability/dataflow/IMF.STA/CPI/%2B/JPN.*.*.*.*/INDEX_TYPE
response:
body:
string: '{"data":{"dataConstraints":[{"links":[{"urn":"urn:sdmx:org.sdmx.infomodel.registry.DataConstraint=IMF.STA:CPI(5.0.0)","type":"dataconstraint","rel":"self"}],"annotations":[{"id":"series_count","title":"78","type":"sdmx_metrics"},{"id":"time_period_start","title":"1900-01-01","type":"sdmx_metrics"},{"id":"time_period_end","title":"2025-11-01","type":"sdmx_metrics"}],"id":"CPI","name":"Data
Constraint","names":{"en":"Data Constraint"},"description":"Data Availability
Constraint","descriptions":{"en":"Data Availability Constraint"},"version":"5.0.0","agencyID":"IMF.STA","role":"Actual","constraintAttachment":{"dataStructures":["urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_CPI(5.0.0)"]},"cubeRegions":[{"include":true,"components":[{"id":"INDEX_TYPE","include":false,"removePrefix":false,"values":[{"value":"CPI"}]}],"keyValues":[],"annotations":[]}]}]},"meta":{"schema":"https://raw.githubusercontent.com/sdmx-twg/sdmx-json/master/metadata-message/tools/schemas/2.0.0/sdmx-json-metadata-schema.json","id":"IDREF9874","test":false,"prepared":"2025-12-16T05:14:39.238805530Z","contentLanguages":["en"],"sender":{"id":"unknown"}}}'
headers:
Connection:
- keep-alive
Content-Length:
- '1156'
Content-Type:
- application/json
Date:
- Tue, 16 Dec 2025 05:14:39 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
x-content-type-options:
- nosniff
x-correlation-id:
- 64e5e0c667fa9ccfb403fc4fe406c32f
x-envoy-upstream-service-time:
- '62'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate, br, zstd
Connection:
- keep-alive
method: GET
uri: https://api.imf.org/external/sdmx/3.0/availability/dataflow/IMF.STA/CPI/%2B/JPN.CPI.*.*.*/COICOP_1999
response:
body:
string: '{"data":{"dataConstraints":[{"links":[{"urn":"urn:sdmx:org.sdmx.infomodel.registry.DataConstraint=IMF.STA:CPI(5.0.0)","type":"dataconstraint","rel":"self"}],"annotations":[{"id":"series_count","title":"78","type":"sdmx_metrics"},{"id":"time_period_start","title":"1900-01-01","type":"sdmx_metrics"},{"id":"time_period_end","title":"2025-11-01","type":"sdmx_metrics"}],"id":"CPI","name":"Data
Constraint","names":{"en":"Data Constraint"},"description":"Data Availability
Constraint","descriptions":{"en":"Data Availability Constraint"},"version":"5.0.0","agencyID":"IMF.STA","role":"Actual","constraintAttachment":{"dataStructures":["urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_CPI(5.0.0)"]},"cubeRegions":[{"include":true,"components":[{"id":"COICOP_1999","include":false,"removePrefix":false,"values":[{"value":"CP01"},{"value":"CP02"},{"value":"CP03"},{"value":"CP04"},{"value":"CP05"},{"value":"CP06"},{"value":"CP07"},{"value":"CP08"},{"value":"CP09"},{"value":"CP10"},{"value":"CP11"},{"value":"CP12"},{"value":"_T"}]}],"keyValues":[],"annotations":[]}]}]},"meta":{"schema":"https://raw.githubusercontent.com/sdmx-twg/sdmx-json/master/metadata-message/tools/schemas/2.0.0/sdmx-json-metadata-schema.json","id":"IDREF9866","test":false,"prepared":"2025-12-16T05:14:39.759048387Z","contentLanguages":["en"],"sender":{"id":"unknown"}}}'
headers:
Connection:
- keep-alive
Content-Length:
- '1360'
Content-Type:
- application/json
Date:
- Tue, 16 Dec 2025 05:14:39 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
x-content-type-options:
- nosniff
x-correlation-id:
- d22e42f0627ff27d2637c530ef4ef1f6
x-envoy-upstream-service-time:
- '83'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate, br, zstd
Connection:
- keep-alive
method: GET
uri: https://api.imf.org/external/sdmx/3.0/availability/dataflow/IMF.STA/CPI/%2B/JPN.CPI._T.*.*/TYPE_OF_TRANSFORMATION
response:
body:
string: '{"data":{"dataConstraints":[{"links":[{"urn":"urn:sdmx:org.sdmx.infomodel.registry.DataConstraint=IMF.STA:CPI(5.0.0)","type":"dataconstraint","rel":"self"}],"annotations":[{"id":"series_count","title":"18","type":"sdmx_metrics"},{"id":"time_period_start","title":"1900-01-01","type":"sdmx_metrics"},{"id":"time_period_end","title":"2025-11-01","type":"sdmx_metrics"}],"id":"CPI","name":"Data
Constraint","names":{"en":"Data Constraint"},"description":"Data Availability
Constraint","descriptions":{"en":"Data Availability Constraint"},"version":"5.0.0","agencyID":"IMF.STA","role":"Actual","constraintAttachment":{"dataStructures":["urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_CPI(5.0.0)"]},"cubeRegions":[{"include":true,"components":[{"id":"TYPE_OF_TRANSFORMATION","include":false,"removePrefix":false,"values":[{"value":"IX"},{"value":"POP_PCH_PA_PT"},{"value":"SRP_IX"},{"value":"SRP_POP_PCH_PA_PT"},{"value":"SRP_YOY_PCH_PA_PT"},{"value":"YOY_PCH_PA_PT"}]}],"keyValues":[],"annotations":[]}]}]},"meta":{"schema":"https://raw.githubusercontent.com/sdmx-twg/sdmx-json/master/metadata-message/tools/schemas/2.0.0/sdmx-json-metadata-schema.json","id":"IDREF1106","test":false,"prepared":"2025-12-16T05:14:40.523815481Z","contentLanguages":["en"],"sender":{"id":"unknown"}}}'
headers:
Connection:
- keep-alive
Content-Length:
- '1298'
Content-Type:
- application/json
Date:
- Tue, 16 Dec 2025 05:14:40 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
x-content-type-options:
- nosniff
x-correlation-id:
- 39c11ff5d858fb19896cd96f2f650a2f
x-envoy-upstream-service-time:
- '59'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate, br, zstd
Connection:
- keep-alive
method: GET
uri: https://api.imf.org/external/sdmx/3.0/availability/dataflow/IMF.STA/CPI/%2B/JPN.CPI._T.YOY_PCH_PA_PT.*/FREQUENCY
response:
body:
string: '{"data":{"dataConstraints":[{"links":[{"urn":"urn:sdmx:org.sdmx.infomodel.registry.DataConstraint=IMF.STA:CPI(5.0.0)","type":"dataconstraint","rel":"self"}],"annotations":[{"id":"series_count","title":"3","type":"sdmx_metrics"},{"id":"time_period_start","title":"1956-01-01","type":"sdmx_metrics"},{"id":"time_period_end","title":"2025-11-01","type":"sdmx_metrics"}],"id":"CPI","name":"Data
Constraint","names":{"en":"Data Constraint"},"description":"Data Availability
Constraint","descriptions":{"en":"Data Availability Constraint"},"version":"5.0.0","agencyID":"IMF.STA","role":"Actual","constraintAttachment":{"dataStructures":["urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_CPI(5.0.0)"]},"cubeRegions":[{"include":true,"components":[{"id":"FREQUENCY","include":false,"removePrefix":false,"values":[{"value":"A"},{"value":"M"},{"value":"Q"}]}],"keyValues":[],"annotations":[]}]}]},"meta":{"schema":"https://raw.githubusercontent.com/sdmx-twg/sdmx-json/master/metadata-message/tools/schemas/2.0.0/sdmx-json-metadata-schema.json","id":"IDREF1244","test":false,"prepared":"2025-12-16T05:14:41.017267863Z","contentLanguages":["en"],"sender":{"id":"unknown"}}}'
headers:
Connection:
- keep-alive
Content-Length:
- '1180'
Content-Type:
- application/json
Date:
- Tue, 16 Dec 2025 05:14:41 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
x-content-type-options:
- nosniff
x-correlation-id:
- 2789ca0ce9b0dad905aa5782956faccf
x-envoy-upstream-service-time:
- '68'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- application/xml
Accept-Encoding:
- gzip, deflate, br, zstd
Cache-Control:
- no-cache
Connection:
- keep-alive
method: GET
uri: https://api.imf.org/external/sdmx/3.0/data/dataflow/IMF.STA/CPI/+/JPN.CPI._T.YOY_PCH_PA_PT.Q?c%5BTIME_PERIOD%5D=ge:2024-01-01+le:2025-02-01&dimensionAtObservation=TIME_PERIOD&detail=full&includeHistory=false
response:
body:
string: <?xml version='1.0' encoding='UTF-8'?><message:StructureSpecificData
xmlns:metadata="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/metadata/generic"
xmlns:common="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/common" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:message="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/message"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xml="http://www.w3.org/XML/1998/namespace"
xmlns:ss="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/data/structurespecific"
xmlns:ns1="urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_CPI(5.0.0):ObsLevelDim:TIME_PERIOD"
xsi:schemaLocation="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/message
../../schemas/SDMXMessage.xsd"><message:Header><message:ID>IDREF8133</message:ID><message:Test>false</message:Test><message:Prepared>2025-12-16T05:14:41.552208978Z</message:Prepared><message:Sender
id="unknown"/><message:Structure structureID="IMF.STA.DSD_CPI.5.0.0" namespace="urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_CPI(5.0.0)"
dimensionAtObservation="TIME_PERIOD"><common:Structure>urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_CPI(5.0.0)</common:Structure></message:Structure></message:Header><message:DataSet
ss:structureRef="IMF.STA.DSD_CPI.5.0.0" action="Replace"><Series COUNTRY="JPN"
INDEX_TYPE="CPI" COICOP_1999="_T" TYPE_OF_TRANSFORMATION="YOY_PCH_PA_PT" FREQUENCY="Q"
IFS_FLAG="true" OVERLAP="OL" SCALE="0"><Obs OBS_VALUE="2.523155541360555"
DERIVATION_TYPE="O" TIME_PERIOD="2024-Q1"/><Obs OBS_VALUE="2.726696258719119"
DERIVATION_TYPE="O" TIME_PERIOD="2024-Q2"/><Obs OBS_VALUE="2.769037130270674"
DERIVATION_TYPE="O" TIME_PERIOD="2024-Q3"/><Obs OBS_VALUE="2.930174563591086"
DERIVATION_TYPE="O" TIME_PERIOD="2024-Q4"/></Series></message:DataSet></message:StructureSpecificData>
headers:
Connection:
- keep-alive
Content-Type:
- application/xml
Date:
- Tue, 16 Dec 2025 05:14:41 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
Transfer-Encoding:
- chunked
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
content-encoding:
- gzip
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
vary:
- accept-encoding
x-content-type-options:
- nosniff
x-correlation-id:
- 48c608bdcd7dd5c8f2f7617595341f19
x-envoy-upstream-service-time:
- '90'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK
version: 1

View File

@@ -5,50 +5,298 @@ interactions:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate
- gzip, deflate, br, zstd
Connection:
- keep-alive
method: GET
uri: http://dataservices.imf.org/REST/SDMX_JSON.svc/CompactData/DOT/A.US.TXG_FOB_USD.W00+B0?startPeriod=2020-01-01&endPeriod=2023-12-31
uri: https://api.imf.org/external/sdmx/3.0/availability/dataflow/IMF.STA/IMTS/%2B/*.*.*.*/COUNTRY
response:
body:
string: !!binary |
H4sIAAAAAAAEA6SSX6+aQBDFv4qhL216gWVBUJ5EwZZExPKnsfaBbGFUEgHDUrUx97t3Fm3uNU2T
m/Rxz57fmZmdvUqzpjqyvHNZxyT7Kk0u1aHm9oWXki3tu+5oq+r5fFbOutK0O5USoqnrYBHne6iY
XNa8Y3UO0tMLWLwB7P28tHkfs2hy1pVN/QjGbrDua7bAm59tDlwVUrBQbxRXTzQjagWcsx0MRK8c
m21hV/Ku/aXworr0/IO9j7ghyoUXPYZUgeNzaE8lllHKatuD+e1pVDdM3uoTmf1w4hX/Zx4M+Qys
gFYsxXcxKtcLTaNkLBcFMWVDHxGZWflQHm1Nczg2iAHWFqkEeIfuLTtwsZZVC0fWQoESJdSQkack
0Ua2MbIJQX8MdXGrMimFS5sJdckqEJXFd7APrN4hDzXa33Vw6fDgB3Pp+UmaNXWHv0dY08hHff/y
Ze7PiFACBzjumxrQ8HGgDd5jKx8GJtVlk1IiPWNQBDmUp9eNbDYbUcHFzcTQ9U+Ai3gliaKiv5pj
7L1ugW4O/94jNhNDWwIi36/SZB55XxB2UJ5E3jxzIs/BcxoLwV+6/sxJwgiVZP0pm4fTLI1dcTUL
02XiRSsnSv4wUyIu0qWfZEG6SJAxhZD4gYdkFDhCWmnfUAx/3Mv3lysv8kMX/ZTQPiOcxtlXZ5F6
QtN1a2golmVpODl2/BeiYeDkAbHo+Her9G7DMAwDAXSX1AHBo/jtskUqj8LdLbuMICCFBzjw+MgE
BdJyk5HfzDBkKqlCXTahsYS8wpzCWcVfffR79ntQ9Hv/5+dpUqhUlZGyMmqz7KKKsEgBmUfx367C
bphGFdAhm1kLrMxeZiCfZxnjkj26+wT887SyqwUAAA==
string: '{"data":{"dataConstraints":[{"links":[{"urn":"urn:sdmx:org.sdmx.infomodel.registry.DataConstraint=IMF.STA:IMTS(1.0.0)","type":"dataconstraint","rel":"self"}],"annotations":[{"id":"series_count","title":"471734","type":"sdmx_metrics"},{"id":"time_period_start","title":"1948-01-01","type":"sdmx_metrics"},{"id":"time_period_end","title":"2025-09-01","type":"sdmx_metrics"}],"id":"IMTS","name":"Data
Constraint","names":{"en":"Data Constraint"},"description":"Data Availability
Constraint","descriptions":{"en":"Data Availability Constraint"},"version":"1.0.0","agencyID":"IMF.STA","role":"Actual","constraintAttachment":{"dataStructures":["urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_IMTS(1.0.0)"]},"cubeRegions":[{"include":true,"components":[{"id":"COUNTRY","include":false,"removePrefix":false,"values":[{"value":"ABW"},{"value":"AFG"},{"value":"AGO"},{"value":"AIA"},{"value":"ALB"},{"value":"ANT"},{"value":"ARE"},{"value":"ARG"},{"value":"ARM"},{"value":"ASM"},{"value":"ATG"},{"value":"AUS"},{"value":"AUT"},{"value":"AZE"},{"value":"BDI"},{"value":"BEL"},{"value":"BEN"},{"value":"BFA"},{"value":"BGD"},{"value":"BGR"},{"value":"BHR"},{"value":"BHS"},{"value":"BIH"},{"value":"BLR"},{"value":"BLZ"},{"value":"BMU"},{"value":"BOL"},{"value":"BRA"},{"value":"BRB"},{"value":"BRN"},{"value":"BTN"},{"value":"BWA"},{"value":"CAF"},{"value":"CAN"},{"value":"CHE"},{"value":"CHL"},{"value":"CHN"},{"value":"CIV"},{"value":"CMR"},{"value":"COD"},{"value":"COG"},{"value":"COL"},{"value":"COM"},{"value":"CPV"},{"value":"CRI"},{"value":"CSK"},{"value":"CUB"},{"value":"CUW"},{"value":"CYP"},{"value":"CZE"},{"value":"DDR"},{"value":"DEU"},{"value":"DJI"},{"value":"DMA"},{"value":"DNK"},{"value":"DOM"},{"value":"DZA"},{"value":"ECU"},{"value":"EGY"},{"value":"ERI"},{"value":"ESP"},{"value":"EST"},{"value":"ETH"},{"value":"FIN"},{"value":"FJI"},{"value":"FLK"},{"value":"FRA"},{"value":"FRO"},{"value":"FSM"},{"value":"G001"},{"value":"G080"},{"value":"G092"},{"value":"G110"},{"value":"G163"},{"value":"G200"},{"value":"G205"},{"value":"G400"},{"value":"G505"},{"value":"G603"},{"value":"G903"},{"value":"G998"},{"value":"GAB"},{"value":"GBR"},{"value":"GEO"},{"value":"GHA"},{"value":"GIB"},{"value":"GIN"},{"value":"GMB"},{"value":"GNB"},{"value":"GNQ"},{"value":"GRC"},{"value":"GRD"},{"value":"GRL"},{"value":"GTM"},{"value":"GUM"},{"value":"GUY"},{"value":"GX170"},{"value":"GX405"},{"value":"GX440"},{"value":"GX605"},{"value":"GX901"},{"value":"HKG"},{"value":"HND"},{"value":"HRV"},{"value":"HTI"},{"value":"HUN"},{"value":"IDN"},{"value":"IND"},{"value":"IRL"},{"value":"IRN"},{"value":"IRQ"},{"value":"ISL"},{"value":"ISR"},{"value":"ITA"},{"value":"JAM"},{"value":"JOR"},{"value":"JPN"},{"value":"KAZ"},{"value":"KEN"},{"value":"KGZ"},{"value":"KHM"},{"value":"KIR"},{"value":"KNA"},{"value":"KOR"},{"value":"KOS"},{"value":"KWT"},{"value":"LAO"},{"value":"LBN"},{"value":"LBR"},{"value":"LBY"},{"value":"LCA"},{"value":"LKA"},{"value":"LSO"},{"value":"LTU"},{"value":"LUX"},{"value":"LVA"},{"value":"MAC"},{"value":"MAR"},{"value":"MDA"},{"value":"MDG"},{"value":"MDV"},{"value":"MEX"},{"value":"MHL"},{"value":"MKD"},{"value":"MLI"},{"value":"MLT"},{"value":"MMR"},{"value":"MNE"},{"value":"MNG"},{"value":"MOZ"},{"value":"MRT"},{"value":"MSR"},{"value":"MUS"},{"value":"MWI"},{"value":"MYS"},{"value":"NAM"},{"value":"NCL"},{"value":"NER"},{"value":"NGA"},{"value":"NIC"},{"value":"NLD"},{"value":"NOR"},{"value":"NPL"},{"value":"NRU"},{"value":"NZL"},{"value":"OMN"},{"value":"PAK"},{"value":"PAN"},{"value":"PER"},{"value":"PHL"},{"value":"PLW"},{"value":"PNG"},{"value":"POL"},{"value":"PRK"},{"value":"PRT"},{"value":"PRY"},{"value":"PYF"},{"value":"QAT"},{"value":"ROU"},{"value":"RUS"},{"value":"RWA"},{"value":"SAU"},{"value":"SCG"},{"value":"SDN"},{"value":"SEN"},{"value":"SGP"},{"value":"SLB"},{"value":"SLE"},{"value":"SLV"},{"value":"SMR"},{"value":"SOM"},{"value":"SRB"},{"value":"SSD"},{"value":"STP"},{"value":"SUN"},{"value":"SUR"},{"value":"SVK"},{"value":"SVN"},{"value":"SWE"},{"value":"SWZ"},{"value":"SXM"},{"value":"SYC"},{"value":"SYR"},{"value":"TCD"},{"value":"TGO"},{"value":"THA"},{"value":"TJK"},{"value":"TKM"},{"value":"TLS"},{"value":"TON"},{"value":"TTO"},{"value":"TUN"},{"value":"TUR"},{"value":"TUV"},{"value":"TX126"},{"value":"TX198"},{"value":"TX910"},{"value":"TZA"},{"value":"UGA"},{"value":"UKR"},{"value":"URY"},{"value":"USA"},{"value":"UZB"},{"value":"VAT"},{"value":"VCT"},{"value":"VEN"},{"value":"VNM"},{"value":"VUT"},{"value":"WBG"},{"value":"WSM"},{"value":"YAR"},{"value":"YEM"},{"value":"YMD"},{"value":"YUG"},{"value":"ZAF"},{"value":"ZMB"},{"value":"ZWE"}]}],"keyValues":[],"annotations":[]}]}]},"meta":{"schema":"https://raw.githubusercontent.com/sdmx-twg/sdmx-json/master/metadata-message/tools/schemas/2.0.0/sdmx-json-metadata-schema.json","id":"IDREF1591","test":false,"prepared":"2025-12-16T05:16:34.072054378Z","contentLanguages":["en"],"sender":{"id":"unknown"}}}'
headers:
Cache-Control:
- private
Connection:
- Keep-Alive
Content-Encoding:
- gzip
Content-Length:
- '658'
- keep-alive
Content-Type:
- application/json; charset=utf-8
- application/json
Date:
- Fri, 20 Sep 2024 22:48:00 GMT
Set-Cookie: null
Vary:
- Accept-Encoding
X-Content-Type-Options:
- Tue, 16 Dec 2025 05:16:34 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
Transfer-Encoding:
- chunked
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
content-encoding:
- gzip
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
vary:
- accept-encoding
x-content-type-options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-XSS-Protection:
- 1; mode=block
x-correlation-id:
- 7e06e7c14cae19f49cc3c6475f9b35e9
x-envoy-upstream-service-time:
- '75'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate, br, zstd
Connection:
- keep-alive
method: GET
uri: https://api.imf.org/external/sdmx/3.0/availability/dataflow/IMF.STA/IMTS/%2B/USA.*.*.*/INDICATOR
response:
body:
string: '{"data":{"dataConstraints":[{"links":[{"urn":"urn:sdmx:org.sdmx.infomodel.registry.DataConstraint=IMF.STA:IMTS(1.0.0)","type":"dataconstraint","rel":"self"}],"annotations":[{"id":"series_count","title":"2175","type":"sdmx_metrics"},{"id":"time_period_start","title":"1948-01-01","type":"sdmx_metrics"},{"id":"time_period_end","title":"2025-09-01","type":"sdmx_metrics"}],"id":"IMTS","name":"Data
Constraint","names":{"en":"Data Constraint"},"description":"Data Availability
Constraint","descriptions":{"en":"Data Availability Constraint"},"version":"1.0.0","agencyID":"IMF.STA","role":"Actual","constraintAttachment":{"dataStructures":["urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_IMTS(1.0.0)"]},"cubeRegions":[{"include":true,"components":[{"id":"INDICATOR","include":false,"removePrefix":false,"values":[{"value":"MG_CIF_USD"},{"value":"TBG_USD"},{"value":"XG_FOB_USD"}]}],"keyValues":[],"annotations":[]}]}]},"meta":{"schema":"https://raw.githubusercontent.com/sdmx-twg/sdmx-json/master/metadata-message/tools/schemas/2.0.0/sdmx-json-metadata-schema.json","id":"IDREF6665","test":false,"prepared":"2025-12-16T05:16:34.568119299Z","contentLanguages":["en"],"sender":{"id":"unknown"}}}'
headers:
Connection:
- keep-alive
Content-Length:
- '1210'
Content-Type:
- application/json
Date:
- Tue, 16 Dec 2025 05:16:34 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
x-content-type-options:
- nosniff
x-correlation-id:
- 0738a84c629ce1356f550e1bd62d0c4e
x-envoy-upstream-service-time:
- '64'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate, br, zstd
Connection:
- keep-alive
method: GET
uri: https://api.imf.org/external/sdmx/3.0/availability/dataflow/IMF.STA/IMTS/%2B/USA.XG_FOB_USD.*.*/COUNTERPART_COUNTRY
response:
body:
string: '{"data":{"dataConstraints":[{"links":[{"urn":"urn:sdmx:org.sdmx.infomodel.registry.DataConstraint=IMF.STA:IMTS(1.0.0)","type":"dataconstraint","rel":"self"}],"annotations":[{"id":"series_count","title":"726","type":"sdmx_metrics"},{"id":"time_period_start","title":"1948-01-01","type":"sdmx_metrics"},{"id":"time_period_end","title":"2025-09-01","type":"sdmx_metrics"}],"id":"IMTS","name":"Data
Constraint","names":{"en":"Data Constraint"},"description":"Data Availability
Constraint","descriptions":{"en":"Data Availability Constraint"},"version":"1.0.0","agencyID":"IMF.STA","role":"Actual","constraintAttachment":{"dataStructures":["urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_IMTS(1.0.0)"]},"cubeRegions":[{"include":true,"components":[{"id":"COUNTERPART_COUNTRY","include":false,"removePrefix":false,"values":[{"value":"ABW"},{"value":"AFG"},{"value":"AGO"},{"value":"AIA"},{"value":"ALB"},{"value":"ANT"},{"value":"ARE"},{"value":"ARG"},{"value":"ARM"},{"value":"ATG"},{"value":"AUS"},{"value":"AUT"},{"value":"AZE"},{"value":"BDI"},{"value":"BEL"},{"value":"BEN"},{"value":"BFA"},{"value":"BGD"},{"value":"BGR"},{"value":"BHR"},{"value":"BHS"},{"value":"BIH"},{"value":"BLR"},{"value":"BLZ"},{"value":"BMU"},{"value":"BOL"},{"value":"BRA"},{"value":"BRB"},{"value":"BRN"},{"value":"BTN"},{"value":"BWA"},{"value":"CAF"},{"value":"CAN"},{"value":"CHE"},{"value":"CHL"},{"value":"CHN"},{"value":"CIV"},{"value":"CMR"},{"value":"COD"},{"value":"COG"},{"value":"COL"},{"value":"COM"},{"value":"CPV"},{"value":"CRI"},{"value":"CSK"},{"value":"CUB"},{"value":"CUW"},{"value":"CYP"},{"value":"CZE"},{"value":"DDR"},{"value":"DEU"},{"value":"DJI"},{"value":"DMA"},{"value":"DNK"},{"value":"DOM"},{"value":"DZA"},{"value":"ECU"},{"value":"EGY"},{"value":"ERI"},{"value":"ESP"},{"value":"EST"},{"value":"ETH"},{"value":"FIN"},{"value":"FJI"},{"value":"FLK"},{"value":"FRA"},{"value":"FRO"},{"value":"FSM"},{"value":"G001"},{"value":"G080"},{"value":"G092"},{"value":"G110"},{"value":"G163"},{"value":"G200"},{"value":"G205"},{"value":"G400"},{"value":"G505"},{"value":"G603"},{"value":"G903"},{"value":"G998"},{"value":"GAB"},{"value":"GBR"},{"value":"GEO"},{"value":"GHA"},{"value":"GIB"},{"value":"GIN"},{"value":"GMB"},{"value":"GNB"},{"value":"GNQ"},{"value":"GRC"},{"value":"GRD"},{"value":"GRL"},{"value":"GTM"},{"value":"GUM"},{"value":"GUY"},{"value":"GX170"},{"value":"GX405"},{"value":"GX440"},{"value":"GX605"},{"value":"GX901"},{"value":"HKG"},{"value":"HND"},{"value":"HRV"},{"value":"HTI"},{"value":"HUN"},{"value":"IDN"},{"value":"IND"},{"value":"IRL"},{"value":"IRN"},{"value":"IRQ"},{"value":"ISL"},{"value":"ISR"},{"value":"ITA"},{"value":"JAM"},{"value":"JOR"},{"value":"JPN"},{"value":"KAZ"},{"value":"KEN"},{"value":"KGZ"},{"value":"KHM"},{"value":"KIR"},{"value":"KNA"},{"value":"KOR"},{"value":"KOS"},{"value":"KWT"},{"value":"LAO"},{"value":"LBN"},{"value":"LBR"},{"value":"LBY"},{"value":"LCA"},{"value":"LKA"},{"value":"LSO"},{"value":"LTU"},{"value":"LUX"},{"value":"LVA"},{"value":"MAC"},{"value":"MAR"},{"value":"MDA"},{"value":"MDG"},{"value":"MDV"},{"value":"MEX"},{"value":"MHL"},{"value":"MKD"},{"value":"MLI"},{"value":"MLT"},{"value":"MMR"},{"value":"MNE"},{"value":"MNG"},{"value":"MOZ"},{"value":"MRT"},{"value":"MSR"},{"value":"MUS"},{"value":"MWI"},{"value":"MYS"},{"value":"NAM"},{"value":"NCL"},{"value":"NER"},{"value":"NGA"},{"value":"NIC"},{"value":"NLD"},{"value":"NOR"},{"value":"NPL"},{"value":"NRU"},{"value":"NZL"},{"value":"OMN"},{"value":"PAK"},{"value":"PAN"},{"value":"PER"},{"value":"PHL"},{"value":"PLW"},{"value":"PNG"},{"value":"POL"},{"value":"PRK"},{"value":"PRT"},{"value":"PRY"},{"value":"PYF"},{"value":"QAT"},{"value":"ROU"},{"value":"RUS"},{"value":"RWA"},{"value":"SAU"},{"value":"SCG"},{"value":"SDN"},{"value":"SEN"},{"value":"SGP"},{"value":"SLB"},{"value":"SLE"},{"value":"SLV"},{"value":"SMR"},{"value":"SOM"},{"value":"SRB"},{"value":"SSD"},{"value":"STP"},{"value":"SUN"},{"value":"SUR"},{"value":"SVK"},{"value":"SVN"},{"value":"SWE"},{"value":"SWZ"},{"value":"SXM"},{"value":"SYC"},{"value":"SYR"},{"value":"TCD"},{"value":"TGO"},{"value":"THA"},{"value":"TJK"},{"value":"TKM"},{"value":"TLS"},{"value":"TON"},{"value":"TTO"},{"value":"TUN"},{"value":"TUR"},{"value":"TUV"},{"value":"TWN"},{"value":"TX126"},{"value":"TX399"},{"value":"TX489"},{"value":"TX598"},{"value":"TX799"},{"value":"TX884"},{"value":"TX898"},{"value":"TX899"},{"value":"TX910"},{"value":"TZA"},{"value":"UGA"},{"value":"UKR"},{"value":"URY"},{"value":"UZB"},{"value":"VAT"},{"value":"VCT"},{"value":"VEN"},{"value":"VNM"},{"value":"VUT"},{"value":"WBG"},{"value":"WSM"},{"value":"YAR"},{"value":"YEM"},{"value":"YMD"},{"value":"YUG"},{"value":"ZAF"},{"value":"ZMB"},{"value":"ZWE"}]}],"keyValues":[],"annotations":[]}]}]},"meta":{"schema":"https://raw.githubusercontent.com/sdmx-twg/sdmx-json/master/metadata-message/tools/schemas/2.0.0/sdmx-json-metadata-schema.json","id":"IDREF5741","test":false,"prepared":"2025-12-16T05:16:35.075876086Z","contentLanguages":["en"],"sender":{"id":"unknown"}}}'
headers:
Connection:
- keep-alive
Content-Type:
- application/json
Date:
- Tue, 16 Dec 2025 05:16:35 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
Transfer-Encoding:
- chunked
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
content-encoding:
- gzip
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
vary:
- accept-encoding
x-content-type-options:
- nosniff
x-correlation-id:
- f236267040a2ba2678df373df8920f7f
x-envoy-upstream-service-time:
- '61'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate, br, zstd
Connection:
- keep-alive
method: GET
uri: https://api.imf.org/external/sdmx/3.0/availability/dataflow/IMF.STA/IMTS/%2B/USA.XG_FOB_USD.G001.*/FREQUENCY
response:
body:
string: '{"data":{"dataConstraints":[{"links":[{"urn":"urn:sdmx:org.sdmx.infomodel.registry.DataConstraint=IMF.STA:IMTS(1.0.0)","type":"dataconstraint","rel":"self"}],"annotations":[{"id":"series_count","title":"3","type":"sdmx_metrics"},{"id":"time_period_start","title":"1948-01-01","type":"sdmx_metrics"},{"id":"time_period_end","title":"2025-09-01","type":"sdmx_metrics"}],"id":"IMTS","name":"Data
Constraint","names":{"en":"Data Constraint"},"description":"Data Availability
Constraint","descriptions":{"en":"Data Availability Constraint"},"version":"1.0.0","agencyID":"IMF.STA","role":"Actual","constraintAttachment":{"dataStructures":["urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_IMTS(1.0.0)"]},"cubeRegions":[{"include":true,"components":[{"id":"FREQUENCY","include":false,"removePrefix":false,"values":[{"value":"A"},{"value":"M"},{"value":"Q"}]}],"keyValues":[],"annotations":[]}]}]},"meta":{"schema":"https://raw.githubusercontent.com/sdmx-twg/sdmx-json/master/metadata-message/tools/schemas/2.0.0/sdmx-json-metadata-schema.json","id":"IDREF5377","test":false,"prepared":"2025-12-16T05:16:35.744587138Z","contentLanguages":["en"],"sender":{"id":"unknown"}}}'
headers:
Connection:
- keep-alive
Content-Length:
- '1183'
Content-Type:
- application/json
Date:
- Tue, 16 Dec 2025 05:16:35 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
x-content-type-options:
- nosniff
x-correlation-id:
- b373baeb60cfcb0263dbc993297cbf97
x-envoy-upstream-service-time:
- '66'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- application/xml
Accept-Encoding:
- gzip, deflate, br, zstd
Cache-Control:
- no-cache
Connection:
- keep-alive
method: GET
uri: https://api.imf.org/external/sdmx/3.0/data/dataflow/IMF.STA/IMTS/+/USA.XG_FOB_USD.G001+G998.A?c%5BTIME_PERIOD%5D=ge:2023-01-01+le:2026-01-01&dimensionAtObservation=TIME_PERIOD&detail=full&includeHistory=false
response:
body:
string: <?xml version='1.0' encoding='UTF-8'?><message:StructureSpecificData
xmlns:metadata="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/metadata/generic"
xmlns:common="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/common" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:message="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/message"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xml="http://www.w3.org/XML/1998/namespace"
xmlns:ss="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/data/structurespecific"
xmlns:ns1="urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_IMTS(1.0.0):ObsLevelDim:TIME_PERIOD"
xsi:schemaLocation="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/message
../../schemas/SDMXMessage.xsd"><message:Header><message:ID>IDREF1833</message:ID><message:Test>false</message:Test><message:Prepared>2025-12-16T05:16:36.306712826Z</message:Prepared><message:Sender
id="unknown"/><message:Structure structureID="IMF.STA.DSD_IMTS.1.0.0" namespace="urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_IMTS(1.0.0)"
dimensionAtObservation="TIME_PERIOD"><common:Structure>urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_IMTS(1.0.0)</common:Structure></message:Structure></message:Header><message:DataSet
ss:structureRef="IMF.STA.DSD_IMTS.1.0.0" action="Replace"><Group INDICATOR="XG_FOB_USD"
ns1:type="GROUP_INDICATOR"><Comp id="UNIT"><Value>USD</Value></Comp><Comp
id="TRADE_FLOW"><Value>XG</Value></Comp><Comp id="VALUATION"><Value>FOB</Value></Comp></Group><Series
COUNTRY="USA" INDICATOR="XG_FOB_USD" COUNTERPART_COUNTRY="G001" FREQUENCY="A"
OVERLAP="OL" SCALE="6"><Obs OBS_VALUE="2019551635133" DERIVATION_TYPE="M"
TIME_PERIOD="2023"/><Obs OBS_VALUE="2065411250609" DERIVATION_TYPE="M" TIME_PERIOD="2024"/></Series><Series
COUNTRY="USA" INDICATOR="XG_FOB_USD" COUNTERPART_COUNTRY="G998" FREQUENCY="A"
OVERLAP="OL" SCALE="6"><Obs OBS_VALUE="369756760426" DERIVATION_TYPE="M" TIME_PERIOD="2023"/><Obs
OBS_VALUE="371064529410" DERIVATION_TYPE="M" TIME_PERIOD="2024"/></Series></message:DataSet></message:StructureSpecificData>
headers:
Connection:
- keep-alive
Content-Type:
- application/xml
Date:
- Tue, 16 Dec 2025 05:16:36 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
Transfer-Encoding:
- chunked
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
content-encoding:
- gzip
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
vary:
- accept-encoding
x-content-type-options:
- nosniff
x-correlation-id:
- fe8af9be0ef160165114bfc9f7924dbc
x-envoy-upstream-service-time:
- '104'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK

View File

@@ -5,49 +5,294 @@ interactions:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate
- gzip, deflate, br, zstd
Connection:
- keep-alive
method: GET
uri: http://dataservices.imf.org/REST/SDMX_JSON.svc/CompactData/IRFCL/M.JP.RAMFDA_USD.S1X?startPeriod=2023-01-01&endPeriod=2023-12-31
uri: https://api.imf.org/external/sdmx/3.0/availability/dataflow/IMF.STA/IL/%2B/*/COUNTRY
response:
body:
string: !!binary |
H4sIAAAAAAAEA6SSXY+aQBSG/4qhN226yDB+ULmS8pHSgFrAxtgLMg5HJREwDKs2Zv97z4w27W6y
zSa95J3nPe/54Kq5TXVkvPNYxzT7qk0v1aEW9kWUmq3tu+5oG8b5fO6fB/2m3RmUENNYxVHK91Ax
vaxFx2oO2sMfY/EGo+JFaQtVJmo468qmfm5MvXilMlsQzWPLQRhSiiPj5hLGiebEqEAItoOe7FVg
sy3sStG1P/uiqC7K/wxXJW6W/kUUyoauAscX0J5KjOmX1VYZ+W01RpgEbvR2UtZVA8pN/s9MWOQL
sAJaeZjQw1IFZeaWTLjOh9ZAH47ISN8UfKOTicVhY32a8BFFVwaiQ3rLDkKeZtHCkbVQoEQJHSKt
EyszB/aQ2qMx8inU95RpKSnTJajOWAUyWf4S9oHVO/RDjQ/vOrh0+BHGgfb0oLlN3TGOwlVbJiHq
8hS4U/nb3FeJpgwOcNw3NSDwsWf23mMrH3pjOtDHlBLtCQslwKE8gRp3WhYIrtdrmeDhdVLo1ArU
Mf4Sf3dYC+TvyQXy4p/XxIZSaEtA01WbBon/Dd0xqtPED3In8R38/rqQQjjzQtfJ5gkqiRMHnpMv
U0++SDT13dtTaq6ktpyFWR4vowzpsRSyMPbzYJ7EjpQWpgyZbzD3Bwarx4WfhHMPeUpwG8SUrvnn
NP/uREsfZd2aDHHgV3D6Eh8R63V68JL+1eecmwAMxEAU7cW5QfdRjEvZ3r2xhDTxY/ikMGupOpJn
rVUj08Kt8dhavHHymGOicZaceTaubiNHaHx7R6w8QOZzqvoli+d85+4HilPlVbAFAAA=
string: '{"data":{"dataConstraints":[{"links":[{"urn":"urn:sdmx:org.sdmx.infomodel.registry.DataConstraint=IMF.STA:IL(13.0.1)","type":"dataconstraint","rel":"self"}],"annotations":[{"id":"series_count","title":"9557","type":"sdmx_metrics"},{"id":"time_period_start","title":"1945-01-01","type":"sdmx_metrics"},{"id":"time_period_end","title":"2025-12-01","type":"sdmx_metrics"}],"id":"IL","name":"Data
Constraint","names":{"en":"Data Constraint"},"description":"Data Availability
Constraint","descriptions":{"en":"Data Availability Constraint"},"version":"13.0.1","agencyID":"IMF.STA","role":"Actual","constraintAttachment":{"dataStructures":["urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_IL(13.0.0)"]},"cubeRegions":[{"include":true,"components":[{"id":"COUNTRY","include":false,"removePrefix":false,"values":[{"value":"ABW"},{"value":"AFG"},{"value":"AGO"},{"value":"AIA"},{"value":"ALB"},{"value":"ANT"},{"value":"ARE"},{"value":"ARG"},{"value":"ARM"},{"value":"ATG"},{"value":"AUS"},{"value":"AUT"},{"value":"AZE"},{"value":"BCE"},{"value":"BDI"},{"value":"BEA"},{"value":"BEL"},{"value":"BGD"},{"value":"BGR"},{"value":"BHR"},{"value":"BHS"},{"value":"BIH"},{"value":"BIS"},{"value":"BLR"},{"value":"BLZ"},{"value":"BOL"},{"value":"BRA"},{"value":"BRB"},{"value":"BRN"},{"value":"BTN"},{"value":"BWA"},{"value":"CAF"},{"value":"CAN"},{"value":"CHE"},{"value":"CHL"},{"value":"CHN"},{"value":"CMR"},{"value":"COD"},{"value":"COG"},{"value":"COL"},{"value":"COM"},{"value":"CPV"},{"value":"CRI"},{"value":"CSK"},{"value":"CUB"},{"value":"CWX"},{"value":"CYM"},{"value":"CYP"},{"value":"CZE"},{"value":"DEU"},{"value":"DJI"},{"value":"DMA"},{"value":"DNK"},{"value":"DOM"},{"value":"DZA"},{"value":"ECB"},{"value":"ECU"},{"value":"EGY"},{"value":"ERI"},{"value":"ESP"},{"value":"EST"},{"value":"ETH"},{"value":"EZB"},{"value":"FIN"},{"value":"FJI"},{"value":"FRA"},{"value":"FSM"},{"value":"G001"},{"value":"G110"},{"value":"G163"},{"value":"G200"},{"value":"G205"},{"value":"G309"},{"value":"G400"},{"value":"G505"},{"value":"G603"},{"value":"G758"},{"value":"G759"},{"value":"G903"},{"value":"GAB"},{"value":"GBR"},{"value":"GEO"},{"value":"GHA"},{"value":"GIN"},{"value":"GMB"},{"value":"GNQ"},{"value":"GRC"},{"value":"GRD"},{"value":"GTM"},{"value":"GUY"},{"value":"GX010"},{"value":"HKG"},{"value":"HND"},{"value":"HRV"},{"value":"HTI"},{"value":"HUN"},{"value":"IDN"},{"value":"IMF"},{"value":"IND"},{"value":"IRL"},{"value":"IRN"},{"value":"IRQ"},{"value":"ISL"},{"value":"ISR"},{"value":"ITA"},{"value":"JAM"},{"value":"JOR"},{"value":"JPN"},{"value":"KAZ"},{"value":"KEN"},{"value":"KGZ"},{"value":"KHM"},{"value":"KNA"},{"value":"KOR"},{"value":"KOS"},{"value":"KWT"},{"value":"LAO"},{"value":"LBN"},{"value":"LBR"},{"value":"LBY"},{"value":"LCA"},{"value":"LKA"},{"value":"LSO"},{"value":"LTU"},{"value":"LUX"},{"value":"LVA"},{"value":"MAC"},{"value":"MAR"},{"value":"MDA"},{"value":"MDG"},{"value":"MDV"},{"value":"MEX"},{"value":"MKD"},{"value":"MLT"},{"value":"MNE"},{"value":"MNG"},{"value":"MOZ"},{"value":"MRT"},{"value":"MSR"},{"value":"MUS"},{"value":"MWI"},{"value":"MYS"},{"value":"NAM"},{"value":"NGA"},{"value":"NIC"},{"value":"NLD"},{"value":"NOR"},{"value":"NPL"},{"value":"NZL"},{"value":"OMN"},{"value":"PAK"},{"value":"PAN"},{"value":"PER"},{"value":"PHL"},{"value":"PNG"},{"value":"POL"},{"value":"PRT"},{"value":"PRY"},{"value":"QAT"},{"value":"ROU"},{"value":"RUS"},{"value":"RWA"},{"value":"SAU"},{"value":"SDN"},{"value":"SGP"},{"value":"SLB"},{"value":"SLE"},{"value":"SLV"},{"value":"SMR"},{"value":"SOM"},{"value":"SRB"},{"value":"SSD"},{"value":"STP"},{"value":"SUR"},{"value":"SVK"},{"value":"SVN"},{"value":"SWE"},{"value":"SWZ"},{"value":"SYC"},{"value":"SYR"},{"value":"TCD"},{"value":"THA"},{"value":"TJK"},{"value":"TKM"},{"value":"TLS"},{"value":"TON"},{"value":"TTO"},{"value":"TUN"},{"value":"TUR"},{"value":"TWN"},{"value":"TX752"},{"value":"TZA"},{"value":"UGA"},{"value":"UKR"},{"value":"URY"},{"value":"USA"},{"value":"UZB"},{"value":"VCT"},{"value":"VEN"},{"value":"VNM"},{"value":"VUT"},{"value":"WBG"},{"value":"WSM"},{"value":"YAR"},{"value":"YEM"},{"value":"YMD"},{"value":"YUG"},{"value":"ZAF"},{"value":"ZMB"},{"value":"ZWE"}]}],"keyValues":[],"annotations":[]}]}]},"meta":{"schema":"https://raw.githubusercontent.com/sdmx-twg/sdmx-json/master/metadata-message/tools/schemas/2.0.0/sdmx-json-metadata-schema.json","id":"IDREF6976","test":false,"prepared":"2025-12-16T05:17:20.433171672Z","contentLanguages":["en"],"sender":{"id":"unknown"}}}'
headers:
Cache-Control:
- private
Connection:
- Keep-Alive
Content-Encoding:
- gzip
Content-Length:
- '620'
- keep-alive
Content-Type:
- application/json; charset=utf-8
- application/json
Date:
- Sat, 07 Sep 2024 17:42:56 GMT
Set-Cookie: null
Vary:
- Accept-Encoding
X-Content-Type-Options:
- Tue, 16 Dec 2025 05:17:20 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
Transfer-Encoding:
- chunked
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
content-encoding:
- gzip
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
vary:
- accept-encoding
x-content-type-options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-XSS-Protection:
- 1; mode=block
x-correlation-id:
- cd9c374bd366cb1e31f4f9124bc59707
x-envoy-upstream-service-time:
- '62'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate, br, zstd
Connection:
- keep-alive
method: GET
uri: https://api.imf.org/external/sdmx/3.0/availability/dataflow/IMF.STA/IL/%2B/JPN.RGV_REVS.*.*/FREQUENCY
response:
body:
string: '{"data":{"dataConstraints":[{"links":[{"urn":"urn:sdmx:org.sdmx.infomodel.registry.DataConstraint=IMF.STA:IL(13.0.1)","type":"dataconstraint","rel":"self"}],"annotations":[{"id":"series_count","title":"3","type":"sdmx_metrics"},{"id":"time_period_start","title":"1950-01-01","type":"sdmx_metrics"},{"id":"time_period_end","title":"2025-12-01","type":"sdmx_metrics"}],"id":"IL","name":"Data
Constraint","names":{"en":"Data Constraint"},"description":"Data Availability
Constraint","descriptions":{"en":"Data Availability Constraint"},"version":"13.0.1","agencyID":"IMF.STA","role":"Actual","constraintAttachment":{"dataStructures":["urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_IL(13.0.0)"]},"cubeRegions":[{"include":true,"components":[{"id":"FREQUENCY","include":false,"removePrefix":false,"values":[{"value":"A"},{"value":"M"},{"value":"Q"}]}],"keyValues":[],"annotations":[]}]}]},"meta":{"schema":"https://raw.githubusercontent.com/sdmx-twg/sdmx-json/master/metadata-message/tools/schemas/2.0.0/sdmx-json-metadata-schema.json","id":"IDREF9258","test":false,"prepared":"2025-12-16T05:17:20.912545745Z","contentLanguages":["en"],"sender":{"id":"unknown"}}}'
headers:
Connection:
- keep-alive
Content-Length:
- '1180'
Content-Type:
- application/json
Date:
- Tue, 16 Dec 2025 05:17:20 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
x-content-type-options:
- nosniff
x-correlation-id:
- bc8d8fa6d41d9785feda7f0b4720497f
x-envoy-upstream-service-time:
- '73'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate, br, zstd
Connection:
- keep-alive
method: GET
uri: https://api.imf.org/external/sdmx/3.0/availability/dataflow/IMF.STA/IL/%2B/*.*.*.*/COUNTRY
response:
body:
string: '{"data":{"dataConstraints":[{"links":[{"urn":"urn:sdmx:org.sdmx.infomodel.registry.DataConstraint=IMF.STA:IL(13.0.1)","type":"dataconstraint","rel":"self"}],"annotations":[{"id":"series_count","title":"9557","type":"sdmx_metrics"},{"id":"time_period_start","title":"1945-01-01","type":"sdmx_metrics"},{"id":"time_period_end","title":"2025-12-01","type":"sdmx_metrics"}],"id":"IL","name":"Data
Constraint","names":{"en":"Data Constraint"},"description":"Data Availability
Constraint","descriptions":{"en":"Data Availability Constraint"},"version":"13.0.1","agencyID":"IMF.STA","role":"Actual","constraintAttachment":{"dataStructures":["urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_IL(13.0.0)"]},"cubeRegions":[{"include":true,"components":[{"id":"COUNTRY","include":false,"removePrefix":false,"values":[{"value":"ABW"},{"value":"AFG"},{"value":"AGO"},{"value":"AIA"},{"value":"ALB"},{"value":"ANT"},{"value":"ARE"},{"value":"ARG"},{"value":"ARM"},{"value":"ATG"},{"value":"AUS"},{"value":"AUT"},{"value":"AZE"},{"value":"BCE"},{"value":"BDI"},{"value":"BEA"},{"value":"BEL"},{"value":"BGD"},{"value":"BGR"},{"value":"BHR"},{"value":"BHS"},{"value":"BIH"},{"value":"BIS"},{"value":"BLR"},{"value":"BLZ"},{"value":"BOL"},{"value":"BRA"},{"value":"BRB"},{"value":"BRN"},{"value":"BTN"},{"value":"BWA"},{"value":"CAF"},{"value":"CAN"},{"value":"CHE"},{"value":"CHL"},{"value":"CHN"},{"value":"CMR"},{"value":"COD"},{"value":"COG"},{"value":"COL"},{"value":"COM"},{"value":"CPV"},{"value":"CRI"},{"value":"CSK"},{"value":"CUB"},{"value":"CWX"},{"value":"CYM"},{"value":"CYP"},{"value":"CZE"},{"value":"DEU"},{"value":"DJI"},{"value":"DMA"},{"value":"DNK"},{"value":"DOM"},{"value":"DZA"},{"value":"ECB"},{"value":"ECU"},{"value":"EGY"},{"value":"ERI"},{"value":"ESP"},{"value":"EST"},{"value":"ETH"},{"value":"EZB"},{"value":"FIN"},{"value":"FJI"},{"value":"FRA"},{"value":"FSM"},{"value":"G001"},{"value":"G110"},{"value":"G163"},{"value":"G200"},{"value":"G205"},{"value":"G309"},{"value":"G400"},{"value":"G505"},{"value":"G603"},{"value":"G758"},{"value":"G759"},{"value":"G903"},{"value":"GAB"},{"value":"GBR"},{"value":"GEO"},{"value":"GHA"},{"value":"GIN"},{"value":"GMB"},{"value":"GNQ"},{"value":"GRC"},{"value":"GRD"},{"value":"GTM"},{"value":"GUY"},{"value":"GX010"},{"value":"HKG"},{"value":"HND"},{"value":"HRV"},{"value":"HTI"},{"value":"HUN"},{"value":"IDN"},{"value":"IMF"},{"value":"IND"},{"value":"IRL"},{"value":"IRN"},{"value":"IRQ"},{"value":"ISL"},{"value":"ISR"},{"value":"ITA"},{"value":"JAM"},{"value":"JOR"},{"value":"JPN"},{"value":"KAZ"},{"value":"KEN"},{"value":"KGZ"},{"value":"KHM"},{"value":"KNA"},{"value":"KOR"},{"value":"KOS"},{"value":"KWT"},{"value":"LAO"},{"value":"LBN"},{"value":"LBR"},{"value":"LBY"},{"value":"LCA"},{"value":"LKA"},{"value":"LSO"},{"value":"LTU"},{"value":"LUX"},{"value":"LVA"},{"value":"MAC"},{"value":"MAR"},{"value":"MDA"},{"value":"MDG"},{"value":"MDV"},{"value":"MEX"},{"value":"MKD"},{"value":"MLT"},{"value":"MNE"},{"value":"MNG"},{"value":"MOZ"},{"value":"MRT"},{"value":"MSR"},{"value":"MUS"},{"value":"MWI"},{"value":"MYS"},{"value":"NAM"},{"value":"NGA"},{"value":"NIC"},{"value":"NLD"},{"value":"NOR"},{"value":"NPL"},{"value":"NZL"},{"value":"OMN"},{"value":"PAK"},{"value":"PAN"},{"value":"PER"},{"value":"PHL"},{"value":"PNG"},{"value":"POL"},{"value":"PRT"},{"value":"PRY"},{"value":"QAT"},{"value":"ROU"},{"value":"RUS"},{"value":"RWA"},{"value":"SAU"},{"value":"SDN"},{"value":"SGP"},{"value":"SLB"},{"value":"SLE"},{"value":"SLV"},{"value":"SMR"},{"value":"SOM"},{"value":"SRB"},{"value":"SSD"},{"value":"STP"},{"value":"SUR"},{"value":"SVK"},{"value":"SVN"},{"value":"SWE"},{"value":"SWZ"},{"value":"SYC"},{"value":"SYR"},{"value":"TCD"},{"value":"THA"},{"value":"TJK"},{"value":"TKM"},{"value":"TLS"},{"value":"TON"},{"value":"TTO"},{"value":"TUN"},{"value":"TUR"},{"value":"TWN"},{"value":"TX752"},{"value":"TZA"},{"value":"UGA"},{"value":"UKR"},{"value":"URY"},{"value":"USA"},{"value":"UZB"},{"value":"VCT"},{"value":"VEN"},{"value":"VNM"},{"value":"VUT"},{"value":"WBG"},{"value":"WSM"},{"value":"YAR"},{"value":"YEM"},{"value":"YMD"},{"value":"YUG"},{"value":"ZAF"},{"value":"ZMB"},{"value":"ZWE"}]}],"keyValues":[],"annotations":[]}]}]},"meta":{"schema":"https://raw.githubusercontent.com/sdmx-twg/sdmx-json/master/metadata-message/tools/schemas/2.0.0/sdmx-json-metadata-schema.json","id":"IDREF5022","test":false,"prepared":"2025-12-16T05:17:21.650952563Z","contentLanguages":["en"],"sender":{"id":"unknown"}}}'
headers:
Connection:
- keep-alive
Content-Type:
- application/json
Date:
- Tue, 16 Dec 2025 05:17:21 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
Transfer-Encoding:
- chunked
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
content-encoding:
- gzip
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
vary:
- accept-encoding
x-content-type-options:
- nosniff
x-correlation-id:
- ac3a1a8245b4b58232acb4810ace02bb
x-envoy-upstream-service-time:
- '61'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate, br, zstd
Connection:
- keep-alive
method: GET
uri: https://api.imf.org/external/sdmx/3.0/availability/dataflow/IMF.STA/IL/%2B/JPN.*.*.*/INDICATOR
response:
body:
string: '{"data":{"dataConstraints":[{"links":[{"urn":"urn:sdmx:org.sdmx.infomodel.registry.DataConstraint=IMF.STA:IL(13.0.1)","type":"dataconstraint","rel":"self"}],"annotations":[{"id":"series_count","title":"48","type":"sdmx_metrics"},{"id":"time_period_start","title":"1945-01-01","type":"sdmx_metrics"},{"id":"time_period_end","title":"2025-12-01","type":"sdmx_metrics"}],"id":"IL","name":"Data
Constraint","names":{"en":"Data Constraint"},"description":"Data Availability
Constraint","descriptions":{"en":"Data Availability Constraint"},"version":"13.0.1","agencyID":"IMF.STA","role":"Actual","constraintAttachment":{"dataStructures":["urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_IL(13.0.0)"]},"cubeRegions":[{"include":true,"components":[{"id":"INDICATOR","include":false,"removePrefix":false,"values":[{"value":"GOLD35P_REVS"},{"value":"NFAOFA_ACO_NRES_S121"},{"value":"NFAOFL_LT_NRES_S121"},{"value":"RGOLDMV_REVS"},{"value":"RGOLDNV_REVS"},{"value":"RGV_REVS"},{"value":"RXDR_REVS"},{"value":"RXF11FX_REVS"},{"value":"RXF11_REVS"},{"value":"TRGMV_REVS"},{"value":"TRGNV_REVS"},{"value":"TRRPIMF_REVS"}]}],"keyValues":[],"annotations":[]}]}]},"meta":{"schema":"https://raw.githubusercontent.com/sdmx-twg/sdmx-json/master/metadata-message/tools/schemas/2.0.0/sdmx-json-metadata-schema.json","id":"IDREF2438","test":false,"prepared":"2025-12-16T05:17:22.366181264Z","contentLanguages":["en"],"sender":{"id":"unknown"}}}'
headers:
Connection:
- keep-alive
Content-Length:
- '1441'
Content-Type:
- application/json
Date:
- Tue, 16 Dec 2025 05:17:22 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
x-content-type-options:
- nosniff
x-correlation-id:
- 8b788ff2683903419391a8480a73df0a
x-envoy-upstream-service-time:
- '61'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- application/xml
Accept-Encoding:
- gzip, deflate, br, zstd
Cache-Control:
- no-cache
Connection:
- keep-alive
method: GET
uri: https://api.imf.org/external/sdmx/3.0/data/dataflow/IMF.STA/IL/+/JPN.RGV_REVS.*.Q?c%5BTIME_PERIOD%5D=ge:2023-01-01+le:2024-02-01&dimensionAtObservation=TIME_PERIOD&detail=full&includeHistory=false
response:
body:
string: <?xml version='1.0' encoding='UTF-8'?><message:StructureSpecificData
xmlns:metadata="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/metadata/generic"
xmlns:common="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/common" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:message="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/message"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xml="http://www.w3.org/XML/1998/namespace"
xmlns:ss="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/data/structurespecific"
xmlns:ns1="urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_IL(13.0.0):ObsLevelDim:TIME_PERIOD"
xsi:schemaLocation="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/message
../../schemas/SDMXMessage.xsd"><message:Header><message:ID>IDREF9203</message:ID><message:Test>false</message:Test><message:Prepared>2025-12-16T05:17:22.970325555Z</message:Prepared><message:Sender
id="unknown"/><message:Structure structureID="IMF.STA.DSD_IL.13.0.0" namespace="urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_IL(13.0.0)"
dimensionAtObservation="TIME_PERIOD"><common:Structure>urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=IMF.STA:DSD_IL(13.0.0)</common:Structure></message:Structure></message:Header><message:DataSet
ss:structureRef="IMF.STA.DSD_IL.13.0.0" action="Replace"><Series COUNTRY="JPN"
INDICATOR="RGV_REVS" UNIT="FTO" FREQUENCY="Q" IFS_FLAG="true" SCALE="6"><Obs
OBS_VALUE="27198880.827" DERIVATION_TYPE="O" TIME_PERIOD="2023-Q1"/><Obs OBS_VALUE="27198880.827"
DERIVATION_TYPE="O" TIME_PERIOD="2023-Q2"/><Obs OBS_VALUE="27198880.827" DERIVATION_TYPE="O"
TIME_PERIOD="2023-Q3"/><Obs OBS_VALUE="27198880.827" DERIVATION_TYPE="O" TIME_PERIOD="2023-Q4"/></Series></message:DataSet></message:StructureSpecificData>
headers:
Connection:
- keep-alive
Content-Type:
- application/xml
Date:
- Tue, 16 Dec 2025 05:17:23 GMT
Request-Context:
- appId=cid-v1:e4518baf-f7f4-42d7-900e-7dd820548dc8
Set-Cookie:
- ApplicationGatewayAffinityCORS=dca9138616203b0df55300d07b8e6a61; Path=/; SameSite=None;
Secure
- ApplicationGatewayAffinity=dca9138616203b0df55300d07b8e6a61; Path=/
Transfer-Encoding:
- chunked
cache-control:
- no-cache, no-store, max-age=0, must-revalidate
content-encoding:
- gzip
expires:
- '0'
pragma:
- no-cache
server:
- istio-envoy
strict-transport-security:
- max-age=31536000 ; includeSubDomains
vary:
- accept-encoding
x-content-type-options:
- nosniff
x-correlation-id:
- 466838ab46fd072666f957e75cd64217
x-envoy-upstream-service-time:
- '111'
x-frame-options:
- DENY
x-xss-protection:
- '0'
status:
code: 200
message: OK

View File

@@ -0,0 +1,329 @@
"""Tests for IMF helper utilities."""
# ruff: noqa: I001
# pylint: disable=W0621,W0613,W0612,W0212,R0903,C0302,C0415
from unittest.mock import MagicMock, patch
import pytest
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_imf.utils.helpers import (
detect_indicator_dimensions,
detect_transform_dimension,
normalize_country_label,
resolve_country_code,
)
class TestNormalizeCountryLabel:
"""Tests for normalize_country_label function."""
def test_simple_name(self):
"""Test normalization of simple country names."""
assert normalize_country_label("United States") == "united_states"
assert normalize_country_label("Japan") == "japan"
assert normalize_country_label("United Kingdom") == "united_kingdom"
def test_name_with_comma(self):
"""Test normalization of names with comma suffix."""
assert normalize_country_label("Armenia, Republic of") == "armenia"
assert normalize_country_label("Korea, Republic of") == "korea"
assert normalize_country_label("Iran, Islamic Republic of") == "iran"
def test_name_with_parentheses(self):
"""Test normalization of names with parenthetical content."""
assert normalize_country_label("Euro Area (EA)") == "euro_area"
assert normalize_country_label("China (Mainland)") == "china"
assert normalize_country_label("Taiwan (Province of China)") == "taiwan"
def test_name_with_hyphen(self):
"""Test normalization of names with hyphens."""
assert normalize_country_label("Guinea-Bissau") == "guinea_bissau"
assert normalize_country_label("Timor-Leste") == "timor_leste"
def test_mixed_cases(self):
"""Test normalization handles mixed cases."""
assert normalize_country_label("UNITED STATES") == "united_states"
assert normalize_country_label("united states") == "united_states"
assert normalize_country_label("United STATES") == "united_states"
class TestResolveCountryCode:
"""Tests for resolve_country_code function."""
@pytest.fixture
def mock_metadata(self):
"""Create mock metadata with country codelist."""
metadata = MagicMock()
metadata._codelist_cache = {
"CL_COUNTRY": {
"USA": "United States",
"JPN": "Japan",
"GBR": "United Kingdom",
"DEU": "Germany",
"FRA": "France",
}
}
return metadata
def test_iso_code_passthrough(self, mock_metadata):
"""Test that valid ISO codes are passed through."""
assert resolve_country_code("USA", mock_metadata) == "USA"
assert resolve_country_code("JPN", mock_metadata) == "JPN"
assert resolve_country_code("gbr", mock_metadata) == "GBR"
def test_name_to_code_resolution(self, mock_metadata):
"""Test that country names are resolved to codes."""
assert resolve_country_code("Japan", mock_metadata) == "JPN"
assert resolve_country_code("japan", mock_metadata) == "JPN"
assert resolve_country_code("United Kingdom", mock_metadata) == "GBR"
def test_unrecognized_returns_uppercase(self, mock_metadata):
"""Test that unrecognized values are returned as uppercase."""
assert resolve_country_code("XYZ", mock_metadata) == "XYZ"
assert resolve_country_code("unknown", mock_metadata) == "UNKNOWN"
def test_whitespace_handling(self, mock_metadata):
"""Test that whitespace is trimmed."""
assert resolve_country_code(" USA ", mock_metadata) == "USA"
assert resolve_country_code(" Japan ", mock_metadata) == "JPN"
def test_empty_codelist_cache(self):
"""Test handling when codelist cache is empty."""
metadata = MagicMock()
metadata._codelist_cache = {}
assert resolve_country_code("USA", metadata) == "USA"
class TestDetectTransformDimension:
"""Tests for detect_transform_dimension function."""
@pytest.fixture
def mock_metadata_transform(self):
"""Mock metadata with TRANSFORM dimension."""
with patch("openbb_imf.utils.metadata.ImfMetadata") as MockMetadata:
mock_instance = MockMetadata.return_value
mock_instance.get_dataflow_parameters.return_value = {
"TRANSFORMATION": [
{"value": "IX", "label": "Index"},
{"value": "PC_PA", "label": "Year-over-year percent change"},
{"value": "PC_PP", "label": "Period-over-period percent change"},
],
"COUNTRY": [
{"value": "USA", "label": "United States"},
],
}
yield MockMetadata
@pytest.fixture
def mock_metadata_unit(self):
"""Mock metadata with UNIT dimension."""
with patch("openbb_imf.utils.metadata.ImfMetadata") as MockMetadata:
mock_instance = MockMetadata.return_value
mock_instance.get_dataflow_parameters.return_value = {
"UNIT": [
{"value": "USD", "label": "US Dollar"},
{"value": "EUR", "label": "Euro"},
{"value": "XDC", "label": "Domestic currency"},
{"value": "IX", "label": "Index"},
],
"COUNTRY": [
{"value": "USA", "label": "United States"},
],
}
yield MockMetadata
def test_detect_transform_dimension(self, mock_metadata_transform):
"""Test detection of TRANSFORM dimension."""
transform_dim, unit_dim, transform_lookup, unit_lookup = (
detect_transform_dimension("CPI")
)
assert transform_dim == "TRANSFORMATION"
assert unit_dim is None
assert "index" in transform_lookup
assert "yoy" in transform_lookup
assert "period" in transform_lookup
assert transform_lookup["index"] == "IX"
assert transform_lookup["yoy"] == "PC_PA"
assert transform_lookup["period"] == "PC_PP"
def test_detect_unit_dimension(self, mock_metadata_unit):
"""Test detection of UNIT dimension."""
transform_dim, unit_dim, transform_lookup, unit_lookup = (
detect_transform_dimension("MFS_MA")
)
assert transform_dim is None
assert unit_dim == "UNIT"
assert "usd" in unit_lookup
assert "eur" in unit_lookup
assert "local" in unit_lookup
assert "index" in unit_lookup
assert unit_lookup["usd"] == "USD"
assert unit_lookup["eur"] == "EUR"
assert unit_lookup["local"] == "XDC"
def test_no_transform_or_unit(self):
"""Test dataflow without transform or unit dimension."""
with patch("openbb_imf.utils.metadata.ImfMetadata") as MockMetadata:
mock_instance = MockMetadata.return_value
mock_instance.get_dataflow_parameters.return_value = {
"COUNTRY": [{"value": "USA", "label": "United States"}],
"INDICATOR": [{"value": "GDP", "label": "GDP"}],
}
transform_dim, unit_dim, transform_lookup, unit_lookup = (
detect_transform_dimension("WEO")
)
assert transform_dim is None
assert unit_dim is None
assert transform_lookup == {}
assert unit_lookup == {}
def test_direct_code_access(self, mock_metadata_transform):
"""Test that direct codes are also in lookup."""
transform_dim, unit_dim, transform_lookup, unit_lookup = (
detect_transform_dimension("CPI")
)
# Direct codes should be accessible (case-insensitive)
assert "ix" in transform_lookup
assert "pc_pa" in transform_lookup
assert transform_lookup["ix"] == "IX"
def test_mixed_transform_and_unit(self):
"""Test detection when both TRANSFORM and UNIT dimensions exist."""
with patch("openbb_imf.utils.metadata.ImfMetadata") as MockMetadata:
mock_instance = MockMetadata.return_value
mock_instance.get_dataflow_parameters.return_value = {
"TRANSFORMATION": [
{"value": "IX", "label": "Index"},
],
"UNIT": [
{"value": "USD", "label": "US Dollar"},
],
}
transform_dim, unit_dim, transform_lookup, unit_lookup = (
detect_transform_dimension("MIXED")
)
assert transform_dim == "TRANSFORMATION"
assert unit_dim == "UNIT"
assert "index" in transform_lookup
assert "usd" in unit_lookup
class TestDetectIndicatorDimensions:
"""Tests for detect_indicator_dimensions function."""
def test_indicator_dimension_mapping(self):
"""Test that indicators are mapped to correct dimensions."""
mock_metadata = MagicMock()
# Mock get_dataflow_parameters to return dimension -> codes mapping
mock_metadata.get_dataflow_parameters.return_value = {
"INDICATOR": [
{"value": "GDP", "label": "GDP"},
{"value": "CPI", "label": "CPI"},
],
"BOP_ACCOUNTING_ENTRY": [
{"value": "CD_T", "label": "Current Account"},
],
}
mock_metadata.dataflows = {}
mock_metadata.datastructures = {}
result = detect_indicator_dimensions(
"TEST", ["GDP", "CPI", "CD_T"], mock_metadata
)
assert "INDICATOR" in result
assert "BOP_ACCOUNTING_ENTRY" in result
assert "GDP" in result["INDICATOR"]
assert "CPI" in result["INDICATOR"]
assert "CD_T" in result["BOP_ACCOUNTING_ENTRY"]
def test_fallback_to_indicator_dimension(self):
"""Test fallback when indicator not found in metadata."""
mock_metadata = MagicMock()
mock_metadata.get_dataflow_parameters.return_value = {
"INDICATOR": [{"value": "GDP", "label": "GDP"}],
}
mock_metadata.dataflows = {}
mock_metadata.datastructures = {}
# "UNKNOWN" is not in any dimension - raises OpenBBError
with pytest.raises(OpenBBError):
detect_indicator_dimensions("TEST", ["GDP", "UNKNOWN"], mock_metadata)
def test_compound_indicator_codes(self):
"""Test parsing of compound indicator codes."""
mock_metadata = MagicMock()
mock_metadata.get_dataflow_parameters.return_value = {
"SECTOR": [{"value": "S1", "label": "Sector 1"}],
"INDICATOR": [{"value": "I1", "label": "Indicator 1"}],
}
mock_metadata.dataflows = {}
mock_metadata.datastructures = {}
# Compound code: S1_I1 (underscore separator)
result = detect_indicator_dimensions("TEST", ["S1_I1"], mock_metadata)
assert "SECTOR" in result
assert "INDICATOR" in result
assert "S1" in result["SECTOR"]
assert "I1" in result["INDICATOR"]
def test_wildcard_fallback(self):
"""Test wildcard fallback for missing dimensions in compound codes."""
mock_metadata = MagicMock()
mock_metadata.get_dataflow_parameters.return_value = {
"SECTOR": [{"value": "S1", "label": "Sector 1"}],
"INDICATOR": [{"value": "I1", "label": "Indicator 1"}],
"FREQ": [{"value": "A", "label": "Annual"}],
}
mock_metadata.dataflows = {"TEST": {"structureRef": {"id": "DSD_TEST"}}}
mock_metadata.datastructures = {
"DSD_TEST": {
"dimensions": [
{"id": "FREQ", "position": 1},
{"id": "SECTOR", "position": 2},
{"id": "INDICATOR", "position": 3},
]
}
}
# Compound code with missing dimension: S1 (implies wildcard for INDICATOR)
result = detect_indicator_dimensions("TEST", ["S1"], mock_metadata)
assert "SECTOR" in result
assert "S1" in result["SECTOR"]
def test_detailed_error_message(self):
"""Test that error message contains helpful information."""
mock_metadata = MagicMock()
mock_metadata.get_dataflow_parameters.return_value = {
"INDICATOR": [{"value": "GDP", "label": "GDP"}],
}
mock_metadata.dataflows = {}
mock_metadata.datastructures = {}
with pytest.raises(OpenBBError) as exc_info:
detect_indicator_dimensions("TEST", ["INVALID"], mock_metadata)
assert "Invalid indicator code(s)" in str(exc_info.value)
assert "INVALID" in str(exc_info.value)
assert "unrecognized" in str(exc_info.value)
def test_exception_fallback(self):
"""Test fallback when metadata raises exception."""
mock_metadata = MagicMock()
mock_metadata.get_dataflow_parameters.side_effect = KeyError("Not found")
mock_metadata.dataflows = {}
mock_metadata.datastructures = {}
# When metadata fails, the function should raise an error for invalid codes
with pytest.raises(OpenBBError):
detect_indicator_dimensions("TEST", ["GDP", "CPI"], mock_metadata)

View File

@@ -5,6 +5,7 @@ from datetime import date
import pytest
from openbb_core.app.service.user_service import UserService
from openbb_imf.models.available_indicators import ImfAvailableIndicatorsFetcher
from openbb_imf.models.consumer_price_index import ImfConsumerPriceIndexFetcher
from openbb_imf.models.direction_of_trade import ImfDirectionOfTradeFetcher
from openbb_imf.models.economic_indicators import ImfEconomicIndicatorsFetcher
from openbb_imf.models.maritime_chokepoint_info import ImfMaritimeChokePointInfoFetcher
@@ -19,36 +20,45 @@ test_credentials = UserService().default_user_settings.credentials.model_dump(
)
def scrub_string(key):
"""Scrub a string from the response."""
def before_record_response(response):
response["headers"][key] = response["headers"].update({key: None})
return response
return before_record_response
@pytest.fixture(scope="module")
def vcr_config():
"""VCR configuration."""
return {
"filter_headers": [("User-Agent", None)],
"before_record_response": [
scrub_string("Set-Cookie"),
],
}
@pytest.mark.record_http
def test_imf_consumer_price_index_fetcher(credentials=test_credentials):
"""Test the IMF ConsumerPriceIndex fetcher."""
params = {
"country": "JPN",
"frequency": "quarter",
"transform": "yoy",
"expenditure": "total",
"start_date": date(2024, 1, 1),
"end_date": date(2025, 1, 1),
"harmonized": False,
"limit": None,
}
fetcher = ImfConsumerPriceIndexFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_imf_economic_indicators_fetcher(credentials=test_credentials):
"""Test the IMF EconomicIndicators fetcher."""
params = {
"country": "JP",
"frequency": "month",
"symbol": "RAMFDA_USD",
"country": "JPN",
"frequency": "quarter",
"symbol": "IL::RGV_REVS",
"start_date": date(2023, 1, 1),
"end_date": date(2023, 12, 31),
"end_date": date(2024, 1, 1),
"limit": None,
"transform": None,
"dimension_values": None,
}
fetcher = ImfEconomicIndicatorsFetcher()
@@ -59,7 +69,7 @@ def test_imf_economic_indicators_fetcher(credentials=test_credentials):
# The data for this request are local files, so we can't record them.
def test_imf_available_indicators_fetcher(credentials=test_credentials):
"""Test the IMF Available Indicators fetcher."""
params = {}
params = {"query": "gold+volume"}
fetcher = ImfAvailableIndicatorsFetcher()
result = fetcher.test(params, credentials)
@@ -70,12 +80,13 @@ def test_imf_available_indicators_fetcher(credentials=test_credentials):
def test_imf_direction_of_trade_fetcher(credentials=test_credentials):
"""Test the ImfDirectionOfTrade fetcher."""
params = {
"country": "us",
"counterpart": "world,eu",
"country": "USA",
"counterpart": "G001,G998",
"frequency": "annual",
"direction": "exports",
"start_date": date(2020, 1, 1),
"end_date": date(2023, 1, 1),
"start_date": date(2023, 1, 1),
"end_date": date(2025, 1, 1),
"limit": None,
}
fetcher = ImfDirectionOfTradeFetcher()

View File

@@ -0,0 +1,155 @@
"""Test ImfMetadata search functionality."""
# ruff: noqa: I001
# pylint: disable=W0621,R0903
import pytest
from openbb_imf.utils import metadata as md
MOCK_DATAFLOWS = [
{
"id": "BOP",
"name": "Balance of Payments",
"description": "International Monetary Fund balance of payments data.",
"structureRef": {"id": "DSD_BOP"},
},
{
"id": "CPI",
"name": "Consumer Price Index",
"description": "International Monetary Fund CPI dataset.",
"structureRef": {"id": "DSD_CPI"},
},
{
"id": "GOV",
"name": "Government Finance Statistics",
"description": "Statistics produced by the IMF.",
"structureRef": {"id": "DSD_GOV"},
},
]
@pytest.fixture
def imf_metadata(monkeypatch):
"""Provide a minimal ImfMetadata with canned dataflows and parameters."""
monkeypatch.setattr(md.ImfMetadata, "_instance", None)
monkeypatch.setattr(md.ImfMetadata, "_load_from_cache", lambda self: True)
meta = md.ImfMetadata()
meta.dataflows = {d["id"]: d for d in MOCK_DATAFLOWS}
meta.dataflow_groups = {"IMF.STA": MOCK_DATAFLOWS}
def _fake_get_dataflow_parameters(dataflow_id: str):
if dataflow_id not in meta.dataflows:
raise ValueError(f"Dataflow '{dataflow_id}' not found.")
# Return a lightweight, deterministic parameter mapping.
return {
"COUNTRY": [
{"value": "USA", "label": "United States"},
{"value": "MEX", "label": "Mexico"},
],
"FREQUENCY": [
{"value": "A", "label": "Annual"},
{"value": "Q", "label": "Quarterly"},
],
"TIME_PERIOD": [
{"value": "YYYY", "label": "Year (Start Date: 1980, End Date: 2024)"},
{
"value": "YYYY-MM",
"label": "Month (Start Date: 1990-01, End Date: 2024-12)",
},
{
"value": "YYYY-QQ",
"label": "Quarter (Start Date: 1990-Q1, End Date: 2024-Q4)",
},
{
"value": "YYYY-SS",
"label": "Semester (Start Date: 1990-S1, End Date: 2024-S2)",
},
{"value": "START", "label": "Start Date: 1990"},
{"value": "END", "label": "End Date: 2024"},
],
}
meta.get_dataflow_parameters = _fake_get_dataflow_parameters
return meta
def test_search_datasets_by_id(imf_metadata):
query = "BOP"
results = imf_metadata.search_dataflows(query)
assert results
assert any(
query.lower() in df["id"].lower()
for group in results
for df in group["dataflows"]
)
def test_search_datasets_by_name(imf_metadata):
query = "Balance of Payments"
results = imf_metadata.search_dataflows(query)
assert results
assert any(
query.lower() in df["name"].lower()
for group in results
for df in group["dataflows"]
)
def test_search_datasets_by_description(imf_metadata):
query = "International Monetary Fund"
results = imf_metadata.search_dataflows(query)
assert results
assert any(
query.lower() in df["description"].lower()
for group in results
for df in group["dataflows"]
)
def test_search_datasets_no_match(imf_metadata):
query = "nonexistent_dataflow_xyz"
results = imf_metadata.search_dataflows(query)
assert results == []
def test_get_dataflow_parameters_cpi(imf_metadata):
dataflow_id = "CPI"
parameters = imf_metadata.get_dataflow_parameters(dataflow_id)
assert "COUNTRY" in parameters
assert "FREQUENCY" in parameters
assert "TIME_PERIOD" in parameters
assert isinstance(parameters["COUNTRY"], list)
assert isinstance(parameters["FREQUENCY"], list)
assert isinstance(parameters["TIME_PERIOD"], list)
assert any(option["value"] == "USA" for option in parameters["COUNTRY"])
assert any(option["value"] == "A" for option in parameters["FREQUENCY"])
assert any(option["value"] == "YYYY" for option in parameters["TIME_PERIOD"])
def test_get_dataflow_parameters_invalid_dataflow(imf_metadata):
dataflow_id = "INVALID_DATAFLOW"
with pytest.raises(ValueError, match=f"Dataflow '{dataflow_id}' not found."):
imf_metadata.get_dataflow_parameters(dataflow_id)
def test_get_dataflow_parameters_time_period_options(imf_metadata):
dataflow_id = "CPI"
parameters = imf_metadata.get_dataflow_parameters(dataflow_id)
time_period_options = parameters.get("TIME_PERIOD")
assert time_period_options is not None
assert len(time_period_options) == 6
assert any(option["value"] == "YYYY" for option in time_period_options)
assert any(option["value"] == "YYYY-MM" for option in time_period_options)
assert any(option["value"] == "YYYY-QQ" for option in time_period_options)
assert any(option["value"] == "YYYY-SS" for option in time_period_options)
assert any("Start Date:" in option["label"] for option in time_period_options)
assert any("End Date:" in option["label"] for option in time_period_options)

View File

@@ -0,0 +1,190 @@
"""Test ImfProgressiveQueryHelper."""
# ruff: noqa: I001
# pylint: disable=W0621,W0613,W0212,R0903
from unittest.mock import patch, MagicMock
import pytest
from openbb_imf.utils.progressive_helper import ImfParamsBuilder
@pytest.fixture
def mock_builder():
"""Mock ImfQueryBuilder."""
with patch(
"openbb_imf.utils.progressive_helper.ImfQueryBuilder"
) as mock_builder_class:
mock_instance = mock_builder_class.return_value
# Configure metadata mock
mock_instance.metadata = MagicMock()
mock_instance.metadata.dataflows = {
"GFS_BS": {
"name": "Government Finance Statistics, Balance Sheet",
"structureRef": {"id": "IMF_GFS_BS"},
"agencyID": "IMF.STA",
}
}
mock_instance.metadata.datastructures = {
"IMF_GFS_BS": {
"id": "IMF_GFS_BS",
"dimensions": [
{"id": "FREQ", "position": 1},
{"id": "REF_AREA", "position": 2},
{
"id": "INDICATOR",
"position": 3,
"conceptRef": {"id": "INDICATOR"},
},
],
}
}
mock_instance.metadata.codelists = {
"CL_FREQ": {"codes": [{"id": "A", "name": "Annual"}]},
"CL_REF_AREA": {"codes": [{"id": "US", "name": "United States"}]},
"CL_GFS_INDICATOR": {
"codes": [{"id": "GG_XDC_G01_XDC_P1_B9", "name": "Gross debt"}]
},
}
def get_codelist_map(
codelist_id, agency_id=None, dataflow_id=None
): # noqa: ARG001
if codelist_id in mock_instance.metadata.codelists:
return {
item["id"]: item["name"]
for item in mock_instance.metadata.codelists[codelist_id].get(
"codes", []
)
}
return {}
mock_instance.metadata._get_codelist_map.side_effect = get_codelist_map
def resolve_codelist_id(dataflow_id, dsd_id, dimension_id, dim_meta):
# Simplified resolver for the mocked DSD
if dimension_id == "FREQ":
return "CL_FREQ"
if dimension_id == "REF_AREA":
return "CL_REF_AREA"
if dimension_id == "INDICATOR":
return "CL_GFS_INDICATOR"
return None
mock_instance.metadata._resolve_codelist_id.side_effect = resolve_codelist_id
mock_instance.metadata.get_available_constraints.return_value = {
"key_values": [
{
"id": "INDICATOR",
"values": ["GG_XDC_G01_XDC_P1_B9"],
}
]
}
yield mock_instance
def test_progressive_helper_gfs_indicator(mock_builder):
"""Test that the progressive helper can find specific codelists."""
helper = ImfParamsBuilder(dataflow_id="GFS_BS")
helper._builder = mock_builder
options = helper.get_options_for_dimension("INDICATOR")
assert options
assert len(options) == 1
assert options[0]["value"] == "GG_XDC_G01_XDC_P1_B9"
assert options[0]["label"] == "Gross debt"
mock_builder.metadata._get_codelist_map.assert_called_with(
"CL_GFS_INDICATOR", "IMF.STA", "GFS_BS"
)
def test_constrained_codelist_resolution(mock_builder):
"""Test that options are filtered based on constraints."""
helper = ImfParamsBuilder(dataflow_id="GFS_BS")
helper._builder = mock_builder
# Mock constraints returning only one indicator
mock_builder.metadata.get_available_constraints.return_value = {
"key_values": [
{
"id": "INDICATOR",
"values": ["GG_XDC_G01_XDC_P1_B9"],
}
]
}
options = helper.get_options_for_dimension("INDICATOR")
assert len(options) == 1
assert options[0]["value"] == "GG_XDC_G01_XDC_P1_B9"
def test_component_label_fallback(mock_builder):
"""Test fallback when label is missing in codelist."""
helper = ImfParamsBuilder(dataflow_id="GFS_BS")
helper._builder = mock_builder
# Add a code to constraints that isn't in the codelist map
mock_builder.metadata.get_available_constraints.return_value = {
"key_values": [
{
"id": "INDICATOR",
"values": ["UNKNOWN_CODE"],
}
]
}
# Mock empty codelist map for this test
mock_builder.metadata._get_codelist_map.return_value = {}
options = helper.get_options_for_dimension("INDICATOR")
assert len(options) == 1
assert options[0]["value"] == "UNKNOWN_CODE"
assert options[0]["label"] == "UNKNOWN_CODE" # Fallback to value
def test_time_period_annotation_propagation(mock_builder):
"""Test that time period annotations are propagated."""
helper = ImfParamsBuilder(dataflow_id="GFS_BS")
helper._builder = mock_builder
mock_builder.metadata.get_available_constraints.return_value = {
"key_values": [],
"time_period": {"start_period": "2000", "end_period": "2020"},
}
# Trigger a constraint update (e.g. by getting options)
helper.get_options_for_dimension("INDICATOR")
assert helper._last_constraints_response["time_period"]["start_period"] == "2000"
assert helper._last_constraints_response["time_period"]["end_period"] == "2020"
def test_case_insensitive_dimension_keys(mock_builder):
"""Test that dimension keys are case-insensitive."""
helper = ImfParamsBuilder(dataflow_id="GFS_BS")
helper._builder = mock_builder
options = helper.get_options_for_dimension("INDICATOR")
assert options
assert options[0]["value"] == "GG_XDC_G01_XDC_P1_B9"
def test_wildcard_on_long_multi_code_inputs(mock_builder):
"""Test that long multi-code inputs trigger wildcard behavior."""
helper = ImfParamsBuilder(dataflow_id="GFS_BS")
helper._builder = mock_builder
# Simulate a long list of codes selected for a previous dimension
long_list = ["A"] * 50
helper.set_dimension(("FREQ", "+".join(long_list)))
helper.get_options_for_dimension("REF_AREA")
call_args = mock_builder.metadata.get_available_constraints.call_args
assert call_args is not None
key_arg = call_args.kwargs.get("key")
assert key_arg is not None
# Key should include the long frequency string, not be blanked to wildcard
assert "+".join(long_list) in key_arg

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,261 @@
"""Comprehensive tests for IMF query parsing, search, and fetch helpers."""
# ruff: noqa: I001, SLF001
# pylint: disable=W0621,W0613,W0212,R0903
from textwrap import dedent
from unittest.mock import MagicMock, patch
import pandas as pd
import pytest
from openbb_imf.utils import metadata as md
from openbb_imf.utils.query_builder import ImfQueryBuilder
MOCK_SDMX_CHUNKS_DATAFLOWS = [
{
"id": "DATAFLOW_A",
"name": "Dataflow A Name",
"description": "Description for dataflow A with keyword gold.",
"structureRef": {"id": "DSD_A"},
},
{
"id": "DATAFLOW_B",
"name": "Dataflow B Name with reserves",
"description": "Another description.",
"structureRef": {"id": "DSD_B"},
},
{
"id": "DATAFLOW_GOLD_STATISTICS",
"name": "Gold Statistics",
"description": "Comprehensive data on gold holdings and reserves.",
"structureRef": {"id": "DSD_GOLD"},
},
{
"id": "DATAFLOW_CENTRAL_BANK",
"name": "Central Bank Operations",
"description": "Data on central bank activities.",
"structureRef": {"id": "DSD_CENTRAL"},
},
]
MOCK_DATAFLOW_GROUPS = {
"IMF.STA": [
{
"id": "DATAFLOW_C",
"name": "Central Bank Data",
"description": "Data related to central banks.",
"structureRef": {"id": "DSD_CENTRAL"},
},
{
"id": "DATAFLOW_D",
"name": "Gold Reserves Statistics",
"description": "Statistics on gold reserves.",
"structureRef": {"id": "DSD_GOLD"},
},
{
"id": "DATAFLOW_E",
"name": "Economic Indicators",
"description": "Various economic indicators.",
"structureRef": {"id": "DSD_ECON"},
},
]
}
MOCK_DATASTRUCTURES = [
{
"id": "DSD_TEST",
"dimensions": [
{"id": "COUNTRY", "position": 0, "conceptRef": {"id": "COUNTRY"}},
{"id": "INDICATOR", "position": 1, "conceptRef": {"id": "INDICATOR"}},
{"id": "TIME_PERIOD", "position": 2, "conceptRef": {"id": "TIME_PERIOD"}},
],
}
]
MOCK_DATAFLOWS_FOR_PIVOT_TEST = {
"TEST_DATAFLOW": {
"id": "TEST_DATAFLOW",
"name": "Test Dataflow",
"description": "A dataflow for testing pivoting.",
"structureRef": {"id": "DSD_TEST"},
"agencyID": "IMF.STA",
"presentations": [
{
"presentation_title": "Indicators by Country",
"presentation_description": "Data pivoted by indicator for each country.",
}
],
}
}
MOCK_CONCEPTSCHEMES = {}
MOCK_DATASET_ID_MAPPING = {}
MOCK_IMF_COUNTRY_MAP = {}
@pytest.fixture
def imf_metadata(monkeypatch):
"""Provide an isolated ImfMetadata instance with canned data."""
monkeypatch.setattr(md.ImfMetadata, "_instance", None)
monkeypatch.setattr(md.ImfMetadata, "_load_from_cache", lambda self: True)
meta = md.ImfMetadata()
meta.dataflows = {d["id"]: d for d in MOCK_SDMX_CHUNKS_DATAFLOWS}
meta.dataflow_groups = MOCK_DATAFLOW_GROUPS
return meta
def test_parse_query_variants(imf_metadata):
assert imf_metadata._parse_query("gold") == [["gold"]]
assert imf_metadata._parse_query('"central bank"') == [["central bank"]]
assert imf_metadata._parse_query("gold + reserves") == [["gold", "reserves"]]
assert imf_metadata._parse_query("gold | reserves") == [["gold"], ["reserves"]]
assert imf_metadata._parse_query('gold + reserves | "central bank"') == [
["gold", "reserves"],
["central bank"],
]
assert imf_metadata._parse_query("") == []
assert imf_metadata._parse_query(" ") == []
def test_search_dataflows_by_keywords(imf_metadata):
results = imf_metadata.search_dataflows("gold")
ids = {df["id"] for group in results for df in group.get("dataflows", [])}
assert "DATAFLOW_A" in ids
assert "DATAFLOW_GOLD_STATISTICS" in ids
def test_search_dataflows_or_operator(imf_metadata):
results = imf_metadata.search_dataflows("DATAFLOW_A | DATAFLOW_B")
ids = {df["id"] for group in results for df in group.get("dataflows", [])}
assert ids == {"DATAFLOW_A", "DATAFLOW_B"}
def test_search_dataflows_phrase(imf_metadata):
results = imf_metadata.search_dataflows('"central bank"')
ids = {df["id"] for group in results for df in group.get("dataflows", [])}
# Central bank phrase should match at least the explicit central bank flow.
assert "DATAFLOW_CENTRAL_BANK" in ids
@pytest.fixture
def mock_imf_query_builder():
"""Return an ImfQueryBuilder wired with canned metadata."""
with patch("openbb_imf.utils.query_builder.ImfMetadata") as MockMetadata:
mock_metadata_instance = MockMetadata.return_value
dataflows_dict = {d["id"]: d for d in MOCK_SDMX_CHUNKS_DATAFLOWS}
dataflows_dict.update(MOCK_DATAFLOWS_FOR_PIVOT_TEST)
mock_metadata_instance.dataflows = dataflows_dict
datastructures_dict = {d["id"]: d for d in MOCK_DATASTRUCTURES}
mock_metadata_instance.datastructures = datastructures_dict
mock_metadata_instance.conceptschemes = MOCK_CONCEPTSCHEMES
mock_metadata_instance.dataflow_groups = MOCK_DATAFLOW_GROUPS
mock_metadata_instance.dataset_id_mapping = MOCK_DATASET_ID_MAPPING
mock_metadata_instance.imf_country_map = MOCK_IMF_COUNTRY_MAP
yield ImfQueryBuilder()
MOCK_XML_RESPONSE = dedent(
"""
<message:StructureSpecificData
xmlns:message="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/message"
xmlns:common="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/common"
xmlns:ss="http://www.sdmx.org/resources/sdmxml/schemas/v3_0/data/structurespecific">
<message:DataSet>
<Series COUNTRY="US" INDICATOR="GDP">
<Obs TIME_PERIOD="2020" OBS_VALUE="10" />
<Obs TIME_PERIOD="2021" OBS_VALUE="12" />
</Series>
<Series COUNTRY="US" INDICATOR="CPI">
<Obs TIME_PERIOD="2020" OBS_VALUE="100" />
<Obs TIME_PERIOD="2021" OBS_VALUE="120" />
</Series>
</message:DataSet>
</message:StructureSpecificData>
"""
)
@pytest.fixture
def mock_imf_query_builder_with_pivot_data():
"""Return an ImfQueryBuilder with request mocked to return canned XML."""
with patch("openbb_imf.utils.query_builder.ImfMetadata") as MockMetadata, patch(
"openbb_core.provider.utils.helpers.make_request"
) as mock_make_request:
mock_metadata_instance = MockMetadata.return_value
dataflows_dict = {d["id"]: d for d in MOCK_SDMX_CHUNKS_DATAFLOWS}
dataflows_dict.update(MOCK_DATAFLOWS_FOR_PIVOT_TEST)
mock_metadata_instance.dataflows = dataflows_dict
datastructures_dict = {d["id"]: d for d in MOCK_DATASTRUCTURES}
mock_metadata_instance.datastructures = datastructures_dict
mock_metadata_instance.conceptschemes = MOCK_CONCEPTSCHEMES
mock_metadata_instance.dataflow_groups = MOCK_DATAFLOW_GROUPS
mock_metadata_instance.get_dataflow_parameters.return_value = {
"COUNTRY": [{"value": "US", "label": "United States"}],
"INDICATOR": [
{"value": "GDP", "label": "Gross Domestic Product"},
{"value": "CPI", "label": "Consumer Price Index"},
],
"TIME_PERIOD": [
{"value": "2020", "label": "2020"},
{"value": "2021", "label": "2021"},
],
}
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.text = MOCK_XML_RESPONSE
mock_response.raise_for_status.return_value = None
mock_make_request.return_value = mock_response
builder = ImfQueryBuilder()
yield builder
def test_fetch_data_structure(mock_imf_query_builder_with_pivot_data):
builder = mock_imf_query_builder_with_pivot_data
result = builder.fetch_data(
"TEST_DATAFLOW", COUNTRY="US", INDICATOR="GDP+CPI", _skip_validation=True
)
assert "data" in result and "metadata" in result
df = pd.DataFrame(result["data"])
expected_cols = {
"COUNTRY",
"country_code",
"INDICATOR",
"INDICATOR_code",
"series_id",
"TIME_PERIOD",
"OBS_VALUE",
}
assert expected_cols.issubset(set(df.columns))
assert set(df["INDICATOR_code"]) == {"GDP", "CPI"}
def test_fetch_data_time_periods(mock_imf_query_builder_with_pivot_data):
builder = mock_imf_query_builder_with_pivot_data
result = builder.fetch_data(
"TEST_DATAFLOW", COUNTRY="US", INDICATOR="GDP+CPI", _skip_validation=True
)
df = pd.DataFrame(result["data"])
# Returned SDMX time periods include end-of-year date strings.
assert set(df["TIME_PERIOD"]) == {"2020-12-31", "2021-12-31"}
def test_strict_error_missing_dataflow(mock_imf_query_builder):
builder = mock_imf_query_builder
with pytest.raises(ValueError, match="Dataflow 'MISSING' not found"):
builder.build_url("MISSING")

View File

@@ -0,0 +1,791 @@
"""Tests for IMF Table Builder."""
# ruff: noqa: I001
# pylint: disable=W0621,W0613,W0212,R0903,C0302,C0415
from unittest.mock import MagicMock, patch
import pytest
class TestImfTableBuilder:
"""Tests for ImfTableBuilder class."""
@pytest.fixture
def mock_query_builder(self):
"""Mock ImfQueryBuilder for table builder tests."""
with patch(
"openbb_imf.utils.query_builder.ImfQueryBuilder"
) as MockQueryBuilder:
mock_instance = MockQueryBuilder.return_value
mock_instance.metadata = MagicMock()
mock_instance.dataflows = {
"BOP": {
"id": "BOP",
"name": "Balance of Payments",
"structureRef": {"id": "IMF_BOP"},
}
}
mock_instance.validate_dimension_constraints = MagicMock()
yield MockQueryBuilder
def test_table_builder_instantiation(self, mock_query_builder):
"""Test that table builder can be instantiated."""
from openbb_imf.utils.table_builder import ImfTableBuilder
builder = ImfTableBuilder()
assert builder is not None
assert builder.query_builder is not None
def test_validate_dimension_constraints_delegates(self, mock_query_builder):
"""Test that dimension validation delegates to query builder."""
from openbb_imf.utils.table_builder import ImfTableBuilder
builder = ImfTableBuilder()
builder._validate_dimension_constraints("BOP", COUNTRY="USA")
mock_query_builder.return_value.validate_dimension_constraints.assert_called_once_with(
"BOP", COUNTRY="USA"
)
class TestTableIdParsing:
"""Tests for table ID parsing logic."""
@pytest.fixture
def mock_query_builder(self):
"""Mock ImfQueryBuilder."""
with patch(
"openbb_imf.utils.query_builder.ImfQueryBuilder"
) as MockQueryBuilder:
mock_instance = MockQueryBuilder.return_value
mock_instance.metadata = MagicMock()
mock_instance.dataflows = {"BOP": {"id": "BOP"}}
mock_instance.validate_dimension_constraints = MagicMock()
yield MockQueryBuilder
def test_table_id_with_dataflow_prefix(self, mock_query_builder):
"""Test parsing table_id with dataflow::table_id format."""
from openbb_imf.utils.table_builder import ImfTableBuilder
builder = ImfTableBuilder()
# Test that table_id parsing handles dataflow::table_id format
# The format "BOP::H_BOP_STANDARD" should be parsed correctly
# This is a smoke test to ensure the builder can be called with this format
# Actual parsing is validated via integration tests
assert builder.query_builder is not None
# Test signature accepts table_id with special characters
import inspect
sig = inspect.signature(builder.get_table)
assert "table_id" in sig.parameters
class TestHierarchyDetection:
"""Tests for hierarchy/table detection in symbols."""
def test_table_id_starts_with_h(self):
"""Test that H_ prefix identifies table IDs."""
# Table IDs start with H_
table_ids = [
"H_BOP_BOP_AGG_STANDARD_PRESENTATION",
"H_IRFCL_TOTAL_RESERVES",
"H_GFS_EXPENSE",
]
for tid in table_ids:
assert tid.startswith("H_"), f"{tid} should start with H_"
def test_indicator_ids_no_h_prefix(self):
"""Test that indicator IDs don't start with H_."""
indicator_ids = [
"CD_T",
"DB_T",
"PCPI_IX",
"BM_MAI",
"GDP",
]
for ind in indicator_ids:
assert not ind.startswith("H_"), f"{ind} should not start with H_"
class TestTableDataStructure:
"""Tests for expected table data structure."""
def test_table_data_contains_hierarchy_fields(self):
"""Test that table data includes hierarchy metadata."""
# Expected fields in table data
expected_fields = [
"order",
"level",
"parent_id",
"series_id",
"title",
"TIME_PERIOD",
"OBS_VALUE",
]
# Create sample table data
sample_row = {
"order": 1,
"level": 0,
"parent_id": None,
"series_id": "IMF_BOP_SERIES",
"COUNTRY": "United States",
"country_code": "USA",
"title": "Current Account Balance",
"TIME_PERIOD": "2024-12-31",
"OBS_VALUE": -300000000000.0,
}
for field in expected_fields:
assert field in sample_row, f"Missing field: {field}"
class TestTableBuilderWithMockedMetadata:
"""Tests with fully mocked metadata."""
@pytest.fixture
def mock_dependencies(self):
"""Mock all dependencies for table builder."""
class FakeImfParamsBuilder:
"""Lightweight stand-in for ImfParamsBuilder used in table tests."""
def __init__(self, dataflow: str): # noqa: ARG002
self._dimensions = ["COUNTRY", "INDICATOR"]
self._selections = {d: None for d in self._dimensions}
def _get_dimensions_in_order(self):
return list(self._dimensions)
def get_options_for_dimension(self, dim_id):
if dim_id.upper() == "COUNTRY":
return [{"value": "US", "label": "United States"}]
if dim_id.upper() == "INDICATOR":
return [
{"value": "CAB", "label": "CAB"},
{"value": "GOODS", "label": "GOODS"},
{"value": "IND", "label": "IND"},
{"value": "IND_XDC", "label": "IND_XDC"},
]
return [{"value": "*", "label": "*"}]
def set_dimension(self, dim_tuple):
dim_id, value = dim_tuple
self._selections[dim_id] = value
return self._selections
def get_next_dimension_to_select(self):
for dim in self._dimensions:
if self._selections.get(dim) is None:
return dim
return None
with patch(
"openbb_imf.utils.query_builder.ImfQueryBuilder"
) as MockQueryBuilder, patch(
"openbb_imf.utils.progressive_helper.ImfParamsBuilder",
FakeImfParamsBuilder,
):
mock_qb = MockQueryBuilder.return_value
# Mock metadata
mock_qb.metadata = MagicMock()
mock_qb.metadata.get_table_in.return_value = {
"id": "H_BOP_STANDARD",
"title": "Balance of Payments Standard",
"hierarchy": [
{
"order": 1,
"level": 0,
"id": "CAB",
"parent_id": None,
"title": "Current Account",
"series_id": "CAB_SERIES",
"dimension_values": {"INDICATOR": ["CAB"]},
},
{
"order": 2,
"level": 1,
"id": "GOODS",
"parent_id": "CAB",
"title": "Goods",
"series_id": "GOODS_SERIES",
"dimension_values": {"INDICATOR": ["GOODS"]},
},
],
}
# Mock validation
mock_qb.validate_dimension_constraints = MagicMock()
# Mock dataflows
mock_qb.dataflows = {
"BOP": {
"id": "BOP",
"name": "Balance of Payments",
"structureRef": {"id": "IMF_BOP"},
}
}
yield MockQueryBuilder
def test_get_table_returns_expected_structure(self, mock_dependencies):
"""Test that get_table method exists and has correct signature."""
import inspect
from openbb_imf.utils.table_builder import ImfTableBuilder
builder = ImfTableBuilder()
# Verify get_table method exists and has expected parameters
assert hasattr(builder, "get_table")
sig = inspect.signature(builder.get_table)
# Check required parameters exist
params = sig.parameters
assert "dataflow" in params or "table_id" in params
# Verify the builder is properly initialized
assert builder.query_builder is not None
assert builder.metadata is not None
def test_hierarchy_to_dimension_mapping(self, mock_dependencies):
"""Test that hierarchy codes are mapped to dimensions."""
from openbb_imf.utils.table_builder import ImfTableBuilder
# We need to mock `fetch_data` to return something that `get_table` can process
# `get_table` calls `fetch_data` on query_builder.
mock_qb = mock_dependencies.return_value
mock_qb.fetch_data.return_value = {
"data": [
{
"series_id": "CAB_SERIES",
"INDICATOR_code": "CAB",
"indicator_code": "CAB",
"TIME_PERIOD": "2020",
"OBS_VALUE": 100,
},
{
"series_id": "GOODS_SERIES",
"INDICATOR_code": "GOODS",
"indicator_code": "GOODS",
"TIME_PERIOD": "2020",
"OBS_VALUE": 50,
},
],
"metadata": {},
}
# Mock table structure
mock_qb.metadata.get_dataflow_table_structure.return_value = {
"hierarchy_id": "H_BOP_STANDARD",
"hierarchy_name": "Balance of Payments Standard",
"hierarchy_description": "",
"dataflow_id": "BOP",
"codelist_id": "CL_INDICATOR",
"agency_id": "IMF",
"version": "1.0",
"total_groups": 2,
"type": "presentation",
"indicators": [
{
"order": 1,
"level": 0,
"id": "CAB",
"parent_id": None,
"title": "Current Account",
"series_id": "CAB_SERIES",
"indicator_code": "CAB",
"dimension_values": {"INDICATOR": ["CAB"]},
"dimension_id": "INDICATOR",
},
{
"order": 2,
"level": 1,
"id": "GOODS",
"parent_id": "CAB",
"title": "Goods",
"series_id": "GOODS_SERIES",
"indicator_code": "GOODS",
"dimension_values": {"INDICATOR": ["GOODS"]},
"dimension_id": "INDICATOR",
},
],
}
builder = ImfTableBuilder()
# Call get_table
result = builder.get_table("BOP", "H_BOP_STANDARD", COUNTRY="US")
rows_by_series = {row["series_id"]: row for row in result["data"]}
assert set(rows_by_series) == {"CAB_SERIES", "GOODS_SERIES"}
assert rows_by_series["CAB_SERIES"]["order"] == 1
assert rows_by_series["CAB_SERIES"]["level"] == 0
assert rows_by_series["GOODS_SERIES"]["order"] == 2
assert rows_by_series["GOODS_SERIES"]["level"] == 1
def test_indicator_list_truncation_and_post_filtering(self, mock_dependencies):
"""Test that long indicator lists are truncated and post-filtered."""
from openbb_imf.utils.table_builder import ImfTableBuilder
mock_qb = mock_dependencies.return_value
# Create a large hierarchy
indicators = []
for i in range(100):
indicators.append(
{
"order": i,
"level": 0,
"id": f"IND_{i}",
"parent_id": None,
"title": f"Indicator {i}",
"series_id": f"SERIES_{i}",
"indicator_code": f"IND_{i}",
"dimension_values": {"INDICATOR": [f"IND_{i}"]},
"dimension_id": "INDICATOR",
}
)
mock_qb.metadata.get_dataflow_table_structure.return_value = {
"hierarchy_id": "H_LARGE",
"hierarchy_name": "Large Table",
"hierarchy_description": "",
"dataflow_id": "BOP",
"codelist_id": "CL_INDICATOR",
"agency_id": "IMF",
"version": "1.0",
"total_groups": 100,
"type": "presentation",
"indicators": indicators,
}
# Empty data is acceptable; this test focuses on request parameters
mock_qb.fetch_data.return_value = {"data": [], "metadata": {}}
builder = ImfTableBuilder()
builder.get_table("BOP", "H_LARGE", COUNTRY="US")
# Check that fetch_data was called with wildcard or truncated list
call_args = mock_qb.fetch_data.call_args
assert call_args is not None
kwargs = call_args[1]
# We can check that the indicator parameter is bounded to a reasonable length
# (table builder uses a 1500-character safeguard in fallback path).
if "INDICATOR" in kwargs:
assert len(kwargs["INDICATOR"]) < 1500
def test_prefix_matching_suffixed_indicators(self, mock_dependencies):
"""Test matching of indicators with suffixes."""
from openbb_imf.utils.table_builder import ImfTableBuilder
mock_qb = mock_dependencies.return_value
mock_qb.metadata.get_dataflow_table_structure.return_value = {
"hierarchy_id": "H_SUFFIX",
"hierarchy_name": "Suffix Table",
"hierarchy_description": "",
"dataflow_id": "BOP",
"codelist_id": "CL_INDICATOR",
"agency_id": "IMF",
"version": "1.0",
"total_groups": 1,
"type": "presentation",
"indicators": [
{
"order": 1,
"level": 0,
"id": "IND",
"parent_id": None,
"title": "Indicator",
"series_id": "IND_SERIES",
"indicator_code": "IND",
"dimension_values": {"INDICATOR": ["IND"]}, # Base code
"dimension_id": "INDICATOR",
}
],
}
# Mock fetch_data returning suffixed version (e.g. IND_XDC)
mock_qb.fetch_data.return_value = {
"data": [
{
"series_id": "IND_XDC",
"INDICATOR_code": "IND_XDC",
"indicator_code": "IND_XDC",
"TIME_PERIOD": "2020",
"OBS_VALUE": 100,
}
],
"metadata": {},
}
# We need to ensure the builder can map IND_XDC back to IND hierarchy entry
# This usually happens if the builder is smart enough to match prefix.
builder = ImfTableBuilder()
result = builder.get_table("BOP", "H_SUFFIX", COUNTRY="US")
row = result["data"][0]
assert row["series_id"] == "IND_XDC"
assert row["order"] == 1
assert row["level"] == 0
def test_time_range_validation_in_table_flow(self, mock_dependencies):
"""Test that time range validation occurs in table flow."""
from openbb_imf.utils.table_builder import ImfTableBuilder
mock_qb = mock_dependencies.return_value
mock_qb.metadata.get_dataflow_table_structure.return_value = {
"hierarchy_id": "H_BOP_STANDARD",
"hierarchy_name": "Balance of Payments Standard",
"hierarchy_description": "",
"dataflow_id": "BOP",
"codelist_id": "CL_INDICATOR",
"agency_id": "IMF",
"version": "1.0",
"total_groups": 1,
"type": "presentation",
"indicators": [
{
"order": 1,
"level": 0,
"id": "CAB",
"parent_id": None,
"title": "Current Account",
"series_id": "CAB_SERIES",
"indicator_code": "CAB",
"dimension_values": {"INDICATOR": ["CAB"]},
"dimension_id": "INDICATOR",
}
],
}
mock_qb.fetch_data.return_value = {
"data": [
{
"series_id": "CAB_SERIES",
"INDICATOR_code": "CAB",
"indicator_code": "CAB",
"TIME_PERIOD": "2020",
"OBS_VALUE": 100,
}
],
"metadata": {},
}
builder = ImfTableBuilder()
builder.get_table(
"BOP", "H_BOP_STANDARD", COUNTRY="US", start_date="2020", end_date="2021"
)
mock_qb.fetch_data.assert_called()
call_kwargs = mock_qb.fetch_data.call_args[1]
assert call_kwargs.get("start_date") == "2020"
assert call_kwargs.get("end_date") == "2021"
class TestBopCompositeHierarchyMatching:
"""Regression tests for BOP hierarchy matching.
These tests are deterministic (no network) and validate that the IMF hierarchy
is treated as the source of truth for parent/child relationships.
"""
@pytest.fixture
def mock_bop_dependencies(self):
"""Mock query builder + params builder for BOP composite matching tests."""
class FakeImfParamsBuilder:
def __init__(self, dataflow: str): # noqa: ARG002
self._dimensions = ["COUNTRY", "INDICATOR", "BOP_ACCOUNTING_ENTRY"]
self._selections = {d: None for d in self._dimensions}
def _get_dimensions_in_order(self):
return list(self._dimensions)
def get_options_for_dimension(self, dim_id):
dim_id = dim_id.upper()
if dim_id == "COUNTRY":
return [{"value": "AU", "label": "Australia"}]
if dim_id == "INDICATOR":
return [
{"value": "SINCEX", "label": "SINCEX"},
{"value": "O", "label": "O"},
]
if dim_id == "BOP_ACCOUNTING_ENTRY":
return [
{"value": "NETCD_T", "label": "Net"},
{"value": "CD_T", "label": "Credit"},
{"value": "DB_T", "label": "Debit"},
{"value": "A_P", "label": "Assets"},
{"value": "L_P", "label": "Liabilities"},
]
return [{"value": "*", "label": "*"}]
def set_dimension(self, dim_tuple):
dim_id, value = dim_tuple
self._selections[dim_id] = value
return self._selections
def get_next_dimension_to_select(self):
for dim in self._dimensions:
if self._selections.get(dim) is None:
return dim
return None
with patch(
"openbb_imf.utils.query_builder.ImfQueryBuilder"
) as MockQueryBuilder, patch(
"openbb_imf.utils.progressive_helper.ImfParamsBuilder",
FakeImfParamsBuilder,
):
mock_qb = MockQueryBuilder.return_value
mock_qb.validate_dimension_constraints = MagicMock()
# Minimal metadata object with required attributes
mock_qb.metadata = MagicMock()
mock_qb.metadata.dataflows = {
"BOP": {
"id": "BOP",
"name": "Balance of Payments",
# Keep structureRef id falsy to avoid datastructure-dependent logic
"structureRef": {"id": ""},
}
}
mock_qb.metadata.datastructures = {}
mock_qb.metadata._codelist_cache = {}
# Ensure builder uses consistent dataflow map
mock_qb.dataflows = mock_qb.metadata.dataflows
# Hierarchy contains:
# - A Net node (NETCD_T)
# - Two leaf nodes with the same indicator_code (SINCEX) under NETCD_T
# - Two leaf nodes with the same indicator_code (O) under A_P and L_P
mock_qb.metadata.get_dataflow_table_structure.return_value = {
"hierarchy_id": "H_BOP_FAKE",
"hierarchy_name": "BOP Fake",
"hierarchy_description": "",
"dataflow_id": "BOP",
"codelist_id": "CL_BOP_INDICATOR",
"agency_id": "IMF",
"version": "1.0",
"total_groups": 7,
"type": "presentation",
"indicators": [
{
"order": 1,
"depth": 0,
"id": "NETCD_T",
"parent_id": None,
"label": "Net (credits less debits)",
"series_id": "",
"indicator_code": "NETCD_T",
"is_group": True,
"dimension_id": "BOP_ACCOUNTING_ENTRY",
},
{
"order": 2,
"depth": 1,
"id": "SINCEX_CD",
"parent_id": "NETCD_T",
"label": "Secondary income excluding exceptional financing",
"series_id": "",
"indicator_code": "SINCEX",
"is_group": False,
"dimension_id": "INDICATOR",
},
{
"order": 3,
"depth": 1,
"id": "SINCEX_DB",
"parent_id": "NETCD_T",
"label": "Secondary income excluding exceptional financing",
"series_id": "",
"indicator_code": "SINCEX",
"is_group": False,
"dimension_id": "INDICATOR",
},
{
"order": 4,
"depth": 0,
"id": "A_P",
"parent_id": None,
"label": "Assets, Positions",
"series_id": "",
"indicator_code": "A_P",
"is_group": True,
"dimension_id": "BOP_ACCOUNTING_ENTRY",
},
{
"order": 5,
"depth": 0,
"id": "L_P",
"parent_id": None,
"label": "Liabilities, Positions",
"series_id": "",
"indicator_code": "L_P",
"is_group": True,
"dimension_id": "BOP_ACCOUNTING_ENTRY",
},
{
"order": 6,
"depth": 1,
"id": "O_A",
"parent_id": "A_P",
"label": "Other investment",
"series_id": "",
"indicator_code": "O",
"is_group": False,
"dimension_id": "INDICATOR",
},
{
"order": 7,
"depth": 1,
"id": "O_L",
"parent_id": "L_P",
"label": "Other investment",
"series_id": "",
"indicator_code": "O",
"is_group": False,
"dimension_id": "INDICATOR",
},
],
}
# Data rows intentionally omit usable series_id to force composite matching
mock_qb.fetch_data.return_value = {
"data": [
{
"series_id": "",
"INDICATOR_code": "SINCEX",
"BOP_ACCOUNTING_ENTRY_code": "CD_T",
"REF_AREA_code": "AU",
"TIME_PERIOD": "2024-12-31",
"OBS_VALUE": 9.86,
},
{
"series_id": "",
"INDICATOR_code": "SINCEX",
"BOP_ACCOUNTING_ENTRY_code": "DB_T",
"REF_AREA_code": "AU",
"TIME_PERIOD": "2024-12-31",
"OBS_VALUE": 10.04,
},
{
"series_id": "",
"INDICATOR_code": "O",
"BOP_ACCOUNTING_ENTRY_code": "A_P",
"REF_AREA_code": "AU",
"TIME_PERIOD": "2024-12-31",
"OBS_VALUE": 1.0,
},
{
"series_id": "",
"INDICATOR_code": "O",
"BOP_ACCOUNTING_ENTRY_code": "L_P",
"REF_AREA_code": "AU",
"TIME_PERIOD": "2024-12-31",
"OBS_VALUE": 2.0,
},
],
"metadata": {},
}
yield MockQueryBuilder
def test_bop_credit_debit_resolves_under_net_parent(self, mock_bop_dependencies):
"""Credit and Debit rows must resolve under the hierarchy's Net parent."""
from openbb_imf.utils.table_builder import ImfTableBuilder
builder = ImfTableBuilder()
result = builder.get_table("BOP", "H_BOP_FAKE", COUNTRY="AU")
rows = [r for r in result["data"] if r.get("INDICATOR_code") == "SINCEX"]
# Expect both Credit and Debit to be kept (not dropped)
assert len(rows) == 2
for row in rows:
# Composite match should use hierarchy parent (NETCD_T), not CD_T/DB_T
assert row.get("parent_code") == "NETCD_T"
assert "excluding exceptional financing" in (row.get("title") or "")
titles = {r.get("title") for r in rows}
assert any(t and t.endswith(", Credit") for t in titles)
assert any(t and t.endswith(", Debit") for t in titles)
def test_bop_assets_liabilities_remain_distinct_paths(self, mock_bop_dependencies):
"""Assets and Liabilities must remain separate hierarchy paths."""
from openbb_imf.utils.table_builder import ImfTableBuilder
builder = ImfTableBuilder()
result = builder.get_table("BOP", "H_BOP_FAKE", COUNTRY="AU")
rows = [r for r in result["data"] if r.get("INDICATOR_code") == "O"]
assert len(rows) == 2
parent_codes = {r.get("parent_code") for r in rows}
assert parent_codes == {"A_P", "L_P"}
titles = {r.get("title") for r in rows}
assert any(t and t.endswith(", Assets") for t in titles)
assert any(t and t.endswith(", Liabilities") for t in titles)
class TestTableBuilderErrorHandling:
"""Tests for error handling in table builder."""
@pytest.fixture
def mock_query_builder(self):
"""Mock ImfQueryBuilder."""
with patch(
"openbb_imf.utils.query_builder.ImfQueryBuilder"
) as MockQueryBuilder:
mock_instance = MockQueryBuilder.return_value
mock_instance.metadata = MagicMock()
mock_instance.dataflows = {"BOP": {"id": "BOP"}}
mock_instance.validate_dimension_constraints = MagicMock()
yield MockQueryBuilder
def test_invalid_dataflow_raises_error(self, mock_query_builder):
"""Test that invalid dimension constraints raises appropriate error."""
from openbb_imf.utils.table_builder import ImfTableBuilder
builder = ImfTableBuilder()
# Mock validation to raise error for invalid dataflow
mock_query_builder.return_value.validate_dimension_constraints.side_effect = (
ValueError("Invalid dataflow: INVALID_DATAFLOW")
)
with pytest.raises(ValueError) as exc_info:
builder._validate_dimension_constraints("INVALID_DATAFLOW", COUNTRY="USA")
assert "Invalid" in str(exc_info.value)
def test_dimension_constraint_validation_error(self, mock_query_builder):
"""Test that invalid dimension values raise validation error."""
from openbb_imf.utils.table_builder import ImfTableBuilder
builder = ImfTableBuilder()
# Mock validation to raise error
mock_query_builder.return_value.validate_dimension_constraints.side_effect = (
ValueError("Invalid country: XYZ")
)
with pytest.raises(ValueError) as exc_info:
builder._validate_dimension_constraints("BOP", COUNTRY="XYZ")
assert (
"Invalid" in str(exc_info.value) or "country" in str(exc_info.value).lower()
)

View File

@@ -325,6 +325,10 @@ def main():
"groupByParamName": "symbol",
},
},
{
"field": "name",
"pinned": "left",
},
],
}
},
@@ -395,6 +399,10 @@ def main():
"groupByParamName": "symbol2",
},
},
{
"field": "name",
"pinned": "left",
},
],
}
},
@@ -560,6 +568,10 @@ def main():
"groupByParamName": "symbol3",
},
},
{
"field": "name",
"pinned": "left",
},
],
}
},

View File

@@ -3,7 +3,7 @@
# pylint: disable=unused-argument
from datetime import date
from typing import Any, Literal
from typing import Any
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.abstract.fetcher import Fetcher
@@ -54,7 +54,7 @@ expenditure_dict_rev = {
}
expenditure_dict = {v: k for k, v in expenditure_dict_rev.items()}
expenditures = tuple(expenditure_dict.keys()) + ("all",)
ExpenditureChoices = Literal[
expenditure_choices = [
"total",
"all",
"actual_rentals",
@@ -87,6 +87,8 @@ ExpenditureChoices = Literal[
"water_supply_other_services",
]
transform_choices = ["index", "yoy", "period"]
class OECDCPIQueryParams(ConsumerPriceIndexQueryParams):
"""OECD CPI Query.
@@ -99,20 +101,22 @@ class OECDCPIQueryParams(ConsumerPriceIndexQueryParams):
"multiple_items_allowed": True,
"choices": CountriesList,
},
"transform": {
"choices": transform_choices,
},
"expenditure": {
"choices": expenditure_choices,
},
}
country: str = Field(
description="Country to get CPI for. This is the list of OECD supported countries",
default="united_states",
)
expenditure: ExpenditureChoices = Field(
expenditure: str = Field(
description="Expenditure component of CPI.",
default="total",
json_schema_extra={"choices": list(expenditures)},
)
@field_validator("country", mode="before", check_fields=False)
def validate_country(cls, c: str): # pylint: disable=E0213
@classmethod
def validate_country(cls, c: str):
"""Validate country."""
result: list = []
values = c.replace(" ", "_").split(",")
@@ -121,6 +125,16 @@ class OECDCPIQueryParams(ConsumerPriceIndexQueryParams):
result.append(v.lower())
return ",".join(result)
@field_validator("expenditure", mode="before", check_fields=False)
@classmethod
def validate_expenditure(cls, v):
"""Validate expenditure."""
if v.lower() not in expenditure_choices:
raise ValueError(
f"Expenditure '{v}' is not a valid choice. Valid choices:\n\n{expenditure_choices}"
)
return v
class OECDCPIData(ConsumerPriceIndexData):
"""OECD CPI Data."""
@@ -156,7 +170,7 @@ class OECDCPIFetcher(Fetcher[OECDCPIQueryParams, list[OECDCPIData]]):
from openbb_oecd.utils import helpers # noqa
methodology = "HICP" if query.harmonized is True else "N"
query.units = "mom" if query.transform == "period" else query.transform
unit = "mom" if query.transform == "period" else query.transform
query.frequency = (
"monthly"
if query.harmonized is True and query.frequency == "quarter"
@@ -167,7 +181,7 @@ class OECDCPIFetcher(Fetcher[OECDCPIQueryParams, list[OECDCPIData]]):
"index": "IX",
"yoy": "PA",
"mom": "PC",
}[query.units]
}[unit]
expenditure = (
"" if query.expenditure == "all" else expenditure_dict[query.expenditure]
)