mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-15 13:51:19 +00:00
[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:
@@ -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."""
|
||||
|
||||
@@ -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)."
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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'.",
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
139
openbb_platform/providers/imf/README.md
vendored
139
openbb_platform/providers/imf/README.md
vendored
@@ -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.
|
||||
|
||||
1
openbb_platform/providers/imf/integration/__init__.py
Normal file
1
openbb_platform/providers/imf/integration/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""IMF Utilities Router Module Integration Tests."""
|
||||
211
openbb_platform/providers/imf/integration/test_imf_utils_api.py
Normal file
211
openbb_platform/providers/imf/integration/test_imf_utils_api.py
Normal 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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
488
openbb_platform/providers/imf/openbb_imf/apps.json
vendored
Normal file
488
openbb_platform/providers/imf/openbb_imf/apps.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}]
|
||||
BIN
openbb_platform/providers/imf/openbb_imf/assets/imf_cache.pkl.gz
vendored
Normal file
BIN
openbb_platform/providers/imf/openbb_imf/assets/imf_cache.pkl.gz
vendored
Normal file
Binary file not shown.
@@ -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"
|
||||
}
|
||||
1130
openbb_platform/providers/imf/openbb_imf/imf_router.py
Normal file
1130
openbb_platform/providers/imf/openbb_imf/imf_router.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
@@ -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")
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.",
|
||||
)
|
||||
@@ -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.",
|
||||
|
||||
@@ -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},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
2170
openbb_platform/providers/imf/openbb_imf/utils/metadata.py
Normal file
2170
openbb_platform/providers/imf/openbb_imf/utils/metadata.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
1454
openbb_platform/providers/imf/openbb_imf/utils/query_builder.py
Normal file
1454
openbb_platform/providers/imf/openbb_imf/utils/query_builder.py
Normal file
File diff suppressed because it is too large
Load Diff
1551
openbb_platform/providers/imf/openbb_imf/utils/table_builder.py
Normal file
1551
openbb_platform/providers/imf/openbb_imf/utils/table_builder.py
Normal file
File diff suppressed because it is too large
Load Diff
2408
openbb_platform/providers/imf/openbb_imf/utils/table_presentation.py
Normal file
2408
openbb_platform/providers/imf/openbb_imf/utils/table_presentation.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
710
openbb_platform/providers/imf/poetry.lock
generated
vendored
710
openbb_platform/providers/imf/poetry.lock
generated
vendored
@@ -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"
|
||||
|
||||
12
openbb_platform/providers/imf/pyproject.toml
vendored
12
openbb_platform/providers/imf/pyproject.toml
vendored
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
329
openbb_platform/providers/imf/tests/test_helpers.py
Normal file
329
openbb_platform/providers/imf/tests/test_helpers.py
Normal 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)
|
||||
@@ -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()
|
||||
|
||||
155
openbb_platform/providers/imf/tests/test_imf_search.py
Normal file
155
openbb_platform/providers/imf/tests/test_imf_search.py
Normal 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)
|
||||
190
openbb_platform/providers/imf/tests/test_progressive_helper.py
Normal file
190
openbb_platform/providers/imf/tests/test_progressive_helper.py
Normal 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
|
||||
1081
openbb_platform/providers/imf/tests/test_progressive_validation.py
Normal file
1081
openbb_platform/providers/imf/tests/test_progressive_validation.py
Normal file
File diff suppressed because it is too large
Load Diff
261
openbb_platform/providers/imf/tests/test_query_builder.py
Normal file
261
openbb_platform/providers/imf/tests/test_query_builder.py
Normal 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")
|
||||
791
openbb_platform/providers/imf/tests/test_table_builder.py
Normal file
791
openbb_platform/providers/imf/tests/test_table_builder.py
Normal 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()
|
||||
)
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user