[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:
Danglewood
2026-01-02 14:11:04 -08:00
committed by GitHub
parent 98fc5ac9c6
commit 67940fd576
24 changed files with 68602 additions and 6 deletions

1
.codespell.skip vendored
View File

@@ -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

View File

@@ -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.",
)

View File

@@ -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.",
)

View File

@@ -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.",
)

View File

@@ -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.",
)

View File

@@ -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

View File

@@ -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

View File

@@ -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", {})

View File

@@ -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.

View File

@@ -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",
)

View File

@@ -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]

View File

@@ -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
)

View File

@@ -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
]

View File

@@ -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

View File

@@ -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")

View File

@@ -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&regionCode=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

View File

@@ -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

View File

@@ -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

View File

@@ -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