mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 12:40:35 +00:00
[Feature] Add USDA FAS Agriculture Commodity Endpoints (#7298)
* add endpoint with as provider to return a PDF widget with historical data by commodity, year, and month * lint * add weather bulletins * codespell * codespell * grammar police * better examples * add psd data endpoint * it's 'aggregate_regions', not 'aggregate_region' * codespell * missing annotation * missing unit test * fix a test expectations for the new year
This commit is contained in:
1
.codespell.skip
vendored
1
.codespell.skip
vendored
@@ -24,6 +24,7 @@
|
||||
./openbb_platform/extensions/economy/openbb_economy/economy_views.py
|
||||
./openbb_platform/providers/fmp/openbb_fmp/models/calendar_ipo.py
|
||||
./openbb_platform/providers/imf/openbb_imf/models/consumer_price_index.py
|
||||
./openbb_platform/providers/government_us/openbb_government_us/utils/psd_codes.py
|
||||
./desktop/src-tauri/src/tauri_handlers/startup.rs
|
||||
./desktop/src/routes/environments.tsx
|
||||
./desktop/src/tests/routes/installation-progress.test.tsx
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Commodity Production Supply & Demand Data Standard Model."""
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class CommodityPsdDataQueryParams(QueryParams):
|
||||
"""Commodity Production Supply & Demand Data Query."""
|
||||
|
||||
|
||||
class CommodityPsdData(Data):
|
||||
"""Commodity Production Supply & Demand Data."""
|
||||
|
||||
region: str | None = Field(default=None, description="Region group category.")
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description="Country or area name.",
|
||||
)
|
||||
commodity: str | None = Field(
|
||||
default=None,
|
||||
description="Commodity name.",
|
||||
)
|
||||
attribute: str | None = Field(
|
||||
default=None,
|
||||
description="Name of the row value.",
|
||||
)
|
||||
marketing_year: str | None = Field(
|
||||
default=None,
|
||||
description="Marketing year for the commodity.",
|
||||
)
|
||||
value: float | int | None = Field(
|
||||
default=None,
|
||||
description="Value for the commodity attribute in the given marketing year.",
|
||||
)
|
||||
unit: str | None = Field(
|
||||
default=None,
|
||||
description="Unit of measurement for the value.",
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Commodity Production Supply & Distribution Report Standard Model."""
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class CommodityPsdReportQueryParams(QueryParams):
|
||||
"""Commodity Production Supply & Distribution Report Query."""
|
||||
|
||||
commodity: str = Field(
|
||||
description="Commodity for the report.",
|
||||
)
|
||||
year: int = Field(
|
||||
description="Year of the report.",
|
||||
)
|
||||
month: int = Field(
|
||||
description="Month of the report.",
|
||||
ge=1,
|
||||
le=12,
|
||||
)
|
||||
|
||||
|
||||
class CommodityPsdReportData(Data):
|
||||
"""Commodity Production Supply & Distribution Report Data."""
|
||||
|
||||
content: str = Field(
|
||||
description="Base64 encoded content.",
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Weather Bulletin Standard Model."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class WeatherBulletinQueryParams(QueryParams):
|
||||
"""Weather Bulletin Query."""
|
||||
|
||||
year: int = Field(
|
||||
description="Year of the data. Default is the current year.",
|
||||
default=datetime.now().year,
|
||||
)
|
||||
month: int | None = Field(
|
||||
description="Month of the data. If not provided, data for the entire year is returned.",
|
||||
ge=1,
|
||||
le=12,
|
||||
default=None,
|
||||
)
|
||||
week: int | None = Field(
|
||||
description="Numeric week of the data, relative to the month."
|
||||
+ " If not provided, data for the entire month is returned.",
|
||||
ge=1,
|
||||
le=5,
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
class WeatherBulletinData(Data):
|
||||
"""Weather Bulletin Data."""
|
||||
|
||||
label: str | None = Field(
|
||||
default=None,
|
||||
description="Label representing the weather bulletin file.",
|
||||
)
|
||||
value: str | None = Field(
|
||||
default=None,
|
||||
description="URL to the weather bulletin document.",
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Weather Bulletin Download Standard Model."""
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
|
||||
class WeatherBulletinDownloadQueryParams(QueryParams):
|
||||
"""Weather Bulletin Query."""
|
||||
|
||||
urls: str | dict | list = Field(
|
||||
kw_only=True,
|
||||
description="URLs for reports to download.",
|
||||
)
|
||||
|
||||
@field_validator("urls", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_urls(cls, v):
|
||||
"""Validate URLs input."""
|
||||
if isinstance(v, str):
|
||||
if "," in v:
|
||||
return v.split(",")
|
||||
return [v]
|
||||
if isinstance(v, dict) and "urls" in v:
|
||||
return v["urls"]
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
raise ValueError("Invalid format for URLs. Must be str, dict, or list.")
|
||||
|
||||
|
||||
class WeatherBulletinDownloadData(Data):
|
||||
"""Weather Bulletin Data."""
|
||||
|
||||
content: str = Field(
|
||||
description="Base64 encoded content of the weather bulletin document.",
|
||||
)
|
||||
@@ -110,3 +110,123 @@ def test_commodity_short_term_energy_outlook(params, headers):
|
||||
result = requests.get(url, headers=headers, timeout=10)
|
||||
assert isinstance(result, requests.Response)
|
||||
assert result.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"commodity": "sugar",
|
||||
"year": 2025,
|
||||
"month": 5,
|
||||
"provider": "government_us",
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_commodity_psd_report(params, headers):
|
||||
"""Test the Commodity PSD Report 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/commodity/psd_report?{query_str}"
|
||||
result = requests.get(url, headers=headers, timeout=10)
|
||||
assert isinstance(result, requests.Response)
|
||||
assert result.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"urls": "https://esmis.nal.usda.gov/sites/default/release-files/cj82k728n/vx023b997/z890tr81b/wwcb1825.pdf"
|
||||
+ ",https://esmis.nal.usda.gov/sites/default/release-files/cj82k728n/w6635s43r/x059dz29h/wwcb1924.pdf",
|
||||
"provider": "government_us",
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_commodity_weather_bulletins_download(params, headers):
|
||||
"""Test the Commodity Weather Bulletin Download endpoint."""
|
||||
params = {p: v for p, v in params.items() if v}
|
||||
urls = params.pop("urls", "")
|
||||
query_str = get_querystring(params, [])
|
||||
url = f"http://0.0.0.0:8000/api/v1/commodity/weather_bulletins_download?{query_str}"
|
||||
result = requests.post(url, headers=headers, json=urls, timeout=30)
|
||||
assert isinstance(result, requests.Response)
|
||||
assert result.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"year": 2025,
|
||||
"month": 5,
|
||||
"week": 2,
|
||||
"provider": "government_us",
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_commodity_weather_bulletins(params, headers):
|
||||
"""Test the Commodity Weather Bulletins 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/commodity/weather_bulletins?{query_str}"
|
||||
result = requests.get(url, headers=headers, timeout=10)
|
||||
assert isinstance(result, requests.Response)
|
||||
assert result.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"provider": "government_us",
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"provider": "government_us",
|
||||
"report_id": "coffee_summary",
|
||||
"commodity": None,
|
||||
"country": None,
|
||||
"attribute": None,
|
||||
"start_year": None,
|
||||
"end_year": None,
|
||||
"aggregate_regions": False,
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"report_id": "world_crop_production_summary", # ignored if commodity is set
|
||||
"commodity": "corn",
|
||||
"country": "united_states,argentina",
|
||||
"attribute": "exports",
|
||||
"start_year": 2025,
|
||||
"end_year": 2025,
|
||||
"provider": "government_us",
|
||||
"aggregate_regions": False,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_commodity_psd_data(params, headers):
|
||||
"""Test the Commodity PSD Data 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/commodity/psd_data?{query_str}"
|
||||
result = requests.get(url, headers=headers, timeout=10)
|
||||
assert isinstance(result, requests.Response)
|
||||
assert result.status_code == 200
|
||||
|
||||
@@ -98,3 +98,122 @@ def test_commodity_short_term_energy_outlook(params, obb):
|
||||
assert result
|
||||
assert isinstance(result, OBBject)
|
||||
assert len(result.results) > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"commodity": "sugar",
|
||||
"year": 2025,
|
||||
"month": 5,
|
||||
"provider": "government_us",
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_commodity_psd_report(params, obb):
|
||||
"""Test Commodity PSD Report endpoint."""
|
||||
result = obb.commodity.psd_report(**params)
|
||||
assert result
|
||||
assert isinstance(result, dict)
|
||||
assert result["data_format"]["data_type"] == "pdf"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"year": 2025,
|
||||
"month": 5,
|
||||
"week": 2,
|
||||
"provider": "government_us",
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_commodity_weather_bulletins(params, obb):
|
||||
"""Test Commodity Weather Bulletins endpoint."""
|
||||
result = obb.commodity.weather_bulletins(**params)
|
||||
assert result
|
||||
assert isinstance(result, list)
|
||||
assert len(result) > 0
|
||||
for bulletin in result:
|
||||
assert bulletin["label"]
|
||||
assert bulletin["value"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"urls": [
|
||||
"https://esmis.nal.usda.gov/sites/default/release-files/cj82k728n/vx023b997/z890tr81b/wwcb1825.pdf",
|
||||
"https://esmis.nal.usda.gov/sites/default/release-files/cj82k728n/w6635s43r/x059dz29h/wwcb1924.pdf",
|
||||
],
|
||||
"provider": "government_us",
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_commodity_weather_bulletins_download(params, obb):
|
||||
"""Test Commodity Weather Bulletins Download endpoint."""
|
||||
result = obb.commodity.weather_bulletins_download(**params)
|
||||
assert result
|
||||
assert isinstance(result, list)
|
||||
assert len(result) > 0
|
||||
for bulletin in result:
|
||||
assert isinstance(bulletin, dict)
|
||||
assert bulletin["content"]
|
||||
assert bulletin["data_format"]["data_type"] == "pdf"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"provider": "government_us",
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"provider": "government_us",
|
||||
"report_id": "coffee_summary",
|
||||
"commodity": None,
|
||||
"country": None,
|
||||
"attribute": None,
|
||||
"start_year": None,
|
||||
"end_year": None,
|
||||
"aggregate_regions": False,
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"report_id": "world_crop_production_summary", # ignored if commodity is set
|
||||
"commodity": "corn",
|
||||
"country": "united_states,argentina",
|
||||
"attribute": "exports",
|
||||
"start_year": 2025,
|
||||
"end_year": 2025,
|
||||
"provider": "government_us",
|
||||
"aggregate_regions": False,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_commodity_psd_data(params, obb):
|
||||
"""Test Commodity PSD Data endpoint."""
|
||||
params = {p: v for p, v in params.items() if v is not None}
|
||||
|
||||
result = obb.commodity.psd_data(**params)
|
||||
assert result
|
||||
assert isinstance(result, OBBject)
|
||||
assert len(result.results) > 0
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
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,
|
||||
@@ -15,13 +17,13 @@ from openbb_core.app.provider_interface import (
|
||||
)
|
||||
from openbb_core.app.query import Query
|
||||
from openbb_core.app.router import Router
|
||||
from openbb_core.app.service.system_service import SystemService
|
||||
|
||||
from openbb_commodity.price.price_router import router as price_router
|
||||
|
||||
router = Router(prefix="", description="Commodity market data.")
|
||||
|
||||
|
||||
router.include_router(price_router)
|
||||
api_prefix = SystemService().system_settings.api_settings.prefix
|
||||
|
||||
|
||||
@router.command(
|
||||
@@ -78,3 +80,288 @@ async def short_term_energy_outlook(
|
||||
Source: www.eia.gov/steo/
|
||||
"""
|
||||
return await OBBject.from_query(Query(**locals()))
|
||||
|
||||
|
||||
@router.command(
|
||||
model="CommodityPsdData",
|
||||
examples=[
|
||||
APIEx(
|
||||
description="Get the World Crop Production Summary table.",
|
||||
parameters={
|
||||
"provider": "government_us",
|
||||
},
|
||||
),
|
||||
APIEx(
|
||||
description="Get the current Corn World Trade table from the PDS report.",
|
||||
parameters={
|
||||
"provider": "government_us",
|
||||
"report_id": "corn_world_trade",
|
||||
},
|
||||
),
|
||||
APIEx(
|
||||
description="Get all attributes for Coffee globally, for a single year.",
|
||||
parameters={
|
||||
"provider": "government_us",
|
||||
"commodity": "coffee",
|
||||
"start_year": 2025,
|
||||
"end_year": 2025,
|
||||
},
|
||||
),
|
||||
APIEx(
|
||||
description="Compare Brazil coffee exports versus the world from 2010 to present.",
|
||||
parameters={
|
||||
"provider": "government_us",
|
||||
"commodity": "coffee",
|
||||
"country": "brazil",
|
||||
"attribute": "exports",
|
||||
"aggregate_regions": True,
|
||||
"start_year": 2010,
|
||||
},
|
||||
),
|
||||
APIEx(
|
||||
description="Get historical production of corn in the US from 2020.",
|
||||
parameters={
|
||||
"provider": "government_us",
|
||||
"commodity": "corn",
|
||||
"country": "united_states",
|
||||
"attribute": "production",
|
||||
"start_year": 2020,
|
||||
},
|
||||
),
|
||||
APIEx(
|
||||
description="Get regional aggregates for wheat beginning and ending stocks from 2020.",
|
||||
parameters={
|
||||
"provider": "government_us",
|
||||
"commodity": "wheat",
|
||||
"country": "world",
|
||||
"attribute": "beginning_stocks,ending_stocks",
|
||||
"aggregate_regions": True,
|
||||
"start_year": 2020,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def psd_data(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Get data tables and historical time series from the USDA FAS Production, Supply, and Distribution (PSD) Reports."""
|
||||
return await OBBject.from_query(Query(**locals()))
|
||||
|
||||
|
||||
@router.command(
|
||||
model="CommodityPsdReport",
|
||||
no_validate=True,
|
||||
widget_config={
|
||||
"name": "USDA FAS Commodity Production Supply & Distribution Reports",
|
||||
"description": "Monthly publications released by the USDA Foreign Agriculture Service.",
|
||||
"type": "pdf",
|
||||
"refetchInterval": False,
|
||||
"gridData": {
|
||||
"w": 20,
|
||||
"h": 30,
|
||||
},
|
||||
"category": "Commodity",
|
||||
"subCategory": "Agriculture",
|
||||
"source": ["USDA", "FAS"],
|
||||
},
|
||||
examples=[
|
||||
APIEx(
|
||||
parameters={
|
||||
"provider": "government_us",
|
||||
"commodity": "sugar",
|
||||
"year": 2022,
|
||||
"month": 5,
|
||||
}
|
||||
),
|
||||
APIEx(
|
||||
description="Get the PSD report for coffee for March 2023.",
|
||||
parameters={
|
||||
"provider": "government_us",
|
||||
"commodity": "coffee",
|
||||
"year": 2023,
|
||||
"month": 3,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def psd_report(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Agriculture commodity production, supply, and distribution PDF reports (World Agricultural Outlook).
|
||||
|
||||
This command returns only the results portion of the OBBject response.
|
||||
It contains a dictionary where the PDF content is base64 encoded under the 'content' key.
|
||||
"""
|
||||
response = await OBBject.from_query(Query(**locals()))
|
||||
return response.model_dump().get("results", {})
|
||||
|
||||
|
||||
@router.command(
|
||||
model="WeatherBulletin",
|
||||
no_validate=True,
|
||||
widget_config={"exclude": True},
|
||||
examples=[
|
||||
APIEx(
|
||||
description="Get weather bulletins for the current year.",
|
||||
parameters={
|
||||
"provider": "government_us",
|
||||
},
|
||||
),
|
||||
APIEx(
|
||||
description="Get weather bulletins for May 2023, week 2.",
|
||||
parameters={
|
||||
"provider": "government_us",
|
||||
"year": 2023,
|
||||
"month": 5,
|
||||
"week": 2,
|
||||
},
|
||||
),
|
||||
PythonEx(
|
||||
description="Get URLs for comparing versus 1 year ago and download the base64-encoded PDF content to memory.",
|
||||
code=[
|
||||
"from datetime import datetime",
|
||||
"urls = []",
|
||||
"for year in [datetime.now().year, datetime.now().year - 1]:",
|
||||
" urls.append(obb.commodity.weather_bulletins(year=year, month=5, week=2)[0]['value'])",
|
||||
"pdfs = obb.commodity.weather_bulletins_download(urls=urls)",
|
||||
"# PDFs are now in a list where each item has 'content' and 'data_format' keys",
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def weather_bulletins(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Get current and historical weather bulletins with their PDF links.
|
||||
|
||||
This command returns only the results portion of the OBBject response.
|
||||
It contains a list of dictionaries where each dictionary has 'label' and 'value' keys.
|
||||
|
||||
Use this endpoint to programmatically access the list of available weather bulletins.
|
||||
Suitable for dropdown selections in a UI.
|
||||
"""
|
||||
response = await OBBject.from_query(Query(**locals()))
|
||||
return response.model_dump().get("results", {})
|
||||
|
||||
|
||||
@router.command(
|
||||
methods=["POST"],
|
||||
model="WeatherBulletinDownload",
|
||||
no_validate=True,
|
||||
widget_config={
|
||||
"name": "USDA Weather & Crop Bulletin",
|
||||
"description": "Weekly Weather and Crop Bulletin from the USDA.",
|
||||
"type": "multi_file_viewer",
|
||||
"refetchInterval": False,
|
||||
"gridData": {
|
||||
"w": 20,
|
||||
"h": 30,
|
||||
},
|
||||
"category": "Commodity",
|
||||
"subCategory": "Agriculture",
|
||||
"source": ["USDA", "WAOB"],
|
||||
"params": [
|
||||
{
|
||||
"paramName": "urls",
|
||||
"type": "endpoint",
|
||||
"optionsEndpoint": f"{api_prefix}/commodity/weather_bulletins",
|
||||
"optionsParams": {
|
||||
"year": "$year",
|
||||
"month": "$month",
|
||||
"week": "$week",
|
||||
"provider": "government_us",
|
||||
},
|
||||
"show": False,
|
||||
"multiSelect": True,
|
||||
"roles": ["fileSelector"],
|
||||
},
|
||||
{
|
||||
"paramName": "year",
|
||||
"type": "number",
|
||||
"label": "Year",
|
||||
"value": datetime.now().year,
|
||||
"options": [
|
||||
{"value": year, "label": str(year)}
|
||||
for year in sorted(
|
||||
list(range(1974, datetime.now().year + 1)),
|
||||
reverse=True,
|
||||
)
|
||||
],
|
||||
},
|
||||
{
|
||||
"paramName": "month",
|
||||
"type": "number",
|
||||
"label": "Month",
|
||||
"value": None,
|
||||
"options": [
|
||||
{"value": i, "label": month}
|
||||
for i, month in enumerate(
|
||||
[
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
],
|
||||
start=1,
|
||||
)
|
||||
]
|
||||
+ [{"value": None, "label": "All Months"}],
|
||||
},
|
||||
{
|
||||
"paramName": "week",
|
||||
"type": "number",
|
||||
"label": "Week",
|
||||
"value": None,
|
||||
"options": [{"value": week, "label": str(week)} for week in range(1, 6)]
|
||||
+ [{"value": None, "label": "All Weeks"}],
|
||||
},
|
||||
{
|
||||
"paramName": "provider",
|
||||
"show": False,
|
||||
"value": "government_us",
|
||||
"type": "text",
|
||||
"options": [{"value": "government_us", "label": "government_us"}],
|
||||
},
|
||||
],
|
||||
},
|
||||
examples=[
|
||||
APIEx(
|
||||
parameters={
|
||||
"provider": "government_us",
|
||||
"urls": [
|
||||
"https://esmis.nal.usda.gov/sites/default/release-files/cj82k728n/9w033w568/x059f4232/wwcb0125.pdf"
|
||||
],
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
async def weather_bulletins_download(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Download one, or more, weather bulletin documents.
|
||||
|
||||
This command returns only the results portion of the OBBject response.
|
||||
It contains a list of dictionaries where the base64 encoded content of the document is under the 'content' key.
|
||||
"""
|
||||
response = await OBBject.from_query(Query(**locals()))
|
||||
return response.model_dump().get("results", {})
|
||||
|
||||
@@ -10,4 +10,17 @@ To install the extension:
|
||||
pip install openbb-us-government
|
||||
```
|
||||
|
||||
Documentation available [here](https://docs.openbb.co/platform/developer_guide/contributing).
|
||||
See the OpenBB Documentation [here](https://docs.openbb.co/python/) for details on getting started.
|
||||
|
||||
## Coverage
|
||||
|
||||
This extension covers services with no registration requirement.
|
||||
|
||||
- `obb.commodity.psd_report`
|
||||
- `obb.commodity.psd_data`
|
||||
- `obb.commodity.weather_bulletins`
|
||||
- `obb.commodity.weather_bulletins_download`
|
||||
- `obb.fixed_income.government.treasury_auctions`
|
||||
- `obb.fixed_income.government.treasury_prices`
|
||||
|
||||
See the [reference](https://docs.openbb.co/python/reference) for breakdowns of each endpoint.
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
"""Government US provider module."""
|
||||
|
||||
from openbb_core.provider.abstract.provider import Provider
|
||||
from openbb_government_us.models.commodity_psd_data import (
|
||||
GovernmentUsCommodityPsdDataFetcher,
|
||||
)
|
||||
from openbb_government_us.models.commodity_psd_report import (
|
||||
GovernmentUsCommodityPsdReportFetcher,
|
||||
)
|
||||
from openbb_government_us.models.treasury_auctions import (
|
||||
GovernmentUSTreasuryAuctionsFetcher,
|
||||
)
|
||||
from openbb_government_us.models.treasury_prices import (
|
||||
GovernmentUSTreasuryPricesFetcher,
|
||||
)
|
||||
from openbb_government_us.models.weather_bulletin import (
|
||||
GovernmentUsWeatherBulletinFetcher,
|
||||
)
|
||||
from openbb_government_us.models.weather_bulletin_download import (
|
||||
GovernmentUsWeatherBulletinDownloadFetcher,
|
||||
)
|
||||
|
||||
government_us_provider = Provider(
|
||||
name="government_us",
|
||||
@@ -15,10 +27,14 @@ government_us_provider = Provider(
|
||||
It provides access to datasets published by agencies across the federal government.
|
||||
Data.gov is intended to provide access to government open data to the public, achieve
|
||||
agency missions, drive innovation, fuel economic activity, and uphold the ideals of
|
||||
an open and transparent government.""",
|
||||
an open and transparent government. https://api.data.gov/signup/""",
|
||||
fetcher_dict={
|
||||
"CommodityPsdData": GovernmentUsCommodityPsdDataFetcher,
|
||||
"CommodityPsdReport": GovernmentUsCommodityPsdReportFetcher,
|
||||
"TreasuryAuctions": GovernmentUSTreasuryAuctionsFetcher,
|
||||
"TreasuryPrices": GovernmentUSTreasuryPricesFetcher,
|
||||
"WeatherBulletin": GovernmentUsWeatherBulletinFetcher,
|
||||
"WeatherBulletinDownload": GovernmentUsWeatherBulletinDownloadFetcher,
|
||||
},
|
||||
repr_name="Data.gov | United States Government",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
"""US Government Production Supply & Distribution Report Data & Time Series Model."""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.standard_models.commodity_psd_data import (
|
||||
CommodityPsdData,
|
||||
CommodityPsdDataQueryParams,
|
||||
)
|
||||
from openbb_government_us.utils.psd_codes import (
|
||||
ATTRIBUTES,
|
||||
COMMODITIES,
|
||||
COUNTRIES,
|
||||
PSD_REPORT_NAMES,
|
||||
REGIONS,
|
||||
)
|
||||
from pydantic import ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
COUNTRY_CHOICES = {
|
||||
**{k: v for k, v in COUNTRIES.items() if k != "world"},
|
||||
**REGIONS,
|
||||
}
|
||||
|
||||
|
||||
class GovernmentUsCommodityPsdDataQueryParams(CommodityPsdDataQueryParams):
|
||||
"""US Government Commodity PSD Data Query Params.
|
||||
|
||||
Source: https://apps.fas.usda.gov/psdonline/app/index.html#/app/downloads
|
||||
"""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"report_id": {
|
||||
"x-widget_config": {
|
||||
"description": "Report ID to retrieve. Gets the current report for the given commodity and subject. "
|
||||
+ "Ignored if 'commodity' is provided.",
|
||||
"value": "world_crop_production_summary",
|
||||
"options": [
|
||||
{
|
||||
"label": name.replace("_", " ")
|
||||
.title()
|
||||
.replace("Eu", "EU")
|
||||
.replace("Us", "US"),
|
||||
"value": name,
|
||||
}
|
||||
for name in sorted(list(PSD_REPORT_NAMES))
|
||||
],
|
||||
"style": {"popupWidth": 350},
|
||||
"row": 1,
|
||||
},
|
||||
},
|
||||
"commodity": {
|
||||
"x-widget_config": {
|
||||
"description": "Commodity name to filter the data. "
|
||||
+ "If provided, overrides the Report ID and retrieves time series data for the given commodity.",
|
||||
"value": None,
|
||||
"options": [{"label": "Report Mode", "value": None}]
|
||||
+ [
|
||||
{"label": comm.replace("_", " ").title(), "value": comm}
|
||||
for comm in sorted(list(COMMODITIES))
|
||||
],
|
||||
"row": 1,
|
||||
},
|
||||
},
|
||||
"attribute": {
|
||||
"multiple_items_allowed": True,
|
||||
"x-widget_config": {
|
||||
"description": "Attribute to filter the data. Ignored when commodity is Report Mode. "
|
||||
+ "If None, retrieves all available attributes for the commodity.",
|
||||
"options": [{"label": "All Attributes", "value": None}]
|
||||
+ [
|
||||
{
|
||||
"label": attr.replace("_", " ")
|
||||
.title()
|
||||
.replace("Us", "US")
|
||||
.replace("Ty", "TY")
|
||||
.replace("Fsi", "FSI"),
|
||||
"value": attr,
|
||||
}
|
||||
for attr in sorted(list(ATTRIBUTES))
|
||||
],
|
||||
"row": 1,
|
||||
},
|
||||
},
|
||||
"country": {
|
||||
"multiple_items_allowed": True,
|
||||
"x-widget_config": {
|
||||
"description": "Country code(s) to filter the data. Ignored when commodity is Report Mode. "
|
||||
+ "If None, retrieves data for all countries.",
|
||||
"options": [{"label": "All Countries", "value": None}]
|
||||
+ [
|
||||
{"label": name.replace("_", " ").title(), "value": code}
|
||||
for name, code in COUNTRY_CHOICES.items()
|
||||
],
|
||||
"row": 1,
|
||||
},
|
||||
},
|
||||
"start_year": {
|
||||
"x-widget_config": {
|
||||
"description": "Start year for filtering time series data. Ignored when commodity is Report Mode. "
|
||||
+ "None returns from the beginning of the series.",
|
||||
"type": "number",
|
||||
"options": [{"label": "Start Year", "value": None}]
|
||||
+ [
|
||||
{"label": str(year), "value": year}
|
||||
for year in sorted(
|
||||
list(range(1960, datetime.now().year + 1)), reverse=True
|
||||
)
|
||||
],
|
||||
"row": 1,
|
||||
},
|
||||
},
|
||||
"end_year": {
|
||||
"x-widget_config": {
|
||||
"description": "End year for filtering time series data."
|
||||
+ " Ignored when commodity is Report Mode. If None, returns up to the most recent year.",
|
||||
"type": "number",
|
||||
"options": [{"label": "End Year", "value": None}]
|
||||
+ [
|
||||
{"label": str(year), "value": year}
|
||||
for year in sorted(
|
||||
list(range(1960, datetime.now().year + 1)), reverse=True
|
||||
)
|
||||
],
|
||||
"row": 1,
|
||||
},
|
||||
},
|
||||
"aggregate_regions": {
|
||||
"x-widget_config": {
|
||||
"description": "Whether to include regional and world aggregates in the data."
|
||||
+ " Ignored when 'commodity' is Report Mode.",
|
||||
"type": "boolean",
|
||||
"value": False,
|
||||
"row": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
report_id: str | None = Field(
|
||||
default="world_crop_production_summary",
|
||||
description="Report ID to retrieve. Gets the current report for the given commodity and subject. "
|
||||
+ "These are predefined tables that are part of the PDF publication data. "
|
||||
+ "This parameter is ignored if 'commodity' is provided. "
|
||||
+ "Use the 'commodity' parameter for time series data. "
|
||||
+ "Valid reports are:\n "
|
||||
+ ", ".join(sorted(list(PSD_REPORT_NAMES.keys())))
|
||||
+ "\n",
|
||||
)
|
||||
commodity: str | None = Field(
|
||||
default=None,
|
||||
description="Commodity name to filter the data. If provided, retrieves time series data for the given commodity. "
|
||||
+ "Supplying both 'report_id' and 'commodity' will prioritize 'commodity' for time series data. "
|
||||
+ "Valid commodities are:\n "
|
||||
+ ", ".join(sorted(list(COMMODITIES)))
|
||||
+ "\n",
|
||||
)
|
||||
attribute: str | list[str] | None = Field(
|
||||
default=None,
|
||||
description="Attribute to filter the data. If None, retrieves all available attributes for the commodity.\n"
|
||||
+ "Parameter is ignored when commodity is None. Valid attributes depend on the commodity, "
|
||||
+ "an invalid choice will show the available attributes for the entered commodity.\n"
|
||||
+ "All attributes choices are:\n"
|
||||
+ ", ".join(sorted(list(ATTRIBUTES)))
|
||||
+ "\n",
|
||||
)
|
||||
country: str | list[str] | None = Field(
|
||||
default=None,
|
||||
description="Country code(s) to filter the data. If None, retrieves data for all countries.\n"
|
||||
+ "Parameter is ignored when commodity is None. Valid country codes include:\n"
|
||||
+ ", ".join(
|
||||
sorted(list(COUNTRY_CHOICES)),
|
||||
)
|
||||
+ "\n",
|
||||
)
|
||||
aggregate_regions: bool = Field(
|
||||
default=False,
|
||||
description="Whether to include regional and world aggregates in the data. "
|
||||
+ "Parameter is ignored when 'commodity' is None.",
|
||||
)
|
||||
start_year: int | None = Field(
|
||||
default=None,
|
||||
description="Start year for filtering time series data. None returns from the beginning of the series.\n"
|
||||
+ "Parameter is ignored when 'commodity' is None.",
|
||||
)
|
||||
end_year: int | None = Field(
|
||||
default=None,
|
||||
description="End year for filtering time series data. If None, returns up to the most recent year.\n"
|
||||
+ "Parameter is ignored when 'commodity' is None.",
|
||||
)
|
||||
|
||||
@field_validator("report_id", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_report_id(cls, v):
|
||||
"""Validate report_id."""
|
||||
if not v:
|
||||
return "world_crop_production_summary"
|
||||
|
||||
if v not in PSD_REPORT_NAMES:
|
||||
raise ValueError(
|
||||
f"Invalid report_id '{v}'. Valid report IDs are: "
|
||||
+ ", ".join(sorted(list(PSD_REPORT_NAMES.keys())))
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("commodity", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_commodity(cls, v):
|
||||
"""Validate commodity."""
|
||||
if not v:
|
||||
return None
|
||||
if v and v not in COMMODITIES:
|
||||
raise ValueError(
|
||||
f"Invalid commodity '{v}'. Valid commodities are: "
|
||||
+ ", ".join(sorted(list(COMMODITIES)))
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("attribute", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_attribute(cls, v):
|
||||
"""Validate attribute."""
|
||||
if not v:
|
||||
return None
|
||||
|
||||
if v and isinstance(v, list) and not v[0]:
|
||||
return None
|
||||
|
||||
attributes = v
|
||||
|
||||
if isinstance(attributes, str):
|
||||
attributes = (
|
||||
[attributes] if "," not in attributes else attributes.split(",")
|
||||
)
|
||||
|
||||
if (
|
||||
isinstance(attributes, list)
|
||||
and len(attributes) == 1
|
||||
and "," in attributes[0]
|
||||
):
|
||||
attributes = attributes[0].split(",")
|
||||
|
||||
if not isinstance(attributes, list):
|
||||
raise ValueError(
|
||||
f"Attribute must be a string or list of strings. Got {type(v)}"
|
||||
)
|
||||
|
||||
invalid_attrs = [attr for attr in attributes if attr not in ATTRIBUTES]
|
||||
|
||||
if invalid_attrs:
|
||||
raise ValueError(
|
||||
f"Invalid attribute(s) '{', '.join(invalid_attrs)}'. Valid attributes are: "
|
||||
+ ", ".join(sorted(list(ATTRIBUTES)))
|
||||
)
|
||||
|
||||
return attributes
|
||||
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate country."""
|
||||
if not v:
|
||||
return None
|
||||
|
||||
if v and isinstance(v, list) and not v[0]:
|
||||
return None
|
||||
|
||||
countries = v
|
||||
|
||||
if isinstance(countries, str):
|
||||
countries = [countries] if "," not in countries else countries.split(",")
|
||||
|
||||
if not isinstance(countries, list):
|
||||
raise ValueError(
|
||||
f"Country must be a string or list of strings. Got {type(v)}"
|
||||
)
|
||||
|
||||
if isinstance(countries, list) and len(countries) == 1 and "," in countries[0]:
|
||||
countries = countries[0].split(",")
|
||||
|
||||
invalid_countries = [
|
||||
country
|
||||
for country in countries
|
||||
if country
|
||||
and country not in COUNTRY_CHOICES.values()
|
||||
and country not in COUNTRY_CHOICES
|
||||
]
|
||||
|
||||
if invalid_countries:
|
||||
raise ValueError(
|
||||
f"Invalid country code(s) '{', '.join(invalid_countries)}'. Valid country codes are: "
|
||||
+ ", ".join(
|
||||
sorted(list(COUNTRY_CHOICES)),
|
||||
)
|
||||
)
|
||||
|
||||
return v
|
||||
|
||||
@field_validator("start_year", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_start_year(cls, v):
|
||||
"""Validate start_year."""
|
||||
if v and v < 1960:
|
||||
raise ValueError("Earliest possible year is 1960.")
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_model(self):
|
||||
"""Validate that start_year is less than or equal to end_year."""
|
||||
report_id = getattr(self, "report_id", None)
|
||||
commodity = getattr(self, "commodity", None)
|
||||
|
||||
if not report_id and not commodity:
|
||||
raise ValueError(
|
||||
"Either 'report_id' or 'commodity' must be provided."
|
||||
+ " If both are provided, 'commodity' takes precedence."
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class GovernmentUsCommodityPsdData(CommodityPsdData):
|
||||
"""US Government Commodity PSD Data."""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"x-widget_config": {
|
||||
"$.name": "USDA FAS Commodity Production Supply & Distribution Data",
|
||||
"$.description": "Predefined presentation tables, or time series data by commodity."
|
||||
+ " Source: USDA Foreign Agricultural Service (FAS) Production Supply & Distribution Online.",
|
||||
"$.source": ["USDA", "FAS"],
|
||||
"$.category": "Commodity",
|
||||
"$.subCategory": "Agriculture",
|
||||
"$.runButton": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class GovernmentUsCommodityPsdDataFetcher(
|
||||
Fetcher[
|
||||
GovernmentUsCommodityPsdDataQueryParams,
|
||||
list[GovernmentUsCommodityPsdData],
|
||||
]
|
||||
):
|
||||
"""US Government Commodity PSD Data Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(
|
||||
params: dict[str, Any],
|
||||
) -> GovernmentUsCommodityPsdDataQueryParams:
|
||||
"""Transform the query parameters into the model."""
|
||||
return GovernmentUsCommodityPsdDataQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: GovernmentUsCommodityPsdDataQueryParams,
|
||||
credentials: dict[str, Any] | None,
|
||||
**kwargs: Any,
|
||||
) -> list:
|
||||
"""Extract data from the provider."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_government_us.utils.psd_data_downloader import (
|
||||
get_psd_report_data,
|
||||
get_timeseries,
|
||||
)
|
||||
|
||||
if query.commodity is not None:
|
||||
# Time series data
|
||||
try:
|
||||
timeseries_data = get_timeseries(
|
||||
commodity=query.commodity,
|
||||
attribute=query.attribute,
|
||||
country=query.country,
|
||||
aggregate_region=query.aggregate_regions,
|
||||
start_year=query.start_year,
|
||||
end_year=query.end_year,
|
||||
)
|
||||
return timeseries_data
|
||||
except (ValueError, OpenBBError) as e:
|
||||
raise OpenBBError(e) from None
|
||||
|
||||
report_id = PSD_REPORT_NAMES[query.report_id or "world_crop_production_summary"]
|
||||
|
||||
try:
|
||||
report_data = await get_psd_report_data(report_id)
|
||||
|
||||
if not report_data.get("data", []):
|
||||
raise OpenBBError(
|
||||
"The request was successful but no data was returned. This might be a bug or a network error."
|
||||
+ f" -> Report ID: {query.report_id}"
|
||||
)
|
||||
if report_data.get("error", ""):
|
||||
raise OpenBBError(
|
||||
f"Error processing data: {report_data['error']} -> Report ID: {query.report_id}"
|
||||
)
|
||||
except OpenBBError as e:
|
||||
raise e from e
|
||||
|
||||
return report_data.get("data", [])
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: GovernmentUsCommodityPsdDataQueryParams,
|
||||
data: list,
|
||||
**kwargs: Any,
|
||||
) -> list[GovernmentUsCommodityPsdData]:
|
||||
"""Transform and validate the data."""
|
||||
return [GovernmentUsCommodityPsdData.model_validate(item) for item in data]
|
||||
@@ -0,0 +1,197 @@
|
||||
"""US Government Production Supply & Distribution Publications Model."""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.standard_models.commodity_psd_report import (
|
||||
CommodityPsdReportData,
|
||||
CommodityPsdReportQueryParams,
|
||||
)
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
COMMODITIES = {
|
||||
"citrus": "Citrus",
|
||||
"coffee": "Coffee",
|
||||
"cotton": "Cotton",
|
||||
"dairy": "Dairy",
|
||||
"fruit": "Fruit",
|
||||
"grain": "Grain",
|
||||
"livestock": "Livestock_poultry",
|
||||
"oilseeds": "Oilseeds",
|
||||
"stone_fruit": "StoneFruit",
|
||||
"sugar": "Sugar",
|
||||
"tree_nuts": "TreeNuts",
|
||||
"world_production": "production",
|
||||
}
|
||||
|
||||
|
||||
class GovernmentUsCommodityPsdReportQueryParams(CommodityPsdReportQueryParams):
|
||||
"""US Government Commodity PSD Report Query Params.
|
||||
|
||||
Source: https://apps.fas.usda.gov/psdonline/app/index.html#/app/downloads
|
||||
"""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"year": {
|
||||
"x-widget_config": {
|
||||
"description": "Year of the report to retrieve. Data is available from 2006 onwards.",
|
||||
"type": "number",
|
||||
"row": 1,
|
||||
}
|
||||
},
|
||||
"month": {
|
||||
"x-widget_config": {
|
||||
"description": "Month of the report to retrieve.",
|
||||
"row": 1,
|
||||
"options": [
|
||||
{"value": i, "label": month}
|
||||
for i, month in enumerate(
|
||||
[
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
],
|
||||
start=1,
|
||||
)
|
||||
],
|
||||
}
|
||||
},
|
||||
"commodity": {
|
||||
"multiple_items_allowed": False,
|
||||
"choices": list(COMMODITIES),
|
||||
"x-widget_config": {
|
||||
"row": 1,
|
||||
"description": "Commodity for the report to retrieve.",
|
||||
"options": [
|
||||
{"value": commodity, "label": commodity.replace("_", " ").title()}
|
||||
for commodity in COMMODITIES
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@field_validator("year", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_year(cls, v):
|
||||
"""Validate year is >= 2006."""
|
||||
if v < 2006:
|
||||
raise OpenBBError(ValueError("Data is only available from 2006 onwards."))
|
||||
return v
|
||||
|
||||
@field_validator("commodity", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_commodity(cls, v):
|
||||
"""Validate commodity is in the list."""
|
||||
if v.lower() not in COMMODITIES:
|
||||
raise OpenBBError(
|
||||
ValueError(
|
||||
f"Commodity '{v}' is not supported. "
|
||||
+ "Please choose from the available commodities -> "
|
||||
+ f"{', '.join(COMMODITIES.keys())}"
|
||||
)
|
||||
)
|
||||
return v.lower()
|
||||
|
||||
|
||||
class GovernmentUsCommodityPsdReportData(CommodityPsdReportData):
|
||||
"""US Government Commodity PSD Report Data."""
|
||||
|
||||
data_format: dict[str, str] = Field(
|
||||
description="Data format information.",
|
||||
default_factory=lambda: {
|
||||
"data_type": "pdf",
|
||||
"filename": "commodity_psd_report.pdf",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class GovernmentUsCommodityPsdReportFetcher(
|
||||
Fetcher[
|
||||
GovernmentUsCommodityPsdReportQueryParams,
|
||||
GovernmentUsCommodityPsdReportData,
|
||||
]
|
||||
):
|
||||
"""US Government Commodity PSD Report Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(
|
||||
params: dict[str, Any],
|
||||
) -> GovernmentUsCommodityPsdReportQueryParams:
|
||||
"""Transform params into the query params model."""
|
||||
return GovernmentUsCommodityPsdReportQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
def extract_data(
|
||||
query: GovernmentUsCommodityPsdReportQueryParams,
|
||||
credentials: dict[str, Any] | None,
|
||||
**kwargs: Any,
|
||||
) -> dict:
|
||||
"""Extract data from the provider."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.provider.utils.helpers import make_request
|
||||
|
||||
api_commodity = COMMODITIES.get(query.commodity.lower(), query.commodity)
|
||||
month = f"{query.month:02d}"
|
||||
url = f"https://apps.fas.usda.gov/PSDOnline/CircularDownloader.ashx?year={query.year}&month={month}&commodity={api_commodity}"
|
||||
output = b""
|
||||
|
||||
try:
|
||||
response = make_request(url)
|
||||
|
||||
if response.status_code == 404 or not response.content:
|
||||
raise OpenBBError(
|
||||
ValueError(
|
||||
"No data found for the given parameters. Please check the commodity, year, and month."
|
||||
)
|
||||
)
|
||||
|
||||
if response.status_code == 200 and response.content[:4] == b"%PDF":
|
||||
output = response.content
|
||||
else:
|
||||
raise OpenBBError(
|
||||
ValueError(
|
||||
"Failed to retrieve data. Unexpected response from the server."
|
||||
+ f" Status code: {response.status_code} -> "
|
||||
+ f"Response content: {response.content.decode('utf-8', errors='ignore')}"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
raise OpenBBError(e) from e
|
||||
|
||||
return {"content": output}
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: GovernmentUsCommodityPsdReportQueryParams,
|
||||
data: dict,
|
||||
**kwargs: Any,
|
||||
) -> GovernmentUsCommodityPsdReportData:
|
||||
"""Transform data into the encoded format."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import base64
|
||||
|
||||
if not data:
|
||||
raise OpenBBError("The request was successful but no data was returned.")
|
||||
|
||||
encoded_content = base64.b64encode(data.get("content", b"")).decode("utf-8") # type: ignore
|
||||
filename = f"psd_report_{query.commodity}_{query.year}_{query.month:02d}.pdf"
|
||||
data_format = {
|
||||
"data_type": "pdf",
|
||||
"filename": filename,
|
||||
}
|
||||
|
||||
return GovernmentUsCommodityPsdReportData(
|
||||
content=encoded_content, data_format=data_format
|
||||
)
|
||||
@@ -0,0 +1,191 @@
|
||||
"""Government US Weather Bulletin Model"""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.standard_models.weather_bulletin import (
|
||||
WeatherBulletinData,
|
||||
WeatherBulletinQueryParams,
|
||||
)
|
||||
from pydantic import ConfigDict, field_validator
|
||||
|
||||
|
||||
class GovernmentUsWeatherBulletinQueryParams(WeatherBulletinQueryParams):
|
||||
"""US Government Weather Bulletin Query Params.
|
||||
|
||||
Source: https://esmis.nal.usda.gov/publication/weekly-weather-and-crop-bulletin
|
||||
"""
|
||||
|
||||
@field_validator("year")
|
||||
@classmethod
|
||||
def _validate_year(cls, value: int) -> int:
|
||||
"""Validate year."""
|
||||
if value < 1974:
|
||||
raise ValueError("Year must not be before 1974.")
|
||||
if value > datetime.now().year:
|
||||
raise OpenBBError(ValueError("Invalid year. Year cannot be in the future."))
|
||||
return value
|
||||
|
||||
|
||||
class GovernmentUsWeatherBulletinData(WeatherBulletinData):
|
||||
"""US Government Weather Bulletin Data.
|
||||
|
||||
The Weekly Weather and Crop Bulletin is released by 4:00 p.m. on the second workday of each week.
|
||||
|
||||
The Bulletin includes the National Summary, State Stories, current data for weather, temperature and precipitation
|
||||
and international agricultural weather. The full report is jointly published by the National Oceanic
|
||||
and Atmospheric Administration of the U.S. Department of Commerce, the National Agricultural Statistics Service,
|
||||
and the World Agricultural Outlook Board.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
|
||||
class GovernmentUsWeatherBulletinFetcher(
|
||||
Fetcher[
|
||||
GovernmentUsWeatherBulletinQueryParams,
|
||||
list[GovernmentUsWeatherBulletinData],
|
||||
]
|
||||
):
|
||||
"""US Government Weather Bulletin Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(
|
||||
params: dict[str, Any],
|
||||
) -> GovernmentUsWeatherBulletinQueryParams:
|
||||
"""Transform params into the query params model."""
|
||||
return GovernmentUsWeatherBulletinQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: GovernmentUsWeatherBulletinQueryParams,
|
||||
credentials: dict[str, str] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> list:
|
||||
"""Asynchronously extract data from the US Government Weather Bulletin API."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import asyncio # noqa
|
||||
import re
|
||||
from openbb_core.provider.utils.helpers import get_async_requests_session
|
||||
|
||||
BASE_URL = "https://esmis.nal.usda.gov"
|
||||
PUBLICATION_URL = f"{BASE_URL}/publication/weekly-weather-and-crop-bulletin"
|
||||
months_to_fetch = (
|
||||
[query.month] if query.month is not None else list(range(1, 13))
|
||||
)
|
||||
year = query.year
|
||||
week = query.week
|
||||
month = query.month
|
||||
|
||||
try:
|
||||
async with await get_async_requests_session() as session:
|
||||
session._max_field_size = 32768 # pylint: disable=protected-access
|
||||
|
||||
async def fetch_month(m: int) -> list[dict]:
|
||||
"""Fetch bulletins for a single month."""
|
||||
url = f"{PUBLICATION_URL}?date={year}-{m:02d}"
|
||||
bulletins = []
|
||||
|
||||
try:
|
||||
resp = await session.get(url)
|
||||
if resp.status != 200:
|
||||
return []
|
||||
html = await resp.text()
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
pattern = (
|
||||
r'href="(/sites/default/release-files/[^"]+\.pdf)"[^>]*>.*?'
|
||||
r'<time datetime="(\d{4}-\d{2}-\d{2})T'
|
||||
)
|
||||
matches = re.findall(pattern, html, re.DOTALL)
|
||||
|
||||
for pdf_path, date_str in matches:
|
||||
# Skip the "latest release" which appears on every page
|
||||
if pdf_path == "/sites/default/release-files/795708/wwcb.pdf":
|
||||
continue
|
||||
|
||||
# Parse the date
|
||||
try:
|
||||
dt = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Only include if it matches the requested year/month
|
||||
if dt.year != year:
|
||||
continue
|
||||
if month is not None and dt.month != m:
|
||||
continue
|
||||
|
||||
# Calculate week of month (1-5)
|
||||
week_of_month = (dt.day - 1) // 7 + 1
|
||||
|
||||
# Filter by week if specified
|
||||
if week is not None and week_of_month != week:
|
||||
continue
|
||||
|
||||
# Extract filename from path
|
||||
filename = pdf_path.split("/")[-1]
|
||||
|
||||
# Create human-readable label
|
||||
label = f"Weekly Weather Bulletin - {dt.strftime('%Y-%m-%d')}"
|
||||
|
||||
bulletins.append(
|
||||
{
|
||||
"label": label,
|
||||
"date": dt,
|
||||
"year": dt.year,
|
||||
"month": dt.month,
|
||||
"day": dt.day,
|
||||
"week_of_month": week_of_month,
|
||||
"pdf_url": f"{BASE_URL}{pdf_path}",
|
||||
"filename": filename,
|
||||
}
|
||||
)
|
||||
|
||||
return bulletins
|
||||
|
||||
results = await asyncio.gather(
|
||||
*[fetch_month(m) for m in months_to_fetch], return_exceptions=True
|
||||
)
|
||||
|
||||
all_bulletins = []
|
||||
for result in results:
|
||||
if isinstance(result, list):
|
||||
all_bulletins.extend(result)
|
||||
|
||||
# Sort by date descending (most recent first)
|
||||
all_bulletins.sort(key=lambda x: x["date"], reverse=True)
|
||||
|
||||
# Remove duplicates (same date can appear in adjacent month pages)
|
||||
seen_dates = set()
|
||||
unique_bulletins = []
|
||||
for b in all_bulletins:
|
||||
date_key = b["date"].strftime("%Y-%m-%d")
|
||||
if date_key not in seen_dates:
|
||||
seen_dates.add(date_key)
|
||||
unique_bulletins.append(b)
|
||||
|
||||
return unique_bulletins
|
||||
except Exception as e:
|
||||
raise OpenBBError(e) from e
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: GovernmentUsWeatherBulletinQueryParams,
|
||||
data: list,
|
||||
**kwargs: Any,
|
||||
) -> list[GovernmentUsWeatherBulletinData]:
|
||||
"""Transform data into the standard model."""
|
||||
return [
|
||||
GovernmentUsWeatherBulletinData(
|
||||
label=item["label"],
|
||||
value=item["pdf_url"],
|
||||
)
|
||||
for item in data
|
||||
]
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Government US Weather Bulletin Download Model."""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.standard_models.weather_bulletin_download import (
|
||||
WeatherBulletinDownloadData,
|
||||
WeatherBulletinDownloadQueryParams,
|
||||
)
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class GovernmentUsWeatherBulletinDownloadQueryParams(
|
||||
WeatherBulletinDownloadQueryParams
|
||||
):
|
||||
"""US Government Weather Bulletin Download Query Params."""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"urls": {
|
||||
"multiple_items_allowed": True,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class GovernmentUsWeatherBulletinDownloadData(WeatherBulletinDownloadData):
|
||||
"""US Government Weather Bulletin Download Data."""
|
||||
|
||||
data_format: dict[str, str] = Field(description="Data format information.")
|
||||
|
||||
|
||||
class GovernmentUsWeatherBulletinDownloadFetcher(
|
||||
Fetcher[
|
||||
GovernmentUsWeatherBulletinDownloadQueryParams,
|
||||
list[GovernmentUsWeatherBulletinDownloadData],
|
||||
]
|
||||
):
|
||||
"""US Government Weather Bulletin Download Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(
|
||||
params: dict[str, Any],
|
||||
) -> GovernmentUsWeatherBulletinDownloadQueryParams:
|
||||
"""Transform params into the query params model."""
|
||||
return GovernmentUsWeatherBulletinDownloadQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: GovernmentUsWeatherBulletinDownloadQueryParams,
|
||||
credentials: dict[str, Any] | None,
|
||||
**kwargs: Any,
|
||||
) -> dict:
|
||||
"""Extract the raw PDF content."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.provider.utils.helpers import get_async_requests_session
|
||||
|
||||
results: dict = {}
|
||||
urls = query.urls
|
||||
|
||||
# Verify that all URLs are going to be valid USDA URLs
|
||||
for url in urls:
|
||||
if not url.lower().startswith("https://esmis.nal.usda.gov/"):
|
||||
raise OpenBBError(
|
||||
ValueError(
|
||||
f"Invalid URL '{url}'. Only URLs from 'esmis.nal.usda.gov' are supported."
|
||||
)
|
||||
)
|
||||
if not url.lower().endswith(".pdf"):
|
||||
raise OpenBBError(
|
||||
ValueError(
|
||||
f"Invalid URL '{url}'. Only PDF documents are supported."
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
async with await get_async_requests_session() as session:
|
||||
session._max_field_size = 32768 # pylint: disable=protected-access
|
||||
|
||||
for url in urls:
|
||||
async with await session.get(url) as response:
|
||||
if response.status != 200:
|
||||
raise OpenBBError(
|
||||
ValueError(
|
||||
f"Failed to download document from {url}. Status code: {response.status}"
|
||||
)
|
||||
)
|
||||
content_bytes = await response.read()
|
||||
results[url] = content_bytes
|
||||
|
||||
return results
|
||||
except Exception as e:
|
||||
raise OpenBBError(e) from e
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: GovernmentUsWeatherBulletinDownloadQueryParams,
|
||||
data: dict,
|
||||
**kwargs: Any,
|
||||
) -> list[GovernmentUsWeatherBulletinDownloadData]:
|
||||
"""Transform the raw PDF content into the data model."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import base64
|
||||
|
||||
output: list[GovernmentUsWeatherBulletinDownloadData] = []
|
||||
|
||||
for url, content in data.items():
|
||||
content_b64 = base64.b64encode(content).decode("utf-8")
|
||||
data_format = {
|
||||
"data_type": "pdf",
|
||||
"filename": url.split("/")[-1],
|
||||
}
|
||||
bulletin_data = GovernmentUsWeatherBulletinDownloadData(
|
||||
content=content_b64,
|
||||
data_format=data_format,
|
||||
)
|
||||
output.append(bulletin_data)
|
||||
|
||||
return output
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,636 @@
|
||||
"""PSD Data Downloader Utils"""
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from openbb_government_us.utils.psd_codes import (
|
||||
ATTRIBUTES,
|
||||
COMMODITIES,
|
||||
COUNTRIES,
|
||||
COUNTRY_TO_REGION,
|
||||
PSD_REPORTS_METADATA,
|
||||
REGION_DISPLAY,
|
||||
REGIONS,
|
||||
)
|
||||
|
||||
REPORT_HANDLER_URL = "https://apps.fas.usda.gov/psdonline/reportHandler.ashx"
|
||||
|
||||
|
||||
def get_report_url(
|
||||
report_id: int,
|
||||
file_format: Literal["csv", "html"] = "csv",
|
||||
) -> str:
|
||||
"""
|
||||
Get the download URL for a PSD report.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
report_id : int
|
||||
The report ID from list_reports()
|
||||
format : str
|
||||
Output format: 'csv' or 'html' (default: 'csv')
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Direct download URL for the report
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> url = get_report_url(2109) # Coffee Summary
|
||||
>>> print(url)
|
||||
'https://apps.fas.usda.gov/psdonline/reportHandler.ashx?reportId=2109&templateId=8&format=csv&fileName=Coffee_Summary'
|
||||
"""
|
||||
# Find the report in metadata
|
||||
for group_data in PSD_REPORTS_METADATA.values():
|
||||
if report_id in group_data["reports"]:
|
||||
report = group_data["reports"][report_id]
|
||||
title = report["title"]
|
||||
template_id = report["templateId"]
|
||||
|
||||
# Create a clean filename from the title
|
||||
filename = (
|
||||
title.replace(" ", "_")
|
||||
.replace(":", "")
|
||||
.replace(",", "")
|
||||
.replace("/", "_")
|
||||
)
|
||||
|
||||
return (
|
||||
f"{REPORT_HANDLER_URL}?"
|
||||
f"reportId={report_id}&"
|
||||
f"templateId={template_id}&"
|
||||
f"format={file_format}&"
|
||||
f"fileName={filename}"
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
f"Report ID {report_id} not found. Use list_reports() to see available reports."
|
||||
)
|
||||
|
||||
|
||||
async def get_psd_report_data(report_id: int) -> dict:
|
||||
"""
|
||||
Download and parse a PSD report into structured data ready for pandas DataFrame.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
report_id : int
|
||||
The report ID from list_reports()
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Parsed report with flat structure for DataFrame compatibility:
|
||||
{
|
||||
'report': str, # Report title
|
||||
'template': int, # Template ID used for parsing
|
||||
'template_description': str, # Human-readable template description
|
||||
'units': dict, # Column name -> unit mapping (use for df.attrs)
|
||||
'columns': list[str], # All column names in order
|
||||
'row_count': int, # Number of data rows
|
||||
'data': list[dict] # Flat row data ready for DataFrame()
|
||||
}
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import pandas as pd
|
||||
>>> result = await get_psd_report_data(2109) # Coffee Summary
|
||||
>>> df = DataFrame(result['data'])
|
||||
>>> df.attrs['units'] = result['units'] # Store units as metadata
|
||||
>>> print(df.head())
|
||||
|
||||
>>> result = await get_psd_report_data(884) # Corn Area, Yield, Production
|
||||
>>> df = DataFrame(result['data'])
|
||||
>>> # All numeric columns are float64, ready for analysis
|
||||
>>> print(df.dtypes)
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from aiohttp import ClientError # noqa
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.provider.utils.helpers import get_async_requests_session
|
||||
from openbb_government_us.utils.psd_template_parser import parse_report
|
||||
|
||||
# Get template ID for this report
|
||||
template_id = None
|
||||
for group_data in PSD_REPORTS_METADATA.values():
|
||||
if report_id in group_data["reports"]:
|
||||
template_id = group_data["reports"][report_id]["templateId"]
|
||||
break
|
||||
|
||||
if template_id is None:
|
||||
raise OpenBBError(f"Invalid report ID -> {report_id} was not found.")
|
||||
|
||||
# Build URLs for both formats
|
||||
html_url = get_report_url(report_id, "html")
|
||||
csv_url = get_report_url(report_id, "csv")
|
||||
session = await get_async_requests_session()
|
||||
|
||||
try:
|
||||
# Fetch both HTML (for units) and CSV (for data)
|
||||
html_resp = await session.get(html_url)
|
||||
html = await html_resp.text()
|
||||
csv_resp = await session.get(csv_url)
|
||||
csv_text = await csv_resp.text()
|
||||
except ClientError as e:
|
||||
raise OpenBBError(f"Error fetching report {report_id} -> {e}") from e
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
# Check for server error
|
||||
if "error" in csv_text.lower():
|
||||
raise OpenBBError(f"Server error fetching report {report_id} -> {csv_text}")
|
||||
|
||||
lines = csv_text.replace("\r", "").strip().split("\n")
|
||||
|
||||
return parse_report(template_id, lines, html)
|
||||
|
||||
|
||||
def _get_commodity_attributes(commodity_code: str) -> list[str]:
|
||||
"""Fetch valid attribute names for a commodity using the metadata API."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.provider.utils.helpers import make_request
|
||||
|
||||
try:
|
||||
resp = make_request(
|
||||
f"https://apps.fas.usda.gov/PSDOnlineApi/api/query/GetMultiCommodityAttributes?commodityCodes={commodity_code},"
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
id_to_key = {v: k for k, v in ATTRIBUTES.items()}
|
||||
valid_keys = set()
|
||||
|
||||
for item in data:
|
||||
attr_id = item.get("attributeId")
|
||||
if attr_id and attr_id in id_to_key:
|
||||
valid_keys.add(id_to_key[attr_id])
|
||||
if valid_keys:
|
||||
return sorted(list(valid_keys))
|
||||
except Exception: # noqa # pylint: disable=broad-except
|
||||
pass
|
||||
# Fallback: return all attributes
|
||||
return list(ATTRIBUTES.keys())
|
||||
|
||||
|
||||
def _get_commodity_countries(commodity_code: str) -> dict[str, str]:
|
||||
"""Fetch valid country names for a commodity using the metadata API.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
commodity_code : str
|
||||
Commodity code (e.g., '0440000' for corn)
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[str, str]
|
||||
Mapping of country name -> country code
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.provider.utils.helpers import make_request
|
||||
|
||||
base_url = "https://apps.fas.usda.gov/PSDOnlineApi/api/CompositeVisualization/GetCountries?regionCode=R00&commodityCode="
|
||||
|
||||
try:
|
||||
resp = make_request(
|
||||
f"{base_url}{commodity_code}",
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
name_to_code = {}
|
||||
for item in data:
|
||||
code = item.get("value")
|
||||
name = item.get("text", "").strip()
|
||||
if code and name and code != "00": # Skip "All Countries" (00)
|
||||
name_to_code[name] = code
|
||||
if name_to_code:
|
||||
return name_to_code
|
||||
except Exception: # noqa # pylint: disable=broad-except
|
||||
pass
|
||||
# Fallback: return empty (will use global COUNTRIES list)
|
||||
return {}
|
||||
|
||||
|
||||
def get_timeseries( # pylint: disable=R0912,R0914,R0915,R0917 # noqa: PLR0912
|
||||
commodity: str,
|
||||
attribute: str | list[str] | None = None,
|
||||
country: str | list[str] | None = None,
|
||||
start_year: int | None = None,
|
||||
end_year: int | None = None,
|
||||
aggregate_region: bool = False,
|
||||
) -> list:
|
||||
"""
|
||||
Get time series for a commodity/attribute using ASYNC parallel requests.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
commodity : str
|
||||
Commodity name (e.g., 'cotton', 'wheat', 'corn', 'cattle')
|
||||
attribute : str | list[str] | None
|
||||
Attribute name(s). Can be:
|
||||
- Single string: 'production', 'exports'
|
||||
- Comma-separated: 'production, exports, ending_stocks'
|
||||
- List: ['production', 'exports', 'ending_stocks']
|
||||
If None, returns ALL attributes for this commodity.
|
||||
country : str | list[str] | None
|
||||
Country/region name(s). Can be:
|
||||
- Single string: 'United States', 'Brazil', 'EU', 'world'
|
||||
- Comma-separated: 'US, China, Brazil'
|
||||
- List: ['US', 'China', 'Brazil']
|
||||
If None, returns ALL countries.
|
||||
start_year : int | None
|
||||
First marketing year to include.
|
||||
end_year : int | None
|
||||
Last marketing year to include.
|
||||
aggregate_region : bool, optional
|
||||
If True, also include World + regional aggregates.
|
||||
Default False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
DataFrame
|
||||
Columns: region, country, commodity, attribute, marketing_year, value, unit
|
||||
Sorted by marketing_year, then value descending.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> get_timeseries('wheat', 'production')
|
||||
>>> get_timeseries('corn', 'exports', start_year=2020, end_year=2025)
|
||||
>>> get_timeseries('wheat', 'production', country='United States')
|
||||
>>> get_timeseries('wheat', 'production', country=['US', 'China', 'Brazil'])
|
||||
>>> get_timeseries('wheat', 'production', country='US, China, Brazil')
|
||||
>>> get_timeseries('wheat', 'production, exports, ending_stocks') # Multiple attributes
|
||||
>>> get_timeseries('wheat', ['production', 'exports']) # List of attributes
|
||||
>>> get_timeseries('wheat', 'production', aggregate_region=True) # World + regions only
|
||||
>>> get_timeseries('wheat') # ALL attributes for wheat
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import asyncio # noqa
|
||||
from datetime import datetime
|
||||
from aiohttp import ClientError, ClientSession
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.provider.utils.helpers import get_async_requests_session, run_async
|
||||
from pandas import DataFrame, notna
|
||||
|
||||
QUERY_URL = "https://apps.fas.usda.gov/PSDOnlineApi/api/query/RunQuery"
|
||||
key = commodity.lower().replace(" ", "_").replace("-", "_")
|
||||
|
||||
if key not in COMMODITIES:
|
||||
raise ValueError(
|
||||
f"Unknown commodity: {commodity} -> Valid choices: {list(COMMODITIES.keys())}"
|
||||
)
|
||||
|
||||
commodity_code = COMMODITIES[key]
|
||||
valid_attrs = _get_commodity_attributes(commodity_code)
|
||||
# Resolve attribute - None means ALL, can be single, list, or comma-separated
|
||||
if attribute is None:
|
||||
attr_ids = [ATTRIBUTES[a] for a in valid_attrs]
|
||||
else:
|
||||
# Normalize to list
|
||||
if isinstance(attribute, str):
|
||||
attr_list = [a.strip() for a in attribute.split(",") if a.strip()]
|
||||
else:
|
||||
attr_list = list(attribute)
|
||||
|
||||
# Validate each attribute
|
||||
attr_ids = []
|
||||
for attr in attr_list:
|
||||
attr_key = attr.lower().replace(" ", "_").replace("-", "_")
|
||||
if attr_key not in ATTRIBUTES:
|
||||
raise ValueError(
|
||||
f"Unknown attribute: '{attr}' for {commodity}. Valid attributes: {valid_attrs}"
|
||||
)
|
||||
if attr_key not in valid_attrs:
|
||||
raise ValueError(
|
||||
f"Attribute '{attr}' is not available for {commodity}. Valid attributes: {valid_attrs}"
|
||||
)
|
||||
attr_ids.append(ATTRIBUTES[attr_key])
|
||||
|
||||
# Resolve country/region - None means ALL
|
||||
# Accepts: lower_snake_case ("united_states"), codes ("US", "R05"), list, comma-separated, or None for all
|
||||
selected_region_codes: list[str] = [] # Track selected regions
|
||||
selected_country_codes: list[str] = [] # Track selected countries
|
||||
valid_countries_map = _get_commodity_countries(commodity_code)
|
||||
valid_country_codes = set(valid_countries_map.values())
|
||||
code_to_key = {}
|
||||
|
||||
for key, code in COUNTRIES.items():
|
||||
if code not in code_to_key:
|
||||
code_to_key[code] = key
|
||||
|
||||
def get_valid_country_keys() -> list[str]:
|
||||
"""Get sorted list of valid country keys from our COUNTRIES dict."""
|
||||
return sorted(
|
||||
[code_to_key.get(c, c) for c in valid_country_codes if c in code_to_key]
|
||||
)
|
||||
|
||||
if country is None:
|
||||
country_codes = list(valid_country_codes) if valid_country_codes else ["ALL"]
|
||||
else:
|
||||
country_list = (
|
||||
[c.strip() for c in country.split(",") if c.strip()]
|
||||
if isinstance(country, str)
|
||||
else [
|
||||
c.strip()
|
||||
for c in country[0].split(",")
|
||||
if isinstance(country, list)
|
||||
and len(country) == 1
|
||||
and isinstance(c, str)
|
||||
and c.strip()
|
||||
]
|
||||
)
|
||||
country_codes = []
|
||||
|
||||
for c in country_list:
|
||||
country_key = c.lower().replace(" ", "_").replace("-", "_")
|
||||
# Check if it's a region code (R00, R01, etc.)
|
||||
if c.upper() in REGION_DISPLAY:
|
||||
selected_region_codes.append(c.upper())
|
||||
country_codes.append(c.upper())
|
||||
# Check if it's a region name (includes "european_union" -> R05)
|
||||
elif country_key in REGIONS:
|
||||
region_code = REGIONS[country_key]
|
||||
selected_region_codes.append(region_code)
|
||||
country_codes.append(region_code)
|
||||
# Check for "eu" shorthand -> treat as region R05
|
||||
elif country_key == "eu":
|
||||
selected_region_codes.append("R05")
|
||||
country_codes.append("R05")
|
||||
# Check if it's a country code
|
||||
elif c.upper() in COUNTRY_TO_REGION:
|
||||
code = c.upper()
|
||||
# Validate against commodity-specific countries
|
||||
if valid_country_codes and code not in valid_country_codes:
|
||||
valid_keys = get_valid_country_keys()
|
||||
raise ValueError(
|
||||
f"Country '{c}' is not available for {commodity}. Valid countries: {valid_keys}"
|
||||
)
|
||||
selected_country_codes.append(code)
|
||||
country_codes.append(code)
|
||||
# Check if it's a country name (snake_case)
|
||||
elif country_key in COUNTRIES:
|
||||
code = COUNTRIES[country_key]
|
||||
# Validate against commodity-specific countries
|
||||
if valid_country_codes and code not in valid_country_codes:
|
||||
valid_keys = get_valid_country_keys()
|
||||
raise ValueError(
|
||||
f"Country '{c}' is not available for {commodity}. Valid countries: {valid_keys}"
|
||||
)
|
||||
selected_country_codes.append(code)
|
||||
country_codes.append(code)
|
||||
else:
|
||||
# Unknown country/region
|
||||
valid_keys = sorted(
|
||||
[k.lower().replace(" ", "_") for k in valid_countries_map]
|
||||
)
|
||||
valid_regions = list(REGIONS.keys())
|
||||
raise ValueError(
|
||||
f"Unknown country/region: '{c}' for {commodity}. "
|
||||
+ f"Valid countries: {valid_keys}. Valid regions: {valid_regions}"
|
||||
)
|
||||
|
||||
# If aggregate_region is True, expand appropriately
|
||||
if aggregate_region:
|
||||
if "R00" in selected_region_codes:
|
||||
# "world" selected - fetch all regional aggregates + any specific countries
|
||||
country_codes = (
|
||||
list(REGIONS.values()) + selected_country_codes
|
||||
) # R00, R01, ... R18 + specific countries
|
||||
country_codes = list(set(country_codes)) # Dedupe
|
||||
elif selected_region_codes:
|
||||
# Specific regions selected - add World + those regions + any countries
|
||||
country_codes = ["R00"] + selected_region_codes + selected_country_codes
|
||||
country_codes = list(set(country_codes)) # Dedupe
|
||||
elif selected_country_codes:
|
||||
# Only countries selected - add World + their regions
|
||||
regions_for_countries = set()
|
||||
for cc in selected_country_codes:
|
||||
if cc in COUNTRY_TO_REGION:
|
||||
regions_for_countries.add(COUNTRY_TO_REGION[cc])
|
||||
country_codes = (
|
||||
["R00"] + list(regions_for_countries) + selected_country_codes
|
||||
)
|
||||
country_codes = list(set(country_codes)) # Dedupe
|
||||
elif country is None:
|
||||
region_codes = set(REGIONS.values())
|
||||
# Exclude E4 (EU as country) since we're requesting R05 (EU as region)
|
||||
country_only_codes = [
|
||||
c for c in valid_country_codes if c not in region_codes and c != "E4"
|
||||
]
|
||||
country_codes = list(REGIONS.values()) + country_only_codes
|
||||
country_codes = list(set(country_codes))
|
||||
|
||||
# Determine year range
|
||||
current_year = datetime.now().year
|
||||
years = list(range(start_year or 1960, (end_year or current_year + 1) + 1))
|
||||
|
||||
# Split attrs into batches for parallel fetching (20 attrs per request is optimal)
|
||||
BATCH_SIZE = 20
|
||||
attr_batches = [
|
||||
attr_ids[i : i + BATCH_SIZE] for i in range(0, len(attr_ids), BATCH_SIZE)
|
||||
]
|
||||
|
||||
async def fetch_batch(session: ClientSession, batch_attrs: list) -> list:
|
||||
"""Fetch one batch of attributes."""
|
||||
payload = {
|
||||
"queryId": 0,
|
||||
"commodityGroupCode": None,
|
||||
"commodities": [commodity_code],
|
||||
"attributes": batch_attrs,
|
||||
"countries": country_codes,
|
||||
"marketYears": years,
|
||||
"chkCommoditySummary": False,
|
||||
"chkAttribSummary": False,
|
||||
"chkCountrySummary": False,
|
||||
"commoditySummaryText": "",
|
||||
"attribSummaryText": "",
|
||||
"countrySummaryText": "",
|
||||
"optionColumn": "year",
|
||||
"chkTopCountry": False,
|
||||
"topCountryCount": "",
|
||||
"chkfileFormat": False,
|
||||
"chkPrevMonth": False,
|
||||
"chkMonthChange": False,
|
||||
"chkCodes": False,
|
||||
"chkYearChange": False,
|
||||
"queryName": "",
|
||||
"sortOrder": "Commodity/Attribute/Country",
|
||||
"topCountryState": False,
|
||||
}
|
||||
try:
|
||||
async with await session.post(QUERY_URL, json=payload) as resp:
|
||||
if resp.status != 200:
|
||||
return []
|
||||
data = await resp.json()
|
||||
return data.get("queryResult", [])
|
||||
except ClientError as e:
|
||||
raise OpenBBError(e) from e
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
async def fetch_all_batches():
|
||||
"""Fetch all batches concurrently."""
|
||||
async with await get_async_requests_session() as session:
|
||||
tasks = [fetch_batch(session, batch) for batch in attr_batches]
|
||||
results = await asyncio.gather(*tasks)
|
||||
# Flatten results
|
||||
all_results = []
|
||||
for r in results:
|
||||
all_results.extend(r)
|
||||
return all_results
|
||||
|
||||
# Run async fetch
|
||||
result = run_async(fetch_all_batches)
|
||||
|
||||
if not result:
|
||||
raise OpenBBError(
|
||||
"No data available for the given parameters. -> "
|
||||
+ f"{commodity} | {attribute} | {country} | {start_year}-{end_year}"
|
||||
)
|
||||
|
||||
df = DataFrame(result)
|
||||
|
||||
# API only fills commodity/attribute on first row of each group - forward fill
|
||||
df["commodity"] = df["commodity"].ffill()
|
||||
df["attribute"] = df["attribute"].ffill()
|
||||
|
||||
# Find year columns (format: 2024/2025)
|
||||
year_cols = [c for c in df.columns if "/" in c and c[0:4].isdigit()]
|
||||
name_to_code = {name.strip(): code for name, code in valid_countries_map.items()}
|
||||
# Also add region display names for lookup
|
||||
for region_code, region_name in REGION_DISPLAY.items():
|
||||
name_to_code[region_name] = region_code
|
||||
|
||||
# Build region name lookup (for detecting when "country" is actually a region)
|
||||
region_names = {v: k for k, v in REGION_DISPLAY.items()} # "North America" -> "R01"
|
||||
|
||||
def get_region(country_name: str) -> str:
|
||||
"""Check if this is actually a region (excluding EU)"""
|
||||
if country_name in region_names and country_name != "European Union":
|
||||
return country_name # It's a region name, return as-is
|
||||
# European Union is treated as a country in the EU region
|
||||
if country_name == "European Union":
|
||||
return "European Union"
|
||||
code = name_to_code.get(country_name)
|
||||
if code and code in COUNTRY_TO_REGION:
|
||||
return REGION_DISPLAY.get(COUNTRY_TO_REGION[code], "Other")
|
||||
return "Other"
|
||||
|
||||
def is_region(country_name: str) -> bool:
|
||||
"""Check if the country name is actually a region aggregate."""
|
||||
# When aggregate_region=True, "European Union" is always a region aggregate
|
||||
if aggregate_region and country_name == "European Union":
|
||||
return True
|
||||
if country_name == "European Union":
|
||||
return False # EU is a country otherwise
|
||||
# "Other" is a region aggregate, not a country
|
||||
if country_name == "Other":
|
||||
return True
|
||||
return country_name in region_names
|
||||
|
||||
# Melt to long format - include attribute per row from API response
|
||||
rows = []
|
||||
for _, row in df.iterrows():
|
||||
country_name = row["country"]
|
||||
# Skip rows with no country name
|
||||
if not notna(country_name) or not country_name:
|
||||
continue
|
||||
commodity_display = row["commodity"] if row["commodity"] else commodity.title()
|
||||
attr_display = row["attribute"] if row["attribute"] else ""
|
||||
unit = row.get("unit Description", "") or ""
|
||||
for yr_col in year_cols:
|
||||
val = row[yr_col]
|
||||
if notna(val):
|
||||
# If country_name is actually a region, set region=name and country=""
|
||||
if is_region(country_name):
|
||||
region_val = country_name
|
||||
country_val = "--"
|
||||
else:
|
||||
region_val = get_region(country_name)
|
||||
country_val = country_name
|
||||
rows.append(
|
||||
{
|
||||
"region": region_val,
|
||||
"country": country_val,
|
||||
"commodity": commodity_display,
|
||||
"attribute": attr_display,
|
||||
"marketing_year": yr_col,
|
||||
"value": val,
|
||||
"unit": unit.strip().strip("()"),
|
||||
}
|
||||
)
|
||||
|
||||
df_long = DataFrame(rows)
|
||||
|
||||
if df_long.empty:
|
||||
raise OpenBBError(
|
||||
"No data available for the given parameters. -> "
|
||||
+ f"{commodity} | {attribute} | {country} | {start_year}-{end_year}"
|
||||
)
|
||||
|
||||
# Filter by year range (marketing_year is full format like "2024/2025")
|
||||
if start_year is not None or end_year is not None:
|
||||
df_long["_year"] = df_long["marketing_year"].str.split("/").str[0].astype(int)
|
||||
|
||||
if start_year is not None:
|
||||
df_long = df_long[df_long["_year"] >= start_year]
|
||||
if end_year is not None:
|
||||
df_long = df_long[df_long["_year"] <= end_year]
|
||||
|
||||
df_long = df_long.drop(columns=["_year"])
|
||||
|
||||
# Reorder columns
|
||||
df_long = df_long[
|
||||
[
|
||||
"region",
|
||||
"country",
|
||||
"commodity",
|
||||
"attribute",
|
||||
"marketing_year",
|
||||
"value",
|
||||
"unit",
|
||||
]
|
||||
]
|
||||
|
||||
# Hierarchical sorting: World first, then regions by value, then countries by value
|
||||
if not df_long[df_long["country"] == "--"].empty:
|
||||
# Get region totals for sorting regions by value (per attribute + year)
|
||||
region_totals = (
|
||||
df_long[df_long["country"] == "--"]
|
||||
.groupby(["region", "attribute", "marketing_year"])["value"]
|
||||
.first()
|
||||
.reset_index()
|
||||
)
|
||||
region_totals = region_totals.rename(columns={"value": "_region_value"})
|
||||
# Merge region totals for sorting
|
||||
df_long = df_long.merge(
|
||||
region_totals, on=["region", "attribute", "marketing_year"], how="left"
|
||||
)
|
||||
# World always sorts first
|
||||
df_long.loc[df_long["region"] == "World", "_region_value"] = float("inf")
|
||||
|
||||
# Sort: region by value desc, then empty country first (aggregate), then countries by value desc
|
||||
df_long["_is_aggregate"] = (df_long["country"] == "--").astype(
|
||||
int
|
||||
) # 1 for aggregate, 0 for country
|
||||
df_long = (
|
||||
df_long.sort_values(
|
||||
by=[
|
||||
"commodity",
|
||||
"attribute",
|
||||
"marketing_year",
|
||||
"_region_value",
|
||||
"_is_aggregate",
|
||||
"value",
|
||||
],
|
||||
ascending=[True, True, True, False, False, False],
|
||||
)
|
||||
.drop(columns=["_region_value", "_is_aggregate"])
|
||||
.reset_index(drop=True)
|
||||
)
|
||||
else:
|
||||
# No regional aggregates - just sort by commodity, attribute, year, value
|
||||
df_long = df_long.sort_values( # type: ignore
|
||||
by=["commodity", "attribute", "marketing_year", "value"],
|
||||
ascending=[True, True, True, False],
|
||||
).reset_index(drop=True)
|
||||
|
||||
return df_long.to_dict(orient="records")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,390 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- application/json
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Connection:
|
||||
- keep-alive
|
||||
method: GET
|
||||
uri: https://apps.fas.usda.gov/psdonline/reportHandler.ashx?fileName=Coffee_Summary&format=html&reportId=2109&templateId=8
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
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
|
||||
headers:
|
||||
Cache-Control:
|
||||
- private
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Length:
|
||||
- '7406'
|
||||
Content-Type:
|
||||
- text/html; charset=utf-8
|
||||
Date:
|
||||
- Fri, 02 Jan 2026 21:26:10 GMT
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000;includeSubdomains;preload
|
||||
Vary:
|
||||
- Accept-Encoding
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- application/json
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Connection:
|
||||
- keep-alive
|
||||
method: GET
|
||||
uri: https://apps.fas.usda.gov/psdonline/reportHandler.ashx?fileName=Coffee_Summary&format=csv&reportId=2109&templateId=8
|
||||
response:
|
||||
body:
|
||||
string: "Coffee Summary\r\n\r\n\r\n ,2021/22,2022/23,2023/24,2024/25,Jun
|
||||
2025/26,Dec 2025/26,\r\nArabica Production,\r\n nr,nr,nr,nr,nr,nr,\r\n
|
||||
Brazil,36400,39800,44900,44000,40900,38000,\r\n Colombia,11800,10700,12760,14800,12500,13800,\r\n
|
||||
Ethiopia,8150,7300,9130,11100,11560,11560,\r\n Honduras,4800,5700,5050,5000,5800,5800,\r\n
|
||||
Peru,4200,3475,3912,3700,4200,4200,\r\n Mexico,3200,3000,3356,3530,3540,3540,\r\n
|
||||
Guatemala,3410,3150,3339,3075,3410,3410,\r\n Nicaragua,2718,2430,2187,2400,2420,2420,\r\n
|
||||
China,1800,1800,1800,1900,1900,1900,\r\n Indonesia,1280,1200,1350,1400,1450,1450,\r\n
|
||||
India,1583,1667,1480,1400,1350,1350,\r\n Vietnam,1100,1000,1000,1000,1000,1200,\r\n
|
||||
Costa Rica,1215,1100,1158,1298,1170,1170,\r\n Uganda,990,990,1000,1030,1060,1060,\r\n
|
||||
Papua New Guinea,725,825,930,800,900,900,\r\n Other,3718,3646,3888,3770,3862,3755,\r\n\r\n
|
||||
\ Total,87089,87783,97240,100203,97022,95515,\r\n\r\nRobusta Production,\r\n
|
||||
\ nr,nr,nr,nr,nr,nr,\r\n Vietnam,30480,27300,26550,28000,30000,29600,\r\n
|
||||
Brazil,21700,22800,21400,21000,24100,25000,\r\n Indonesia,9300,9500,6800,9300,9800,11000,\r\n
|
||||
Uganda,5060,5575,5400,5670,5815,5815,\r\n India,4117,4200,5080,4800,4700,4700,\r\n
|
||||
Malaysia,2000,2000,1330,1400,1500,1500,\r\n Cote dchr(39)Ivoire,1110,1020,1325,650,1300,1300,\r\n
|
||||
Thailand,650,700,750,900,900,900,\r\n Tanzania,550,550,600,650,700,700,\r\n
|
||||
Laos,385,410,410,430,430,430,\r\n Other,2603,2551,2460,2313,2413,2388,\r\n\r\n
|
||||
\ Total,77955,76606,72105,75113,81658,83333,\r\n\r\nProduction,\r\n nr,nr,nr,nr,nr,nr,\r\n
|
||||
Brazil,58100,62600,66300,65000,65000,63000,\r\n Vietnam,31580,28300,27550,29000,31000,30800,\r\n
|
||||
Colombia,11800,10700,12760,14800,12500,13800,\r\n Indonesia,10580,10700,8150,10700,11250,12450,\r\n
|
||||
Ethiopia,8150,7300,9130,11100,11560,11560,\r\n Uganda,6050,6565,6400,6700,6875,6875,\r\n
|
||||
India,5700,5867,6560,6200,6050,6050,\r\n Honduras,4800,5700,5050,5000,5800,5800,\r\n
|
||||
Peru,4200,3475,3912,3700,4200,4200,\r\n Mexico,3740,3545,3856,3870,3903,3903,\r\n
|
||||
Guatemala,3540,3270,3469,3205,3540,3540,\r\n Nicaragua,2838,2590,2347,2560,2580,2580,\r\n
|
||||
China,1800,1800,1800,1900,1900,1900,\r\n Malaysia,2000,2000,1330,1400,1500,1500,\r\n
|
||||
Tanzania,1200,1200,1350,1350,1450,1450,\r\n Cote dchr(39)Ivoire,1110,1020,1325,650,1300,1300,\r\n
|
||||
Costa Rica,1215,1100,1158,1298,1170,1170,\r\n Papua New Guinea,775,875,980,840,950,950,\r\n
|
||||
Thailand,650,700,750,900,900,900,\r\n Kenya,860,750,825,1000,850,850,\r\n
|
||||
El Salvador,600,550,604,450,597,500,\r\n Venezuela,500,500,500,500,500,500,\r\n
|
||||
Philippines,450,475,450,450,450,450,\r\n Laos,385,410,410,430,430,430,\r\n
|
||||
Ecuador,260,354,370,355,350,350,\r\n Other,2161,2043,2009,1958,2075,2040,\r\n\r\n
|
||||
\ Total,165044,164389,169345,175316,178680,178848,"
|
||||
headers:
|
||||
Cache-Control:
|
||||
- private
|
||||
Content-Disposition:
|
||||
- attachment;filename=Coffee_Summary.csv
|
||||
Content-Length:
|
||||
- '2651'
|
||||
Content-Type:
|
||||
- application/vnd.ms-excel; charset=utf-8
|
||||
Date:
|
||||
- Fri, 02 Jan 2026 21:26:10 GMT
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000;includeSubdomains;preload
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Connection:
|
||||
- keep-alive
|
||||
method: GET
|
||||
uri: https://apps.fas.usda.gov/PSDOnlineApi/api/query/GetMultiCommodityAttributes?commodityCodes=0711100%2C
|
||||
response:
|
||||
body:
|
||||
string: '[{"attributeId":20,"attributeName":"Beginning Stocks ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"20","text":"Beginning
|
||||
Stocks ","comments":null},{"attributeId":29,"attributeName":"Arabica
|
||||
Production ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"29","text":"Arabica
|
||||
Production ","comments":null},{"attributeId":53,"attributeName":"Robusta
|
||||
Production ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"53","text":"Robusta
|
||||
Production ","comments":null},{"attributeId":56,"attributeName":"Other
|
||||
Production ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"56","text":"Other
|
||||
Production ","comments":null},{"attributeId":28,"attributeName":"Production ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"28","text":"Production ","comments":null},{"attributeId":58,"attributeName":"Bean
|
||||
Imports ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"58","text":"Bean
|
||||
Imports ","comments":null},{"attributeId":75,"attributeName":"Roast
|
||||
& Ground Imports ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"75","text":"Roast
|
||||
& Ground Imports ","comments":null},{"attributeId":82,"attributeName":"Soluble
|
||||
Imports ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"82","text":"Soluble
|
||||
Imports ","comments":null},{"attributeId":57,"attributeName":"Imports ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"57","text":"Imports ","comments":null},{"attributeId":86,"attributeName":"Total
|
||||
Supply ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"86","text":"Total
|
||||
Supply ","comments":null},{"attributeId":90,"attributeName":"Bean
|
||||
Exports ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"90","text":"Bean
|
||||
Exports ","comments":null},{"attributeId":107,"attributeName":"Roast
|
||||
& Ground Exports ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"107","text":"Roast
|
||||
& Ground Exports ","comments":null},{"attributeId":114,"attributeName":"Soluble
|
||||
Exports ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"114","text":"Soluble
|
||||
Exports ","comments":null},{"attributeId":88,"attributeName":"Exports ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"88","text":"Exports ","comments":null},{"attributeId":141,"attributeName":"Rst,Ground
|
||||
Dom. Consum ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"141","text":"Rst,Ground
|
||||
Dom. Consum ","comments":null},{"attributeId":154,"attributeName":"Soluble
|
||||
Dom. Cons. ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"154","text":"Soluble
|
||||
Dom. Cons. ","comments":null},{"attributeId":125,"attributeName":"Domestic
|
||||
Consumption ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"125","text":"Domestic
|
||||
Consumption ","comments":null},{"attributeId":176,"attributeName":"Ending
|
||||
Stocks ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"176","text":"Ending
|
||||
Stocks ","comments":null},{"attributeId":178,"attributeName":"Total
|
||||
Distribution ","unitOfMeasure":null,"calculationRuleName":null,"constituentAttributes":null,"publicAccess":false,"value":"178","text":"Total
|
||||
Distribution ","comments":null}]'
|
||||
headers:
|
||||
Content-Length:
|
||||
- '4480'
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Date:
|
||||
- Fri, 02 Jan 2026 21:26:10 GMT
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000;includeSubdomains;preload
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Connection:
|
||||
- keep-alive
|
||||
method: GET
|
||||
uri: https://apps.fas.usda.gov/PSDOnlineApi/api/CompositeVisualization/GetCountries?commodityCode=0711100®ionCode=R00
|
||||
response:
|
||||
body:
|
||||
string: '[{"text":"All Countries","value":"00"},{"text":"Albania ","value":"AL"},{"text":"Algeria ","value":"AG"},{"text":"Angola ","value":"AO"},{"text":"Argentina ","value":"AR"},{"text":"Armenia ","value":"AM"},{"text":"Australia ","value":"AS"},{"text":"Benin ","value":"DM"},{"text":"Bolivia ","value":"BL"},{"text":"Bosnia
|
||||
and Herzegovina ","value":"BK"},{"text":"Brazil ","value":"BR"},{"text":"Burundi ","value":"BY"},{"text":"Cameroon ","value":"CM"},{"text":"Canada ","value":"CA"},{"text":"Central
|
||||
African Republic ","value":"CT"},{"text":"Chile ","value":"CI"},{"text":"China ","value":"CH"},{"text":"Colombia ","value":"CO"},{"text":"Congo
|
||||
(Brazzaville) ","value":"CF"},{"text":"Congo (Kinshasa) ","value":"CG"},{"text":"Costa
|
||||
Rica ","value":"CS"},{"text":"Cote d''Ivoire ","value":"IV"},{"text":"Croatia ","value":"HR"},{"text":"Cuba ","value":"CU"},{"text":"Dominican
|
||||
Republic ","value":"DR"},{"text":"Ecuador ","value":"EC"},{"text":"Egypt ","value":"EG"},{"text":"El
|
||||
Salvador ","value":"ES"},{"text":"Equatorial Guinea ","value":"EK"},{"text":"Ethiopia ","value":"ET"},{"text":"European
|
||||
Union ","value":"E4"},{"text":"Gabon ","value":"GB"},{"text":"Georgia ","value":"GG"},{"text":"Ghana ","value":"GH"},{"text":"Guatemala ","value":"GT"},{"text":"Guinea ","value":"GU"},{"text":"Guyana ","value":"GY"},{"text":"Haiti ","value":"HA"},{"text":"Honduras ","value":"HO"},{"text":"India ","value":"IN"},{"text":"Indonesia ","value":"ID"},{"text":"Iran ","value":"IR"},{"text":"Jamaica ","value":"JM"},{"text":"Japan ","value":"JA"},{"text":"Jordan ","value":"JO"},{"text":"Kazakhstan ","value":"KZ"},{"text":"Kenya ","value":"KE"},{"text":"Korea,
|
||||
South ","value":"KS"},{"text":"Kosovo ","value":"KV"},{"text":"Laos ","value":"LA"},{"text":"Liberia ","value":"LI"},{"text":"Madagascar ","value":"MA"},{"text":"Malawi ","value":"MI"},{"text":"Malaysia ","value":"MY"},{"text":"Mexico ","value":"MX"},{"text":"Montenegro ","value":"MJ"},{"text":"Morocco ","value":"MO"},{"text":"New
|
||||
Caledonia ","value":"NC"},{"text":"New Zealand ","value":"NZ"},{"text":"Nicaragua ","value":"NU"},{"text":"Nigeria ","value":"NI"},{"text":"North
|
||||
Macedonia ","value":"MK"},{"text":"Norway ","value":"NO"},{"text":"Panama ","value":"PN"},{"text":"Papua
|
||||
New Guinea ","value":"PP"},{"text":"Paraguay ","value":"PA"},{"text":"Peru ","value":"PE"},{"text":"Philippines ","value":"RP"},{"text":"Russia ","value":"RS"},{"text":"Rwanda ","value":"RW"},{"text":"Saudi
|
||||
Arabia ","value":"SA"},{"text":"Senegal ","value":"SG"},{"text":"Serbia ","value":"RB"},{"text":"Sierra
|
||||
Leone ","value":"SL"},{"text":"Singapore ","value":"SN"},{"text":"South
|
||||
Africa ","value":"SF"},{"text":"Sri Lanka ","value":"CE"},{"text":"Switzerland ","value":"SZ"},{"text":"Taiwan ","value":"TW"},{"text":"Tanzania ","value":"TZ"},{"text":"Thailand ","value":"TH"},{"text":"Togo ","value":"TO"},{"text":"Trinidad
|
||||
and Tobago ","value":"TD"},{"text":"Turkey ","value":"TU"},{"text":"Uganda ","value":"UG"},{"text":"Ukraine ","value":"UP"},{"text":"United
|
||||
Kingdom ","value":"UK"},{"text":"United States ","value":"US"},{"text":"Uruguay ","value":"UY"},{"text":"Venezuela ","value":"VE"},{"text":"Vietnam ","value":"VM"},{"text":"Yemen ","value":"YM"},{"text":"Yemen
|
||||
(Sanaa) ","value":"YE"},{"text":"Zambia ","value":"ZA"},{"text":"Zimbabwe ","value":"RH"}]'
|
||||
headers:
|
||||
Content-Length:
|
||||
- '5209'
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Date:
|
||||
- Fri, 02 Jan 2026 21:26:11 GMT
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000;includeSubdomains;preload
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body:
|
||||
attribSummaryText: ''
|
||||
attributes:
|
||||
- 88
|
||||
chkAttribSummary: false
|
||||
chkCodes: false
|
||||
chkCommoditySummary: false
|
||||
chkCountrySummary: false
|
||||
chkMonthChange: false
|
||||
chkPrevMonth: false
|
||||
chkTopCountry: false
|
||||
chkYearChange: false
|
||||
chkfileFormat: false
|
||||
commodities:
|
||||
- '0711100'
|
||||
commodityGroupCode: null
|
||||
commoditySummaryText: ''
|
||||
countries:
|
||||
- BR
|
||||
countrySummaryText: ''
|
||||
marketYears:
|
||||
- 2025
|
||||
optionColumn: year
|
||||
queryId: 0
|
||||
queryName: ''
|
||||
sortOrder: Commodity/Attribute/Country
|
||||
topCountryCount: ''
|
||||
topCountryState: false
|
||||
headers:
|
||||
Accept:
|
||||
- application/json
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Connection:
|
||||
- keep-alive
|
||||
method: POST
|
||||
uri: https://apps.fas.usda.gov/PSDOnlineApi/api/query/RunQuery
|
||||
response:
|
||||
body:
|
||||
string: '{"tableHeaders":["Commodity","Attribute","Country","2025/2026","Unit
|
||||
Description"],"queryResult":[{"commodity":"Coffee, Green","attribute":"Exports","country":"Brazil","2025/2026":40750.000,"unit
|
||||
Description":"(1000 60 KG BAGS) "}]}'
|
||||
headers:
|
||||
Content-Length:
|
||||
- '234'
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Date:
|
||||
- Fri, 02 Jan 2026 21:26:11 GMT
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000;includeSubdomains;preload
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,380 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- application/json
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Connection:
|
||||
- keep-alive
|
||||
method: GET
|
||||
uri: https://esmis.nal.usda.gov/publication/weekly-weather-and-crop-bulletin?date=2024-12
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
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
|
||||
qgOxp9i2uQmWTasZbMCf/DfoXDzEhh0BAA==
|
||||
headers:
|
||||
Cache-Control:
|
||||
- max-age=3600, public
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Language:
|
||||
- en
|
||||
Content-Length:
|
||||
- '11425'
|
||||
Content-Security-Policy:
|
||||
- 'default-src ''self''; child-src ''self'' https://platform.twitter.com https://youtube.com;
|
||||
connect-src ''self'' https://dap.digitalgov.gov https://*.doubleclick.net
|
||||
https://orcid.org/userStatus.json https://search.usa.gov https://translate.googleapis.com
|
||||
https://usda.libanswers.com https://*.google-analytics.com https://*.analytics.google.com
|
||||
https://*.googletagmanager.com https://*.g.doubleclick.net https://*.google.com
|
||||
https://*.google.com https://*.google.ad https://*.google.ae https://*.google.com.af
|
||||
https://*.google.com.ag https://*.google.al https://*.google.am https://*.google.co.ao
|
||||
https://*.google.com.ar https://*.google.as https://*.google.at https://*.google.com.au
|
||||
https://*.google.az https://*.google.ba https://*.google.com.bd https://*.google.be
|
||||
https://*.google.bf https://*.google.bg https://*.google.com.bh https://*.google.bi
|
||||
https://*.google.bj https://*.google.com.bn https://*.google.com.bo https://*.google.com.br
|
||||
https://*.google.bs https://*.google.bt https://*.google.co.bw https://*.google.by
|
||||
https://*.google.com.bz https://*.google.ca https://*.google.cd https://*.google.cf
|
||||
https://*.google.cg https://*.google.ch https://*.google.ci https://*.google.co.ck
|
||||
https://*.google.cl https://*.google.cm https://*.google.cn https://*.google.com.co
|
||||
https://*.google.co.cr https://*.google.com.cu https://*.google.cv https://*.google.com.cy
|
||||
https://*.google.cz https://*.google.de https://*.google.dj https://*.google.dk
|
||||
https://*.google.dm https://*.google.com.do https://*.google.dz https://*.google.com.ec
|
||||
https://*.google.ee https://*.google.com.eg https://*.google.es https://*.google.com.et
|
||||
https://*.google.fi https://*.google.com.fj https://*.google.fm https://*.google.fr
|
||||
https://*.google.ga https://*.google.ge https://*.google.gg https://*.google.com.gh
|
||||
https://*.google.com.gi https://*.google.gl https://*.google.gm https://*.google.gr
|
||||
https://*.google.com.gt https://*.google.gy https://*.google.com.hk https://*.google.hn
|
||||
https://*.google.hr https://*.google.ht https://*.google.hu https://*.google.co.id
|
||||
https://*.google.ie https://*.google.co.il https://*.google.im https://*.google.co.in
|
||||
https://*.google.iq https://*.google.is https://*.google.it https://*.google.je
|
||||
https://*.google.com.jm https://*.google.jo https://*.google.co.jp https://*.google.co.ke
|
||||
https://*.google.com.kh https://*.google.ki https://*.google.kg https://*.google.co.kr
|
||||
https://*.google.com.kw https://*.google.kz https://*.google.la https://*.google.com.lb
|
||||
https://*.google.li https://*.google.lk https://*.google.co.ls https://*.google.lt
|
||||
https://*.google.lu https://*.google.lv https://*.google.com.ly https://*.google.co.ma
|
||||
https://*.google.md https://*.google.me https://*.google.mg https://*.google.mk
|
||||
https://*.google.ml https://*.google.com.mm https://*.google.mn https://*.google.com.mt
|
||||
https://*.google.mu https://*.google.mv https://*.google.mw https://*.google.com.mx
|
||||
https://*.google.com.my https://*.google.co.mz https://*.google.com.na https://*.google.com.ng
|
||||
https://*.google.com.ni https://*.google.ne https://*.google.nl https://*.google.no
|
||||
https://*.google.com.np https://*.google.nr https://*.google.nu https://*.google.co.nz
|
||||
https://*.google.com.om https://*.google.com.pa https://*.google.com.pe https://*.google.com.pg
|
||||
https://*.google.com.ph https://*.google.com.pk https://*.google.pl https://*.google.pn
|
||||
https://*.google.com.pr https://*.google.ps https://*.google.pt https://*.google.com.py
|
||||
https://*.google.com.qa https://*.google.ro https://*.google.ru https://*.google.rw
|
||||
https://*.google.com.sa https://*.google.com.sb https://*.google.sc https://*.google.se
|
||||
https://*.google.com.sg https://*.google.sh https://*.google.si https://*.google.sk
|
||||
https://*.google.com.sl https://*.google.sn https://*.google.so https://*.google.sm
|
||||
https://*.google.sr https://*.google.st https://*.google.com.sv https://*.google.td
|
||||
https://*.google.tg https://*.google.co.th https://*.google.com.tj https://*.google.tl
|
||||
https://*.google.tm https://*.google.tn https://*.google.to https://*.google.com.tr
|
||||
https://*.google.tt https://*.google.com.tw https://*.google.co.tz https://*.google.com.ua
|
||||
https://*.google.co.ug https://*.google.co.uk https://*.google.com.uy https://*.google.co.uz
|
||||
https://*.google.com.vc https://*.google.co.ve https://*.google.co.vi https://*.google.com.vn
|
||||
https://*.google.vu https://*.google.ws https://*.google.rs https://*.google.co.za
|
||||
https://*.google.co.zm https://*.google.co.zw https://*.google.cat; font-src
|
||||
''self'' data: https://cdn.knightlab.com https://fonts.gstatic.com; frame-src
|
||||
''self'' https://publicdashboards.dl.usda.gov https://*.twitter.com https://usda.libanswers.com
|
||||
https://player.vimeo.com https://*.youtube.com; frame-ancestors ''self'';
|
||||
img-src ''self'' data: https://cdn.knightlab.com https://feed.informer.com
|
||||
https://fonts.gstatic.com https://*.nal.usda.gov https://orcid.org https://info.orcid.org
|
||||
https://*.twitter.com https://*.usa.gov https://validator.swagger.io https://*.ytimg.com
|
||||
https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com
|
||||
https://*.g.doubleclick.net https://*.google.com https://*.google.com https://*.google.ad
|
||||
https://*.google.ae https://*.google.com.af https://*.google.com.ag https://*.google.al
|
||||
https://*.google.am https://*.google.co.ao https://*.google.com.ar https://*.google.as
|
||||
https://*.google.at https://*.google.com.au https://*.google.az https://*.google.ba
|
||||
https://*.google.com.bd https://*.google.be https://*.google.bf https://*.google.bg
|
||||
https://*.google.com.bh https://*.google.bi https://*.google.bj https://*.google.com.bn
|
||||
https://*.google.com.bo https://*.google.com.br https://*.google.bs https://*.google.bt
|
||||
https://*.google.co.bw https://*.google.by https://*.google.com.bz https://*.google.ca
|
||||
https://*.google.cd https://*.google.cf https://*.google.cg https://*.google.ch
|
||||
https://*.google.ci https://*.google.co.ck https://*.google.cl https://*.google.cm
|
||||
https://*.google.cn https://*.google.com.co https://*.google.co.cr https://*.google.com.cu
|
||||
https://*.google.cv https://*.google.com.cy https://*.google.cz https://*.google.de
|
||||
https://*.google.dj https://*.google.dk https://*.google.dm https://*.google.com.do
|
||||
https://*.google.dz https://*.google.com.ec https://*.google.ee https://*.google.com.eg
|
||||
https://*.google.es https://*.google.com.et https://*.google.fi https://*.google.com.fj
|
||||
https://*.google.fm https://*.google.fr https://*.google.ga https://*.google.ge
|
||||
https://*.google.gg https://*.google.com.gh https://*.google.com.gi https://*.google.gl
|
||||
https://*.google.gm https://*.google.gr https://*.google.com.gt https://*.google.gy
|
||||
https://*.google.com.hk https://*.google.hn https://*.google.hr https://*.google.ht
|
||||
https://*.google.hu https://*.google.co.id https://*.google.ie https://*.google.co.il
|
||||
https://*.google.im https://*.google.co.in https://*.google.iq https://*.google.is
|
||||
https://*.google.it https://*.google.je https://*.google.com.jm https://*.google.jo
|
||||
https://*.google.co.jp https://*.google.co.ke https://*.google.com.kh https://*.google.ki
|
||||
https://*.google.kg https://*.google.co.kr https://*.google.com.kw https://*.google.kz
|
||||
https://*.google.la https://*.google.com.lb https://*.google.li https://*.google.lk
|
||||
https://*.google.co.ls https://*.google.lt https://*.google.lu https://*.google.lv
|
||||
https://*.google.com.ly https://*.google.co.ma https://*.google.md https://*.google.me
|
||||
https://*.google.mg https://*.google.mk https://*.google.ml https://*.google.com.mm
|
||||
https://*.google.mn https://*.google.com.mt https://*.google.mu https://*.google.mv
|
||||
https://*.google.mw https://*.google.com.mx https://*.google.com.my https://*.google.co.mz
|
||||
https://*.google.com.na https://*.google.com.ng https://*.google.com.ni https://*.google.ne
|
||||
https://*.google.nl https://*.google.no https://*.google.com.np https://*.google.nr
|
||||
https://*.google.nu https://*.google.co.nz https://*.google.com.om https://*.google.com.pa
|
||||
https://*.google.com.pe https://*.google.com.pg https://*.google.com.ph https://*.google.com.pk
|
||||
https://*.google.pl https://*.google.pn https://*.google.com.pr https://*.google.ps
|
||||
https://*.google.pt https://*.google.com.py https://*.google.com.qa https://*.google.ro
|
||||
https://*.google.ru https://*.google.rw https://*.google.com.sa https://*.google.com.sb
|
||||
https://*.google.sc https://*.google.se https://*.google.com.sg https://*.google.sh
|
||||
https://*.google.si https://*.google.sk https://*.google.com.sl https://*.google.sn
|
||||
https://*.google.so https://*.google.sm https://*.google.sr https://*.google.st
|
||||
https://*.google.com.sv https://*.google.td https://*.google.tg https://*.google.co.th
|
||||
https://*.google.com.tj https://*.google.tl https://*.google.tm https://*.google.tn
|
||||
https://*.google.to https://*.google.com.tr https://*.google.tt https://*.google.com.tw
|
||||
https://*.google.co.tz https://*.google.com.ua https://*.google.co.ug https://*.google.co.uk
|
||||
https://*.google.com.uy https://*.google.co.uz https://*.google.com.vc https://*.google.co.ve
|
||||
https://*.google.co.vi https://*.google.com.vn https://*.google.vu https://*.google.ws
|
||||
https://*.google.rs https://*.google.co.za https://*.google.co.zm https://*.google.co.zw
|
||||
https://*.google.cat; script-src ''self'' https://ajax.googleapis.com https://cdn.knightlab.com
|
||||
https://dap.digitalgov.gov https://feed.informer.com https://*.google-analytics.com
|
||||
https://*.libanswers.com https://www.nal.usda.gov https://platform.twitter.com
|
||||
https://search.usa.gov https://www.googletagmanager.com https://www.youtube.com;
|
||||
script-src-elem ''self'' ''unsafe-inline'' https://ajax.googleapis.com https://cdn.knightlab.com
|
||||
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/ https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/
|
||||
https://dap.digitalgov.gov https://feed.informer.com https://*.libanswers.com
|
||||
https://www.nal.usda.gov https://publicdashboards.dl.usda.gov https://search.usa.gov
|
||||
https://*.twitter.com https://unpkg.com/chart.js@4.4.0/ https://unpkg.com/chartjs-adapter-date-fns@3.0.0/
|
||||
https://unpkg.com/chartjs-plugin-datalabels@2.0.0/ https://*.vimeo.com https://www.google-analytics.com
|
||||
https://www.googletagmanager.com https://www.youtube.com; style-src ''self''
|
||||
''unsafe-inline'' https://cdn.knightlab.com https://fonts.googleapis.com https://search.usa.gov;
|
||||
style-src-elem ''self'' ''unsafe-inline'' https://cdn.knightlab.com https://static-assets-us.libanswers.com
|
||||
https://search.usa.gov;;'
|
||||
Content-Type:
|
||||
- text/html; charset=UTF-8
|
||||
Date:
|
||||
- Wed, 31 Dec 2025 05:25:42 GMT
|
||||
Etag:
|
||||
- '"1767143210-gzip"'
|
||||
Expires:
|
||||
- Sun, 19 Nov 1978 05:00:00 GMT
|
||||
Last-Modified:
|
||||
- Wed, 31 Dec 2025 01:06:50 GMT
|
||||
Server:
|
||||
- Apache
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Vary:
|
||||
- Cookie,Accept-Encoding
|
||||
X-Backend-Server:
|
||||
- '2'
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Drupal-Cache:
|
||||
- HIT
|
||||
X-Drupal-Dynamic-Cache:
|
||||
- UNCACHEABLE (poor cacheability)
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Generator:
|
||||
- Drupal 11 (https://www.drupal.org)
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -4,12 +4,24 @@ import datetime
|
||||
|
||||
import pytest
|
||||
from openbb_core.app.service.user_service import UserService
|
||||
from openbb_government_us.models.commodity_psd_data import (
|
||||
GovernmentUsCommodityPsdDataFetcher,
|
||||
)
|
||||
from openbb_government_us.models.commodity_psd_report import (
|
||||
GovernmentUsCommodityPsdReportFetcher,
|
||||
)
|
||||
from openbb_government_us.models.treasury_auctions import (
|
||||
GovernmentUSTreasuryAuctionsFetcher,
|
||||
)
|
||||
from openbb_government_us.models.treasury_prices import (
|
||||
GovernmentUSTreasuryPricesFetcher,
|
||||
)
|
||||
from openbb_government_us.models.weather_bulletin import (
|
||||
GovernmentUsWeatherBulletinFetcher,
|
||||
)
|
||||
from openbb_government_us.models.weather_bulletin_download import (
|
||||
GovernmentUsWeatherBulletinDownloadFetcher,
|
||||
)
|
||||
|
||||
test_credentials = UserService().default_user_settings.credentials.model_dump(
|
||||
mode="json"
|
||||
@@ -48,3 +60,66 @@ def test_government_us_treasury_prices_fetcher(credentials=test_credentials):
|
||||
fetcher = GovernmentUSTreasuryPricesFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.record_http
|
||||
def test_government_us_commodity_psd_report_fetcher(credentials=test_credentials):
|
||||
"""Test GovernmentUsCommodityPsdReportFetcher."""
|
||||
params = {
|
||||
"commodity": "sugar",
|
||||
"year": 2025,
|
||||
"month": 5,
|
||||
}
|
||||
|
||||
fetcher = GovernmentUsCommodityPsdReportFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.record_http
|
||||
def test_government_us_weather_bulletin_fetcher(credentials=test_credentials):
|
||||
"""Test GovernmentUsWeatherBulletinFetcher."""
|
||||
params = {"year": 2024, "month": 12, "week": 2}
|
||||
|
||||
fetcher = GovernmentUsWeatherBulletinFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.record_http
|
||||
def test_government_us_weather_bulletin_download_fetcher(credentials=test_credentials):
|
||||
"""Test GovernmentUsWeatherBulletinDownloadFetcher."""
|
||||
params = {
|
||||
"urls": [
|
||||
"https://esmis.nal.usda.gov/sites/default/release-files/cj82k728n/9w033w568/x059f4232/wwcb0125.pdf"
|
||||
],
|
||||
}
|
||||
|
||||
fetcher = GovernmentUsWeatherBulletinDownloadFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.record_http
|
||||
def test_government_us_commodity_psd_data_fetcher(credentials=test_credentials):
|
||||
"""Test GovernmentUsCommodityPsdDataFetcher."""
|
||||
|
||||
params = {
|
||||
"report_id": "coffee_summary",
|
||||
}
|
||||
|
||||
fetcher = GovernmentUsCommodityPsdDataFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
time_series_params = {
|
||||
"report_id": "world_crop_production_summary",
|
||||
"commodity": "coffee",
|
||||
"attribute": ["exports"],
|
||||
"country": "BR",
|
||||
"start_year": 2025,
|
||||
"end_year": 2025,
|
||||
"aggregate_regions": False,
|
||||
}
|
||||
result = fetcher.test(time_series_params, credentials)
|
||||
assert result is None
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -43,9 +43,13 @@
|
||||
"paramName": "year",
|
||||
"label": "Calendar Year",
|
||||
"description": "Calendar year for the filings.",
|
||||
"value": 2025,
|
||||
"value": 2026,
|
||||
"type": "number",
|
||||
"options": [
|
||||
{
|
||||
"label": "2026",
|
||||
"value": 2026
|
||||
},
|
||||
{
|
||||
"label": "2025",
|
||||
"value": 2025
|
||||
|
||||
Reference in New Issue
Block a user