mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 04:31:13 +00:00
[V5] Feature: Add JODI World Oil & Gas Database (#7593)
* add the JODI oil and gas database as a provider/router extension * lint fixes * add grouping to apps.json --------- Co-authored-by: deeleeramone <>
This commit is contained in:
166
.github/workflows/publish-openbb-jodi.yml
vendored
Normal file
166
.github/workflows/publish-openbb-jodi.yml
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
name: 📦 publish openbb-jodi
|
||||
|
||||
# Triggers exclusively on the merge of a ``release/openbb-jodi-v*``
|
||||
# branch into ``develop`` (or ``v5``). No push / tag triggers. Four
|
||||
# sequential jobs — verify the version bump, build once, test the built
|
||||
# source across the full cross-platform matrix, and only publish to PyPI
|
||||
# if the entire matrix is green.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [develop, v5]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
name: Verify version bump
|
||||
if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/openbb-jodi-v')) || github.event_name == 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.parse.outputs.version }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: openbb_platform/providers/jodi
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref || 'develop' }}
|
||||
|
||||
- name: Parse [project].version from pyproject.toml
|
||||
id: parse
|
||||
run: |
|
||||
VERSION=$(grep -m1 -E '^version[[:space:]]*=' pyproject.toml | sed -E 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/')
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "::error::Could not parse [project].version from pyproject.toml."
|
||||
exit 1
|
||||
fi
|
||||
echo "Local version: $VERSION"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Fail if version already exists on PyPI
|
||||
env:
|
||||
VERSION: ${{ steps.parse.outputs.version }}
|
||||
run: |
|
||||
STATUS=$(curl -sS -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/openbb-jodi/${VERSION}/json")
|
||||
case "$STATUS" in
|
||||
200)
|
||||
echo "::error::openbb-jodi ${VERSION} is already published on PyPI."
|
||||
echo "Bump openbb_platform/providers/jodi/pyproject.toml [project].version and re-merge a fresh release branch."
|
||||
exit 1
|
||||
;;
|
||||
404)
|
||||
echo "openbb-jodi ${VERSION} is not yet on PyPI — proceeding."
|
||||
;;
|
||||
*)
|
||||
echo "::error::Unexpected status $STATUS from PyPI JSON API. Aborting to avoid a silent skip."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Verify release branch name matches pyproject version
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
VERSION: ${{ steps.parse.outputs.version }}
|
||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
EXPECTED="release/openbb-jodi-v${VERSION}"
|
||||
if [ "$BRANCH" != "$EXPECTED" ]; then
|
||||
echo "::error::Release branch '${BRANCH}' does not match pyproject version '${VERSION}'."
|
||||
echo "Expected branch name: '${EXPECTED}'."
|
||||
echo "Either rename the release branch or update [project].version in pyproject.toml so they agree."
|
||||
exit 1
|
||||
fi
|
||||
echo "Branch '${BRANCH}' matches pyproject version '${VERSION}'."
|
||||
|
||||
build:
|
||||
name: Build sdist + wheel
|
||||
needs: version-check
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: openbb_platform/providers/jodi
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref || 'develop' }}
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- run: python -m pip install --upgrade pip uv
|
||||
|
||||
- name: Build sdist + wheel
|
||||
run: uv build --no-sources
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: openbb-jodi-dist
|
||||
path: openbb_platform/providers/jodi/dist/
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
test:
|
||||
name: Test (${{ matrix.os }} / Python ${{ matrix.python_version }})
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Linux: full sweep across every released Python.
|
||||
- { os: ubuntu-latest, python_version: "3.10" }
|
||||
- { os: ubuntu-latest, python_version: "3.11" }
|
||||
- { os: ubuntu-latest, python_version: "3.12" }
|
||||
- { os: ubuntu-latest, python_version: "3.13" }
|
||||
- { os: ubuntu-latest, python_version: "3.14" }
|
||||
# macOS + Windows: min and max supported.
|
||||
- { os: macos-latest, python_version: "3.10" }
|
||||
- { os: macos-latest, python_version: "3.14" }
|
||||
- { os: windows-latest, python_version: "3.10" }
|
||||
- { os: windows-latest, python_version: "3.14" }
|
||||
runs-on: ${{ matrix.os }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: openbb_platform/providers/jodi
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref || 'develop' }}
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python_version }}
|
||||
allow-prereleases: true
|
||||
|
||||
- run: python -m pip install --upgrade pip uv
|
||||
|
||||
- name: Install openbb-jodi from source (+ dev group)
|
||||
run: |
|
||||
uv pip install --system -e .
|
||||
uv pip install --system --group dev
|
||||
|
||||
- name: Run unit tests
|
||||
run: pytest tests --cov=openbb_jodi --cov-report=term-missing
|
||||
|
||||
publish:
|
||||
name: Publish to PyPI
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write # required for PyPI OIDC trusted publishing
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: openbb-jodi-dist
|
||||
path: dist
|
||||
|
||||
- uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: dist
|
||||
130
.github/workflows/test-unit-openbb-jodi.yml
vendored
Normal file
130
.github/workflows/test-unit-openbb-jodi.yml
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
name: 🧪 test openbb-jodi
|
||||
|
||||
# PR-test for the JODI provider. Triggers when a PR touches any file
|
||||
# under ``openbb_platform/providers/jodi/`` or this workflow itself.
|
||||
#
|
||||
# Build-once, test-many: a single ``build`` job produces the wheel + sdist;
|
||||
# the ``lint`` and ``test`` jobs install the provider editable from source
|
||||
# (``[tool.uv.sources]`` pins openbb-core to the sibling tree, since
|
||||
# openbb-core>=2.0.0 is not yet on PyPI) and run across every supported
|
||||
# (OS, Python) combination. The provider has no build-time assets, so the
|
||||
# test suite runs entirely from committed fixture samples and VCR
|
||||
# cassettes — no network in CI.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [develop, main, v5, feature/v5-platform-api]
|
||||
paths:
|
||||
- "openbb_platform/providers/jodi/**"
|
||||
- ".github/workflows/test-unit-openbb-jodi.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
# Cancel in-flight matrix runs when a new commit lands on the same PR.
|
||||
group: test-openbb-jodi-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build sdist + wheel
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: openbb_platform/providers/jodi
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- run: python -m pip install --upgrade pip uv
|
||||
|
||||
- name: Build sdist + wheel
|
||||
run: uv build --no-sources
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: openbb-jodi-dist
|
||||
path: openbb_platform/providers/jodi/dist/
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
lint:
|
||||
name: Lint (ruff + ty)
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: openbb_platform/providers/jodi
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- run: python -m pip install --upgrade pip uv
|
||||
|
||||
- name: Install openbb-jodi in editable mode (+ dev group)
|
||||
# ``-e .`` reads ``[tool.uv.sources]`` and resolves openbb-core /
|
||||
# openbb-devtools from the sibling source trees. ``--group dev``
|
||||
# pulls in ruff, ty, pytest, pytest-cov, pytest-recorder, etc.
|
||||
run: |
|
||||
uv pip install --system -e .
|
||||
uv pip install --system --group dev
|
||||
|
||||
- name: ruff check
|
||||
run: ruff check openbb_jodi tests integration
|
||||
|
||||
- name: ruff format --check
|
||||
run: ruff format --check openbb_jodi tests integration
|
||||
|
||||
- name: ty check
|
||||
run: ty check openbb_jodi
|
||||
|
||||
test:
|
||||
name: Test (${{ matrix.os }} / Python ${{ matrix.python_version }})
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Linux: full sweep across every released Python.
|
||||
- { os: ubuntu-latest, python_version: "3.10" }
|
||||
- { os: ubuntu-latest, python_version: "3.11" }
|
||||
- { os: ubuntu-latest, python_version: "3.12" }
|
||||
- { os: ubuntu-latest, python_version: "3.13" }
|
||||
- { os: ubuntu-latest, python_version: "3.14" }
|
||||
# macOS + Windows: min and max supported.
|
||||
- { os: macos-latest, python_version: "3.10" }
|
||||
- { os: macos-latest, python_version: "3.14" }
|
||||
- { os: windows-latest, python_version: "3.10" }
|
||||
- { os: windows-latest, python_version: "3.14" }
|
||||
runs-on: ${{ matrix.os }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: openbb_platform/providers/jodi
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python_version }}
|
||||
allow-prereleases: true
|
||||
|
||||
- run: python -m pip install --upgrade pip uv
|
||||
|
||||
- name: Install openbb-jodi from source (+ dev group)
|
||||
run: |
|
||||
uv pip install --system -e .
|
||||
uv pip install --system --group dev
|
||||
|
||||
- name: Run unit tests
|
||||
run: pytest tests --cov=openbb_jodi --cov-report=term-missing --cov-fail-under=100
|
||||
46
openbb_platform/providers/jodi/README.md
vendored
Normal file
46
openbb_platform/providers/jodi/README.md
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# OpenBB JODI Provider
|
||||
|
||||
This extension integrates the [Joint Organisations Data Initiative (JODI)](https://www.jodidata.org) data provider into the OpenBB Platform.
|
||||
|
||||
JODI publishes free monthly petroleum and natural gas supply, demand, and stock statistics, self-reported by over 100 countries, through the JODI-Oil and JODI-Gas World Databases.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the extension:
|
||||
|
||||
```bash
|
||||
pip install openbb-jodi
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
Each endpoint returns one presentation-ready view of the raw data, as a date-indexed table, with the series assessments and units in the results metadata.
|
||||
|
||||
### `obb.jodi.oil` — JODI-Oil World Database (monthly, from January 2002)
|
||||
|
||||
- `balance` — one country and product; one column per flow of the JODI questionnaire (production, trade, stocks, refinery activity).
|
||||
- `production` — one primary product; one column per reporting country.
|
||||
- `demand` — one refined product; one column per reporting country.
|
||||
- `demand_by_product` — one country; one column per refined product (the demand barrel).
|
||||
- `imports` / `exports` — one product; one column per reporting country.
|
||||
- `stocks` — closing stock levels of one product; one column per reporting country.
|
||||
|
||||
Units: kb/d, kbbl, kl, kmt, and the barrels/kmt conversion factor.
|
||||
|
||||
### `obb.jodi.gas` — JODI-Gas World Database (monthly, from January 2009)
|
||||
|
||||
- `balance` — one country; one column per flow of the JODI questionnaire.
|
||||
- `production` — one column per reporting country.
|
||||
- `demand` — gross inland deliveries (observed or calculated); one column per reporting country.
|
||||
- `imports` / `exports` — total, pipeline, or LNG; one column per reporting country.
|
||||
- `stocks` — closing stock levels; one column per reporting country.
|
||||
|
||||
Units: million m3, TJ, and kmt of LNG.
|
||||
|
||||
## OpenBB Workspace
|
||||
|
||||
The package bundles a "JODI Oil & Gas" app template (`openbb_jodi/assets/apps.json`), served at `/api/v1/jodi/apps.json` and folded into the Workspace app catalogue automatically when running `openbb-api`. It has three dashboards - Country Balances (oil balance, demand barrel, and gas balance, synchronized on one country), Oil Markets, and Gas Markets.
|
||||
|
||||
No API key is required. Source tables are cached under the OpenBB user cache directory, partitioned by product and flow; a table is downloaded again only when JODI publishes an update.
|
||||
|
||||
Documentation available [here](https://docs.openbb.co/platform/developer_guide/contributing).
|
||||
1
openbb_platform/providers/jodi/__init__.py
Normal file
1
openbb_platform/providers/jodi/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""The JODI provider."""
|
||||
1
openbb_platform/providers/jodi/integration/__init__.py
Normal file
1
openbb_platform/providers/jodi/integration/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""OpenBB JODI Provider Integration Tests."""
|
||||
53
openbb_platform/providers/jodi/integration/conftest.py
Normal file
53
openbb_platform/providers/jodi/integration/conftest.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Integration-test fixtures for openbb-jodi.
|
||||
|
||||
The API-interface tests (``test_jodi_api.py``) need a live HTTP server.
|
||||
This module runs ``openbb_core.api.rest_api:app`` in a background thread for
|
||||
the duration of the integration session and exposes the auth ``headers``.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def api_server(pytestconfig):
|
||||
"""Run the OpenBB REST API on localhost:8000 for the integration session."""
|
||||
if pytestconfig.getoption("markexpr") == "not integration":
|
||||
yield None
|
||||
return
|
||||
|
||||
import uvicorn
|
||||
from openbb_core.api.rest_api import app
|
||||
|
||||
config = uvicorn.Config(app, host="127.0.0.1", port=8000, log_level="warning")
|
||||
server = uvicorn.Server(config)
|
||||
thread = threading.Thread(target=server.run, daemon=True)
|
||||
thread.start()
|
||||
|
||||
deadline = time.time() + 60
|
||||
while time.time() < deadline:
|
||||
if server.started:
|
||||
break
|
||||
time.sleep(0.25)
|
||||
else: # pragma: no cover
|
||||
raise RuntimeError("The OpenBB REST API did not start within 60s.")
|
||||
|
||||
yield server
|
||||
|
||||
server.should_exit = True
|
||||
thread.join(timeout=15)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def headers(api_server):
|
||||
"""HTTP Basic auth headers; depends on api_server so the server is live."""
|
||||
from openbb_core.env import Env
|
||||
|
||||
userpass = f"{Env().API_USERNAME}:{Env().API_PASSWORD}"
|
||||
userpass_bytes = userpass.encode("ascii")
|
||||
base64_bytes = base64.b64encode(userpass_bytes)
|
||||
|
||||
return {"Authorization": f"Basic {base64_bytes.decode('ascii')}"}
|
||||
322
openbb_platform/providers/jodi/integration/test_jodi_api.py
Normal file
322
openbb_platform/providers/jodi/integration/test_jodi_api.py
Normal file
@@ -0,0 +1,322 @@
|
||||
"""Test the JODI API endpoints."""
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from openbb_core.provider.utils.helpers import get_querystring
|
||||
|
||||
BASE_URL = "http://localhost:8000/api/v1/jodi"
|
||||
|
||||
|
||||
def run(path: str, params: dict, headers) -> None:
|
||||
"""Run one endpoint and make the common assertions."""
|
||||
params = {p: v for p, v in params.items() if v is not None}
|
||||
query_str = get_querystring(params, [])
|
||||
result = requests.get(
|
||||
f"{BASE_URL}/{path}?{query_str}", headers=headers, timeout=600
|
||||
)
|
||||
assert isinstance(result, requests.Response)
|
||||
assert result.status_code == 200
|
||||
assert result.json()["results"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": None,
|
||||
"product": None,
|
||||
"unit": None,
|
||||
"start_date": None,
|
||||
"end_date": None,
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"country": "india",
|
||||
"product": "gasoline",
|
||||
"unit": "ktons",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_oil_balance(params, headers):
|
||||
"""Test the oil balance endpoint."""
|
||||
run("oil/balance", params, headers)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "saudi_arabia,iraq,united_states",
|
||||
"product": "crude_oil",
|
||||
"unit": "kbd",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_oil_production(params, headers):
|
||||
"""Test the oil production endpoint."""
|
||||
run("oil/production", params, headers)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "united_states,china,india",
|
||||
"product": "gasoline",
|
||||
"unit": "kbd",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_oil_demand(params, headers):
|
||||
"""Test the oil demand endpoint."""
|
||||
run("oil/demand", params, headers)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "japan",
|
||||
"unit": "kbd",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_oil_demand_by_product(params, headers):
|
||||
"""Test the oil demand by product endpoint."""
|
||||
run("oil/demand_by_product", params, headers)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "china,india,south_korea",
|
||||
"product": "crude_oil",
|
||||
"unit": "kbd",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_oil_imports(params, headers):
|
||||
"""Test the oil imports endpoint."""
|
||||
run("oil/imports", params, headers)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "saudi_arabia,united_arab_emirates",
|
||||
"product": "crude_oil",
|
||||
"unit": "kbd",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_oil_exports(params, headers):
|
||||
"""Test the oil exports endpoint."""
|
||||
run("oil/exports", params, headers)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "united_states,japan",
|
||||
"product": "crude_oil",
|
||||
"unit": "kbbl",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_oil_stocks(params, headers):
|
||||
"""Test the oil stocks endpoint."""
|
||||
run("oil/stocks", params, headers)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": None,
|
||||
"unit": None,
|
||||
"start_date": None,
|
||||
"end_date": None,
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"country": "norway",
|
||||
"unit": "tj",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_gas_balance(params, headers):
|
||||
"""Test the gas balance endpoint."""
|
||||
run("gas/balance", params, headers)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "united_states,qatar,norway",
|
||||
"unit": "m3",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_gas_production(params, headers):
|
||||
"""Test the gas production endpoint."""
|
||||
run("gas/production", params, headers)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "germany,france,italy",
|
||||
"measure": "observed",
|
||||
"unit": "m3",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_gas_demand(params, headers):
|
||||
"""Test the gas demand endpoint."""
|
||||
run("gas/demand", params, headers)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "japan,china,south_korea",
|
||||
"flow": "lng",
|
||||
"unit": "m3",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_gas_imports(params, headers):
|
||||
"""Test the gas imports endpoint."""
|
||||
run("gas/imports", params, headers)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "united_states,qatar,australia",
|
||||
"flow": "lng",
|
||||
"unit": "m3",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_gas_exports(params, headers):
|
||||
"""Test the gas exports endpoint."""
|
||||
run("gas/exports", params, headers)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "germany,france,netherlands",
|
||||
"unit": "m3",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_gas_stocks(params, headers):
|
||||
"""Test the gas stocks endpoint."""
|
||||
run("gas/stocks", params, headers)
|
||||
342
openbb_platform/providers/jodi/integration/test_jodi_python.py
Normal file
342
openbb_platform/providers/jodi/integration/test_jodi_python.py
Normal file
@@ -0,0 +1,342 @@
|
||||
"""Test the JODI extension through the Python interface."""
|
||||
|
||||
import pytest
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def obb(pytestconfig):
|
||||
"""Fixture to setup obb."""
|
||||
if pytestconfig.getoption("markexpr") != "not integration":
|
||||
import openbb
|
||||
|
||||
return openbb.obb
|
||||
|
||||
pytest.skip("Integration tests are not enabled.")
|
||||
|
||||
|
||||
def run(obb, path, params):
|
||||
"""Run one endpoint and make the common assertions."""
|
||||
params = {p: v for p, v in params.items() if v is not None}
|
||||
func = obb.jodi
|
||||
for part in path.split("."):
|
||||
func = getattr(func, part)
|
||||
result = func(**params)
|
||||
assert result
|
||||
assert isinstance(result, OBBject)
|
||||
assert len(result.results) > 0
|
||||
assert result.extra["results_metadata"]["source"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": None,
|
||||
"product": None,
|
||||
"unit": None,
|
||||
"start_date": None,
|
||||
"end_date": None,
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"country": "india",
|
||||
"product": "gasoline",
|
||||
"unit": "ktons",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_oil_balance(params, obb):
|
||||
"""Test the oil balance endpoint."""
|
||||
run(obb, "oil.balance", params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": None,
|
||||
"product": None,
|
||||
"unit": None,
|
||||
"start_date": None,
|
||||
"end_date": None,
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"country": "saudi_arabia,iraq,united_states",
|
||||
"product": "crude_oil",
|
||||
"unit": "kbd",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_oil_production(params, obb):
|
||||
"""Test the oil production endpoint."""
|
||||
run(obb, "oil.production", params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "united_states,china,india",
|
||||
"product": "gasoline",
|
||||
"unit": "kbd",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_oil_demand(params, obb):
|
||||
"""Test the oil demand endpoint."""
|
||||
run(obb, "oil.demand", params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "japan",
|
||||
"unit": "kbd",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_oil_demand_by_product(params, obb):
|
||||
"""Test the oil demand by product endpoint."""
|
||||
run(obb, "oil.demand_by_product", params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "china,india,south_korea",
|
||||
"product": "crude_oil",
|
||||
"unit": "kbd",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_oil_imports(params, obb):
|
||||
"""Test the oil imports endpoint."""
|
||||
run(obb, "oil.imports", params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "saudi_arabia,united_arab_emirates",
|
||||
"product": "crude_oil",
|
||||
"unit": "kbd",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_oil_exports(params, obb):
|
||||
"""Test the oil exports endpoint."""
|
||||
run(obb, "oil.exports", params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "united_states,japan",
|
||||
"product": "crude_oil",
|
||||
"unit": "kbbl",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_oil_stocks(params, obb):
|
||||
"""Test the oil stocks endpoint."""
|
||||
run(obb, "oil.stocks", params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": None,
|
||||
"unit": None,
|
||||
"start_date": None,
|
||||
"end_date": None,
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"country": "norway",
|
||||
"unit": "tj",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_gas_balance(params, obb):
|
||||
"""Test the gas balance endpoint."""
|
||||
run(obb, "gas.balance", params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "united_states,qatar,norway",
|
||||
"unit": "m3",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_gas_production(params, obb):
|
||||
"""Test the gas production endpoint."""
|
||||
run(obb, "gas.production", params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "germany,france,italy",
|
||||
"measure": "observed",
|
||||
"unit": "m3",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_gas_demand(params, obb):
|
||||
"""Test the gas demand endpoint."""
|
||||
run(obb, "gas.demand", params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "japan,china,south_korea",
|
||||
"flow": "lng",
|
||||
"unit": "m3",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_gas_imports(params, obb):
|
||||
"""Test the gas imports endpoint."""
|
||||
run(obb, "gas.imports", params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "united_states,qatar,australia",
|
||||
"flow": "lng",
|
||||
"unit": "m3",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_gas_exports(params, obb):
|
||||
"""Test the gas exports endpoint."""
|
||||
run(obb, "gas.exports", params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params",
|
||||
[
|
||||
(
|
||||
{
|
||||
"country": "germany,france,netherlands",
|
||||
"unit": "m3",
|
||||
"start_date": "2023-01-01",
|
||||
"end_date": "2023-12-31",
|
||||
"provider": "jodi",
|
||||
"use_cache": True,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.integration
|
||||
def test_jodi_gas_stocks(params, obb):
|
||||
"""Test the gas stocks endpoint."""
|
||||
run(obb, "gas.stocks", params)
|
||||
42
openbb_platform/providers/jodi/openbb_jodi/__init__.py
Normal file
42
openbb_platform/providers/jodi/openbb_jodi/__init__.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""OpenBB JODI Provider Module."""
|
||||
|
||||
from openbb_core.provider.abstract.provider import Provider
|
||||
|
||||
from openbb_jodi.models.gas_balance import JodiGasBalanceFetcher
|
||||
from openbb_jodi.models.gas_demand import JodiGasDemandFetcher
|
||||
from openbb_jodi.models.gas_exports import JodiGasExportsFetcher
|
||||
from openbb_jodi.models.gas_imports import JodiGasImportsFetcher
|
||||
from openbb_jodi.models.gas_production import JodiGasProductionFetcher
|
||||
from openbb_jodi.models.gas_stocks import JodiGasStocksFetcher
|
||||
from openbb_jodi.models.oil_balance import JodiOilBalanceFetcher
|
||||
from openbb_jodi.models.oil_demand import JodiOilDemandFetcher
|
||||
from openbb_jodi.models.oil_demand_by_product import JodiOilDemandByProductFetcher
|
||||
from openbb_jodi.models.oil_exports import JodiOilExportsFetcher
|
||||
from openbb_jodi.models.oil_imports import JodiOilImportsFetcher
|
||||
from openbb_jodi.models.oil_production import JodiOilProductionFetcher
|
||||
from openbb_jodi.models.oil_stocks import JodiOilStocksFetcher
|
||||
|
||||
jodi_provider = Provider(
|
||||
name="jodi",
|
||||
website="https://www.jodidata.org",
|
||||
description="The Joint Organisations Data Initiative (JODI) provides free monthly"
|
||||
+ " petroleum and natural gas supply, demand, and stock statistics,"
|
||||
+ " self-reported by over 100 countries, through the JODI-Oil and JODI-Gas"
|
||||
+ " World Databases.",
|
||||
fetcher_dict={
|
||||
"JodiGasBalance": JodiGasBalanceFetcher,
|
||||
"JodiGasDemand": JodiGasDemandFetcher,
|
||||
"JodiGasExports": JodiGasExportsFetcher,
|
||||
"JodiGasImports": JodiGasImportsFetcher,
|
||||
"JodiGasProduction": JodiGasProductionFetcher,
|
||||
"JodiGasStocks": JodiGasStocksFetcher,
|
||||
"JodiOilBalance": JodiOilBalanceFetcher,
|
||||
"JodiOilDemand": JodiOilDemandFetcher,
|
||||
"JodiOilDemandByProduct": JodiOilDemandByProductFetcher,
|
||||
"JodiOilExports": JodiOilExportsFetcher,
|
||||
"JodiOilImports": JodiOilImportsFetcher,
|
||||
"JodiOilProduction": JodiOilProductionFetcher,
|
||||
"JodiOilStocks": JodiOilStocksFetcher,
|
||||
},
|
||||
repr_name="Joint Organisations Data Initiative (JODI) World Databases",
|
||||
)
|
||||
260
openbb_platform/providers/jodi/openbb_jodi/assets/apps.json
vendored
Normal file
260
openbb_platform/providers/jodi/openbb_jodi/assets/apps.json
vendored
Normal file
@@ -0,0 +1,260 @@
|
||||
[
|
||||
{
|
||||
"name": "JODI Oil & Gas",
|
||||
"img": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNjAwIDkwMCI+CiAgPHJlY3Qgd2lkdGg9IjE2MDAiIGhlaWdodD0iOTAwIiBmaWxsPSIjMTAxMzE4Ii8+CiAgPHJlY3QgeD0iNDAiIHk9Ijg0NiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjM0IiByeD0iNiIgZmlsbD0iIzFDQjBDRiIgb3BhY2l0eT0iMC4zMiIvPjxyZWN0IHg9IjExOCIgeT0iODI4IiB3aWR0aD0iNDYiIGhlaWdodD0iNTIiIHJ4PSI2IiBmaWxsPSIjOERDNjNGIiBvcGFjaXR5PSIwLjMyIi8+PHJlY3QgeD0iMTk2IiB5PSI4MzYiIHdpZHRoPSI0NiIgaGVpZ2h0PSI0NCIgcng9IjYiIGZpbGw9IiMxQ0IwQ0YiIG9wYWNpdHk9IjAuMzIiLz48cmVjdCB4PSIyNzQiIHk9IjgxMiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjY4IiByeD0iNiIgZmlsbD0iIzhEQzYzRiIgb3BhY2l0eT0iMC4zMiIvPjxyZWN0IHg9IjM1MiIgeT0iODIyIiB3aWR0aD0iNDYiIGhlaWdodD0iNTgiIHJ4PSI2IiBmaWxsPSIjMUNCMENGIiBvcGFjaXR5PSIwLjMyIi8+PHJlY3QgeD0iNDMwIiB5PSI3OTgiIHdpZHRoPSI0NiIgaGVpZ2h0PSI4MiIgcng9IjYiIGZpbGw9IiM4REM2M0YiIG9wYWNpdHk9IjAuMzIiLz48cmVjdCB4PSI1MDgiIHk9IjgxNiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjY0IiByeD0iNiIgZmlsbD0iIzFDQjBDRiIgb3BhY2l0eT0iMC4zMiIvPjxyZWN0IHg9IjU4NiIgeT0iNzg0IiB3aWR0aD0iNDYiIGhlaWdodD0iOTYiIHJ4PSI2IiBmaWxsPSIjOERDNjNGIiBvcGFjaXR5PSIwLjMyIi8+PHJlY3QgeD0iNjY0IiB5PSI4MDYiIHdpZHRoPSI0NiIgaGVpZ2h0PSI3NCIgcng9IjYiIGZpbGw9IiMxQ0IwQ0YiIG9wYWNpdHk9IjAuMzIiLz48cmVjdCB4PSI3NDIiIHk9Ijc3MiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjEwOCIgcng9IjYiIGZpbGw9IiM4REM2M0YiIG9wYWNpdHk9IjAuMzIiLz48cmVjdCB4PSI4MjAiIHk9Ijc5MiIgd2lkdGg9IjQ2IiBoZWlnaHQ9Ijg4IiByeD0iNiIgZmlsbD0iIzFDQjBDRiIgb3BhY2l0eT0iMC4zMiIvPjxyZWN0IHg9Ijg5OCIgeT0iNzU4IiB3aWR0aD0iNDYiIGhlaWdodD0iMTIyIiByeD0iNiIgZmlsbD0iIzhEQzYzRiIgb3BhY2l0eT0iMC4zMiIvPjxyZWN0IHg9Ijk3NiIgeT0iNzg0IiB3aWR0aD0iNDYiIGhlaWdodD0iOTYiIHJ4PSI2IiBmaWxsPSIjMUNCMENGIiBvcGFjaXR5PSIwLjMyIi8+PHJlY3QgeD0iMTA1NCIgeT0iNzQ4IiB3aWR0aD0iNDYiIGhlaWdodD0iMTMyIiByeD0iNiIgZmlsbD0iIzhEQzYzRiIgb3BhY2l0eT0iMC4zMiIvPjxyZWN0IHg9IjExMzIiIHk9Ijc3NiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjEwNCIgcng9IjYiIGZpbGw9IiMxQ0IwQ0YiIG9wYWNpdHk9IjAuMzIiLz48cmVjdCB4PSIxMjEwIiB5PSI3MzQiIHdpZHRoPSI0NiIgaGVpZ2h0PSIxNDYiIHJ4PSI2IiBmaWxsPSIjOERDNjNGIiBvcGFjaXR5PSIwLjMyIi8+PHJlY3QgeD0iMTI4OCIgeT0iNzYyIiB3aWR0aD0iNDYiIGhlaWdodD0iMTE4IiByeD0iNiIgZmlsbD0iIzFDQjBDRiIgb3BhY2l0eT0iMC4zMiIvPjxyZWN0IHg9IjEzNjYiIHk9IjcyMiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjE1OCIgcng9IjYiIGZpbGw9IiM4REM2M0YiIG9wYWNpdHk9IjAuMzIiLz48cmVjdCB4PSIxNDQ0IiB5PSI3NTIiIHdpZHRoPSI0NiIgaGVpZ2h0PSIxMjgiIHJ4PSI2IiBmaWxsPSIjMUNCMENGIiBvcGFjaXR5PSIwLjMyIi8+PHJlY3QgeD0iMTUyMiIgeT0iNzEwIiB3aWR0aD0iNDYiIGhlaWdodD0iMTcwIiByeD0iNiIgZmlsbD0iIzhEQzYzRiIgb3BhY2l0eT0iMC4zMiIvPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDc0MiwzMTgpIj4KICAgIDxwYXRoIGQ9Ik0wLC05MiBDNTAsLTIyIDY4LDE0IDY4LDQ2IEE2OCw2OCAwIDEsMSAtNjgsNDYgQy02OCwxNCAtNTAsLTIyIDAsLTkyIFoiIGZpbGw9IiMxQ0IwQ0YiLz4KICA8L2c+CiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoODkyLDMxOCkiPgogICAgPHBhdGggZD0iTTAsLTk2IEMzNCwtNTIgNjIsLTE0IDYyLDM4IEE2Miw2NiAwIDEsMSAtNjIsMzggQy02Miw2IC00MCwtMTggLTI2LC0zOCBDLTE0LC0yMCAtOCwtOCAtMTIsMTAgQzYsLTYgOCwtNDAgMCwtOTYgWiIgZmlsbD0iIzhEQzYzRiIvPgogIDwvZz4KICA8dGV4dCB4PSI4MDAiIHk9IjU2MiIgZm9udC1mYW1pbHk9IkFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTY4IiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjRjJGNEY3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBsZXR0ZXItc3BhY2luZz0iMTAiPkpPREk8L3RleHQ+CiAgPHRleHQgeD0iODAwIiB5PSI2NDgiIGZvbnQtZmFtaWx5PSJBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjUyIiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjRjJGNEY3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBvcGFjaXR5PSIwLjkyIj5PaWwgJmFtcDsgR2FzIFdvcmxkIERhdGFiYXNlczwvdGV4dD4KICA8dGV4dCB4PSI4MDAiIHk9IjcxNiIgZm9udC1mYW1pbHk9IkFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMzQiIGZpbGw9IiM5QUE0QjIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkpvaW50IE9yZ2FuaXNhdGlvbnMgRGF0YSBJbml0aWF0aXZlPC90ZXh0Pgo8L3N2Zz4=",
|
||||
"img_dark": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNjAwIDkwMCI+CiAgPHJlY3Qgd2lkdGg9IjE2MDAiIGhlaWdodD0iOTAwIiBmaWxsPSIjMTAxMzE4Ii8+CiAgPHJlY3QgeD0iNDAiIHk9Ijg0NiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjM0IiByeD0iNiIgZmlsbD0iIzFDQjBDRiIgb3BhY2l0eT0iMC4zMiIvPjxyZWN0IHg9IjExOCIgeT0iODI4IiB3aWR0aD0iNDYiIGhlaWdodD0iNTIiIHJ4PSI2IiBmaWxsPSIjOERDNjNGIiBvcGFjaXR5PSIwLjMyIi8+PHJlY3QgeD0iMTk2IiB5PSI4MzYiIHdpZHRoPSI0NiIgaGVpZ2h0PSI0NCIgcng9IjYiIGZpbGw9IiMxQ0IwQ0YiIG9wYWNpdHk9IjAuMzIiLz48cmVjdCB4PSIyNzQiIHk9IjgxMiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjY4IiByeD0iNiIgZmlsbD0iIzhEQzYzRiIgb3BhY2l0eT0iMC4zMiIvPjxyZWN0IHg9IjM1MiIgeT0iODIyIiB3aWR0aD0iNDYiIGhlaWdodD0iNTgiIHJ4PSI2IiBmaWxsPSIjMUNCMENGIiBvcGFjaXR5PSIwLjMyIi8+PHJlY3QgeD0iNDMwIiB5PSI3OTgiIHdpZHRoPSI0NiIgaGVpZ2h0PSI4MiIgcng9IjYiIGZpbGw9IiM4REM2M0YiIG9wYWNpdHk9IjAuMzIiLz48cmVjdCB4PSI1MDgiIHk9IjgxNiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjY0IiByeD0iNiIgZmlsbD0iIzFDQjBDRiIgb3BhY2l0eT0iMC4zMiIvPjxyZWN0IHg9IjU4NiIgeT0iNzg0IiB3aWR0aD0iNDYiIGhlaWdodD0iOTYiIHJ4PSI2IiBmaWxsPSIjOERDNjNGIiBvcGFjaXR5PSIwLjMyIi8+PHJlY3QgeD0iNjY0IiB5PSI4MDYiIHdpZHRoPSI0NiIgaGVpZ2h0PSI3NCIgcng9IjYiIGZpbGw9IiMxQ0IwQ0YiIG9wYWNpdHk9IjAuMzIiLz48cmVjdCB4PSI3NDIiIHk9Ijc3MiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjEwOCIgcng9IjYiIGZpbGw9IiM4REM2M0YiIG9wYWNpdHk9IjAuMzIiLz48cmVjdCB4PSI4MjAiIHk9Ijc5MiIgd2lkdGg9IjQ2IiBoZWlnaHQ9Ijg4IiByeD0iNiIgZmlsbD0iIzFDQjBDRiIgb3BhY2l0eT0iMC4zMiIvPjxyZWN0IHg9Ijg5OCIgeT0iNzU4IiB3aWR0aD0iNDYiIGhlaWdodD0iMTIyIiByeD0iNiIgZmlsbD0iIzhEQzYzRiIgb3BhY2l0eT0iMC4zMiIvPjxyZWN0IHg9Ijk3NiIgeT0iNzg0IiB3aWR0aD0iNDYiIGhlaWdodD0iOTYiIHJ4PSI2IiBmaWxsPSIjMUNCMENGIiBvcGFjaXR5PSIwLjMyIi8+PHJlY3QgeD0iMTA1NCIgeT0iNzQ4IiB3aWR0aD0iNDYiIGhlaWdodD0iMTMyIiByeD0iNiIgZmlsbD0iIzhEQzYzRiIgb3BhY2l0eT0iMC4zMiIvPjxyZWN0IHg9IjExMzIiIHk9Ijc3NiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjEwNCIgcng9IjYiIGZpbGw9IiMxQ0IwQ0YiIG9wYWNpdHk9IjAuMzIiLz48cmVjdCB4PSIxMjEwIiB5PSI3MzQiIHdpZHRoPSI0NiIgaGVpZ2h0PSIxNDYiIHJ4PSI2IiBmaWxsPSIjOERDNjNGIiBvcGFjaXR5PSIwLjMyIi8+PHJlY3QgeD0iMTI4OCIgeT0iNzYyIiB3aWR0aD0iNDYiIGhlaWdodD0iMTE4IiByeD0iNiIgZmlsbD0iIzFDQjBDRiIgb3BhY2l0eT0iMC4zMiIvPjxyZWN0IHg9IjEzNjYiIHk9IjcyMiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjE1OCIgcng9IjYiIGZpbGw9IiM4REM2M0YiIG9wYWNpdHk9IjAuMzIiLz48cmVjdCB4PSIxNDQ0IiB5PSI3NTIiIHdpZHRoPSI0NiIgaGVpZ2h0PSIxMjgiIHJ4PSI2IiBmaWxsPSIjMUNCMENGIiBvcGFjaXR5PSIwLjMyIi8+PHJlY3QgeD0iMTUyMiIgeT0iNzEwIiB3aWR0aD0iNDYiIGhlaWdodD0iMTcwIiByeD0iNiIgZmlsbD0iIzhEQzYzRiIgb3BhY2l0eT0iMC4zMiIvPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDc0MiwzMTgpIj4KICAgIDxwYXRoIGQ9Ik0wLC05MiBDNTAsLTIyIDY4LDE0IDY4LDQ2IEE2OCw2OCAwIDEsMSAtNjgsNDYgQy02OCwxNCAtNTAsLTIyIDAsLTkyIFoiIGZpbGw9IiMxQ0IwQ0YiLz4KICA8L2c+CiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoODkyLDMxOCkiPgogICAgPHBhdGggZD0iTTAsLTk2IEMzNCwtNTIgNjIsLTE0IDYyLDM4IEE2Miw2NiAwIDEsMSAtNjIsMzggQy02Miw2IC00MCwtMTggLTI2LC0zOCBDLTE0LC0yMCAtOCwtOCAtMTIsMTAgQzYsLTYgOCwtNDAgMCwtOTYgWiIgZmlsbD0iIzhEQzYzRiIvPgogIDwvZz4KICA8dGV4dCB4PSI4MDAiIHk9IjU2MiIgZm9udC1mYW1pbHk9IkFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTY4IiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjRjJGNEY3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBsZXR0ZXItc3BhY2luZz0iMTAiPkpPREk8L3RleHQ+CiAgPHRleHQgeD0iODAwIiB5PSI2NDgiIGZvbnQtZmFtaWx5PSJBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjUyIiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjRjJGNEY3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBvcGFjaXR5PSIwLjkyIj5PaWwgJmFtcDsgR2FzIFdvcmxkIERhdGFiYXNlczwvdGV4dD4KICA8dGV4dCB4PSI4MDAiIHk9IjcxNiIgZm9udC1mYW1pbHk9IkFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMzQiIGZpbGw9IiM5QUE0QjIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkpvaW50IE9yZ2FuaXNhdGlvbnMgRGF0YSBJbml0aWF0aXZlPC90ZXh0Pgo8L3N2Zz4=",
|
||||
"img_light": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNjAwIDkwMCI+CiAgPHJlY3Qgd2lkdGg9IjE2MDAiIGhlaWdodD0iOTAwIiBmaWxsPSIjRjdGOEZBIi8+CiAgPHJlY3QgeD0iNDAiIHk9Ijg0NiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjM0IiByeD0iNiIgZmlsbD0iIzFDQjBDRiIgb3BhY2l0eT0iMC40NSIvPjxyZWN0IHg9IjExOCIgeT0iODI4IiB3aWR0aD0iNDYiIGhlaWdodD0iNTIiIHJ4PSI2IiBmaWxsPSIjOERDNjNGIiBvcGFjaXR5PSIwLjQ1Ii8+PHJlY3QgeD0iMTk2IiB5PSI4MzYiIHdpZHRoPSI0NiIgaGVpZ2h0PSI0NCIgcng9IjYiIGZpbGw9IiMxQ0IwQ0YiIG9wYWNpdHk9IjAuNDUiLz48cmVjdCB4PSIyNzQiIHk9IjgxMiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjY4IiByeD0iNiIgZmlsbD0iIzhEQzYzRiIgb3BhY2l0eT0iMC40NSIvPjxyZWN0IHg9IjM1MiIgeT0iODIyIiB3aWR0aD0iNDYiIGhlaWdodD0iNTgiIHJ4PSI2IiBmaWxsPSIjMUNCMENGIiBvcGFjaXR5PSIwLjQ1Ii8+PHJlY3QgeD0iNDMwIiB5PSI3OTgiIHdpZHRoPSI0NiIgaGVpZ2h0PSI4MiIgcng9IjYiIGZpbGw9IiM4REM2M0YiIG9wYWNpdHk9IjAuNDUiLz48cmVjdCB4PSI1MDgiIHk9IjgxNiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjY0IiByeD0iNiIgZmlsbD0iIzFDQjBDRiIgb3BhY2l0eT0iMC40NSIvPjxyZWN0IHg9IjU4NiIgeT0iNzg0IiB3aWR0aD0iNDYiIGhlaWdodD0iOTYiIHJ4PSI2IiBmaWxsPSIjOERDNjNGIiBvcGFjaXR5PSIwLjQ1Ii8+PHJlY3QgeD0iNjY0IiB5PSI4MDYiIHdpZHRoPSI0NiIgaGVpZ2h0PSI3NCIgcng9IjYiIGZpbGw9IiMxQ0IwQ0YiIG9wYWNpdHk9IjAuNDUiLz48cmVjdCB4PSI3NDIiIHk9Ijc3MiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjEwOCIgcng9IjYiIGZpbGw9IiM4REM2M0YiIG9wYWNpdHk9IjAuNDUiLz48cmVjdCB4PSI4MjAiIHk9Ijc5MiIgd2lkdGg9IjQ2IiBoZWlnaHQ9Ijg4IiByeD0iNiIgZmlsbD0iIzFDQjBDRiIgb3BhY2l0eT0iMC40NSIvPjxyZWN0IHg9Ijg5OCIgeT0iNzU4IiB3aWR0aD0iNDYiIGhlaWdodD0iMTIyIiByeD0iNiIgZmlsbD0iIzhEQzYzRiIgb3BhY2l0eT0iMC40NSIvPjxyZWN0IHg9Ijk3NiIgeT0iNzg0IiB3aWR0aD0iNDYiIGhlaWdodD0iOTYiIHJ4PSI2IiBmaWxsPSIjMUNCMENGIiBvcGFjaXR5PSIwLjQ1Ii8+PHJlY3QgeD0iMTA1NCIgeT0iNzQ4IiB3aWR0aD0iNDYiIGhlaWdodD0iMTMyIiByeD0iNiIgZmlsbD0iIzhEQzYzRiIgb3BhY2l0eT0iMC40NSIvPjxyZWN0IHg9IjExMzIiIHk9Ijc3NiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjEwNCIgcng9IjYiIGZpbGw9IiMxQ0IwQ0YiIG9wYWNpdHk9IjAuNDUiLz48cmVjdCB4PSIxMjEwIiB5PSI3MzQiIHdpZHRoPSI0NiIgaGVpZ2h0PSIxNDYiIHJ4PSI2IiBmaWxsPSIjOERDNjNGIiBvcGFjaXR5PSIwLjQ1Ii8+PHJlY3QgeD0iMTI4OCIgeT0iNzYyIiB3aWR0aD0iNDYiIGhlaWdodD0iMTE4IiByeD0iNiIgZmlsbD0iIzFDQjBDRiIgb3BhY2l0eT0iMC40NSIvPjxyZWN0IHg9IjEzNjYiIHk9IjcyMiIgd2lkdGg9IjQ2IiBoZWlnaHQ9IjE1OCIgcng9IjYiIGZpbGw9IiM4REM2M0YiIG9wYWNpdHk9IjAuNDUiLz48cmVjdCB4PSIxNDQ0IiB5PSI3NTIiIHdpZHRoPSI0NiIgaGVpZ2h0PSIxMjgiIHJ4PSI2IiBmaWxsPSIjMUNCMENGIiBvcGFjaXR5PSIwLjQ1Ii8+PHJlY3QgeD0iMTUyMiIgeT0iNzEwIiB3aWR0aD0iNDYiIGhlaWdodD0iMTcwIiByeD0iNiIgZmlsbD0iIzhEQzYzRiIgb3BhY2l0eT0iMC40NSIvPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDc0MiwzMTgpIj4KICAgIDxwYXRoIGQ9Ik0wLC05MiBDNTAsLTIyIDY4LDE0IDY4LDQ2IEE2OCw2OCAwIDEsMSAtNjgsNDYgQy02OCwxNCAtNTAsLTIyIDAsLTkyIFoiIGZpbGw9IiMxQ0IwQ0YiLz4KICA8L2c+CiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoODkyLDMxOCkiPgogICAgPHBhdGggZD0iTTAsLTk2IEMzNCwtNTIgNjIsLTE0IDYyLDM4IEE2Miw2NiAwIDEsMSAtNjIsMzggQy02Miw2IC00MCwtMTggLTI2LC0zOCBDLTE0LC0yMCAtOCwtOCAtMTIsMTAgQzYsLTYgOCwtNDAgMCwtOTYgWiIgZmlsbD0iIzhEQzYzRiIvPgogIDwvZz4KICA8dGV4dCB4PSI4MDAiIHk9IjU2MiIgZm9udC1mYW1pbHk9IkFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTY4IiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMTYxQjIyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBsZXR0ZXItc3BhY2luZz0iMTAiPkpPREk8L3RleHQ+CiAgPHRleHQgeD0iODAwIiB5PSI2NDgiIGZvbnQtZmFtaWx5PSJBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjUyIiBmb250LXdlaWdodD0iNjAwIiBmaWxsPSIjMTYxQjIyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBvcGFjaXR5PSIwLjkyIj5PaWwgJmFtcDsgR2FzIFdvcmxkIERhdGFiYXNlczwvdGV4dD4KICA8dGV4dCB4PSI4MDAiIHk9IjcxNiIgZm9udC1mYW1pbHk9IkFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMzQiIGZpbGw9IiM1QjY1NzIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkpvaW50IE9yZ2FuaXNhdGlvbnMgRGF0YSBJbml0aWF0aXZlPC90ZXh0Pgo8L3N2Zz4=",
|
||||
"description": "Monthly world petroleum and natural gas supply, demand, and stock statistics, self-reported by over 100 countries through the Joint Organisations Data Initiative (JODI) World Databases.",
|
||||
"allowCustomization": true,
|
||||
"tabs": {
|
||||
"country_balances": {
|
||||
"id": "country_balances",
|
||||
"name": "Country Balances",
|
||||
"layout": [
|
||||
{
|
||||
"i": "jodi_oil_balance_jodi_obb",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 40,
|
||||
"h": 14,
|
||||
"groups": [
|
||||
"Group 1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": "jodi_oil_demand_by_product_jodi_obb",
|
||||
"x": 0,
|
||||
"y": 14,
|
||||
"w": 20,
|
||||
"h": 13,
|
||||
"groups": [
|
||||
"Group 1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": "jodi_gas_balance_jodi_obb",
|
||||
"x": 20,
|
||||
"y": 14,
|
||||
"w": 20,
|
||||
"h": 13,
|
||||
"groups": [
|
||||
"Group 1"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"oil_markets": {
|
||||
"id": "oil_markets",
|
||||
"name": "Oil Markets",
|
||||
"layout": [
|
||||
{
|
||||
"i": "jodi_oil_production_jodi_obb",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 20,
|
||||
"h": 13,
|
||||
"groups": [
|
||||
"Group 2",
|
||||
"Group 4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": "jodi_oil_demand_jodi_obb",
|
||||
"x": 20,
|
||||
"y": 0,
|
||||
"w": 20,
|
||||
"h": 13,
|
||||
"groups": [
|
||||
"Group 2",
|
||||
"Group 4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": "jodi_oil_imports_jodi_obb",
|
||||
"x": 0,
|
||||
"y": 13,
|
||||
"w": 20,
|
||||
"h": 13,
|
||||
"groups": [
|
||||
"Group 2",
|
||||
"Group 3",
|
||||
"Group 4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": "jodi_oil_exports_jodi_obb",
|
||||
"x": 20,
|
||||
"y": 13,
|
||||
"w": 20,
|
||||
"h": 13,
|
||||
"groups": [
|
||||
"Group 2",
|
||||
"Group 3",
|
||||
"Group 4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": "jodi_oil_stocks_jodi_obb",
|
||||
"x": 0,
|
||||
"y": 26,
|
||||
"w": 40,
|
||||
"h": 13,
|
||||
"groups": [
|
||||
"Group 2",
|
||||
"Group 3"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gas_markets": {
|
||||
"id": "gas_markets",
|
||||
"name": "Gas Markets",
|
||||
"layout": [
|
||||
{
|
||||
"i": "jodi_gas_production_jodi_obb",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 20,
|
||||
"h": 13,
|
||||
"groups": [
|
||||
"Group 5",
|
||||
"Group 6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": "jodi_gas_demand_jodi_obb",
|
||||
"x": 20,
|
||||
"y": 0,
|
||||
"w": 20,
|
||||
"h": 13,
|
||||
"groups": [
|
||||
"Group 5",
|
||||
"Group 6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": "jodi_gas_imports_jodi_obb",
|
||||
"x": 0,
|
||||
"y": 13,
|
||||
"w": 20,
|
||||
"h": 13,
|
||||
"groups": [
|
||||
"Group 5",
|
||||
"Group 6",
|
||||
"Group 7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": "jodi_gas_exports_jodi_obb",
|
||||
"x": 20,
|
||||
"y": 13,
|
||||
"w": 20,
|
||||
"h": 13,
|
||||
"groups": [
|
||||
"Group 5",
|
||||
"Group 6",
|
||||
"Group 7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"i": "jodi_gas_stocks_jodi_obb",
|
||||
"x": 0,
|
||||
"y": 26,
|
||||
"w": 40,
|
||||
"h": 13,
|
||||
"groups": [
|
||||
"Group 5",
|
||||
"Group 6"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"groups": [
|
||||
{
|
||||
"name": "Group 1",
|
||||
"type": "param",
|
||||
"paramName": "country",
|
||||
"defaultValue": "united_states",
|
||||
"widgetIds": [
|
||||
"jodi_oil_balance_jodi_obb",
|
||||
"jodi_oil_demand_by_product_jodi_obb",
|
||||
"jodi_gas_balance_jodi_obb"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 2",
|
||||
"type": "param",
|
||||
"paramName": "country",
|
||||
"defaultValue": "united_states,saudi_arabia,russia,china,india",
|
||||
"widgetIds": [
|
||||
"jodi_oil_production_jodi_obb",
|
||||
"jodi_oil_demand_jodi_obb",
|
||||
"jodi_oil_imports_jodi_obb",
|
||||
"jodi_oil_exports_jodi_obb",
|
||||
"jodi_oil_stocks_jodi_obb"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 3",
|
||||
"type": "param",
|
||||
"paramName": "product",
|
||||
"defaultValue": "crude_oil",
|
||||
"widgetIds": [
|
||||
"jodi_oil_imports_jodi_obb",
|
||||
"jodi_oil_exports_jodi_obb",
|
||||
"jodi_oil_stocks_jodi_obb"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 4",
|
||||
"type": "param",
|
||||
"paramName": "unit",
|
||||
"defaultValue": "kbd",
|
||||
"widgetIds": [
|
||||
"jodi_oil_production_jodi_obb",
|
||||
"jodi_oil_demand_jodi_obb",
|
||||
"jodi_oil_imports_jodi_obb",
|
||||
"jodi_oil_exports_jodi_obb"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 5",
|
||||
"type": "param",
|
||||
"paramName": "country",
|
||||
"defaultValue": "united_states,qatar,norway,china,japan",
|
||||
"widgetIds": [
|
||||
"jodi_gas_production_jodi_obb",
|
||||
"jodi_gas_demand_jodi_obb",
|
||||
"jodi_gas_imports_jodi_obb",
|
||||
"jodi_gas_exports_jodi_obb",
|
||||
"jodi_gas_stocks_jodi_obb"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 6",
|
||||
"type": "param",
|
||||
"paramName": "unit",
|
||||
"defaultValue": "m3",
|
||||
"widgetIds": [
|
||||
"jodi_gas_production_jodi_obb",
|
||||
"jodi_gas_demand_jodi_obb",
|
||||
"jodi_gas_imports_jodi_obb",
|
||||
"jodi_gas_exports_jodi_obb",
|
||||
"jodi_gas_stocks_jodi_obb"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Group 7",
|
||||
"type": "param",
|
||||
"paramName": "flow",
|
||||
"defaultValue": "total",
|
||||
"widgetIds": [
|
||||
"jodi_gas_imports_jodi_obb",
|
||||
"jodi_gas_exports_jodi_obb"
|
||||
]
|
||||
}
|
||||
],
|
||||
"prompts": []
|
||||
}
|
||||
]
|
||||
183
openbb_platform/providers/jodi/openbb_jodi/gas_router.py
Normal file
183
openbb_platform/providers/jodi/openbb_jodi/gas_router.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""JODI Gas Router."""
|
||||
|
||||
from openbb_core.app.model.command_context import CommandContext
|
||||
from openbb_core.app.model.example import APIEx
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
from openbb_core.app.provider_interface import (
|
||||
ExtraParams,
|
||||
ProviderChoices,
|
||||
StandardParams,
|
||||
)
|
||||
from openbb_core.app.query import Query as OpenBBQuery
|
||||
from openbb_core.app.router import Router
|
||||
|
||||
router = Router(
|
||||
prefix="/gas",
|
||||
description="JODI-Gas World Database."
|
||||
+ " Monthly data from January 2009 to the most recent month.",
|
||||
)
|
||||
|
||||
|
||||
@router.command(
|
||||
model="JodiGasBalance",
|
||||
examples=[
|
||||
APIEx(
|
||||
description="Get the United States natural gas balance - one column per questionnaire flow.",
|
||||
parameters={"provider": "jodi"},
|
||||
),
|
||||
APIEx(
|
||||
description="Get Norway's natural gas balance, in terajoules.",
|
||||
parameters={
|
||||
"provider": "jodi",
|
||||
"country": "norway",
|
||||
"unit": "tj",
|
||||
"start_date": "2020-01-01",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def balance(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Get a country's monthly natural gas balance - production, pipeline and LNG trade, deliveries, and stocks, one column per flow of the JODI questionnaire."""
|
||||
return await OBBject.from_query(OpenBBQuery(**locals()))
|
||||
|
||||
|
||||
@router.command(
|
||||
model="JodiGasProduction",
|
||||
examples=[
|
||||
APIEx(
|
||||
description="Get monthly natural gas production - one column per reporting country.",
|
||||
parameters={"provider": "jodi"},
|
||||
),
|
||||
APIEx(
|
||||
description="Compare natural gas production in the United States, Qatar, and Norway.",
|
||||
parameters={
|
||||
"provider": "jodi",
|
||||
"country": "united_states,qatar,norway",
|
||||
"start_date": "2020-01-01",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def production(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Monthly natural gas production - one column per reporting country."""
|
||||
return await OBBject.from_query(OpenBBQuery(**locals()))
|
||||
|
||||
|
||||
@router.command(
|
||||
model="JodiGasDemand",
|
||||
examples=[
|
||||
APIEx(
|
||||
description="Get monthly natural gas demand - one column per reporting country.",
|
||||
parameters={"provider": "jodi"},
|
||||
),
|
||||
APIEx(
|
||||
description="Compare natural gas demand in Germany, France, and Italy.",
|
||||
parameters={
|
||||
"provider": "jodi",
|
||||
"country": "germany,france,italy",
|
||||
"start_date": "2020-01-01",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def demand(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Monthly natural gas demand, as gross inland deliveries - one column per reporting country."""
|
||||
return await OBBject.from_query(OpenBBQuery(**locals()))
|
||||
|
||||
|
||||
@router.command(
|
||||
model="JodiGasImports",
|
||||
examples=[
|
||||
APIEx(
|
||||
description="Get monthly natural gas imports - one column per reporting country.",
|
||||
parameters={"provider": "jodi"},
|
||||
),
|
||||
APIEx(
|
||||
description="Compare LNG imports of Japan, China, and South Korea.",
|
||||
parameters={
|
||||
"provider": "jodi",
|
||||
"country": "japan,china,south_korea",
|
||||
"flow": "lng",
|
||||
"start_date": "2020-01-01",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def imports(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Monthly natural gas imports - total, pipeline, or LNG - one column per reporting country."""
|
||||
return await OBBject.from_query(OpenBBQuery(**locals()))
|
||||
|
||||
|
||||
@router.command(
|
||||
model="JodiGasExports",
|
||||
examples=[
|
||||
APIEx(
|
||||
description="Get monthly natural gas exports - one column per reporting country.",
|
||||
parameters={"provider": "jodi"},
|
||||
),
|
||||
APIEx(
|
||||
description="Compare LNG exports from the United States, Qatar, and Australia.",
|
||||
parameters={
|
||||
"provider": "jodi",
|
||||
"country": "united_states,qatar,australia",
|
||||
"flow": "lng",
|
||||
"start_date": "2020-01-01",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def exports(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Monthly natural gas exports - total, pipeline, or LNG - one column per reporting country."""
|
||||
return await OBBject.from_query(OpenBBQuery(**locals()))
|
||||
|
||||
|
||||
@router.command(
|
||||
model="JodiGasStocks",
|
||||
examples=[
|
||||
APIEx(
|
||||
description="Get monthly natural gas closing stock levels - one column per reporting country.",
|
||||
parameters={"provider": "jodi"},
|
||||
),
|
||||
APIEx(
|
||||
description="Compare natural gas stocks in Germany, France, and the Netherlands.",
|
||||
parameters={
|
||||
"provider": "jodi",
|
||||
"country": "germany,france,netherlands",
|
||||
"start_date": "2020-01-01",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def stocks(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Monthly natural gas closing stock levels - one column per reporting country."""
|
||||
return await OBBject.from_query(OpenBBQuery(**locals()))
|
||||
39
openbb_platform/providers/jodi/openbb_jodi/jodi_router.py
Normal file
39
openbb_platform/providers/jodi/openbb_jodi/jodi_router.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""JODI Router."""
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from openbb_core.app.router import Router
|
||||
|
||||
from openbb_jodi.gas_router import router as gas_router
|
||||
from openbb_jodi.oil_router import router as oil_router
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_):
|
||||
"""Warm the JODI table cache in the background at application startup."""
|
||||
from openbb_jodi.utils.helpers import start_prefetch
|
||||
|
||||
start_prefetch()
|
||||
yield
|
||||
|
||||
|
||||
router = Router(
|
||||
prefix="",
|
||||
description="Monthly petroleum and natural gas supply, demand, and stock"
|
||||
+ " statistics, self-reported by over 100 countries, from the Joint"
|
||||
+ " Organisations Data Initiative (JODI) World Databases.",
|
||||
)
|
||||
router.api_router.lifespan_context = lifespan
|
||||
router.include_router(oil_router)
|
||||
router.include_router(gas_router)
|
||||
|
||||
|
||||
@router.api_router.get("/apps.json", include_in_schema=False)
|
||||
async def jodi_apps() -> list:
|
||||
"""Serve the bundled JODI dashboard template (``assets/apps.json``)."""
|
||||
import json # noqa
|
||||
from pathlib import Path
|
||||
|
||||
apps_json = Path(__file__).parent / "assets" / "apps.json"
|
||||
|
||||
return json.loads(apps_json.read_text(encoding="utf-8"))
|
||||
@@ -0,0 +1 @@
|
||||
"""OpenBB JODI Provider Models."""
|
||||
@@ -0,0 +1,607 @@
|
||||
"""JODI Country Series Data Model."""
|
||||
|
||||
from datetime import date as dateType
|
||||
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.utils.descriptions import DATA_DESCRIPTIONS
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class JodiCountryData(Data):
|
||||
"""JODI Country Series Data.
|
||||
|
||||
One column per reporting country, in alphabetical order. Countries
|
||||
without an observation for the period and measure are None.
|
||||
"""
|
||||
|
||||
date: dateType = Field(description=DATA_DESCRIPTIONS.get("date", ""))
|
||||
albania: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Albania.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Albania"}},
|
||||
)
|
||||
algeria: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Algeria.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Algeria"}},
|
||||
)
|
||||
angola: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Angola.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Angola"}},
|
||||
)
|
||||
argentina: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Argentina.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Argentina"}},
|
||||
)
|
||||
armenia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Armenia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Armenia"}},
|
||||
)
|
||||
australia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Australia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Australia"}},
|
||||
)
|
||||
austria: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Austria.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Austria"}},
|
||||
)
|
||||
azerbaijan: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Azerbaijan.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Azerbaijan"}},
|
||||
)
|
||||
bahrain: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Bahrain.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Bahrain"}},
|
||||
)
|
||||
bangladesh: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Bangladesh.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Bangladesh"}},
|
||||
)
|
||||
barbados: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Barbados.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Barbados"}},
|
||||
)
|
||||
belarus: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Belarus.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Belarus"}},
|
||||
)
|
||||
belgium: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Belgium.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Belgium"}},
|
||||
)
|
||||
belize: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Belize.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Belize"}},
|
||||
)
|
||||
bermuda: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Bermuda.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Bermuda"}},
|
||||
)
|
||||
bolivia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Bolivia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Bolivia"}},
|
||||
)
|
||||
brazil: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Brazil.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Brazil"}},
|
||||
)
|
||||
brunei: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Brunei.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Brunei"}},
|
||||
)
|
||||
bulgaria: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Bulgaria.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Bulgaria"}},
|
||||
)
|
||||
canada: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Canada.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Canada"}},
|
||||
)
|
||||
chile: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Chile.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Chile"}},
|
||||
)
|
||||
china: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by China.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "China"}},
|
||||
)
|
||||
colombia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Colombia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Colombia"}},
|
||||
)
|
||||
costa_rica: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Costa Rica.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Costa Rica"}},
|
||||
)
|
||||
croatia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Croatia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Croatia"}},
|
||||
)
|
||||
cuba: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Cuba.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Cuba"}},
|
||||
)
|
||||
cyprus: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Cyprus.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Cyprus"}},
|
||||
)
|
||||
czechia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Czechia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Czechia"}},
|
||||
)
|
||||
denmark: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Denmark.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Denmark"}},
|
||||
)
|
||||
dominican_republic: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Dominican Republic.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Dominican Republic"}},
|
||||
)
|
||||
ecuador: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Ecuador.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Ecuador"}},
|
||||
)
|
||||
egypt: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Egypt.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Egypt"}},
|
||||
)
|
||||
el_salvador: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by El Salvador.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "El Salvador"}},
|
||||
)
|
||||
equatorial_guinea: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Equatorial Guinea.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Equatorial Guinea"}},
|
||||
)
|
||||
estonia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Estonia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Estonia"}},
|
||||
)
|
||||
eswatini: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Eswatini.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Eswatini"}},
|
||||
)
|
||||
finland: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Finland.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Finland"}},
|
||||
)
|
||||
france: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by France.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "France"}},
|
||||
)
|
||||
gabon: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Gabon.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Gabon"}},
|
||||
)
|
||||
gambia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Gambia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Gambia"}},
|
||||
)
|
||||
georgia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Georgia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Georgia"}},
|
||||
)
|
||||
germany: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Germany.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Germany"}},
|
||||
)
|
||||
greece: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Greece.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Greece"}},
|
||||
)
|
||||
grenada: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Grenada.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Grenada"}},
|
||||
)
|
||||
guatemala: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Guatemala.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Guatemala"}},
|
||||
)
|
||||
guyana: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Guyana.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Guyana"}},
|
||||
)
|
||||
haiti: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Haiti.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Haiti"}},
|
||||
)
|
||||
honduras: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Honduras.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Honduras"}},
|
||||
)
|
||||
hong_kong: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Hong Kong.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Hong Kong"}},
|
||||
)
|
||||
hungary: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Hungary.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Hungary"}},
|
||||
)
|
||||
iceland: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Iceland.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Iceland"}},
|
||||
)
|
||||
india: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by India.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "India"}},
|
||||
)
|
||||
indonesia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Indonesia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Indonesia"}},
|
||||
)
|
||||
iran: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Iran.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Iran"}},
|
||||
)
|
||||
iraq: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Iraq.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Iraq"}},
|
||||
)
|
||||
ireland: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Ireland.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Ireland"}},
|
||||
)
|
||||
italy: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Italy.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Italy"}},
|
||||
)
|
||||
jamaica: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Jamaica.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Jamaica"}},
|
||||
)
|
||||
japan: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Japan.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Japan"}},
|
||||
)
|
||||
kazakhstan: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Kazakhstan.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Kazakhstan"}},
|
||||
)
|
||||
kuwait: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Kuwait.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Kuwait"}},
|
||||
)
|
||||
latvia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Latvia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Latvia"}},
|
||||
)
|
||||
libya: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Libya.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Libya"}},
|
||||
)
|
||||
lithuania: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Lithuania.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Lithuania"}},
|
||||
)
|
||||
luxembourg: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Luxembourg.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Luxembourg"}},
|
||||
)
|
||||
malaysia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Malaysia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Malaysia"}},
|
||||
)
|
||||
malta: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Malta.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Malta"}},
|
||||
)
|
||||
mauritius: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Mauritius.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Mauritius"}},
|
||||
)
|
||||
mexico: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Mexico.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Mexico"}},
|
||||
)
|
||||
moldova: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Moldova.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Moldova"}},
|
||||
)
|
||||
morocco: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Morocco.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Morocco"}},
|
||||
)
|
||||
myanmar: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Myanmar.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Myanmar"}},
|
||||
)
|
||||
nepal: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Nepal.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Nepal"}},
|
||||
)
|
||||
netherlands: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Netherlands.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Netherlands"}},
|
||||
)
|
||||
new_zealand: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by New Zealand.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "New Zealand"}},
|
||||
)
|
||||
nicaragua: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Nicaragua.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Nicaragua"}},
|
||||
)
|
||||
niger: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Niger.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Niger"}},
|
||||
)
|
||||
nigeria: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Nigeria.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Nigeria"}},
|
||||
)
|
||||
north_macedonia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by North Macedonia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "North Macedonia"}},
|
||||
)
|
||||
norway: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Norway.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Norway"}},
|
||||
)
|
||||
oman: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Oman.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Oman"}},
|
||||
)
|
||||
panama: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Panama.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Panama"}},
|
||||
)
|
||||
papua_new_guinea: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Papua New Guinea.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Papua New Guinea"}},
|
||||
)
|
||||
paraguay: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Paraguay.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Paraguay"}},
|
||||
)
|
||||
peru: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Peru.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Peru"}},
|
||||
)
|
||||
philippines: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Philippines.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Philippines"}},
|
||||
)
|
||||
poland: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Poland.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Poland"}},
|
||||
)
|
||||
portugal: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Portugal.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Portugal"}},
|
||||
)
|
||||
qatar: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Qatar.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Qatar"}},
|
||||
)
|
||||
romania: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Romania.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Romania"}},
|
||||
)
|
||||
russia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Russia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Russia"}},
|
||||
)
|
||||
saudi_arabia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Saudi Arabia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Saudi Arabia"}},
|
||||
)
|
||||
serbia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Serbia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Serbia"}},
|
||||
)
|
||||
singapore: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Singapore.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Singapore"}},
|
||||
)
|
||||
slovakia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Slovakia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Slovakia"}},
|
||||
)
|
||||
slovenia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Slovenia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Slovenia"}},
|
||||
)
|
||||
south_africa: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by South Africa.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "South Africa"}},
|
||||
)
|
||||
south_korea: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by South Korea.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "South Korea"}},
|
||||
)
|
||||
spain: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Spain.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Spain"}},
|
||||
)
|
||||
sudan: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Sudan.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Sudan"}},
|
||||
)
|
||||
suriname: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Suriname.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Suriname"}},
|
||||
)
|
||||
sweden: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Sweden.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Sweden"}},
|
||||
)
|
||||
switzerland: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Switzerland.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Switzerland"}},
|
||||
)
|
||||
syria: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Syria.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Syria"}},
|
||||
)
|
||||
taiwan: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Taiwan.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Taiwan"}},
|
||||
)
|
||||
tajikistan: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Tajikistan.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Tajikistan"}},
|
||||
)
|
||||
thailand: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Thailand.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Thailand"}},
|
||||
)
|
||||
trinidad_and_tobago: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Trinidad and Tobago.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Trinidad and Tobago"}},
|
||||
)
|
||||
tunisia: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Tunisia.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Tunisia"}},
|
||||
)
|
||||
turkiye: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Turkiye.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Turkiye"}},
|
||||
)
|
||||
ukraine: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Ukraine.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Ukraine"}},
|
||||
)
|
||||
united_arab_emirates: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by United Arab Emirates.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "United Arab Emirates"}},
|
||||
)
|
||||
united_kingdom: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by United Kingdom.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "United Kingdom"}},
|
||||
)
|
||||
united_states: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by United States.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "United States"}},
|
||||
)
|
||||
uruguay: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Uruguay.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Uruguay"}},
|
||||
)
|
||||
venezuela: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Venezuela.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Venezuela"}},
|
||||
)
|
||||
vietnam: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Vietnam.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Vietnam"}},
|
||||
)
|
||||
yemen: float | None = Field(
|
||||
default=None,
|
||||
description="Monthly value reported by Yemen.",
|
||||
json_schema_extra={"x-widget_config": {"headerName": "Yemen"}},
|
||||
)
|
||||
210
openbb_platform/providers/jodi/openbb_jodi/models/gas_balance.py
Normal file
210
openbb_platform/providers/jodi/openbb_jodi/models/gas_balance.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""JODI Gas Balance Model."""
|
||||
|
||||
from datetime import date as dateType
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_jodi.utils.constants import (
|
||||
COUNTRY_SCHEMA_SINGLE,
|
||||
COUNTRY_SINGLE_DESCRIPTION,
|
||||
GAS_BALANCE_FIELDS,
|
||||
GAS_FLOW_DEFINITIONS,
|
||||
GAS_UNIT_DESCRIPTION,
|
||||
GAS_UNIT_LABELS,
|
||||
GAS_UNITS,
|
||||
USE_CACHE_DESCRIPTION,
|
||||
GasUnitType,
|
||||
)
|
||||
|
||||
|
||||
class JodiGasBalanceQueryParams(QueryParams):
|
||||
"""JODI Gas Balance Query.
|
||||
|
||||
Natural gas is a mixture of gaseous hydrocarbons, primarily methane,
|
||||
but generally also including ethane, propane, and higher hydrocarbons
|
||||
in much smaller amounts, and some non-combustible gases such as
|
||||
nitrogen and carbon dioxide. It includes both non-associated and
|
||||
associated gas. Colliery gas, coal seam gas, and shale gas are
|
||||
included, while manufactured gas and biogas are excluded except when
|
||||
blended with natural gas for final consumption. Natural gas liquids
|
||||
are excluded.
|
||||
|
||||
Source: https://www.jodidata.org/gas/database/data-downloads.aspx
|
||||
"""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"country": COUNTRY_SCHEMA_SINGLE,
|
||||
"unit": {"choices": list(GAS_UNITS)},
|
||||
}
|
||||
|
||||
country: str = Field(
|
||||
default="united_states",
|
||||
description=COUNTRY_SINGLE_DESCRIPTION,
|
||||
)
|
||||
unit: GasUnitType = Field(
|
||||
default="m3",
|
||||
description=GAS_UNIT_DESCRIPTION,
|
||||
)
|
||||
start_date: dateType | None = Field(
|
||||
default=None,
|
||||
description=QUERY_DESCRIPTIONS.get("start_date", "")
|
||||
+ " Defaults to the first data period, 2009-01-01.",
|
||||
)
|
||||
end_date: dateType | None = Field(
|
||||
default=None, description=QUERY_DESCRIPTIONS.get("end_date", "")
|
||||
)
|
||||
use_cache: bool = Field(default=True, description=USE_CACHE_DESCRIPTION)
|
||||
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate country."""
|
||||
from openbb_jodi.utils.helpers import validate_country_param
|
||||
|
||||
return validate_country_param(v, multiple=False, default="united_states")
|
||||
|
||||
@field_validator("unit", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _normalize_unit(cls, v):
|
||||
"""Normalize unit."""
|
||||
from openbb_jodi.utils.helpers import normalize_token
|
||||
|
||||
return normalize_token(v, "m3")
|
||||
|
||||
|
||||
class JodiGasBalanceData(Data):
|
||||
"""JODI Gas Balance Data.
|
||||
|
||||
One column per flow of the JODI-Gas questionnaire, in published order.
|
||||
"""
|
||||
|
||||
date: dateType = Field(description=DATA_DESCRIPTIONS.get("date", ""))
|
||||
production: float | None = Field(
|
||||
default=None,
|
||||
description="Production. " + GAS_FLOW_DEFINITIONS["production"],
|
||||
)
|
||||
from_other_sources: float | None = Field(
|
||||
default=None,
|
||||
description="Receipts from other sources. "
|
||||
+ GAS_FLOW_DEFINITIONS["from_other_sources"],
|
||||
)
|
||||
imports: float | None = Field(
|
||||
default=None,
|
||||
description="Total imports. " + GAS_FLOW_DEFINITIONS["imports"],
|
||||
)
|
||||
pipeline_imports: float | None = Field(
|
||||
default=None,
|
||||
description="Of which: pipeline imports. "
|
||||
+ GAS_FLOW_DEFINITIONS["pipeline_imports"],
|
||||
)
|
||||
lng_imports: float | None = Field(
|
||||
default=None,
|
||||
description="Of which: LNG imports. " + GAS_FLOW_DEFINITIONS["lng_imports"],
|
||||
)
|
||||
exports: float | None = Field(
|
||||
default=None,
|
||||
description="Total exports. " + GAS_FLOW_DEFINITIONS["exports"],
|
||||
)
|
||||
pipeline_exports: float | None = Field(
|
||||
default=None,
|
||||
description="Of which: pipeline exports. "
|
||||
+ GAS_FLOW_DEFINITIONS["pipeline_exports"],
|
||||
)
|
||||
lng_exports: float | None = Field(
|
||||
default=None,
|
||||
description="Of which: LNG exports. " + GAS_FLOW_DEFINITIONS["lng_exports"],
|
||||
)
|
||||
stock_change: float | None = Field(
|
||||
default=None,
|
||||
description="Stock change. " + GAS_FLOW_DEFINITIONS["stock_change"],
|
||||
)
|
||||
demand_calculated: float | None = Field(
|
||||
default=None,
|
||||
description=GAS_FLOW_DEFINITIONS["demand_calculated"],
|
||||
)
|
||||
statistical_difference: float | None = Field(
|
||||
default=None,
|
||||
description="Statistical difference. "
|
||||
+ GAS_FLOW_DEFINITIONS["statistical_difference"],
|
||||
)
|
||||
demand: float | None = Field(
|
||||
default=None,
|
||||
description=GAS_FLOW_DEFINITIONS["demand"],
|
||||
)
|
||||
electricity_and_heat_generation: float | None = Field(
|
||||
default=None,
|
||||
description="Of which: electricity and heat generation. "
|
||||
+ GAS_FLOW_DEFINITIONS["electricity_and_heat_generation"],
|
||||
)
|
||||
closing_stocks: float | None = Field(
|
||||
default=None,
|
||||
description="Closing stocks. " + GAS_FLOW_DEFINITIONS["closing_stocks"],
|
||||
)
|
||||
|
||||
|
||||
class JodiGasBalanceFetcher(
|
||||
Fetcher[JodiGasBalanceQueryParams, list[JodiGasBalanceData]]
|
||||
):
|
||||
"""JODI Gas Balance Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: dict[str, Any]) -> JodiGasBalanceQueryParams:
|
||||
"""Transform the query params."""
|
||||
return JodiGasBalanceQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: JodiGasBalanceQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict]:
|
||||
"""Extract the data from the JODI-Gas world database file."""
|
||||
from openbb_jodi.utils.constants import COUNTRIES, GAS_START_YEAR
|
||||
from openbb_jodi.utils.helpers import get_filtered_gas, resolve_date_range
|
||||
|
||||
start_date, end_date = resolve_date_range(
|
||||
query.start_date, query.end_date, GAS_START_YEAR
|
||||
)
|
||||
return await get_filtered_gas(
|
||||
countries={COUNTRIES[query.country]},
|
||||
flow_codes=set(GAS_BALANCE_FIELDS),
|
||||
unit_codes={GAS_UNITS[query.unit]},
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
use_cache=query.use_cache,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: JodiGasBalanceQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any,
|
||||
) -> AnnotatedResult[list[JodiGasBalanceData]]:
|
||||
"""Transform the raw data into the balance table."""
|
||||
from openbb_jodi.utils.constants import COUNTRY_LABELS
|
||||
from openbb_jodi.utils.helpers import build_assessments, build_field_table
|
||||
|
||||
rows = build_field_table(data, "FLOW_BREAKDOWN", GAS_BALANCE_FIELDS)
|
||||
code = data[0]["REF_AREA"]
|
||||
metadata = {
|
||||
"country": COUNTRY_LABELS.get(code, code),
|
||||
"product": "Natural gas",
|
||||
"unit": GAS_UNIT_LABELS[query.unit],
|
||||
"assessments": build_assessments(
|
||||
data, "FLOW_BREAKDOWN", GAS_BALANCE_FIELDS
|
||||
),
|
||||
"source": "JODI-Gas World Database (www.jodidata.org)",
|
||||
}
|
||||
return AnnotatedResult(
|
||||
result=[JodiGasBalanceData.model_validate(r) for r in rows],
|
||||
metadata=metadata,
|
||||
)
|
||||
160
openbb_platform/providers/jodi/openbb_jodi/models/gas_demand.py
Normal file
160
openbb_platform/providers/jodi/openbb_jodi/models/gas_demand.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""JODI Gas Demand Model."""
|
||||
|
||||
from datetime import date as dateType
|
||||
from typing import Any, Literal
|
||||
|
||||
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_jodi.models.country_data import JodiCountryData
|
||||
from openbb_jodi.utils.constants import (
|
||||
COUNTRY_MULTI_DESCRIPTION,
|
||||
COUNTRY_SCHEMA_MULTI,
|
||||
GAS_FLOW_DEFINITIONS,
|
||||
GAS_UNIT_DESCRIPTION,
|
||||
GAS_UNIT_LABELS,
|
||||
GAS_UNITS,
|
||||
USE_CACHE_DESCRIPTION,
|
||||
GasUnitType,
|
||||
)
|
||||
|
||||
MEASURE_CODES = {"observed": "TOTDEMO", "calculated": "TOTDEMC"}
|
||||
MEASURE_LABELS = {
|
||||
"observed": "Gross inland deliveries (observed)",
|
||||
"calculated": "Gross inland deliveries (calculated)",
|
||||
}
|
||||
|
||||
|
||||
class JodiGasDemandQueryParams(QueryParams):
|
||||
"""JODI Gas Demand Query.
|
||||
|
||||
Natural gas is a mixture of gaseous hydrocarbons, primarily methane,
|
||||
but generally also including ethane, propane, and higher hydrocarbons
|
||||
in much smaller amounts, and some non-combustible gases such as
|
||||
nitrogen and carbon dioxide. It includes both non-associated and
|
||||
associated gas. Colliery gas, coal seam gas, and shale gas are
|
||||
included, while manufactured gas and biogas are excluded except when
|
||||
blended with natural gas for final consumption. Natural gas liquids
|
||||
are excluded.
|
||||
|
||||
Source: https://www.jodidata.org/gas/database/data-downloads.aspx
|
||||
"""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"country": COUNTRY_SCHEMA_MULTI,
|
||||
"measure": {"choices": list(MEASURE_CODES)},
|
||||
"unit": {"choices": list(GAS_UNITS)},
|
||||
}
|
||||
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description=COUNTRY_MULTI_DESCRIPTION,
|
||||
)
|
||||
measure: Literal["observed", "calculated"] = Field(
|
||||
default="observed",
|
||||
description="The demand measure. JODI questionnaire definitions:\n"
|
||||
+ " observed: "
|
||||
+ GAS_FLOW_DEFINITIONS["demand"]
|
||||
+ "\n calculated: "
|
||||
+ GAS_FLOW_DEFINITIONS["demand_calculated"],
|
||||
)
|
||||
unit: GasUnitType = Field(
|
||||
default="m3",
|
||||
description=GAS_UNIT_DESCRIPTION,
|
||||
)
|
||||
start_date: dateType | None = Field(
|
||||
default=None,
|
||||
description=QUERY_DESCRIPTIONS.get("start_date", "")
|
||||
+ " Defaults to the first data period, 2009-01-01.",
|
||||
)
|
||||
end_date: dateType | None = Field(
|
||||
default=None, description=QUERY_DESCRIPTIONS.get("end_date", "")
|
||||
)
|
||||
use_cache: bool = Field(default=True, description=USE_CACHE_DESCRIPTION)
|
||||
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate country."""
|
||||
from openbb_jodi.utils.helpers import validate_country_param
|
||||
|
||||
return validate_country_param(v, multiple=True, default=None)
|
||||
|
||||
@field_validator("measure", "unit", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _normalize_tokens(cls, v, info):
|
||||
"""Normalize token parameters."""
|
||||
from openbb_jodi.utils.helpers import normalize_token
|
||||
|
||||
defaults = {"measure": "observed", "unit": "m3"}
|
||||
return normalize_token(v, defaults[str(info.field_name)])
|
||||
|
||||
|
||||
class JodiGasDemandData(JodiCountryData):
|
||||
"""JODI Gas Demand Data.
|
||||
|
||||
One column per reporting country. Values are gross inland deliveries
|
||||
of natural gas, as selected by the measure parameter.
|
||||
"""
|
||||
|
||||
|
||||
class JodiGasDemandFetcher(Fetcher[JodiGasDemandQueryParams, list[JodiGasDemandData]]):
|
||||
"""JODI Gas Demand Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: dict[str, Any]) -> JodiGasDemandQueryParams:
|
||||
"""Transform the query params."""
|
||||
return JodiGasDemandQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: JodiGasDemandQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict]:
|
||||
"""Extract the data from the JODI-Gas world database file."""
|
||||
from openbb_jodi.utils.constants import COUNTRIES, GAS_START_YEAR
|
||||
from openbb_jodi.utils.helpers import get_filtered_gas, resolve_date_range
|
||||
|
||||
start_date, end_date = resolve_date_range(
|
||||
query.start_date, query.end_date, GAS_START_YEAR
|
||||
)
|
||||
countries = (
|
||||
{COUNTRIES[country] for country in query.country.split(",")}
|
||||
if query.country
|
||||
else None
|
||||
)
|
||||
return await get_filtered_gas(
|
||||
countries=countries,
|
||||
flow_codes={MEASURE_CODES[query.measure]},
|
||||
unit_codes={GAS_UNITS[query.unit]},
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
use_cache=query.use_cache,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: JodiGasDemandQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any,
|
||||
) -> AnnotatedResult[list[JodiGasDemandData]]:
|
||||
"""Transform the raw data into the by-country table."""
|
||||
from openbb_jodi.utils.constants import CODE_TO_COUNTRY
|
||||
from openbb_jodi.utils.helpers import build_assessments, build_country_table
|
||||
|
||||
rows = build_country_table(data)
|
||||
metadata = {
|
||||
"product": "Natural gas",
|
||||
"flow": MEASURE_LABELS[query.measure],
|
||||
"unit": GAS_UNIT_LABELS[query.unit],
|
||||
"assessments": build_assessments(data, "REF_AREA", CODE_TO_COUNTRY),
|
||||
"source": "JODI-Gas World Database (www.jodidata.org)",
|
||||
}
|
||||
return AnnotatedResult(
|
||||
result=[JodiGasDemandData.model_validate(r) for r in rows],
|
||||
metadata=metadata,
|
||||
)
|
||||
167
openbb_platform/providers/jodi/openbb_jodi/models/gas_exports.py
Normal file
167
openbb_platform/providers/jodi/openbb_jodi/models/gas_exports.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""JODI Gas Exports Model."""
|
||||
|
||||
from datetime import date as dateType
|
||||
from typing import Any, Literal
|
||||
|
||||
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_jodi.models.country_data import JodiCountryData
|
||||
from openbb_jodi.utils.constants import (
|
||||
COUNTRY_MULTI_DESCRIPTION,
|
||||
COUNTRY_SCHEMA_MULTI,
|
||||
GAS_FLOW_DEFINITIONS,
|
||||
GAS_UNIT_DESCRIPTION,
|
||||
GAS_UNIT_LABELS,
|
||||
GAS_UNITS,
|
||||
USE_CACHE_DESCRIPTION,
|
||||
GasUnitType,
|
||||
)
|
||||
|
||||
FLOW_CODES = {"total": "TOTEXPSB", "pipeline": "EXPPIP", "lng": "EXPLNG"}
|
||||
FLOW_LABELS = {
|
||||
"total": "Exports",
|
||||
"pipeline": "Pipeline exports",
|
||||
"lng": "LNG exports",
|
||||
}
|
||||
|
||||
|
||||
class JodiGasExportsQueryParams(QueryParams):
|
||||
"""JODI Gas Exports Query.
|
||||
|
||||
Natural gas is a mixture of gaseous hydrocarbons, primarily methane,
|
||||
but generally also including ethane, propane, and higher hydrocarbons
|
||||
in much smaller amounts, and some non-combustible gases such as
|
||||
nitrogen and carbon dioxide. It includes both non-associated and
|
||||
associated gas. Colliery gas, coal seam gas, and shale gas are
|
||||
included, while manufactured gas and biogas are excluded except when
|
||||
blended with natural gas for final consumption. Natural gas liquids
|
||||
are excluded.
|
||||
|
||||
Source: https://www.jodidata.org/gas/database/data-downloads.aspx
|
||||
"""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"country": COUNTRY_SCHEMA_MULTI,
|
||||
"flow": {"choices": list(FLOW_CODES)},
|
||||
"unit": {"choices": list(GAS_UNITS)},
|
||||
}
|
||||
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description=COUNTRY_MULTI_DESCRIPTION,
|
||||
)
|
||||
flow: Literal["total", "pipeline", "lng"] = Field(
|
||||
default="total",
|
||||
description="The exports breakdown. JODI questionnaire definitions:\n"
|
||||
+ " total: "
|
||||
+ GAS_FLOW_DEFINITIONS["exports"]
|
||||
+ "\n pipeline: "
|
||||
+ GAS_FLOW_DEFINITIONS["pipeline_exports"]
|
||||
+ "\n lng: "
|
||||
+ GAS_FLOW_DEFINITIONS["lng_exports"],
|
||||
)
|
||||
unit: GasUnitType = Field(
|
||||
default="m3",
|
||||
description=GAS_UNIT_DESCRIPTION,
|
||||
)
|
||||
start_date: dateType | None = Field(
|
||||
default=None,
|
||||
description=QUERY_DESCRIPTIONS.get("start_date", "")
|
||||
+ " Defaults to the first data period, 2009-01-01.",
|
||||
)
|
||||
end_date: dateType | None = Field(
|
||||
default=None, description=QUERY_DESCRIPTIONS.get("end_date", "")
|
||||
)
|
||||
use_cache: bool = Field(default=True, description=USE_CACHE_DESCRIPTION)
|
||||
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate country."""
|
||||
from openbb_jodi.utils.helpers import validate_country_param
|
||||
|
||||
return validate_country_param(v, multiple=True, default=None)
|
||||
|
||||
@field_validator("flow", "unit", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _normalize_tokens(cls, v, info):
|
||||
"""Normalize token parameters."""
|
||||
from openbb_jodi.utils.helpers import normalize_token
|
||||
|
||||
defaults = {"flow": "total", "unit": "m3"}
|
||||
return normalize_token(v, defaults[str(info.field_name)])
|
||||
|
||||
|
||||
class JodiGasExportsData(JodiCountryData):
|
||||
"""JODI Gas Exports Data.
|
||||
|
||||
One column per reporting country. Exports are amounts that have
|
||||
crossed the physical boundaries of the country, whether customs
|
||||
clearance has taken place or not; deliveries for international
|
||||
bunkers are excluded.
|
||||
"""
|
||||
|
||||
|
||||
class JodiGasExportsFetcher(
|
||||
Fetcher[JodiGasExportsQueryParams, list[JodiGasExportsData]]
|
||||
):
|
||||
"""JODI Gas Exports Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: dict[str, Any]) -> JodiGasExportsQueryParams:
|
||||
"""Transform the query params."""
|
||||
return JodiGasExportsQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: JodiGasExportsQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict]:
|
||||
"""Extract the data from the JODI-Gas world database file."""
|
||||
from openbb_jodi.utils.constants import COUNTRIES, GAS_START_YEAR
|
||||
from openbb_jodi.utils.helpers import get_filtered_gas, resolve_date_range
|
||||
|
||||
start_date, end_date = resolve_date_range(
|
||||
query.start_date, query.end_date, GAS_START_YEAR
|
||||
)
|
||||
countries = (
|
||||
{COUNTRIES[country] for country in query.country.split(",")}
|
||||
if query.country
|
||||
else None
|
||||
)
|
||||
return await get_filtered_gas(
|
||||
countries=countries,
|
||||
flow_codes={FLOW_CODES[query.flow]},
|
||||
unit_codes={GAS_UNITS[query.unit]},
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
use_cache=query.use_cache,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: JodiGasExportsQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any,
|
||||
) -> AnnotatedResult[list[JodiGasExportsData]]:
|
||||
"""Transform the raw data into the by-country table."""
|
||||
from openbb_jodi.utils.constants import CODE_TO_COUNTRY
|
||||
from openbb_jodi.utils.helpers import build_assessments, build_country_table
|
||||
|
||||
rows = build_country_table(data)
|
||||
metadata = {
|
||||
"product": "Natural gas",
|
||||
"flow": FLOW_LABELS[query.flow],
|
||||
"unit": GAS_UNIT_LABELS[query.unit],
|
||||
"assessments": build_assessments(data, "REF_AREA", CODE_TO_COUNTRY),
|
||||
"source": "JODI-Gas World Database (www.jodidata.org)",
|
||||
}
|
||||
return AnnotatedResult(
|
||||
result=[JodiGasExportsData.model_validate(r) for r in rows],
|
||||
metadata=metadata,
|
||||
)
|
||||
167
openbb_platform/providers/jodi/openbb_jodi/models/gas_imports.py
Normal file
167
openbb_platform/providers/jodi/openbb_jodi/models/gas_imports.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""JODI Gas Imports Model."""
|
||||
|
||||
from datetime import date as dateType
|
||||
from typing import Any, Literal
|
||||
|
||||
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_jodi.models.country_data import JodiCountryData
|
||||
from openbb_jodi.utils.constants import (
|
||||
COUNTRY_MULTI_DESCRIPTION,
|
||||
COUNTRY_SCHEMA_MULTI,
|
||||
GAS_FLOW_DEFINITIONS,
|
||||
GAS_UNIT_DESCRIPTION,
|
||||
GAS_UNIT_LABELS,
|
||||
GAS_UNITS,
|
||||
USE_CACHE_DESCRIPTION,
|
||||
GasUnitType,
|
||||
)
|
||||
|
||||
FLOW_CODES = {"total": "TOTIMPSB", "pipeline": "IMPPIP", "lng": "IMPLNG"}
|
||||
FLOW_LABELS = {
|
||||
"total": "Imports",
|
||||
"pipeline": "Pipeline imports",
|
||||
"lng": "LNG imports",
|
||||
}
|
||||
|
||||
|
||||
class JodiGasImportsQueryParams(QueryParams):
|
||||
"""JODI Gas Imports Query.
|
||||
|
||||
Natural gas is a mixture of gaseous hydrocarbons, primarily methane,
|
||||
but generally also including ethane, propane, and higher hydrocarbons
|
||||
in much smaller amounts, and some non-combustible gases such as
|
||||
nitrogen and carbon dioxide. It includes both non-associated and
|
||||
associated gas. Colliery gas, coal seam gas, and shale gas are
|
||||
included, while manufactured gas and biogas are excluded except when
|
||||
blended with natural gas for final consumption. Natural gas liquids
|
||||
are excluded.
|
||||
|
||||
Source: https://www.jodidata.org/gas/database/data-downloads.aspx
|
||||
"""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"country": COUNTRY_SCHEMA_MULTI,
|
||||
"flow": {"choices": list(FLOW_CODES)},
|
||||
"unit": {"choices": list(GAS_UNITS)},
|
||||
}
|
||||
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description=COUNTRY_MULTI_DESCRIPTION,
|
||||
)
|
||||
flow: Literal["total", "pipeline", "lng"] = Field(
|
||||
default="total",
|
||||
description="The imports breakdown. JODI questionnaire definitions:\n"
|
||||
+ " total: "
|
||||
+ GAS_FLOW_DEFINITIONS["imports"]
|
||||
+ "\n pipeline: "
|
||||
+ GAS_FLOW_DEFINITIONS["pipeline_imports"]
|
||||
+ "\n lng: "
|
||||
+ GAS_FLOW_DEFINITIONS["lng_imports"],
|
||||
)
|
||||
unit: GasUnitType = Field(
|
||||
default="m3",
|
||||
description=GAS_UNIT_DESCRIPTION,
|
||||
)
|
||||
start_date: dateType | None = Field(
|
||||
default=None,
|
||||
description=QUERY_DESCRIPTIONS.get("start_date", "")
|
||||
+ " Defaults to the first data period, 2009-01-01.",
|
||||
)
|
||||
end_date: dateType | None = Field(
|
||||
default=None, description=QUERY_DESCRIPTIONS.get("end_date", "")
|
||||
)
|
||||
use_cache: bool = Field(default=True, description=USE_CACHE_DESCRIPTION)
|
||||
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate country."""
|
||||
from openbb_jodi.utils.helpers import validate_country_param
|
||||
|
||||
return validate_country_param(v, multiple=True, default=None)
|
||||
|
||||
@field_validator("flow", "unit", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _normalize_tokens(cls, v, info):
|
||||
"""Normalize token parameters."""
|
||||
from openbb_jodi.utils.helpers import normalize_token
|
||||
|
||||
defaults = {"flow": "total", "unit": "m3"}
|
||||
return normalize_token(v, defaults[str(info.field_name)])
|
||||
|
||||
|
||||
class JodiGasImportsData(JodiCountryData):
|
||||
"""JODI Gas Imports Data.
|
||||
|
||||
One column per reporting country. Imports are amounts that have
|
||||
crossed the physical boundaries of the country, whether customs
|
||||
clearance has taken place or not; deliveries for international
|
||||
bunkers are excluded.
|
||||
"""
|
||||
|
||||
|
||||
class JodiGasImportsFetcher(
|
||||
Fetcher[JodiGasImportsQueryParams, list[JodiGasImportsData]]
|
||||
):
|
||||
"""JODI Gas Imports Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: dict[str, Any]) -> JodiGasImportsQueryParams:
|
||||
"""Transform the query params."""
|
||||
return JodiGasImportsQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: JodiGasImportsQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict]:
|
||||
"""Extract the data from the JODI-Gas world database file."""
|
||||
from openbb_jodi.utils.constants import COUNTRIES, GAS_START_YEAR
|
||||
from openbb_jodi.utils.helpers import get_filtered_gas, resolve_date_range
|
||||
|
||||
start_date, end_date = resolve_date_range(
|
||||
query.start_date, query.end_date, GAS_START_YEAR
|
||||
)
|
||||
countries = (
|
||||
{COUNTRIES[country] for country in query.country.split(",")}
|
||||
if query.country
|
||||
else None
|
||||
)
|
||||
return await get_filtered_gas(
|
||||
countries=countries,
|
||||
flow_codes={FLOW_CODES[query.flow]},
|
||||
unit_codes={GAS_UNITS[query.unit]},
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
use_cache=query.use_cache,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: JodiGasImportsQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any,
|
||||
) -> AnnotatedResult[list[JodiGasImportsData]]:
|
||||
"""Transform the raw data into the by-country table."""
|
||||
from openbb_jodi.utils.constants import CODE_TO_COUNTRY
|
||||
from openbb_jodi.utils.helpers import build_assessments, build_country_table
|
||||
|
||||
rows = build_country_table(data)
|
||||
metadata = {
|
||||
"product": "Natural gas",
|
||||
"flow": FLOW_LABELS[query.flow],
|
||||
"unit": GAS_UNIT_LABELS[query.unit],
|
||||
"assessments": build_assessments(data, "REF_AREA", CODE_TO_COUNTRY),
|
||||
"source": "JODI-Gas World Database (www.jodidata.org)",
|
||||
}
|
||||
return AnnotatedResult(
|
||||
result=[JodiGasImportsData.model_validate(r) for r in rows],
|
||||
metadata=metadata,
|
||||
)
|
||||
@@ -0,0 +1,149 @@
|
||||
"""JODI Gas Production Model."""
|
||||
|
||||
from datetime import date as dateType
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_jodi.models.country_data import JodiCountryData
|
||||
from openbb_jodi.utils.constants import (
|
||||
COUNTRY_MULTI_DESCRIPTION,
|
||||
COUNTRY_SCHEMA_MULTI,
|
||||
GAS_UNIT_DESCRIPTION,
|
||||
GAS_UNIT_LABELS,
|
||||
GAS_UNITS,
|
||||
USE_CACHE_DESCRIPTION,
|
||||
GasUnitType,
|
||||
)
|
||||
|
||||
|
||||
class JodiGasProductionQueryParams(QueryParams):
|
||||
"""JODI Gas Production Query.
|
||||
|
||||
Natural gas is a mixture of gaseous hydrocarbons, primarily methane,
|
||||
but generally also including ethane, propane, and higher hydrocarbons
|
||||
in much smaller amounts, and some non-combustible gases such as
|
||||
nitrogen and carbon dioxide. It includes both non-associated and
|
||||
associated gas. Colliery gas, coal seam gas, and shale gas are
|
||||
included, while manufactured gas and biogas are excluded except when
|
||||
blended with natural gas for final consumption. Natural gas liquids
|
||||
are excluded.
|
||||
|
||||
Source: https://www.jodidata.org/gas/database/data-downloads.aspx
|
||||
"""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"country": COUNTRY_SCHEMA_MULTI,
|
||||
"unit": {"choices": list(GAS_UNITS)},
|
||||
}
|
||||
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description=COUNTRY_MULTI_DESCRIPTION,
|
||||
)
|
||||
unit: GasUnitType = Field(
|
||||
default="m3",
|
||||
description=GAS_UNIT_DESCRIPTION,
|
||||
)
|
||||
start_date: dateType | None = Field(
|
||||
default=None,
|
||||
description=QUERY_DESCRIPTIONS.get("start_date", "")
|
||||
+ " Defaults to the first data period, 2009-01-01.",
|
||||
)
|
||||
end_date: dateType | None = Field(
|
||||
default=None, description=QUERY_DESCRIPTIONS.get("end_date", "")
|
||||
)
|
||||
use_cache: bool = Field(default=True, description=USE_CACHE_DESCRIPTION)
|
||||
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate country."""
|
||||
from openbb_jodi.utils.helpers import validate_country_param
|
||||
|
||||
return validate_country_param(v, multiple=True, default=None)
|
||||
|
||||
@field_validator("unit", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _normalize_unit(cls, v):
|
||||
"""Normalize unit."""
|
||||
from openbb_jodi.utils.helpers import normalize_token
|
||||
|
||||
return normalize_token(v, "m3")
|
||||
|
||||
|
||||
class JodiGasProductionData(JodiCountryData):
|
||||
"""JODI Gas Production Data.
|
||||
|
||||
One column per reporting country. Production is dry marketable
|
||||
production within national boundaries, including offshore production,
|
||||
measured after purification and extraction of NGL and sulphur; it
|
||||
excludes quantities reinjected, extraction losses, and quantities
|
||||
vented or flared, and includes quantities used within the natural gas
|
||||
industry.
|
||||
"""
|
||||
|
||||
|
||||
class JodiGasProductionFetcher(
|
||||
Fetcher[JodiGasProductionQueryParams, list[JodiGasProductionData]]
|
||||
):
|
||||
"""JODI Gas Production Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: dict[str, Any]) -> JodiGasProductionQueryParams:
|
||||
"""Transform the query params."""
|
||||
return JodiGasProductionQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: JodiGasProductionQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict]:
|
||||
"""Extract the data from the JODI-Gas world database file."""
|
||||
from openbb_jodi.utils.constants import COUNTRIES, GAS_START_YEAR
|
||||
from openbb_jodi.utils.helpers import get_filtered_gas, resolve_date_range
|
||||
|
||||
start_date, end_date = resolve_date_range(
|
||||
query.start_date, query.end_date, GAS_START_YEAR
|
||||
)
|
||||
countries = (
|
||||
{COUNTRIES[country] for country in query.country.split(",")}
|
||||
if query.country
|
||||
else None
|
||||
)
|
||||
return await get_filtered_gas(
|
||||
countries=countries,
|
||||
flow_codes={"INDPROD"},
|
||||
unit_codes={GAS_UNITS[query.unit]},
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
use_cache=query.use_cache,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: JodiGasProductionQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any,
|
||||
) -> AnnotatedResult[list[JodiGasProductionData]]:
|
||||
"""Transform the raw data into the by-country table."""
|
||||
from openbb_jodi.utils.constants import CODE_TO_COUNTRY
|
||||
from openbb_jodi.utils.helpers import build_assessments, build_country_table
|
||||
|
||||
rows = build_country_table(data)
|
||||
metadata = {
|
||||
"product": "Natural gas",
|
||||
"flow": "Production",
|
||||
"unit": GAS_UNIT_LABELS[query.unit],
|
||||
"assessments": build_assessments(data, "REF_AREA", CODE_TO_COUNTRY),
|
||||
"source": "JODI-Gas World Database (www.jodidata.org)",
|
||||
}
|
||||
return AnnotatedResult(
|
||||
result=[JodiGasProductionData.model_validate(r) for r in rows],
|
||||
metadata=metadata,
|
||||
)
|
||||
144
openbb_platform/providers/jodi/openbb_jodi/models/gas_stocks.py
Normal file
144
openbb_platform/providers/jodi/openbb_jodi/models/gas_stocks.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""JODI Gas Stocks Model."""
|
||||
|
||||
from datetime import date as dateType
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_jodi.models.country_data import JodiCountryData
|
||||
from openbb_jodi.utils.constants import (
|
||||
COUNTRY_MULTI_DESCRIPTION,
|
||||
COUNTRY_SCHEMA_MULTI,
|
||||
GAS_UNIT_DESCRIPTION,
|
||||
GAS_UNIT_LABELS,
|
||||
GAS_UNITS,
|
||||
USE_CACHE_DESCRIPTION,
|
||||
GasUnitType,
|
||||
)
|
||||
|
||||
|
||||
class JodiGasStocksQueryParams(QueryParams):
|
||||
"""JODI Gas Stocks Query.
|
||||
|
||||
Natural gas is a mixture of gaseous hydrocarbons, primarily methane,
|
||||
but generally also including ethane, propane, and higher hydrocarbons
|
||||
in much smaller amounts, and some non-combustible gases such as
|
||||
nitrogen and carbon dioxide. It includes both non-associated and
|
||||
associated gas. Colliery gas, coal seam gas, and shale gas are
|
||||
included, while manufactured gas and biogas are excluded except when
|
||||
blended with natural gas for final consumption. Natural gas liquids
|
||||
are excluded.
|
||||
|
||||
Source: https://www.jodidata.org/gas/database/data-downloads.aspx
|
||||
"""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"country": COUNTRY_SCHEMA_MULTI,
|
||||
"unit": {"choices": list(GAS_UNITS)},
|
||||
}
|
||||
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description=COUNTRY_MULTI_DESCRIPTION,
|
||||
)
|
||||
unit: GasUnitType = Field(
|
||||
default="m3",
|
||||
description=GAS_UNIT_DESCRIPTION,
|
||||
)
|
||||
start_date: dateType | None = Field(
|
||||
default=None,
|
||||
description=QUERY_DESCRIPTIONS.get("start_date", "")
|
||||
+ " Defaults to the first data period, 2009-01-01.",
|
||||
)
|
||||
end_date: dateType | None = Field(
|
||||
default=None, description=QUERY_DESCRIPTIONS.get("end_date", "")
|
||||
)
|
||||
use_cache: bool = Field(default=True, description=USE_CACHE_DESCRIPTION)
|
||||
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate country."""
|
||||
from openbb_jodi.utils.helpers import validate_country_param
|
||||
|
||||
return validate_country_param(v, multiple=True, default=None)
|
||||
|
||||
@field_validator("unit", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _normalize_unit(cls, v):
|
||||
"""Normalize unit."""
|
||||
from openbb_jodi.utils.helpers import normalize_token
|
||||
|
||||
return normalize_token(v, "m3")
|
||||
|
||||
|
||||
class JodiGasStocksData(JodiCountryData):
|
||||
"""JODI Gas Stocks Data.
|
||||
|
||||
One column per reporting country. Closing stocks are the stock level
|
||||
held on the national territory on the last day of the reference
|
||||
month.
|
||||
"""
|
||||
|
||||
|
||||
class JodiGasStocksFetcher(Fetcher[JodiGasStocksQueryParams, list[JodiGasStocksData]]):
|
||||
"""JODI Gas Stocks Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: dict[str, Any]) -> JodiGasStocksQueryParams:
|
||||
"""Transform the query params."""
|
||||
return JodiGasStocksQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: JodiGasStocksQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict]:
|
||||
"""Extract the data from the JODI-Gas world database file."""
|
||||
from openbb_jodi.utils.constants import COUNTRIES, GAS_START_YEAR
|
||||
from openbb_jodi.utils.helpers import get_filtered_gas, resolve_date_range
|
||||
|
||||
start_date, end_date = resolve_date_range(
|
||||
query.start_date, query.end_date, GAS_START_YEAR
|
||||
)
|
||||
countries = (
|
||||
{COUNTRIES[country] for country in query.country.split(",")}
|
||||
if query.country
|
||||
else None
|
||||
)
|
||||
return await get_filtered_gas(
|
||||
countries=countries,
|
||||
flow_codes={"CLOSTLV"},
|
||||
unit_codes={GAS_UNITS[query.unit]},
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
use_cache=query.use_cache,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: JodiGasStocksQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any,
|
||||
) -> AnnotatedResult[list[JodiGasStocksData]]:
|
||||
"""Transform the raw data into the by-country table."""
|
||||
from openbb_jodi.utils.constants import CODE_TO_COUNTRY
|
||||
from openbb_jodi.utils.helpers import build_assessments, build_country_table
|
||||
|
||||
rows = build_country_table(data)
|
||||
metadata = {
|
||||
"product": "Natural gas",
|
||||
"flow": "Closing stocks",
|
||||
"unit": GAS_UNIT_LABELS[query.unit],
|
||||
"assessments": build_assessments(data, "REF_AREA", CODE_TO_COUNTRY),
|
||||
"source": "JODI-Gas World Database (www.jodidata.org)",
|
||||
}
|
||||
return AnnotatedResult(
|
||||
result=[JodiGasStocksData.model_validate(r) for r in rows],
|
||||
metadata=metadata,
|
||||
)
|
||||
225
openbb_platform/providers/jodi/openbb_jodi/models/oil_balance.py
Normal file
225
openbb_platform/providers/jodi/openbb_jodi/models/oil_balance.py
Normal file
@@ -0,0 +1,225 @@
|
||||
"""JODI Oil Balance Model."""
|
||||
|
||||
from datetime import date as dateType
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_jodi.utils.constants import (
|
||||
COUNTRY_SCHEMA_SINGLE,
|
||||
COUNTRY_SINGLE_DESCRIPTION,
|
||||
OIL_FLOW_DEFINITIONS,
|
||||
OIL_PRODUCT_CHOICES_DESCRIPTION,
|
||||
OIL_PRODUCT_LABELS,
|
||||
OIL_PRODUCTS,
|
||||
OIL_UNIT_DESCRIPTION,
|
||||
OIL_UNIT_LABELS,
|
||||
OIL_UNITS,
|
||||
USE_CACHE_DESCRIPTION,
|
||||
OilProductType,
|
||||
OilUnitType,
|
||||
)
|
||||
|
||||
|
||||
class JodiOilBalanceQueryParams(QueryParams):
|
||||
"""JODI Oil Balance Query.
|
||||
|
||||
Source: https://www.jodidata.org/oil/database/data-downloads.aspx
|
||||
"""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"country": COUNTRY_SCHEMA_SINGLE,
|
||||
"product": {"choices": list(OIL_PRODUCTS)},
|
||||
"unit": {"choices": list(OIL_UNITS)},
|
||||
}
|
||||
|
||||
country: str = Field(
|
||||
default="united_states",
|
||||
description=COUNTRY_SINGLE_DESCRIPTION,
|
||||
)
|
||||
product: OilProductType = Field(
|
||||
default="crude_oil",
|
||||
description="The petroleum product to get the balance for."
|
||||
+ " The first four are primary products, the rest are secondary"
|
||||
+ " products. JODI questionnaire definitions:\n"
|
||||
+ OIL_PRODUCT_CHOICES_DESCRIPTION,
|
||||
)
|
||||
unit: OilUnitType = Field(
|
||||
default="kbd",
|
||||
description=OIL_UNIT_DESCRIPTION,
|
||||
)
|
||||
start_date: dateType | None = Field(
|
||||
default=None,
|
||||
description=QUERY_DESCRIPTIONS.get("start_date", "")
|
||||
+ " Defaults to the first data period, 2002-01-01.",
|
||||
)
|
||||
end_date: dateType | None = Field(
|
||||
default=None, description=QUERY_DESCRIPTIONS.get("end_date", "")
|
||||
)
|
||||
use_cache: bool = Field(default=True, description=USE_CACHE_DESCRIPTION)
|
||||
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate country."""
|
||||
from openbb_jodi.utils.helpers import validate_country_param
|
||||
|
||||
return validate_country_param(v, multiple=False, default="united_states")
|
||||
|
||||
@field_validator("product", "unit", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _normalize_tokens(cls, v, info):
|
||||
"""Normalize token parameters."""
|
||||
from openbb_jodi.utils.helpers import normalize_token
|
||||
|
||||
defaults = {"product": "crude_oil", "unit": "kbd"}
|
||||
return normalize_token(v, defaults[str(info.field_name)])
|
||||
|
||||
|
||||
class JodiOilBalanceData(Data):
|
||||
"""JODI Oil Balance Data.
|
||||
|
||||
One column per flow of the JODI-Oil questionnaire, in published order.
|
||||
Primary-only flows are None for secondary products, and vice versa.
|
||||
"""
|
||||
|
||||
date: dateType = Field(description=DATA_DESCRIPTIONS.get("date", ""))
|
||||
production: float | None = Field(
|
||||
default=None,
|
||||
description="Production (primary products). "
|
||||
+ OIL_FLOW_DEFINITIONS["production"],
|
||||
)
|
||||
refinery_output: float | None = Field(
|
||||
default=None,
|
||||
description="Refinery output (secondary products). "
|
||||
+ OIL_FLOW_DEFINITIONS["refinery_output"],
|
||||
)
|
||||
from_other_sources: float | None = Field(
|
||||
default=None,
|
||||
description="From other sources (primary products). "
|
||||
+ OIL_FLOW_DEFINITIONS["from_other_sources"],
|
||||
)
|
||||
receipts: float | None = Field(
|
||||
default=None,
|
||||
description="Receipts (secondary products). "
|
||||
+ OIL_FLOW_DEFINITIONS["receipts"],
|
||||
)
|
||||
imports: float | None = Field(
|
||||
default=None,
|
||||
description="Imports. " + OIL_FLOW_DEFINITIONS["imports"],
|
||||
)
|
||||
exports: float | None = Field(
|
||||
default=None,
|
||||
description="Exports. " + OIL_FLOW_DEFINITIONS["exports"],
|
||||
)
|
||||
products_transferred: float | None = Field(
|
||||
default=None,
|
||||
description="Products transferred. "
|
||||
+ OIL_FLOW_DEFINITIONS["products_transferred"],
|
||||
)
|
||||
interproduct_transfers: float | None = Field(
|
||||
default=None,
|
||||
description="Interproduct transfers (secondary products). "
|
||||
+ OIL_FLOW_DEFINITIONS["interproduct_transfers"],
|
||||
)
|
||||
direct_use: float | None = Field(
|
||||
default=None,
|
||||
description="Direct use (primary products). "
|
||||
+ OIL_FLOW_DEFINITIONS["direct_use"],
|
||||
)
|
||||
stock_change: float | None = Field(
|
||||
default=None,
|
||||
description="Stock change. " + OIL_FLOW_DEFINITIONS["stock_change"],
|
||||
)
|
||||
statistical_difference: float | None = Field(
|
||||
default=None,
|
||||
description="Statistical difference. "
|
||||
+ OIL_FLOW_DEFINITIONS["statistical_difference"],
|
||||
)
|
||||
refinery_intake: float | None = Field(
|
||||
default=None,
|
||||
description="Refinery intake (primary products). "
|
||||
+ OIL_FLOW_DEFINITIONS["refinery_intake"],
|
||||
)
|
||||
demand: float | None = Field(
|
||||
default=None,
|
||||
description="Demand (secondary products). " + OIL_FLOW_DEFINITIONS["demand"],
|
||||
)
|
||||
closing_stocks: float | None = Field(
|
||||
default=None,
|
||||
description="Closing stocks. " + OIL_FLOW_DEFINITIONS["closing_stocks"],
|
||||
)
|
||||
|
||||
|
||||
class JodiOilBalanceFetcher(
|
||||
Fetcher[JodiOilBalanceQueryParams, list[JodiOilBalanceData]]
|
||||
):
|
||||
"""JODI Oil Balance Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: dict[str, Any]) -> JodiOilBalanceQueryParams:
|
||||
"""Transform the query params."""
|
||||
return JodiOilBalanceQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: JodiOilBalanceQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict]:
|
||||
"""Extract the data from the JODI-Oil annual CSV files."""
|
||||
from openbb_jodi.utils.constants import (
|
||||
COUNTRIES,
|
||||
OIL_BALANCE_FIELDS,
|
||||
OIL_START_YEAR,
|
||||
)
|
||||
from openbb_jodi.utils.helpers import get_filtered_oil, resolve_date_range
|
||||
|
||||
start_date, end_date = resolve_date_range(
|
||||
query.start_date, query.end_date, OIL_START_YEAR
|
||||
)
|
||||
product_code, table = OIL_PRODUCTS[query.product]
|
||||
return await get_filtered_oil(
|
||||
tables=[table],
|
||||
countries={COUNTRIES[query.country]},
|
||||
product_codes={product_code},
|
||||
flow_codes=set(OIL_BALANCE_FIELDS),
|
||||
unit_codes={OIL_UNITS[query.unit]},
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
use_cache=query.use_cache,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: JodiOilBalanceQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any,
|
||||
) -> AnnotatedResult[list[JodiOilBalanceData]]:
|
||||
"""Transform the raw data into the balance table."""
|
||||
from openbb_jodi.utils.constants import COUNTRY_LABELS, OIL_BALANCE_FIELDS
|
||||
from openbb_jodi.utils.helpers import build_assessments, build_field_table
|
||||
|
||||
rows = build_field_table(data, "FLOW_BREAKDOWN", OIL_BALANCE_FIELDS)
|
||||
code = data[0]["REF_AREA"]
|
||||
metadata = {
|
||||
"country": COUNTRY_LABELS.get(code, code),
|
||||
"product": OIL_PRODUCT_LABELS[OIL_PRODUCTS[query.product][0]],
|
||||
"unit": OIL_UNIT_LABELS[query.unit],
|
||||
"assessments": build_assessments(
|
||||
data, "FLOW_BREAKDOWN", OIL_BALANCE_FIELDS
|
||||
),
|
||||
"source": "JODI-Oil World Database (www.jodidata.org)",
|
||||
}
|
||||
return AnnotatedResult(
|
||||
result=[JodiOilBalanceData.model_validate(r) for r in rows],
|
||||
metadata=metadata,
|
||||
)
|
||||
154
openbb_platform/providers/jodi/openbb_jodi/models/oil_demand.py
Normal file
154
openbb_platform/providers/jodi/openbb_jodi/models/oil_demand.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""JODI Oil Demand Model."""
|
||||
|
||||
from datetime import date as dateType
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_jodi.models.country_data import JodiCountryData
|
||||
from openbb_jodi.utils.constants import (
|
||||
COUNTRY_MULTI_DESCRIPTION,
|
||||
COUNTRY_SCHEMA_MULTI,
|
||||
OIL_PRODUCT_FIELDS,
|
||||
OIL_PRODUCT_LABELS,
|
||||
OIL_PRODUCTS,
|
||||
OIL_SECONDARY_PRODUCT_CHOICES_DESCRIPTION,
|
||||
OIL_UNIT_DESCRIPTION,
|
||||
OIL_UNIT_LABELS,
|
||||
OIL_UNITS,
|
||||
USE_CACHE_DESCRIPTION,
|
||||
OilSecondaryProductType,
|
||||
OilUnitType,
|
||||
)
|
||||
|
||||
SECONDARY_PRODUCTS = [OIL_PRODUCT_FIELDS[code] for code in OIL_PRODUCT_FIELDS]
|
||||
|
||||
|
||||
class JodiOilDemandQueryParams(QueryParams):
|
||||
"""JODI Oil Demand Query.
|
||||
|
||||
Source: https://www.jodidata.org/oil/database/data-downloads.aspx
|
||||
"""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"country": COUNTRY_SCHEMA_MULTI,
|
||||
"product": {"choices": SECONDARY_PRODUCTS},
|
||||
"unit": {"choices": list(OIL_UNITS)},
|
||||
}
|
||||
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description=COUNTRY_MULTI_DESCRIPTION,
|
||||
)
|
||||
product: OilSecondaryProductType = Field(
|
||||
default="total_products",
|
||||
description="The refined petroleum product to get demand of."
|
||||
+ " Demand is reported for secondary products only."
|
||||
+ " JODI questionnaire definitions:\n"
|
||||
+ OIL_SECONDARY_PRODUCT_CHOICES_DESCRIPTION,
|
||||
)
|
||||
unit: OilUnitType = Field(
|
||||
default="kbd",
|
||||
description=OIL_UNIT_DESCRIPTION,
|
||||
)
|
||||
start_date: dateType | None = Field(
|
||||
default=None,
|
||||
description=QUERY_DESCRIPTIONS.get("start_date", "")
|
||||
+ " Defaults to the first data period, 2002-01-01.",
|
||||
)
|
||||
end_date: dateType | None = Field(
|
||||
default=None, description=QUERY_DESCRIPTIONS.get("end_date", "")
|
||||
)
|
||||
use_cache: bool = Field(default=True, description=USE_CACHE_DESCRIPTION)
|
||||
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate country."""
|
||||
from openbb_jodi.utils.helpers import validate_country_param
|
||||
|
||||
return validate_country_param(v, multiple=True, default=None)
|
||||
|
||||
@field_validator("product", "unit", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _normalize_tokens(cls, v, info):
|
||||
"""Normalize token parameters."""
|
||||
from openbb_jodi.utils.helpers import normalize_token
|
||||
|
||||
defaults = {"product": "total_products", "unit": "kbd"}
|
||||
return normalize_token(v, defaults[str(info.field_name)])
|
||||
|
||||
|
||||
class JodiOilDemandData(JodiCountryData):
|
||||
"""JODI Oil Demand Data.
|
||||
|
||||
One column per reporting country. Demand is deliveries or sales to
|
||||
the inland market (domestic consumption) plus refinery fuel plus
|
||||
international marine and aviation bunkers.
|
||||
"""
|
||||
|
||||
|
||||
class JodiOilDemandFetcher(Fetcher[JodiOilDemandQueryParams, list[JodiOilDemandData]]):
|
||||
"""JODI Oil Demand Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: dict[str, Any]) -> JodiOilDemandQueryParams:
|
||||
"""Transform the query params."""
|
||||
return JodiOilDemandQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: JodiOilDemandQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict]:
|
||||
"""Extract the data from the JODI-Oil annual CSV files."""
|
||||
from openbb_jodi.utils.constants import COUNTRIES, OIL_START_YEAR
|
||||
from openbb_jodi.utils.helpers import get_filtered_oil, resolve_date_range
|
||||
|
||||
start_date, end_date = resolve_date_range(
|
||||
query.start_date, query.end_date, OIL_START_YEAR
|
||||
)
|
||||
product_code, table = OIL_PRODUCTS[query.product]
|
||||
countries = (
|
||||
{COUNTRIES[country] for country in query.country.split(",")}
|
||||
if query.country
|
||||
else None
|
||||
)
|
||||
return await get_filtered_oil(
|
||||
tables=[table],
|
||||
countries=countries,
|
||||
product_codes={product_code},
|
||||
flow_codes={"TOTDEMO"},
|
||||
unit_codes={OIL_UNITS[query.unit]},
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
use_cache=query.use_cache,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: JodiOilDemandQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any,
|
||||
) -> AnnotatedResult[list[JodiOilDemandData]]:
|
||||
"""Transform the raw data into the by-country table."""
|
||||
from openbb_jodi.utils.constants import CODE_TO_COUNTRY
|
||||
from openbb_jodi.utils.helpers import build_assessments, build_country_table
|
||||
|
||||
rows = build_country_table(data)
|
||||
metadata = {
|
||||
"product": OIL_PRODUCT_LABELS[OIL_PRODUCTS[query.product][0]],
|
||||
"flow": "Demand",
|
||||
"unit": OIL_UNIT_LABELS[query.unit],
|
||||
"assessments": build_assessments(data, "REF_AREA", CODE_TO_COUNTRY),
|
||||
"source": "JODI-Oil World Database (www.jodidata.org)",
|
||||
}
|
||||
return AnnotatedResult(
|
||||
result=[JodiOilDemandData.model_validate(r) for r in rows],
|
||||
metadata=metadata,
|
||||
)
|
||||
@@ -0,0 +1,184 @@
|
||||
"""JODI Oil Demand By Product Model."""
|
||||
|
||||
from datetime import date as dateType
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import (
|
||||
DATA_DESCRIPTIONS,
|
||||
QUERY_DESCRIPTIONS,
|
||||
)
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_jodi.utils.constants import (
|
||||
COUNTRY_SCHEMA_SINGLE,
|
||||
COUNTRY_SINGLE_DESCRIPTION,
|
||||
OIL_PRODUCT_DEFINITIONS,
|
||||
OIL_PRODUCT_FIELDS,
|
||||
OIL_UNIT_DESCRIPTION,
|
||||
OIL_UNIT_LABELS,
|
||||
OIL_UNITS,
|
||||
USE_CACHE_DESCRIPTION,
|
||||
OilUnitType,
|
||||
)
|
||||
|
||||
|
||||
class JodiOilDemandByProductQueryParams(QueryParams):
|
||||
"""JODI Oil Demand By Product Query.
|
||||
|
||||
Source: https://www.jodidata.org/oil/database/data-downloads.aspx
|
||||
"""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"country": COUNTRY_SCHEMA_SINGLE,
|
||||
"unit": {"choices": list(OIL_UNITS)},
|
||||
}
|
||||
|
||||
country: str = Field(
|
||||
default="united_states",
|
||||
description=COUNTRY_SINGLE_DESCRIPTION,
|
||||
)
|
||||
unit: OilUnitType = Field(
|
||||
default="kbd",
|
||||
description=OIL_UNIT_DESCRIPTION,
|
||||
)
|
||||
start_date: dateType | None = Field(
|
||||
default=None,
|
||||
description=QUERY_DESCRIPTIONS.get("start_date", "")
|
||||
+ " Defaults to the first data period, 2002-01-01.",
|
||||
)
|
||||
end_date: dateType | None = Field(
|
||||
default=None, description=QUERY_DESCRIPTIONS.get("end_date", "")
|
||||
)
|
||||
use_cache: bool = Field(default=True, description=USE_CACHE_DESCRIPTION)
|
||||
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate country."""
|
||||
from openbb_jodi.utils.helpers import validate_country_param
|
||||
|
||||
return validate_country_param(v, multiple=False, default="united_states")
|
||||
|
||||
@field_validator("unit", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _normalize_unit(cls, v):
|
||||
"""Normalize unit."""
|
||||
from openbb_jodi.utils.helpers import normalize_token
|
||||
|
||||
return normalize_token(v, "kbd")
|
||||
|
||||
|
||||
class JodiOilDemandByProductData(Data):
|
||||
"""JODI Oil Demand By Product Data.
|
||||
|
||||
One column per refined product, in published order. Demand is deliveries
|
||||
or sales to the inland market (domestic consumption) plus refinery fuel
|
||||
plus international marine and aviation bunkers.
|
||||
"""
|
||||
|
||||
date: dateType = Field(description=DATA_DESCRIPTIONS.get("date", ""))
|
||||
lpg: float | None = Field(
|
||||
default=None,
|
||||
description="LPG demand. " + OIL_PRODUCT_DEFINITIONS["lpg"],
|
||||
)
|
||||
naphtha: float | None = Field(
|
||||
default=None,
|
||||
description="Naphtha demand. " + OIL_PRODUCT_DEFINITIONS["naphtha"],
|
||||
)
|
||||
gasoline: float | None = Field(
|
||||
default=None,
|
||||
description="Gasoline demand. " + OIL_PRODUCT_DEFINITIONS["gasoline"],
|
||||
)
|
||||
kerosene: float | None = Field(
|
||||
default=None,
|
||||
description="Kerosenes demand. " + OIL_PRODUCT_DEFINITIONS["kerosene"],
|
||||
)
|
||||
jet_fuel: float | None = Field(
|
||||
default=None,
|
||||
description="Jet fuel demand. " + OIL_PRODUCT_DEFINITIONS["jet_fuel"],
|
||||
)
|
||||
gas_diesel_oil: float | None = Field(
|
||||
default=None,
|
||||
description="Gas/diesel oil demand. "
|
||||
+ OIL_PRODUCT_DEFINITIONS["gas_diesel_oil"],
|
||||
)
|
||||
fuel_oil: float | None = Field(
|
||||
default=None,
|
||||
description="Fuel oil demand. " + OIL_PRODUCT_DEFINITIONS["fuel_oil"],
|
||||
)
|
||||
other_products: float | None = Field(
|
||||
default=None,
|
||||
description="Other oil products demand. "
|
||||
+ OIL_PRODUCT_DEFINITIONS["other_products"]
|
||||
+ " Includes direct use of crude oil, NGL, and other primary hydrocarbons.",
|
||||
)
|
||||
total_products: float | None = Field(
|
||||
default=None,
|
||||
description="Total oil products demand. "
|
||||
+ OIL_PRODUCT_DEFINITIONS["total_products"],
|
||||
)
|
||||
|
||||
|
||||
class JodiOilDemandByProductFetcher(
|
||||
Fetcher[JodiOilDemandByProductQueryParams, list[JodiOilDemandByProductData]]
|
||||
):
|
||||
"""JODI Oil Demand By Product Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: dict[str, Any]) -> JodiOilDemandByProductQueryParams:
|
||||
"""Transform the query params."""
|
||||
return JodiOilDemandByProductQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: JodiOilDemandByProductQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict]:
|
||||
"""Extract the data from the JODI-Oil annual CSV files."""
|
||||
from openbb_jodi.utils.constants import COUNTRIES, OIL_START_YEAR
|
||||
from openbb_jodi.utils.helpers import get_filtered_oil, resolve_date_range
|
||||
|
||||
start_date, end_date = resolve_date_range(
|
||||
query.start_date, query.end_date, OIL_START_YEAR
|
||||
)
|
||||
return await get_filtered_oil(
|
||||
tables=["secondary"],
|
||||
countries={COUNTRIES[query.country]},
|
||||
product_codes=set(OIL_PRODUCT_FIELDS),
|
||||
flow_codes={"TOTDEMO"},
|
||||
unit_codes={OIL_UNITS[query.unit]},
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
use_cache=query.use_cache,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: JodiOilDemandByProductQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any,
|
||||
) -> AnnotatedResult[list[JodiOilDemandByProductData]]:
|
||||
"""Transform the raw data into the by-product table."""
|
||||
from openbb_jodi.utils.constants import COUNTRY_LABELS
|
||||
from openbb_jodi.utils.helpers import build_assessments, build_field_table
|
||||
|
||||
rows = build_field_table(data, "ENERGY_PRODUCT", OIL_PRODUCT_FIELDS)
|
||||
code = data[0]["REF_AREA"]
|
||||
metadata = {
|
||||
"country": COUNTRY_LABELS.get(code, code),
|
||||
"flow": "Demand",
|
||||
"unit": OIL_UNIT_LABELS[query.unit],
|
||||
"assessments": build_assessments(
|
||||
data, "ENERGY_PRODUCT", OIL_PRODUCT_FIELDS
|
||||
),
|
||||
"source": "JODI-Oil World Database (www.jodidata.org)",
|
||||
}
|
||||
return AnnotatedResult(
|
||||
result=[JodiOilDemandByProductData.model_validate(r) for r in rows],
|
||||
metadata=metadata,
|
||||
)
|
||||
152
openbb_platform/providers/jodi/openbb_jodi/models/oil_exports.py
Normal file
152
openbb_platform/providers/jodi/openbb_jodi/models/oil_exports.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""JODI Oil Exports Model."""
|
||||
|
||||
from datetime import date as dateType
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_jodi.models.country_data import JodiCountryData
|
||||
from openbb_jodi.utils.constants import (
|
||||
COUNTRY_MULTI_DESCRIPTION,
|
||||
COUNTRY_SCHEMA_MULTI,
|
||||
OIL_PRODUCT_CHOICES_DESCRIPTION,
|
||||
OIL_PRODUCT_LABELS,
|
||||
OIL_PRODUCTS,
|
||||
OIL_UNIT_DESCRIPTION,
|
||||
OIL_UNIT_LABELS,
|
||||
OIL_UNITS,
|
||||
USE_CACHE_DESCRIPTION,
|
||||
OilProductType,
|
||||
OilUnitType,
|
||||
)
|
||||
|
||||
|
||||
class JodiOilExportsQueryParams(QueryParams):
|
||||
"""JODI Oil Exports Query.
|
||||
|
||||
Source: https://www.jodidata.org/oil/database/data-downloads.aspx
|
||||
"""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"country": COUNTRY_SCHEMA_MULTI,
|
||||
"product": {"choices": list(OIL_PRODUCTS)},
|
||||
"unit": {"choices": list(OIL_UNITS)},
|
||||
}
|
||||
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description=COUNTRY_MULTI_DESCRIPTION,
|
||||
)
|
||||
product: OilProductType = Field(
|
||||
default="crude_oil",
|
||||
description="The petroleum product to get exports of."
|
||||
+ " JODI questionnaire definitions:\n"
|
||||
+ OIL_PRODUCT_CHOICES_DESCRIPTION,
|
||||
)
|
||||
unit: OilUnitType = Field(
|
||||
default="kbd",
|
||||
description=OIL_UNIT_DESCRIPTION,
|
||||
)
|
||||
start_date: dateType | None = Field(
|
||||
default=None,
|
||||
description=QUERY_DESCRIPTIONS.get("start_date", "")
|
||||
+ " Defaults to the first data period, 2002-01-01.",
|
||||
)
|
||||
end_date: dateType | None = Field(
|
||||
default=None, description=QUERY_DESCRIPTIONS.get("end_date", "")
|
||||
)
|
||||
use_cache: bool = Field(default=True, description=USE_CACHE_DESCRIPTION)
|
||||
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate country."""
|
||||
from openbb_jodi.utils.helpers import validate_country_param
|
||||
|
||||
return validate_country_param(v, multiple=True, default=None)
|
||||
|
||||
@field_validator("product", "unit", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _normalize_tokens(cls, v, info):
|
||||
"""Normalize token parameters."""
|
||||
from openbb_jodi.utils.helpers import normalize_token
|
||||
|
||||
defaults = {"product": "crude_oil", "unit": "kbd"}
|
||||
return normalize_token(v, defaults[str(info.field_name)])
|
||||
|
||||
|
||||
class JodiOilExportsData(JodiCountryData):
|
||||
"""JODI Oil Exports Data.
|
||||
|
||||
One column per reporting country. Exports are goods having physically
|
||||
crossed the international boundaries, excluding transit trade and
|
||||
international marine and aviation bunkers.
|
||||
"""
|
||||
|
||||
|
||||
class JodiOilExportsFetcher(
|
||||
Fetcher[JodiOilExportsQueryParams, list[JodiOilExportsData]]
|
||||
):
|
||||
"""JODI Oil Exports Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: dict[str, Any]) -> JodiOilExportsQueryParams:
|
||||
"""Transform the query params."""
|
||||
return JodiOilExportsQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: JodiOilExportsQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict]:
|
||||
"""Extract the data from the JODI-Oil annual CSV files."""
|
||||
from openbb_jodi.utils.constants import COUNTRIES, OIL_START_YEAR
|
||||
from openbb_jodi.utils.helpers import get_filtered_oil, resolve_date_range
|
||||
|
||||
start_date, end_date = resolve_date_range(
|
||||
query.start_date, query.end_date, OIL_START_YEAR
|
||||
)
|
||||
product_code, table = OIL_PRODUCTS[query.product]
|
||||
countries = (
|
||||
{COUNTRIES[country] for country in query.country.split(",")}
|
||||
if query.country
|
||||
else None
|
||||
)
|
||||
return await get_filtered_oil(
|
||||
tables=[table],
|
||||
countries=countries,
|
||||
product_codes={product_code},
|
||||
flow_codes={"TOTEXPSB"},
|
||||
unit_codes={OIL_UNITS[query.unit]},
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
use_cache=query.use_cache,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: JodiOilExportsQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any,
|
||||
) -> AnnotatedResult[list[JodiOilExportsData]]:
|
||||
"""Transform the raw data into the by-country table."""
|
||||
from openbb_jodi.utils.constants import CODE_TO_COUNTRY
|
||||
from openbb_jodi.utils.helpers import build_assessments, build_country_table
|
||||
|
||||
rows = build_country_table(data)
|
||||
metadata = {
|
||||
"product": OIL_PRODUCT_LABELS[OIL_PRODUCTS[query.product][0]],
|
||||
"flow": "Exports",
|
||||
"unit": OIL_UNIT_LABELS[query.unit],
|
||||
"assessments": build_assessments(data, "REF_AREA", CODE_TO_COUNTRY),
|
||||
"source": "JODI-Oil World Database (www.jodidata.org)",
|
||||
}
|
||||
return AnnotatedResult(
|
||||
result=[JodiOilExportsData.model_validate(r) for r in rows],
|
||||
metadata=metadata,
|
||||
)
|
||||
152
openbb_platform/providers/jodi/openbb_jodi/models/oil_imports.py
Normal file
152
openbb_platform/providers/jodi/openbb_jodi/models/oil_imports.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""JODI Oil Imports Model."""
|
||||
|
||||
from datetime import date as dateType
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_jodi.models.country_data import JodiCountryData
|
||||
from openbb_jodi.utils.constants import (
|
||||
COUNTRY_MULTI_DESCRIPTION,
|
||||
COUNTRY_SCHEMA_MULTI,
|
||||
OIL_PRODUCT_CHOICES_DESCRIPTION,
|
||||
OIL_PRODUCT_LABELS,
|
||||
OIL_PRODUCTS,
|
||||
OIL_UNIT_DESCRIPTION,
|
||||
OIL_UNIT_LABELS,
|
||||
OIL_UNITS,
|
||||
USE_CACHE_DESCRIPTION,
|
||||
OilProductType,
|
||||
OilUnitType,
|
||||
)
|
||||
|
||||
|
||||
class JodiOilImportsQueryParams(QueryParams):
|
||||
"""JODI Oil Imports Query.
|
||||
|
||||
Source: https://www.jodidata.org/oil/database/data-downloads.aspx
|
||||
"""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"country": COUNTRY_SCHEMA_MULTI,
|
||||
"product": {"choices": list(OIL_PRODUCTS)},
|
||||
"unit": {"choices": list(OIL_UNITS)},
|
||||
}
|
||||
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description=COUNTRY_MULTI_DESCRIPTION,
|
||||
)
|
||||
product: OilProductType = Field(
|
||||
default="crude_oil",
|
||||
description="The petroleum product to get imports of."
|
||||
+ " JODI questionnaire definitions:\n"
|
||||
+ OIL_PRODUCT_CHOICES_DESCRIPTION,
|
||||
)
|
||||
unit: OilUnitType = Field(
|
||||
default="kbd",
|
||||
description=OIL_UNIT_DESCRIPTION,
|
||||
)
|
||||
start_date: dateType | None = Field(
|
||||
default=None,
|
||||
description=QUERY_DESCRIPTIONS.get("start_date", "")
|
||||
+ " Defaults to the first data period, 2002-01-01.",
|
||||
)
|
||||
end_date: dateType | None = Field(
|
||||
default=None, description=QUERY_DESCRIPTIONS.get("end_date", "")
|
||||
)
|
||||
use_cache: bool = Field(default=True, description=USE_CACHE_DESCRIPTION)
|
||||
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate country."""
|
||||
from openbb_jodi.utils.helpers import validate_country_param
|
||||
|
||||
return validate_country_param(v, multiple=True, default=None)
|
||||
|
||||
@field_validator("product", "unit", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _normalize_tokens(cls, v, info):
|
||||
"""Normalize token parameters."""
|
||||
from openbb_jodi.utils.helpers import normalize_token
|
||||
|
||||
defaults = {"product": "crude_oil", "unit": "kbd"}
|
||||
return normalize_token(v, defaults[str(info.field_name)])
|
||||
|
||||
|
||||
class JodiOilImportsData(JodiCountryData):
|
||||
"""JODI Oil Imports Data.
|
||||
|
||||
One column per reporting country. Imports are goods having physically
|
||||
crossed the international boundaries, excluding transit trade and
|
||||
international marine and aviation bunkers.
|
||||
"""
|
||||
|
||||
|
||||
class JodiOilImportsFetcher(
|
||||
Fetcher[JodiOilImportsQueryParams, list[JodiOilImportsData]]
|
||||
):
|
||||
"""JODI Oil Imports Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: dict[str, Any]) -> JodiOilImportsQueryParams:
|
||||
"""Transform the query params."""
|
||||
return JodiOilImportsQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: JodiOilImportsQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict]:
|
||||
"""Extract the data from the JODI-Oil annual CSV files."""
|
||||
from openbb_jodi.utils.constants import COUNTRIES, OIL_START_YEAR
|
||||
from openbb_jodi.utils.helpers import get_filtered_oil, resolve_date_range
|
||||
|
||||
start_date, end_date = resolve_date_range(
|
||||
query.start_date, query.end_date, OIL_START_YEAR
|
||||
)
|
||||
product_code, table = OIL_PRODUCTS[query.product]
|
||||
countries = (
|
||||
{COUNTRIES[country] for country in query.country.split(",")}
|
||||
if query.country
|
||||
else None
|
||||
)
|
||||
return await get_filtered_oil(
|
||||
tables=[table],
|
||||
countries=countries,
|
||||
product_codes={product_code},
|
||||
flow_codes={"TOTIMPSB"},
|
||||
unit_codes={OIL_UNITS[query.unit]},
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
use_cache=query.use_cache,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: JodiOilImportsQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any,
|
||||
) -> AnnotatedResult[list[JodiOilImportsData]]:
|
||||
"""Transform the raw data into the by-country table."""
|
||||
from openbb_jodi.utils.constants import CODE_TO_COUNTRY
|
||||
from openbb_jodi.utils.helpers import build_assessments, build_country_table
|
||||
|
||||
rows = build_country_table(data)
|
||||
metadata = {
|
||||
"product": OIL_PRODUCT_LABELS[OIL_PRODUCTS[query.product][0]],
|
||||
"flow": "Imports",
|
||||
"unit": OIL_UNIT_LABELS[query.unit],
|
||||
"assessments": build_assessments(data, "REF_AREA", CODE_TO_COUNTRY),
|
||||
"source": "JODI-Oil World Database (www.jodidata.org)",
|
||||
}
|
||||
return AnnotatedResult(
|
||||
result=[JodiOilImportsData.model_validate(r) for r in rows],
|
||||
metadata=metadata,
|
||||
)
|
||||
@@ -0,0 +1,154 @@
|
||||
"""JODI Oil Production Model."""
|
||||
|
||||
from datetime import date as dateType
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_jodi.models.country_data import JodiCountryData
|
||||
from openbb_jodi.utils.constants import (
|
||||
COUNTRY_MULTI_DESCRIPTION,
|
||||
COUNTRY_SCHEMA_MULTI,
|
||||
OIL_PRIMARY_PRODUCT_CHOICES_DESCRIPTION,
|
||||
OIL_PRODUCT_LABELS,
|
||||
OIL_PRODUCTS,
|
||||
OIL_UNIT_DESCRIPTION,
|
||||
OIL_UNIT_LABELS,
|
||||
OIL_UNITS,
|
||||
USE_CACHE_DESCRIPTION,
|
||||
OilPrimaryProductType,
|
||||
OilUnitType,
|
||||
)
|
||||
|
||||
PRIMARY_PRODUCTS = ["crude_oil", "ngl", "other_crude", "total_crude"]
|
||||
|
||||
|
||||
class JodiOilProductionQueryParams(QueryParams):
|
||||
"""JODI Oil Production Query.
|
||||
|
||||
Source: https://www.jodidata.org/oil/database/data-downloads.aspx
|
||||
"""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"country": COUNTRY_SCHEMA_MULTI,
|
||||
"product": {"choices": PRIMARY_PRODUCTS},
|
||||
"unit": {"choices": list(OIL_UNITS)},
|
||||
}
|
||||
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description=COUNTRY_MULTI_DESCRIPTION,
|
||||
)
|
||||
product: OilPrimaryProductType = Field(
|
||||
default="crude_oil",
|
||||
description="The primary petroleum product to get production of."
|
||||
+ " JODI questionnaire definitions:\n"
|
||||
+ OIL_PRIMARY_PRODUCT_CHOICES_DESCRIPTION,
|
||||
)
|
||||
unit: OilUnitType = Field(
|
||||
default="kbd",
|
||||
description=OIL_UNIT_DESCRIPTION,
|
||||
)
|
||||
start_date: dateType | None = Field(
|
||||
default=None,
|
||||
description=QUERY_DESCRIPTIONS.get("start_date", "")
|
||||
+ " Defaults to the first data period, 2002-01-01.",
|
||||
)
|
||||
end_date: dateType | None = Field(
|
||||
default=None, description=QUERY_DESCRIPTIONS.get("end_date", "")
|
||||
)
|
||||
use_cache: bool = Field(default=True, description=USE_CACHE_DESCRIPTION)
|
||||
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate country."""
|
||||
from openbb_jodi.utils.helpers import validate_country_param
|
||||
|
||||
return validate_country_param(v, multiple=True, default=None)
|
||||
|
||||
@field_validator("product", "unit", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _normalize_tokens(cls, v, info):
|
||||
"""Normalize token parameters."""
|
||||
from openbb_jodi.utils.helpers import normalize_token
|
||||
|
||||
defaults = {"product": "crude_oil", "unit": "kbd"}
|
||||
return normalize_token(v, defaults[str(info.field_name)])
|
||||
|
||||
|
||||
class JodiOilProductionData(JodiCountryData):
|
||||
"""JODI Oil Production Data.
|
||||
|
||||
One column per reporting country. Production is marketed production,
|
||||
after removal of impurities but including quantities consumed by the
|
||||
producer in the production process.
|
||||
"""
|
||||
|
||||
|
||||
class JodiOilProductionFetcher(
|
||||
Fetcher[JodiOilProductionQueryParams, list[JodiOilProductionData]]
|
||||
):
|
||||
"""JODI Oil Production Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: dict[str, Any]) -> JodiOilProductionQueryParams:
|
||||
"""Transform the query params."""
|
||||
return JodiOilProductionQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: JodiOilProductionQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict]:
|
||||
"""Extract the data from the JODI-Oil annual CSV files."""
|
||||
from openbb_jodi.utils.constants import COUNTRIES, OIL_START_YEAR
|
||||
from openbb_jodi.utils.helpers import get_filtered_oil, resolve_date_range
|
||||
|
||||
start_date, end_date = resolve_date_range(
|
||||
query.start_date, query.end_date, OIL_START_YEAR
|
||||
)
|
||||
product_code, table = OIL_PRODUCTS[query.product]
|
||||
countries = (
|
||||
{COUNTRIES[country] for country in query.country.split(",")}
|
||||
if query.country
|
||||
else None
|
||||
)
|
||||
return await get_filtered_oil(
|
||||
tables=[table],
|
||||
countries=countries,
|
||||
product_codes={product_code},
|
||||
flow_codes={"INDPROD"},
|
||||
unit_codes={OIL_UNITS[query.unit]},
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
use_cache=query.use_cache,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: JodiOilProductionQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any,
|
||||
) -> AnnotatedResult[list[JodiOilProductionData]]:
|
||||
"""Transform the raw data into the by-country table."""
|
||||
from openbb_jodi.utils.constants import CODE_TO_COUNTRY
|
||||
from openbb_jodi.utils.helpers import build_assessments, build_country_table
|
||||
|
||||
rows = build_country_table(data)
|
||||
metadata = {
|
||||
"product": OIL_PRODUCT_LABELS[OIL_PRODUCTS[query.product][0]],
|
||||
"flow": "Production",
|
||||
"unit": OIL_UNIT_LABELS[query.unit],
|
||||
"assessments": build_assessments(data, "REF_AREA", CODE_TO_COUNTRY),
|
||||
"source": "JODI-Oil World Database (www.jodidata.org)",
|
||||
}
|
||||
return AnnotatedResult(
|
||||
result=[JodiOilProductionData.model_validate(r) for r in rows],
|
||||
metadata=metadata,
|
||||
)
|
||||
151
openbb_platform/providers/jodi/openbb_jodi/models/oil_stocks.py
Normal file
151
openbb_platform/providers/jodi/openbb_jodi/models/oil_stocks.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""JODI Oil Stocks Model."""
|
||||
|
||||
from datetime import date as dateType
|
||||
from typing import Any
|
||||
|
||||
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
||||
from openbb_core.provider.abstract.fetcher import Fetcher
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from openbb_jodi.models.country_data import JodiCountryData
|
||||
from openbb_jodi.utils.constants import (
|
||||
COUNTRY_MULTI_DESCRIPTION,
|
||||
COUNTRY_SCHEMA_MULTI,
|
||||
OIL_PRODUCT_CHOICES_DESCRIPTION,
|
||||
OIL_PRODUCT_LABELS,
|
||||
OIL_PRODUCTS,
|
||||
OIL_UNIT_DESCRIPTION,
|
||||
OIL_UNIT_LABELS,
|
||||
OIL_UNITS,
|
||||
USE_CACHE_DESCRIPTION,
|
||||
OilProductType,
|
||||
OilUnitType,
|
||||
)
|
||||
|
||||
|
||||
class JodiOilStocksQueryParams(QueryParams):
|
||||
"""JODI Oil Stocks Query.
|
||||
|
||||
Source: https://www.jodidata.org/oil/database/data-downloads.aspx
|
||||
"""
|
||||
|
||||
__json_schema_extra__ = {
|
||||
"country": COUNTRY_SCHEMA_MULTI,
|
||||
"product": {"choices": list(OIL_PRODUCTS)},
|
||||
"unit": {"choices": list(OIL_UNITS)},
|
||||
}
|
||||
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description=COUNTRY_MULTI_DESCRIPTION,
|
||||
)
|
||||
product: OilProductType = Field(
|
||||
default="crude_oil",
|
||||
description="The petroleum product to get closing stock levels of."
|
||||
+ " JODI questionnaire definitions:\n"
|
||||
+ OIL_PRODUCT_CHOICES_DESCRIPTION,
|
||||
)
|
||||
unit: OilUnitType = Field(
|
||||
default="kbbl",
|
||||
description=OIL_UNIT_DESCRIPTION,
|
||||
)
|
||||
start_date: dateType | None = Field(
|
||||
default=None,
|
||||
description=QUERY_DESCRIPTIONS.get("start_date", "")
|
||||
+ " Defaults to the first data period, 2002-01-01.",
|
||||
)
|
||||
end_date: dateType | None = Field(
|
||||
default=None, description=QUERY_DESCRIPTIONS.get("end_date", "")
|
||||
)
|
||||
use_cache: bool = Field(default=True, description=USE_CACHE_DESCRIPTION)
|
||||
|
||||
@field_validator("country", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _validate_country(cls, v):
|
||||
"""Validate country."""
|
||||
from openbb_jodi.utils.helpers import validate_country_param
|
||||
|
||||
return validate_country_param(v, multiple=True, default=None)
|
||||
|
||||
@field_validator("product", "unit", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _normalize_tokens(cls, v, info):
|
||||
"""Normalize token parameters."""
|
||||
from openbb_jodi.utils.helpers import normalize_token
|
||||
|
||||
defaults = {"product": "crude_oil", "unit": "kbbl"}
|
||||
return normalize_token(v, defaults[str(info.field_name)])
|
||||
|
||||
|
||||
class JodiOilStocksData(JodiCountryData):
|
||||
"""JODI Oil Stocks Data.
|
||||
|
||||
One column per reporting country. Closing stocks are the primary
|
||||
stock level at the end of the month within national territories,
|
||||
including stocks held by importers, refiners, stock-holding
|
||||
organisations, and governments.
|
||||
"""
|
||||
|
||||
|
||||
class JodiOilStocksFetcher(Fetcher[JodiOilStocksQueryParams, list[JodiOilStocksData]]):
|
||||
"""JODI Oil Stocks Fetcher."""
|
||||
|
||||
@staticmethod
|
||||
def transform_query(params: dict[str, Any]) -> JodiOilStocksQueryParams:
|
||||
"""Transform the query params."""
|
||||
return JodiOilStocksQueryParams(**params)
|
||||
|
||||
@staticmethod
|
||||
async def aextract_data(
|
||||
query: JodiOilStocksQueryParams,
|
||||
credentials: dict[str, str] | None,
|
||||
**kwargs: Any,
|
||||
) -> list[dict]:
|
||||
"""Extract the data from the JODI-Oil annual CSV files."""
|
||||
from openbb_jodi.utils.constants import COUNTRIES, OIL_START_YEAR
|
||||
from openbb_jodi.utils.helpers import get_filtered_oil, resolve_date_range
|
||||
|
||||
start_date, end_date = resolve_date_range(
|
||||
query.start_date, query.end_date, OIL_START_YEAR
|
||||
)
|
||||
product_code, table = OIL_PRODUCTS[query.product]
|
||||
countries = (
|
||||
{COUNTRIES[country] for country in query.country.split(",")}
|
||||
if query.country
|
||||
else None
|
||||
)
|
||||
return await get_filtered_oil(
|
||||
tables=[table],
|
||||
countries=countries,
|
||||
product_codes={product_code},
|
||||
flow_codes={"CLOSTLV"},
|
||||
unit_codes={OIL_UNITS[query.unit]},
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
use_cache=query.use_cache,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def transform_data(
|
||||
query: JodiOilStocksQueryParams,
|
||||
data: list[dict],
|
||||
**kwargs: Any,
|
||||
) -> AnnotatedResult[list[JodiOilStocksData]]:
|
||||
"""Transform the raw data into the by-country table."""
|
||||
from openbb_jodi.utils.constants import CODE_TO_COUNTRY
|
||||
from openbb_jodi.utils.helpers import build_assessments, build_country_table
|
||||
|
||||
rows = build_country_table(data)
|
||||
metadata = {
|
||||
"product": OIL_PRODUCT_LABELS[OIL_PRODUCTS[query.product][0]],
|
||||
"flow": "Closing stocks",
|
||||
"unit": OIL_UNIT_LABELS[query.unit],
|
||||
"assessments": build_assessments(data, "REF_AREA", CODE_TO_COUNTRY),
|
||||
"source": "JODI-Oil World Database (www.jodidata.org)",
|
||||
}
|
||||
return AnnotatedResult(
|
||||
result=[JodiOilStocksData.model_validate(r) for r in rows],
|
||||
metadata=metadata,
|
||||
)
|
||||
214
openbb_platform/providers/jodi/openbb_jodi/oil_router.py
Normal file
214
openbb_platform/providers/jodi/openbb_jodi/oil_router.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""JODI Oil Router."""
|
||||
|
||||
from openbb_core.app.model.command_context import CommandContext
|
||||
from openbb_core.app.model.example import APIEx
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
from openbb_core.app.provider_interface import (
|
||||
ExtraParams,
|
||||
ProviderChoices,
|
||||
StandardParams,
|
||||
)
|
||||
from openbb_core.app.query import Query as OpenBBQuery
|
||||
from openbb_core.app.router import Router
|
||||
|
||||
router = Router(
|
||||
prefix="/oil",
|
||||
description="JODI-Oil World Database."
|
||||
+ " Monthly data from January 2002 to the most recent month.",
|
||||
)
|
||||
|
||||
|
||||
@router.command(
|
||||
model="JodiOilBalance",
|
||||
examples=[
|
||||
APIEx(
|
||||
description="Get the United States crude oil balance - one column per questionnaire flow.",
|
||||
parameters={"provider": "jodi"},
|
||||
),
|
||||
APIEx(
|
||||
description="Get India's gasoline balance, in thousand metric tons.",
|
||||
parameters={
|
||||
"provider": "jodi",
|
||||
"country": "india",
|
||||
"product": "gasoline",
|
||||
"unit": "ktons",
|
||||
"start_date": "2020-01-01",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def balance(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Get a country's monthly petroleum balance for one product - production, trade, stocks, and refinery activity, one column per flow of the JODI questionnaire."""
|
||||
return await OBBject.from_query(OpenBBQuery(**locals()))
|
||||
|
||||
|
||||
@router.command(
|
||||
model="JodiOilProduction",
|
||||
examples=[
|
||||
APIEx(
|
||||
description="Get monthly crude oil production - one column per reporting country.",
|
||||
parameters={"provider": "jodi"},
|
||||
),
|
||||
APIEx(
|
||||
description="Compare crude oil production in Saudi Arabia, Iraq, and the United States.",
|
||||
parameters={
|
||||
"provider": "jodi",
|
||||
"country": "saudi_arabia,iraq,united_states",
|
||||
"start_date": "2020-01-01",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def production(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Monthly production of a primary petroleum product - one column per reporting country."""
|
||||
return await OBBject.from_query(OpenBBQuery(**locals()))
|
||||
|
||||
|
||||
@router.command(
|
||||
model="JodiOilDemand",
|
||||
examples=[
|
||||
APIEx(
|
||||
description="Get monthly total petroleum products demand - one column per reporting country.",
|
||||
parameters={"provider": "jodi"},
|
||||
),
|
||||
APIEx(
|
||||
description="Compare gasoline demand in the United States, China, and India.",
|
||||
parameters={
|
||||
"provider": "jodi",
|
||||
"country": "united_states,china,india",
|
||||
"product": "gasoline",
|
||||
"start_date": "2020-01-01",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def demand(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Monthly demand for a refined petroleum product - one column per reporting country."""
|
||||
return await OBBject.from_query(OpenBBQuery(**locals()))
|
||||
|
||||
|
||||
@router.command(
|
||||
model="JodiOilDemandByProduct",
|
||||
examples=[
|
||||
APIEx(
|
||||
description="Get monthly United States demand - one column per refined product.",
|
||||
parameters={"provider": "jodi"},
|
||||
),
|
||||
APIEx(
|
||||
description="Get Japan's demand barrel, in thousand barrels per day.",
|
||||
parameters={
|
||||
"provider": "jodi",
|
||||
"country": "japan",
|
||||
"unit": "kbd",
|
||||
"start_date": "2020-01-01",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def demand_by_product(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Get a country's monthly petroleum demand - one column per refined product, in published order."""
|
||||
return await OBBject.from_query(OpenBBQuery(**locals()))
|
||||
|
||||
|
||||
@router.command(
|
||||
model="JodiOilImports",
|
||||
examples=[
|
||||
APIEx(
|
||||
description="Get monthly crude oil imports - one column per reporting country.",
|
||||
parameters={"provider": "jodi"},
|
||||
),
|
||||
APIEx(
|
||||
description="Compare crude oil imports of China, India, and South Korea.",
|
||||
parameters={
|
||||
"provider": "jodi",
|
||||
"country": "china,india,south_korea",
|
||||
"start_date": "2020-01-01",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def imports(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Monthly imports of a petroleum product - one column per reporting country."""
|
||||
return await OBBject.from_query(OpenBBQuery(**locals()))
|
||||
|
||||
|
||||
@router.command(
|
||||
model="JodiOilExports",
|
||||
examples=[
|
||||
APIEx(
|
||||
description="Get monthly crude oil exports - one column per reporting country.",
|
||||
parameters={"provider": "jodi"},
|
||||
),
|
||||
APIEx(
|
||||
description="Compare fuel oil exports from Russia and Saudi Arabia.",
|
||||
parameters={
|
||||
"provider": "jodi",
|
||||
"country": "russia,saudi_arabia",
|
||||
"product": "fuel_oil",
|
||||
"start_date": "2020-01-01",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def exports(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Monthly exports of a petroleum product - one column per reporting country."""
|
||||
return await OBBject.from_query(OpenBBQuery(**locals()))
|
||||
|
||||
|
||||
@router.command(
|
||||
model="JodiOilStocks",
|
||||
examples=[
|
||||
APIEx(
|
||||
description="Get monthly crude oil closing stock levels - one column per reporting country.",
|
||||
parameters={"provider": "jodi"},
|
||||
),
|
||||
APIEx(
|
||||
description="Compare gasoline stocks in the United States and Japan, in thousand barrels.",
|
||||
parameters={
|
||||
"provider": "jodi",
|
||||
"country": "united_states,japan",
|
||||
"product": "gasoline",
|
||||
"unit": "kbbl",
|
||||
"start_date": "2020-01-01",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def stocks(
|
||||
cc: CommandContext,
|
||||
provider_choices: ProviderChoices,
|
||||
standard_params: StandardParams,
|
||||
extra_params: ExtraParams,
|
||||
) -> OBBject:
|
||||
"""Monthly closing stock levels of a petroleum product - one column per reporting country."""
|
||||
return await OBBject.from_query(OpenBBQuery(**locals()))
|
||||
0
openbb_platform/providers/jodi/openbb_jodi/py.typed
vendored
Normal file
0
openbb_platform/providers/jodi/openbb_jodi/py.typed
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"""OpenBB JODI Provider Utilities."""
|
||||
540
openbb_platform/providers/jodi/openbb_jodi/utils/constants.py
Normal file
540
openbb_platform/providers/jodi/openbb_jodi/utils/constants.py
Normal file
@@ -0,0 +1,540 @@
|
||||
"""OpenBB JODI Provider Module Constants."""
|
||||
|
||||
from typing import Literal
|
||||
|
||||
OilProductType = Literal[
|
||||
"crude_oil",
|
||||
"ngl",
|
||||
"other_crude",
|
||||
"total_crude",
|
||||
"lpg",
|
||||
"naphtha",
|
||||
"gasoline",
|
||||
"kerosene",
|
||||
"jet_fuel",
|
||||
"gas_diesel_oil",
|
||||
"fuel_oil",
|
||||
"other_products",
|
||||
"total_products",
|
||||
]
|
||||
OilPrimaryProductType = Literal["crude_oil", "ngl", "other_crude", "total_crude"]
|
||||
OilSecondaryProductType = Literal[
|
||||
"lpg",
|
||||
"naphtha",
|
||||
"gasoline",
|
||||
"kerosene",
|
||||
"jet_fuel",
|
||||
"gas_diesel_oil",
|
||||
"fuel_oil",
|
||||
"other_products",
|
||||
"total_products",
|
||||
]
|
||||
OilUnitType = Literal["kbd", "kbbl", "kl", "ktons", "conversion_factor"]
|
||||
GasUnitType = Literal["m3", "tj", "ktons"]
|
||||
|
||||
OIL_TABLE_URLS = {
|
||||
"primary": "https://www.jodidata.org/_resources/files/downloads/oil-data/world_Primary_CSV.zip",
|
||||
"secondary": "https://www.jodidata.org/_resources/files/downloads/oil-data/world_Secondary_CSV.zip",
|
||||
}
|
||||
GAS_FILES_URL = "https://api.publisher.jodidata.org/web/files/gas"
|
||||
GAS_DOWNLOAD_URL = (
|
||||
"https://www.jodidata.org/jodi-publisher/gas/{publication_id}/{filename}"
|
||||
)
|
||||
|
||||
OIL_START_YEAR = 2002
|
||||
GAS_START_YEAR = 2009
|
||||
|
||||
# Item codes and full names per 'A Guide on JODI-Oil World Database Item Names' (2017)
|
||||
# and 'A Guide on JODI-Gas World Database Item Names' (2025).
|
||||
OIL_PRODUCTS = {
|
||||
"crude_oil": ("CRUDEOIL", "primary"),
|
||||
"ngl": ("NGL", "primary"),
|
||||
"other_crude": ("OTHERCRUDE", "primary"),
|
||||
"total_crude": ("TOTCRUDE", "primary"),
|
||||
"lpg": ("LPG", "secondary"),
|
||||
"naphtha": ("NAPHTHA", "secondary"),
|
||||
"gasoline": ("GASOLINE", "secondary"),
|
||||
"kerosene": ("KEROSENE", "secondary"),
|
||||
"jet_fuel": ("JETKERO", "secondary"),
|
||||
"gas_diesel_oil": ("GASDIES", "secondary"),
|
||||
"fuel_oil": ("RESFUEL", "secondary"),
|
||||
"other_products": ("ONONSPEC", "secondary"),
|
||||
"total_products": ("TOTPRODS", "secondary"),
|
||||
}
|
||||
|
||||
OIL_PRODUCT_LABELS = {
|
||||
"CRUDEOIL": "Crude oil",
|
||||
"NGL": "NGL",
|
||||
"OTHERCRUDE": "Other primary",
|
||||
"TOTCRUDE": "Total primary",
|
||||
"LPG": "Liquefied petroleum gases",
|
||||
"NAPHTHA": "Naphtha",
|
||||
"GASOLINE": "Motor and aviation gasoline",
|
||||
"KEROSENE": "Kerosenes",
|
||||
"JETKERO": "Kerosene type jet fuel",
|
||||
"GASDIES": "Gas/diesel oil",
|
||||
"RESFUEL": "Fuel oil",
|
||||
"ONONSPEC": "Other oil products",
|
||||
"TOTPRODS": "Total oil products",
|
||||
}
|
||||
|
||||
OIL_FLOWS = {
|
||||
"production": {"primary": "INDPROD"},
|
||||
"from_other_sources": {"primary": "OSOURCES"},
|
||||
"imports": {"primary": "TOTIMPSB", "secondary": "TOTIMPSB"},
|
||||
"exports": {"primary": "TOTEXPSB", "secondary": "TOTEXPSB"},
|
||||
"products_transferred": {"primary": "TRANSBAK", "secondary": "PTRANSF"},
|
||||
"direct_use": {"primary": "DIRECUSE"},
|
||||
"stock_change": {"primary": "STOCKCH", "secondary": "STOCKCH"},
|
||||
"statistical_difference": {"primary": "STATDIFF", "secondary": "STATDIFF"},
|
||||
"refinery_intake": {"primary": "REFINOBS"},
|
||||
"refinery_output": {"secondary": "REFGROUT"},
|
||||
"receipts": {"secondary": "RECEIPTS"},
|
||||
"interproduct_transfers": {"secondary": "IPTRANSF"},
|
||||
"demand": {"secondary": "TOTDEMO"},
|
||||
"closing_stocks": {"primary": "CLOSTLV", "secondary": "CLOSTLV"},
|
||||
}
|
||||
|
||||
OIL_FLOW_LABELS = {
|
||||
"INDPROD": "Production",
|
||||
"OSOURCES": "From other sources",
|
||||
"TOTIMPSB": "Imports",
|
||||
"TOTEXPSB": "Exports",
|
||||
"TRANSBAK": "Products transferred/Backflows",
|
||||
"PTRANSF": "Products transferred",
|
||||
"DIRECUSE": "Direct use",
|
||||
"STOCKCH": "Stock change",
|
||||
"STATDIFF": "Statistical difference",
|
||||
"REFINOBS": "Refinery intake",
|
||||
"REFGROUT": "Refinery output",
|
||||
"RECEIPTS": "Receipts",
|
||||
"IPTRANSF": "Interproduct transfers",
|
||||
"TOTDEMO": "Demand",
|
||||
"CLOSTLV": "Closing stocks",
|
||||
}
|
||||
|
||||
OIL_UNITS = {
|
||||
"kbd": "KBD",
|
||||
"kbbl": "KBBL",
|
||||
"kl": "KL",
|
||||
"ktons": "KTONS",
|
||||
"conversion_factor": "CONVBBL",
|
||||
}
|
||||
|
||||
OIL_UNIT_LABELS = {
|
||||
"kbd": "Thousand barrels per day",
|
||||
"kbbl": "Thousand barrels",
|
||||
"kl": "Thousand kilolitres",
|
||||
"ktons": "Thousand metric tons",
|
||||
"conversion_factor": "Conversion factor barrels/ktons",
|
||||
}
|
||||
|
||||
GAS_FLOWS = {
|
||||
"production": "INDPROD",
|
||||
"from_other_sources": "OSOURCES",
|
||||
"imports": "TOTIMPSB",
|
||||
"lng_imports": "IMPLNG",
|
||||
"pipeline_imports": "IMPPIP",
|
||||
"exports": "TOTEXPSB",
|
||||
"lng_exports": "EXPLNG",
|
||||
"pipeline_exports": "EXPPIP",
|
||||
"stock_change": "STOCKCH",
|
||||
"demand": "TOTDEMO",
|
||||
"demand_calculated": "TOTDEMC",
|
||||
"electricity_and_heat_generation": "MAINTOT",
|
||||
"statistical_difference": "STATDIFF",
|
||||
"closing_stocks": "CLOSTLV",
|
||||
}
|
||||
|
||||
GAS_FLOW_LABELS = {
|
||||
"INDPROD": "Production",
|
||||
"OSOURCES": "Receipts from other sources",
|
||||
"TOTIMPSB": "Imports",
|
||||
"IMPLNG": "LNG imports",
|
||||
"IMPPIP": "Pipeline imports",
|
||||
"TOTEXPSB": "Exports",
|
||||
"EXPLNG": "LNG exports",
|
||||
"EXPPIP": "Pipeline exports",
|
||||
"STOCKCH": "Stock change",
|
||||
"TOTDEMO": "Gross inland deliveries (observed)",
|
||||
"TOTDEMC": "Gross inland deliveries (calculated)",
|
||||
"MAINTOT": "Of which: electricity and heat generation",
|
||||
"STATDIFF": "Statistical difference",
|
||||
"CLOSTLV": "Closing stocks",
|
||||
}
|
||||
|
||||
GAS_UNITS = {
|
||||
"m3": "M3",
|
||||
"tj": "TJ",
|
||||
"ktons": "KTONS",
|
||||
}
|
||||
|
||||
GAS_UNIT_LABELS = {
|
||||
"m3": "Million cubic metres",
|
||||
"tj": "Terajoules",
|
||||
"ktons": "Thousand metric tons (LNG)",
|
||||
}
|
||||
|
||||
ASSESSMENT_LABELS = {
|
||||
"1": "Reasonable levels of comparability",
|
||||
"2": "Consult metadata/use with caution",
|
||||
"3": "Not assessed",
|
||||
"4": "Under verification",
|
||||
}
|
||||
|
||||
# Definitions per the JODI-Oil Monthly Questionnaire Short Definitions and the
|
||||
# JODI-Gas Questionnaire brief reporting guide (www.jodidata.org).
|
||||
NATURAL_GAS_DEFINITION = (
|
||||
"Natural gas is a mixture of gaseous hydrocarbons, primarily methane, but"
|
||||
" generally also including ethane, propane, and higher hydrocarbons in much"
|
||||
" smaller amounts, and some non-combustible gases such as nitrogen and carbon"
|
||||
" dioxide. It includes both non-associated and associated gas. Colliery gas,"
|
||||
" coal seam gas, and shale gas are included, while manufactured gas and biogas"
|
||||
" are excluded except when blended with natural gas for final consumption."
|
||||
" Natural gas liquids are excluded."
|
||||
)
|
||||
|
||||
OIL_PRODUCT_DEFINITIONS = {
|
||||
"crude_oil": "Crude oil, including lease condensate and excluding NGL.",
|
||||
"ngl": "Liquid or liquefied hydrocarbons recovered from gas separation"
|
||||
" plants and gas processing facilities.",
|
||||
"other_crude": "Refinery feedstocks, additives/oxygenates, and other hydrocarbons.",
|
||||
"total_crude": "The sum of the primary categories: crude oil + NGL + other.",
|
||||
"lpg": "Liquefied petroleum gases, comprising propane and butane.",
|
||||
"naphtha": "Naphtha used as feedstock for producing high octane gasoline"
|
||||
" and as feedstock for the chemical/petrochemical industries.",
|
||||
"gasoline": "Motor gasoline and aviation gasoline; motor gasoline includes"
|
||||
" biogasoline, e.g. ethanol blends.",
|
||||
"kerosene": "Kerosenes, comprising jet kerosene and other kerosene.",
|
||||
"jet_fuel": "Kerosene type jet fuel: aviation fuel used for aviation"
|
||||
" turbine power units; a subset of the amount reported under kerosenes.",
|
||||
"gas_diesel_oil": "Gas/diesel oil, for automotive and other purposes;"
|
||||
" biodiesel is included.",
|
||||
"fuel_oil": "Heavy residual oil/boiler oil, including bunker oil.",
|
||||
"other_products": "Refinery gas, ethane, petroleum coke, lubricants, white"
|
||||
" spirit and SBP, bitumen, paraffin waxes, and other petroleum products.",
|
||||
"total_products": "The sum of all secondary product categories; demand for"
|
||||
" total products includes direct use of crude oil, NGL, and other"
|
||||
" hydrocarbons.",
|
||||
}
|
||||
|
||||
OIL_FLOW_DEFINITIONS = {
|
||||
"production": "Marketed production, after removal of impurities but"
|
||||
" including quantities consumed by the producer in the production process.",
|
||||
"refinery_output": "Gross refinery output of finished products, including"
|
||||
" refinery fuel.",
|
||||
"from_other_sources": "Inputs of additives, biofuels, and other"
|
||||
" hydrocarbons produced from non-oil sources such as coal, natural gas,"
|
||||
" and renewable energy.",
|
||||
"receipts": "Primary product receipts (quantities of oil used directly"
|
||||
" without processing in a refinery) plus recycled products. Receipts for"
|
||||
" other oil products include direct use of crude oil and NGL.",
|
||||
"imports": "Goods having physically crossed the international boundaries,"
|
||||
" excluding transit trade and international marine and aviation bunkers.",
|
||||
"exports": "Goods having physically crossed the international boundaries,"
|
||||
" excluding transit trade and international marine and aviation bunkers.",
|
||||
"products_transferred": "For primary products, the sum of products"
|
||||
" transferred and backflows from the petrochemical industry; for secondary"
|
||||
" products, imported petroleum products reclassified as feedstocks for"
|
||||
" further processing in the refinery, without delivery to final consumers.",
|
||||
"interproduct_transfers": "Reclassification of products because their"
|
||||
" specification has changed or because they are blended into another"
|
||||
" product; a negative entry indicates a product to be reclassified, a"
|
||||
" positive entry a reclassified product. Interproduct transfers for other"
|
||||
" oil products include interproduct transfers of crude oil and NGL.",
|
||||
"direct_use": "Crude oil, NGL, and other hydrocarbons used directly"
|
||||
" without being processed in oil refineries, for example crude oil burned"
|
||||
" for electricity generation.",
|
||||
"stock_change": "Closing minus opening stock level; a positive number"
|
||||
" corresponds to a stock build, a negative number to a stock draw.",
|
||||
"statistical_difference": "Differences between observed supply flows and"
|
||||
" refinery intake or demand.",
|
||||
"refinery_intake": "Observed refinery throughputs.",
|
||||
"demand": "Deliveries or sales to the inland market (domestic consumption)"
|
||||
" plus refinery fuel plus international marine and aviation bunkers."
|
||||
" Demand for other oil products includes direct use of crude oil, NGL,"
|
||||
" and other primary hydrocarbons.",
|
||||
"closing_stocks": "The primary stock level at the end of the month within"
|
||||
" national territories; includes stocks held by importers, refiners,"
|
||||
" stock-holding organisations, and governments.",
|
||||
}
|
||||
|
||||
GAS_FLOW_DEFINITIONS = {
|
||||
"production": "Dry marketable production within national boundaries,"
|
||||
" including offshore production, measured after purification and extraction"
|
||||
" of NGL and sulphur; excludes quantities reinjected, extraction losses,"
|
||||
" and quantities vented or flared; includes quantities used within the"
|
||||
" natural gas industry, in gas extraction, pipeline systems, and processing"
|
||||
" plants.",
|
||||
"from_other_sources": "Gas from energy products already accounted for in"
|
||||
" the production of other energy products, for example petroleum gases or"
|
||||
" biogases blended with natural gas.",
|
||||
"imports": "Amounts that have crossed the physical boundaries of the"
|
||||
" country, whether customs clearance has taken place or not; excludes goods"
|
||||
" in transit and goods temporarily admitted/withdrawn, includes re-imports;"
|
||||
" deliveries for international bunkers are excluded.",
|
||||
"pipeline_imports": "Imports of gaseous natural gas through pipelines.",
|
||||
"lng_imports": "Imports of liquefied natural gas through ocean tankers, on"
|
||||
" a re-gasified equivalent basis.",
|
||||
"exports": "Amounts that have crossed the physical boundaries of the"
|
||||
" country, whether customs clearance has taken place or not; excludes goods"
|
||||
" in transit and goods temporarily admitted/withdrawn, includes re-exports;"
|
||||
" deliveries for international bunkers are excluded.",
|
||||
"pipeline_exports": "Exports of gaseous natural gas through pipelines.",
|
||||
"lng_exports": "Exports of liquefied natural gas through ocean tankers, on"
|
||||
" a re-gasified equivalent basis.",
|
||||
"stock_change": "The difference between the closing and opening stock"
|
||||
" level of recoverable gas already extracted; a stock build is shown as a"
|
||||
" positive number, a stock draw as a negative number.",
|
||||
"demand_calculated": "Gross inland deliveries (calculated): production +"
|
||||
" receipts from other sources + imports - exports - stock change.",
|
||||
"statistical_difference": "The difference between the calculated and"
|
||||
" observed gross inland deliveries.",
|
||||
"demand": "Gross inland deliveries (observed): deliveries of marketable"
|
||||
" gas to the inland market, including gas used by the gas industry for"
|
||||
" heating and operation of equipment, losses in distribution, and"
|
||||
" deliveries to international marine and aviation bunkers.",
|
||||
"electricity_and_heat_generation": "Deliveries of natural gas for the"
|
||||
" generation of electricity and heat in power plants, including both"
|
||||
" main-activity and autoproducer plants.",
|
||||
"closing_stocks": "The stock level held on the national territory on the"
|
||||
" last day of the reference month.",
|
||||
}
|
||||
|
||||
# Flow code -> balance-view field name, in questionnaire order.
|
||||
OIL_BALANCE_FIELDS = {
|
||||
"INDPROD": "production",
|
||||
"REFGROUT": "refinery_output",
|
||||
"OSOURCES": "from_other_sources",
|
||||
"RECEIPTS": "receipts",
|
||||
"TOTIMPSB": "imports",
|
||||
"TOTEXPSB": "exports",
|
||||
"TRANSBAK": "products_transferred",
|
||||
"PTRANSF": "products_transferred",
|
||||
"IPTRANSF": "interproduct_transfers",
|
||||
"DIRECUSE": "direct_use",
|
||||
"STOCKCH": "stock_change",
|
||||
"STATDIFF": "statistical_difference",
|
||||
"REFINOBS": "refinery_intake",
|
||||
"TOTDEMO": "demand",
|
||||
"CLOSTLV": "closing_stocks",
|
||||
}
|
||||
|
||||
GAS_BALANCE_FIELDS = {
|
||||
"INDPROD": "production",
|
||||
"OSOURCES": "from_other_sources",
|
||||
"TOTIMPSB": "imports",
|
||||
"IMPPIP": "pipeline_imports",
|
||||
"IMPLNG": "lng_imports",
|
||||
"TOTEXPSB": "exports",
|
||||
"EXPPIP": "pipeline_exports",
|
||||
"EXPLNG": "lng_exports",
|
||||
"STOCKCH": "stock_change",
|
||||
"TOTDEMC": "demand_calculated",
|
||||
"STATDIFF": "statistical_difference",
|
||||
"TOTDEMO": "demand",
|
||||
"MAINTOT": "electricity_and_heat_generation",
|
||||
"CLOSTLV": "closing_stocks",
|
||||
}
|
||||
|
||||
# Product code -> demand-by-product field name, in published order.
|
||||
OIL_PRODUCT_FIELDS = {
|
||||
"LPG": "lpg",
|
||||
"NAPHTHA": "naphtha",
|
||||
"GASOLINE": "gasoline",
|
||||
"KEROSENE": "kerosene",
|
||||
"JETKERO": "jet_fuel",
|
||||
"GASDIES": "gas_diesel_oil",
|
||||
"RESFUEL": "fuel_oil",
|
||||
"ONONSPEC": "other_products",
|
||||
"TOTPRODS": "total_products",
|
||||
}
|
||||
|
||||
# ISO 3166-1 alpha-2 codes observed in the JODI world databases.
|
||||
COUNTRIES = {
|
||||
"albania": "AL",
|
||||
"algeria": "DZ",
|
||||
"angola": "AO",
|
||||
"argentina": "AR",
|
||||
"armenia": "AM",
|
||||
"australia": "AU",
|
||||
"austria": "AT",
|
||||
"azerbaijan": "AZ",
|
||||
"bahrain": "BH",
|
||||
"bangladesh": "BD",
|
||||
"barbados": "BB",
|
||||
"belarus": "BY",
|
||||
"belgium": "BE",
|
||||
"belize": "BZ",
|
||||
"bermuda": "BM",
|
||||
"bolivia": "BO",
|
||||
"brazil": "BR",
|
||||
"brunei": "BN",
|
||||
"bulgaria": "BG",
|
||||
"canada": "CA",
|
||||
"chile": "CL",
|
||||
"china": "CN",
|
||||
"colombia": "CO",
|
||||
"costa_rica": "CR",
|
||||
"croatia": "HR",
|
||||
"cuba": "CU",
|
||||
"cyprus": "CY",
|
||||
"czechia": "CZ",
|
||||
"denmark": "DK",
|
||||
"dominican_republic": "DO",
|
||||
"ecuador": "EC",
|
||||
"egypt": "EG",
|
||||
"el_salvador": "SV",
|
||||
"equatorial_guinea": "GQ",
|
||||
"estonia": "EE",
|
||||
"eswatini": "SZ",
|
||||
"finland": "FI",
|
||||
"france": "FR",
|
||||
"gabon": "GA",
|
||||
"gambia": "GM",
|
||||
"georgia": "GE",
|
||||
"germany": "DE",
|
||||
"greece": "GR",
|
||||
"grenada": "GD",
|
||||
"guatemala": "GT",
|
||||
"guyana": "GY",
|
||||
"haiti": "HT",
|
||||
"honduras": "HN",
|
||||
"hong_kong": "HK",
|
||||
"hungary": "HU",
|
||||
"iceland": "IS",
|
||||
"india": "IN",
|
||||
"indonesia": "ID",
|
||||
"iran": "IR",
|
||||
"iraq": "IQ",
|
||||
"ireland": "IE",
|
||||
"italy": "IT",
|
||||
"jamaica": "JM",
|
||||
"japan": "JP",
|
||||
"kazakhstan": "KZ",
|
||||
"kuwait": "KW",
|
||||
"latvia": "LV",
|
||||
"libya": "LY",
|
||||
"lithuania": "LT",
|
||||
"luxembourg": "LU",
|
||||
"malaysia": "MY",
|
||||
"malta": "MT",
|
||||
"mauritius": "MU",
|
||||
"mexico": "MX",
|
||||
"moldova": "MD",
|
||||
"morocco": "MA",
|
||||
"myanmar": "MM",
|
||||
"nepal": "NP",
|
||||
"netherlands": "NL",
|
||||
"new_zealand": "NZ",
|
||||
"nicaragua": "NI",
|
||||
"niger": "NE",
|
||||
"nigeria": "NG",
|
||||
"north_macedonia": "MK",
|
||||
"norway": "NO",
|
||||
"oman": "OM",
|
||||
"panama": "PA",
|
||||
"papua_new_guinea": "PG",
|
||||
"paraguay": "PY",
|
||||
"peru": "PE",
|
||||
"philippines": "PH",
|
||||
"poland": "PL",
|
||||
"portugal": "PT",
|
||||
"qatar": "QA",
|
||||
"romania": "RO",
|
||||
"russia": "RU",
|
||||
"saudi_arabia": "SA",
|
||||
"serbia": "RS",
|
||||
"singapore": "SG",
|
||||
"slovakia": "SK",
|
||||
"slovenia": "SI",
|
||||
"south_africa": "ZA",
|
||||
"south_korea": "KR",
|
||||
"spain": "ES",
|
||||
"sudan": "SD",
|
||||
"suriname": "SR",
|
||||
"sweden": "SE",
|
||||
"switzerland": "CH",
|
||||
"syria": "SY",
|
||||
"taiwan": "TW",
|
||||
"tajikistan": "TJ",
|
||||
"thailand": "TH",
|
||||
"trinidad_and_tobago": "TT",
|
||||
"tunisia": "TN",
|
||||
"turkiye": "TR",
|
||||
"ukraine": "UA",
|
||||
"united_arab_emirates": "AE",
|
||||
"united_kingdom": "GB",
|
||||
"united_states": "US",
|
||||
"uruguay": "UY",
|
||||
"venezuela": "VE",
|
||||
"vietnam": "VN",
|
||||
"yemen": "YE",
|
||||
}
|
||||
|
||||
CODE_TO_COUNTRY = {v: k for k, v in COUNTRIES.items()}
|
||||
|
||||
COUNTRY_LABELS = {
|
||||
code: name.replace("_", " ").title().replace(" And ", " and ")
|
||||
for name, code in COUNTRIES.items()
|
||||
}
|
||||
|
||||
COUNTRY_WIDGET_OPTIONS = [
|
||||
{"label": COUNTRY_LABELS[code], "value": name} for name, code in COUNTRIES.items()
|
||||
]
|
||||
|
||||
# 'choices' is never set for country params: the static package rejects None defaults
|
||||
# when choices are set, and choices would reject ISO code input before validators run.
|
||||
COUNTRY_SCHEMA_MULTI = {
|
||||
"multiple_items_allowed": True,
|
||||
"x-widget_config": {
|
||||
"options": COUNTRY_WIDGET_OPTIONS,
|
||||
"style": {"popupWidth": 450},
|
||||
},
|
||||
}
|
||||
|
||||
COUNTRY_SCHEMA_SINGLE = {
|
||||
"x-widget_config": {
|
||||
"options": COUNTRY_WIDGET_OPTIONS,
|
||||
"style": {"popupWidth": 450},
|
||||
},
|
||||
}
|
||||
|
||||
COUNTRY_MULTI_DESCRIPTION = (
|
||||
"The country or countries to get data for, by name or ISO 3166-1 alpha-2 code."
|
||||
+ " If None, all reporting countries are returned."
|
||||
)
|
||||
COUNTRY_SINGLE_DESCRIPTION = (
|
||||
"The country to get data for, by name or ISO 3166-1 alpha-2 code."
|
||||
)
|
||||
OIL_UNIT_DESCRIPTION = (
|
||||
"The unit of measurement."
|
||||
+ " kbd: thousand barrels per day; kbbl: thousand barrels;"
|
||||
+ " kl: thousand kilolitres; ktons: thousand metric tons;"
|
||||
+ " conversion_factor: barrels per thousand metric tons."
|
||||
)
|
||||
GAS_UNIT_DESCRIPTION = (
|
||||
"The unit of measurement."
|
||||
+ " m3: million cubic metres (at 15C, 760 mm Hg);"
|
||||
+ " tj: terajoules, on a gross calorific value basis;"
|
||||
+ " ktons: thousand metric tons, on a re-gasified equivalent basis,"
|
||||
+ " reported only for LNG trade amounts."
|
||||
)
|
||||
|
||||
PRIMARY_PRODUCTS = ["crude_oil", "ngl", "other_crude", "total_crude"]
|
||||
SECONDARY_PRODUCTS = [name for name in OIL_PRODUCTS if name not in PRIMARY_PRODUCTS]
|
||||
|
||||
OIL_PRODUCT_CHOICES_DESCRIPTION = "\n".join(
|
||||
f" {name}: {OIL_PRODUCT_DEFINITIONS[name]}" for name in OIL_PRODUCTS
|
||||
)
|
||||
OIL_PRIMARY_PRODUCT_CHOICES_DESCRIPTION = "\n".join(
|
||||
f" {name}: {OIL_PRODUCT_DEFINITIONS[name]}" for name in PRIMARY_PRODUCTS
|
||||
)
|
||||
OIL_SECONDARY_PRODUCT_CHOICES_DESCRIPTION = "\n".join(
|
||||
f" {name}: {OIL_PRODUCT_DEFINITIONS[name]}" for name in SECONDARY_PRODUCTS
|
||||
)
|
||||
USE_CACHE_DESCRIPTION = (
|
||||
"If True, the source tables are cached under the user cache directory"
|
||||
+ " and revalidated against the source at most once per day,"
|
||||
+ " downloading again only when JODI has published an update."
|
||||
)
|
||||
934
openbb_platform/providers/jodi/openbb_jodi/utils/helpers.py
Normal file
934
openbb_platform/providers/jodi/openbb_jodi/utils/helpers.py
Normal file
@@ -0,0 +1,934 @@
|
||||
"""OpenBB JODI Provider Module Helpers."""
|
||||
|
||||
import weakref
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
|
||||
from openbb_jodi.utils.constants import (
|
||||
GAS_DOWNLOAD_URL,
|
||||
GAS_FILES_URL,
|
||||
OIL_TABLE_URLS,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import asyncio # noqa
|
||||
from collections.abc import Iterator, Mapping # noqa
|
||||
from datetime import date as dateType # noqa
|
||||
from pathlib import Path # noqa
|
||||
from typing import IO # noqa
|
||||
|
||||
from pandas import DataFrame # noqa
|
||||
|
||||
REVALIDATE_INTERVAL = 86400
|
||||
PROBE_TIMEOUT = 60
|
||||
MAX_RANGE_CONNECTIONS = 16
|
||||
MIN_RANGE_CHUNK = 2 * 1024 * 1024
|
||||
INGEST_CHUNK_ROWS = 1_000_000
|
||||
PARTITION_ROW_GROUP = 32768
|
||||
|
||||
EXPECTED_COLUMNS: list = [
|
||||
"REF_AREA",
|
||||
"TIME_PERIOD",
|
||||
"ENERGY_PRODUCT",
|
||||
"FLOW_BREAKDOWN",
|
||||
"UNIT_MEASURE",
|
||||
"OBS_VALUE",
|
||||
"ASSESSMENT_CODE",
|
||||
]
|
||||
|
||||
# Dimension columns are categorical: the full tables have millions of rows.
|
||||
CSV_DTYPES: dict = {
|
||||
"REF_AREA": "category",
|
||||
"TIME_PERIOD": "category",
|
||||
"ENERGY_PRODUCT": "category",
|
||||
"FLOW_BREAKDOWN": "category",
|
||||
"UNIT_MEASURE": "category",
|
||||
"OBS_VALUE": str,
|
||||
"ASSESSMENT_CODE": "category",
|
||||
}
|
||||
# TIME_PERIOD stays a plain string in partitions so parquet range filters
|
||||
# compare lexicographically ("YYYY-MM" sorts chronologically).
|
||||
PARTITION_DTYPES: dict = {**CSV_DTYPES, "TIME_PERIOD": str, "OBS_VALUE": float}
|
||||
|
||||
# Per-event-loop download locks and prefetch tasks: locks coalesce concurrent
|
||||
# requests for the same table, and asyncio primitives cannot cross loops.
|
||||
# Weak keys drop entries when a loop is garbage collected.
|
||||
_DOWNLOAD_LOCKS: "weakref.WeakKeyDictionary" = weakref.WeakKeyDictionary()
|
||||
_PREFETCH_TASKS: "weakref.WeakKeyDictionary" = weakref.WeakKeyDictionary()
|
||||
|
||||
|
||||
def _download_lock(table: str) -> "asyncio.Lock":
|
||||
"""Get the download lock for a table on the running event loop."""
|
||||
import asyncio
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
locks = _DOWNLOAD_LOCKS.setdefault(loop, {})
|
||||
if table not in locks:
|
||||
locks[table] = asyncio.Lock()
|
||||
return locks[table]
|
||||
|
||||
|
||||
def _lock_handle_nt(handle) -> None: # pragma: no cover - Windows-only
|
||||
"""Take an exclusive byte lock, waiting for a concurrent holder."""
|
||||
import msvcrt # noqa
|
||||
from time import sleep
|
||||
|
||||
handle.seek(0)
|
||||
while True:
|
||||
try:
|
||||
msvcrt.locking( # ty: ignore[unresolved-attribute]
|
||||
handle.fileno(),
|
||||
msvcrt.LK_NBLCK, # ty: ignore[unresolved-attribute]
|
||||
1,
|
||||
)
|
||||
return
|
||||
except OSError:
|
||||
sleep(0.05)
|
||||
|
||||
|
||||
def _unlock_handle_nt(handle) -> None: # pragma: no cover - Windows-only
|
||||
"""Release the exclusive byte lock."""
|
||||
import msvcrt
|
||||
|
||||
handle.seek(0)
|
||||
msvcrt.locking( # ty: ignore[unresolved-attribute]
|
||||
handle.fileno(),
|
||||
msvcrt.LK_UNLCK, # ty: ignore[unresolved-attribute]
|
||||
1,
|
||||
)
|
||||
|
||||
|
||||
def _lock_handle_posix(handle) -> None: # pragma: no cover - POSIX-only
|
||||
"""Take an exclusive flock, waiting for a concurrent holder."""
|
||||
import fcntl
|
||||
|
||||
fcntl.flock(handle.fileno(), fcntl.LOCK_EX) # ty: ignore[unresolved-attribute]
|
||||
|
||||
|
||||
def _unlock_handle_posix(handle) -> None: # pragma: no cover - POSIX-only
|
||||
"""Release the exclusive flock."""
|
||||
import fcntl
|
||||
|
||||
fcntl.flock(handle.fileno(), fcntl.LOCK_UN) # ty: ignore[unresolved-attribute]
|
||||
|
||||
|
||||
def acquire_table_lock(table: str) -> "IO[bytes]":
|
||||
"""Acquire the cross-process lock for a table; released by release_table_lock.
|
||||
|
||||
The asyncio locks only coalesce work within one event loop; this file lock
|
||||
serializes refreshes of the same table across processes and loops, and is
|
||||
released by the OS if the holder dies.
|
||||
"""
|
||||
import os
|
||||
|
||||
lock = _lock_handle_nt if os.name == "nt" else _lock_handle_posix
|
||||
path = get_cache_directory() / f"{table}.lock"
|
||||
handle = open(path, "a+b") # noqa: SIM115
|
||||
try:
|
||||
lock(handle)
|
||||
except BaseException:
|
||||
handle.close()
|
||||
raise
|
||||
return handle
|
||||
|
||||
|
||||
def release_table_lock(handle: "IO[bytes]") -> None:
|
||||
"""Release a cross-process table lock."""
|
||||
import os
|
||||
|
||||
unlock = _unlock_handle_nt if os.name == "nt" else _unlock_handle_posix
|
||||
try:
|
||||
unlock(handle)
|
||||
finally:
|
||||
handle.close()
|
||||
|
||||
|
||||
def clear_table_temp(table: str) -> None:
|
||||
"""Remove staging leftovers and spent archives; the caller holds the table lock."""
|
||||
import shutil
|
||||
|
||||
for path in get_cache_directory().glob(f"{table}.*.tmp"):
|
||||
if path.is_dir():
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
else:
|
||||
path.unlink(missing_ok=True)
|
||||
# A raw archive left by an interrupted run, or by an older version.
|
||||
(get_cache_directory() / f"{table}.zip").unlink(missing_ok=True)
|
||||
|
||||
|
||||
def get_cache_directory() -> "Path":
|
||||
"""Get the JODI cache directory, creating it if needed."""
|
||||
from pathlib import Path # noqa
|
||||
from openbb_core.app.utils import get_user_cache_directory
|
||||
|
||||
cache_dir = Path(get_user_cache_directory()) / "jodi"
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
return cache_dir
|
||||
|
||||
|
||||
def table_directory(table: str) -> "Path":
|
||||
"""Get the partition directory for a table."""
|
||||
return get_cache_directory() / table
|
||||
|
||||
|
||||
def partition_filename(name: str) -> str:
|
||||
"""Get a partition's file name; parquet is portable across pandas versions."""
|
||||
return f"{name}.parquet"
|
||||
|
||||
|
||||
def write_cached_raw(filename: str, content: bytes) -> None:
|
||||
"""Write bytes to a path under the cache directory, atomically."""
|
||||
import os # noqa
|
||||
from secrets import token_hex
|
||||
|
||||
path = get_cache_directory() / filename
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
temp_path = path.with_name(f"{path.name}.{os.getpid()}{token_hex(4)}.tmp")
|
||||
temp_path.write_bytes(content)
|
||||
os.replace(temp_path, path)
|
||||
|
||||
|
||||
async def request_range(
|
||||
url: str,
|
||||
timeout: int,
|
||||
start: int | None = None,
|
||||
end: int | None = None,
|
||||
validator: str | None = None,
|
||||
) -> "tuple[int, Mapping, bytes]":
|
||||
"""Make one request, optionally for a byte range guarded by a validator."""
|
||||
from openbb_core.provider.utils.helpers import amake_request
|
||||
|
||||
# The JODI web server responds with a 406 error without a browser-like User-Agent.
|
||||
headers = {"User-Agent": "Mozilla/5.0"}
|
||||
if start is not None:
|
||||
headers["Range"] = f"bytes={start}-{end}"
|
||||
if validator:
|
||||
# A mid-transfer file replacement returns 200 instead of a torn 206.
|
||||
headers["If-Range"] = validator
|
||||
|
||||
async def response_callback(response, _):
|
||||
"""Read the status, response headers, and body."""
|
||||
return (response.status, response.headers, await response.read())
|
||||
|
||||
response = await amake_request(
|
||||
url, timeout=timeout, response_callback=response_callback, headers=headers
|
||||
)
|
||||
return cast("tuple[int, Mapping, bytes]", response)
|
||||
|
||||
|
||||
def source_meta(status: int, headers: "Mapping") -> dict:
|
||||
"""Extract the cache validators from response headers."""
|
||||
total_text = str(headers.get("Content-Range", "")).rpartition("/")[2]
|
||||
if not total_text.isdigit() and status == 200:
|
||||
total_text = str(headers.get("Content-Length", ""))
|
||||
return {
|
||||
"etag": headers.get("ETag"),
|
||||
"last_modified": headers.get("Last-Modified"),
|
||||
"size": int(total_text) if total_text.isdigit() else 0,
|
||||
}
|
||||
|
||||
|
||||
def _range_part_ok(part, start: int, end: int) -> bool:
|
||||
"""Check one gathered range response for usability."""
|
||||
if isinstance(part, BaseException):
|
||||
return False
|
||||
status, _, body = part
|
||||
return status == 206 and len(body) == end - start + 1
|
||||
|
||||
|
||||
async def download(url: str, timeout: int = 600) -> "tuple[bytes, dict]":
|
||||
"""Download a file with its cache validators, using concurrent byte ranges.
|
||||
|
||||
The JODI web server throttles per connection; segmented requests multiply
|
||||
the download throughput. Any unusable or failed segment falls back to one
|
||||
plain request.
|
||||
"""
|
||||
import asyncio # noqa
|
||||
from math import ceil
|
||||
|
||||
status, headers, body = await request_range(url, timeout, 0, 0)
|
||||
if status == 200:
|
||||
# Ranges are not supported: the probe returned the complete file.
|
||||
return body, source_meta(status, headers)
|
||||
if status != 206:
|
||||
raise OpenBBError(f"Failed to download {url} -> Status Code: {status}")
|
||||
|
||||
meta = source_meta(status, headers)
|
||||
total = meta["size"]
|
||||
if total:
|
||||
validator = meta["etag"] or meta["last_modified"] or None
|
||||
chunk_size = ceil(
|
||||
total / min(MAX_RANGE_CONNECTIONS, max(1, ceil(total / MIN_RANGE_CHUNK)))
|
||||
)
|
||||
offsets = [
|
||||
(start, min(start + chunk_size, total) - 1)
|
||||
for start in range(0, total, chunk_size)
|
||||
]
|
||||
parts = await asyncio.gather(
|
||||
*(
|
||||
request_range(url, timeout, start, end, validator)
|
||||
for start, end in offsets
|
||||
),
|
||||
return_exceptions=True,
|
||||
)
|
||||
if all(
|
||||
_range_part_ok(part, start, end)
|
||||
for part, (start, end) in zip(parts, offsets)
|
||||
):
|
||||
return (
|
||||
b"".join(cast("tuple", part)[2] for part in parts),
|
||||
meta,
|
||||
)
|
||||
|
||||
# The range responses were unusable: fall back to one full request.
|
||||
status, headers, body = await request_range(url, timeout)
|
||||
if status != 200:
|
||||
raise OpenBBError(f"Failed to download {url} -> Status Code: {status}")
|
||||
return body, source_meta(status, headers)
|
||||
|
||||
|
||||
async def probe_source(url: str) -> dict | None:
|
||||
"""Get the source file's current cache validators, None when unreachable."""
|
||||
try:
|
||||
status, headers, _ = await request_range(url, PROBE_TIMEOUT, 0, 0)
|
||||
except Exception:
|
||||
# The revalidation probe must never break a cached read.
|
||||
return None
|
||||
if status in (200, 206):
|
||||
return source_meta(status, headers)
|
||||
return {}
|
||||
|
||||
|
||||
def source_unchanged(cached: dict, probed: dict) -> bool:
|
||||
"""Compare cache validators, strongest first."""
|
||||
for key in ("etag", "last_modified", "size"):
|
||||
ours, theirs = cached.get(key), probed.get(key)
|
||||
if ours and theirs:
|
||||
return ours == theirs
|
||||
return False
|
||||
|
||||
|
||||
def read_manifest(table: str) -> dict | None:
|
||||
"""Read a table's cache manifest, None when missing or unreadable."""
|
||||
import json
|
||||
|
||||
path = table_directory(table) / "manifest.json"
|
||||
try:
|
||||
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
if (
|
||||
not isinstance(manifest, dict)
|
||||
or "source" not in manifest
|
||||
or "partitions" not in manifest
|
||||
):
|
||||
return None
|
||||
return manifest
|
||||
|
||||
|
||||
def write_manifest(table: str, manifest: dict) -> None:
|
||||
"""Write a table's cache manifest."""
|
||||
import json
|
||||
|
||||
write_cached_raw(f"{table}/manifest.json", json.dumps(manifest).encode("utf-8"))
|
||||
|
||||
|
||||
def manifest_is_fresh(manifest: dict) -> bool:
|
||||
"""Check whether a manifest was validated within the revalidation interval."""
|
||||
from time import time
|
||||
|
||||
return time() - manifest.get("checked_at", 0) <= REVALIDATE_INTERVAL
|
||||
|
||||
|
||||
def bump_checked_at(table: str, snapshot: dict) -> dict:
|
||||
"""Refresh a manifest's checked_at without clobbering a concurrent refresh."""
|
||||
from time import time
|
||||
|
||||
handle = acquire_table_lock(table)
|
||||
try:
|
||||
current = read_manifest(table)
|
||||
if current is not None and current.get("source") != snapshot.get("source"):
|
||||
# A concurrent refresh installed a newer manifest: keep it.
|
||||
return current
|
||||
manifest = current or snapshot
|
||||
manifest["checked_at"] = time()
|
||||
write_manifest(table, manifest)
|
||||
return manifest
|
||||
finally:
|
||||
release_table_lock(handle)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def open_archive_csv(path: "Path") -> "Iterator":
|
||||
"""Open the CSV stream inside a raw source file, unzipping when needed."""
|
||||
import zipfile
|
||||
|
||||
with open(path, "rb") as handle:
|
||||
if handle.read(4) == b"PK\x03\x04":
|
||||
handle.seek(0)
|
||||
try:
|
||||
archive = zipfile.ZipFile(handle)
|
||||
except zipfile.BadZipFile as exc:
|
||||
raise OpenBBError(f"The JODI archive is unreadable. -> {exc}") from exc
|
||||
with archive:
|
||||
members = [n for n in archive.namelist() if n.lower().endswith(".csv")]
|
||||
if not members:
|
||||
raise OpenBBError("No CSV member was found in the JODI archive.")
|
||||
with archive.open(members[0]) as stream:
|
||||
yield stream
|
||||
else:
|
||||
handle.seek(0)
|
||||
yield handle
|
||||
|
||||
|
||||
def clean_chunk(chunk: "DataFrame") -> "DataFrame":
|
||||
"""Validate a parsed CSV chunk and drop the non-numeric observations."""
|
||||
from pandas import to_numeric
|
||||
|
||||
if list(chunk.columns) != EXPECTED_COLUMNS:
|
||||
raise OpenBBError(
|
||||
"Unexpected JODI file format."
|
||||
+ f" Expected columns: {EXPECTED_COLUMNS} Got: {list(chunk.columns)}"
|
||||
)
|
||||
chunk["OBS_VALUE"] = to_numeric(chunk["OBS_VALUE"], errors="coerce")
|
||||
return chunk[chunk["OBS_VALUE"].notna()]
|
||||
|
||||
|
||||
def ingest_table(table: str, source_path: "Path") -> dict[str, list[str]]:
|
||||
"""Split a raw source file into per-product-and-flow partition files.
|
||||
|
||||
The CSV is streamed in chunks: the full table is never held in memory.
|
||||
The caller holds the table lock.
|
||||
"""
|
||||
import os # noqa
|
||||
import shutil
|
||||
from secrets import token_hex
|
||||
from time import sleep
|
||||
|
||||
from pandas import read_csv
|
||||
|
||||
directory = table_directory(table)
|
||||
staging = directory.with_name(f"{directory.name}.{os.getpid()}{token_hex(4)}.tmp")
|
||||
staging.mkdir(parents=True)
|
||||
partitions: dict[str, list[str]] = {}
|
||||
try:
|
||||
with open_archive_csv(source_path) as stream:
|
||||
try:
|
||||
for chunk in read_csv(
|
||||
stream, dtype=CSV_DTYPES, chunksize=INGEST_CHUNK_ROWS
|
||||
):
|
||||
groups = clean_chunk(chunk).groupby(
|
||||
["ENERGY_PRODUCT", "FLOW_BREAKDOWN"],
|
||||
observed=True,
|
||||
sort=False,
|
||||
)
|
||||
for key, rows in groups:
|
||||
product, flow = cast("tuple[str, str]", key)
|
||||
name = f"{product}_{flow}"
|
||||
rows.to_csv(
|
||||
staging / f"{name}.csv",
|
||||
mode="a",
|
||||
header=name not in partitions,
|
||||
index=False,
|
||||
)
|
||||
partitions[name] = [str(product), str(flow)]
|
||||
except ValueError as exc:
|
||||
raise OpenBBError(f"Unexpected JODI file format. -> {exc}") from exc
|
||||
if not partitions:
|
||||
# Committing an empty table would destroy the existing good cache.
|
||||
raise OpenBBError("The JODI source file contains no observations.")
|
||||
for name in partitions:
|
||||
csv_path = staging / f"{name}.csv"
|
||||
# Sorting groups each country into contiguous row groups, so the
|
||||
# parquet statistics prune non-matching row groups at read time.
|
||||
read_csv(csv_path, dtype=PARTITION_DTYPES).sort_values(
|
||||
["REF_AREA", "TIME_PERIOD"]
|
||||
).to_parquet(
|
||||
staging / partition_filename(name),
|
||||
engine="pyarrow",
|
||||
index=False,
|
||||
row_group_size=PARTITION_ROW_GROUP,
|
||||
)
|
||||
csv_path.unlink()
|
||||
for attempt in range(3):
|
||||
try:
|
||||
if directory.exists():
|
||||
shutil.rmtree(directory)
|
||||
os.replace(staging, directory)
|
||||
break
|
||||
except OSError:
|
||||
# A reader may hold a partition file open (Windows).
|
||||
if attempt == 2:
|
||||
raise
|
||||
sleep(0.25)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return partitions
|
||||
|
||||
|
||||
def store_table(table: str, url: str, raw: bytes, meta: dict) -> dict:
|
||||
"""Ingest downloaded bytes into partitions and write the manifest.
|
||||
|
||||
The caller holds the table lock. The raw archive is deleted after ingest:
|
||||
the partitions are the cache.
|
||||
"""
|
||||
import os # noqa
|
||||
from secrets import token_hex
|
||||
from time import time
|
||||
|
||||
directory = get_cache_directory()
|
||||
temp_path = directory / f"{table}.zip.{os.getpid()}{token_hex(4)}.tmp"
|
||||
temp_path.write_bytes(raw)
|
||||
try:
|
||||
partitions = ingest_table(table, temp_path)
|
||||
finally:
|
||||
temp_path.unlink(missing_ok=True)
|
||||
manifest = {
|
||||
"source": {"url": url, **meta},
|
||||
"checked_at": time(),
|
||||
"partitions": partitions,
|
||||
}
|
||||
write_manifest(table, manifest)
|
||||
return manifest
|
||||
|
||||
|
||||
async def resolve_source_url(table: str) -> str:
|
||||
"""Get a table's current download URL; the gas URL rotates per publication."""
|
||||
import json
|
||||
|
||||
if table != "gas_world":
|
||||
return OIL_TABLE_URLS[table.removeprefix("oil_")]
|
||||
listing, _ = await download(GAS_FILES_URL)
|
||||
info = json.loads(listing)
|
||||
files = [
|
||||
f
|
||||
for f in info.get("files", [])
|
||||
if f.get("format") == "CSV" and not f.get("ignore")
|
||||
]
|
||||
if not files:
|
||||
raise OpenBBError("No CSV file is listed by the JODI-Gas publisher API.")
|
||||
return GAS_DOWNLOAD_URL.format(
|
||||
publication_id=info.get("publicationId"), filename=files[0].get("filename")
|
||||
)
|
||||
|
||||
|
||||
async def source_changed(table: str, manifest: dict) -> bool | None:
|
||||
"""Check a cached table against the source: True/False, or None if unknowable."""
|
||||
url = manifest["source"]["url"]
|
||||
if table == "gas_world":
|
||||
try:
|
||||
url = await resolve_source_url(table)
|
||||
except (OpenBBError, ValueError):
|
||||
return None
|
||||
if url != manifest["source"]["url"]:
|
||||
return True # a new publication is listed
|
||||
probed = await probe_source(url)
|
||||
if not probed:
|
||||
# Unreachable, or an HTTP error status: keep serving the cache.
|
||||
return None
|
||||
return not source_unchanged(manifest["source"], probed)
|
||||
|
||||
|
||||
async def refresh_table(table: str, use_cache: bool = True) -> dict:
|
||||
"""Download a table's source archive and rebuild its partitioned cache.
|
||||
|
||||
Ingest and file I/O run in a thread so the event loop stays responsive;
|
||||
the cross-process table lock is held for the whole refresh.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
handle = await asyncio.to_thread(acquire_table_lock, table)
|
||||
try:
|
||||
# A concurrent process may have refreshed while this one waited.
|
||||
manifest = read_manifest(table) if use_cache else None
|
||||
if manifest is not None and manifest_is_fresh(manifest):
|
||||
return manifest
|
||||
await asyncio.to_thread(clear_table_temp, table)
|
||||
url = await resolve_source_url(table)
|
||||
raw, meta = await download(url)
|
||||
return await asyncio.to_thread(store_table, table, url, raw, meta)
|
||||
finally:
|
||||
await asyncio.to_thread(release_table_lock, handle)
|
||||
|
||||
|
||||
async def ensure_table(table: str, use_cache: bool = True) -> dict:
|
||||
"""Get a table's cache manifest, revalidating or downloading as needed.
|
||||
|
||||
A cached table is revalidated against the source's validators at most once
|
||||
per day; the archive is downloaded again only when the source has changed.
|
||||
A failed revalidation or refresh serves the cached table instead of erroring.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
manifest = read_manifest(table) if use_cache else None
|
||||
if manifest is not None and manifest_is_fresh(manifest):
|
||||
return manifest
|
||||
async with _download_lock(table):
|
||||
# A concurrent request may have refreshed the table while waiting.
|
||||
manifest = read_manifest(table) if use_cache else None
|
||||
if manifest is not None and manifest_is_fresh(manifest):
|
||||
return manifest
|
||||
if manifest is not None:
|
||||
changed = await source_changed(table, manifest)
|
||||
if not changed:
|
||||
# Unchanged, or the source could not be checked: keep the cache.
|
||||
return await asyncio.to_thread(bump_checked_at, table, manifest)
|
||||
try:
|
||||
return await refresh_table(table, use_cache)
|
||||
except Exception:
|
||||
if manifest is not None:
|
||||
# The refresh failed but a readable table is on disk: serve it.
|
||||
return await asyncio.to_thread(bump_checked_at, table, manifest)
|
||||
raise
|
||||
|
||||
|
||||
def partition_filters(
|
||||
countries: set[str] | None,
|
||||
units: set[str] | None,
|
||||
start_date: "dateType | None",
|
||||
end_date: "dateType | None",
|
||||
) -> list | None:
|
||||
"""Build parquet predicate-pushdown filters for the in-file dimensions."""
|
||||
filters: list = []
|
||||
if countries is not None:
|
||||
filters.append(("REF_AREA", "in", sorted(countries)))
|
||||
if units is not None:
|
||||
filters.append(("UNIT_MEASURE", "in", sorted(units)))
|
||||
if start_date is not None:
|
||||
filters.append(("TIME_PERIOD", ">=", start_date.strftime("%Y-%m")))
|
||||
if end_date is not None:
|
||||
filters.append(("TIME_PERIOD", "<=", end_date.strftime("%Y-%m")))
|
||||
return filters or None
|
||||
|
||||
|
||||
def load_partitions(
|
||||
table: str,
|
||||
product_codes: set[str] | None,
|
||||
flow_codes: set[str] | None,
|
||||
manifest: dict,
|
||||
countries: set[str] | None = None,
|
||||
units: set[str] | None = None,
|
||||
start_date: "dateType | None" = None,
|
||||
end_date: "dateType | None" = None,
|
||||
) -> "DataFrame | None":
|
||||
"""Read the partition slices for the requested codes, None on a broken cache.
|
||||
|
||||
The partition files select the product and flow; the country, unit, and
|
||||
period predicates push down into the parquet reads.
|
||||
"""
|
||||
from pandas import DataFrame, concat, read_parquet
|
||||
|
||||
filters = partition_filters(countries, units, start_date, end_date)
|
||||
frames: list[DataFrame] = []
|
||||
for name, (product, flow) in manifest["partitions"].items():
|
||||
if product_codes is not None and product not in product_codes:
|
||||
continue
|
||||
if flow_codes is not None and flow not in flow_codes:
|
||||
continue
|
||||
try:
|
||||
frame = read_parquet(
|
||||
table_directory(table) / partition_filename(name),
|
||||
engine="pyarrow",
|
||||
filters=filters,
|
||||
)
|
||||
except Exception:
|
||||
# Any unreadable partition invalidates the table cache.
|
||||
return None
|
||||
if not frame.empty:
|
||||
frames.append(frame)
|
||||
if not frames:
|
||||
return DataFrame({column: [] for column in EXPECTED_COLUMNS})
|
||||
return concat(frames, ignore_index=True) if len(frames) > 1 else frames[0]
|
||||
|
||||
|
||||
async def get_table_slice(
|
||||
table: str,
|
||||
product_codes: set[str] | None,
|
||||
flow_codes: set[str] | None,
|
||||
use_cache: bool = True,
|
||||
countries: set[str] | None = None,
|
||||
units: set[str] | None = None,
|
||||
start_date: "dateType | None" = None,
|
||||
end_date: "dateType | None" = None,
|
||||
) -> "DataFrame":
|
||||
"""Get the requested slices of one JODI table, with predicate pushdown."""
|
||||
import asyncio
|
||||
from functools import partial
|
||||
|
||||
load = partial(
|
||||
load_partitions,
|
||||
table,
|
||||
product_codes,
|
||||
flow_codes,
|
||||
countries=countries,
|
||||
units=units,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
)
|
||||
manifest = await ensure_table(table, use_cache)
|
||||
df = await asyncio.to_thread(load, manifest)
|
||||
if df is not None:
|
||||
return df
|
||||
async with _download_lock(table):
|
||||
# A concurrent request may have healed the table while waiting.
|
||||
manifest = read_manifest(table) or manifest
|
||||
df = await asyncio.to_thread(load, manifest)
|
||||
if df is not None:
|
||||
return df
|
||||
# Self-heal: the raw archive is not kept, so download fresh.
|
||||
healed = await refresh_table(table, use_cache=False)
|
||||
df = await asyncio.to_thread(load, healed)
|
||||
if df is None:
|
||||
raise OpenBBError(f"The cached JODI table '{table}' could not be read.")
|
||||
return df
|
||||
|
||||
|
||||
async def prefetch_tables() -> None:
|
||||
"""Warm the partitioned cache for every JODI table."""
|
||||
import asyncio
|
||||
|
||||
await asyncio.gather(
|
||||
ensure_table("oil_primary"),
|
||||
ensure_table("oil_secondary"),
|
||||
ensure_table("gas_world"),
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
|
||||
def start_prefetch() -> None:
|
||||
"""Start one background prefetch of all tables per event loop."""
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
return
|
||||
task = _PREFETCH_TASKS.get(loop)
|
||||
if task is None or task.done():
|
||||
_PREFETCH_TASKS[loop] = loop.create_task(prefetch_tables())
|
||||
|
||||
|
||||
def apply_filters(
|
||||
df: "DataFrame",
|
||||
countries: set[str] | None,
|
||||
products: set[str] | None,
|
||||
flows: set[str] | None,
|
||||
units: set[str] | None,
|
||||
start_date: "dateType",
|
||||
end_date: "dateType",
|
||||
) -> "DataFrame":
|
||||
"""Filter a JODI DataFrame by dimension codes and period."""
|
||||
if countries is not None:
|
||||
df = df[df["REF_AREA"].isin(countries)]
|
||||
if products is not None:
|
||||
df = df[df["ENERGY_PRODUCT"].isin(products)]
|
||||
if flows is not None:
|
||||
df = df[df["FLOW_BREAKDOWN"].isin(flows)]
|
||||
if units is not None:
|
||||
df = df[df["UNIT_MEASURE"].isin(units)]
|
||||
# The period filter runs on the reduced frame: TIME_PERIOD is categorical.
|
||||
period = df["TIME_PERIOD"].astype(str)
|
||||
df = df[
|
||||
(period >= start_date.strftime("%Y-%m"))
|
||||
& (period <= end_date.strftime("%Y-%m"))
|
||||
]
|
||||
return df.reset_index(drop=True)
|
||||
|
||||
|
||||
def normalize_country_names(v) -> list[str]:
|
||||
"""Normalize country input into snake names, accepting names or ISO codes."""
|
||||
from openbb_jodi.utils.constants import CODE_TO_COUNTRY, COUNTRIES
|
||||
|
||||
items = v if isinstance(v, list) else str(v).split(",")
|
||||
result: list[str] = []
|
||||
for item in items:
|
||||
country = str(item).strip().lower().replace(" ", "_")
|
||||
if country in COUNTRIES:
|
||||
result.append(country)
|
||||
elif country.upper() in CODE_TO_COUNTRY:
|
||||
result.append(CODE_TO_COUNTRY[country.upper()])
|
||||
else:
|
||||
raise OpenBBError(
|
||||
f"Invalid country: '{item}'. Valid values are: {list(COUNTRIES)}"
|
||||
)
|
||||
return list(dict.fromkeys(result))
|
||||
|
||||
|
||||
def validate_country_param(v, multiple: bool, default: str | None) -> str | None:
|
||||
"""Validate a country query parameter."""
|
||||
if not v:
|
||||
return default
|
||||
names = normalize_country_names(v)
|
||||
if not multiple and len(names) > 1:
|
||||
raise OpenBBError("Only one country can be selected for this endpoint.")
|
||||
return ",".join(names)
|
||||
|
||||
|
||||
def normalize_token(v, default: str) -> str:
|
||||
"""Normalize a single-select token parameter."""
|
||||
if not v:
|
||||
return default
|
||||
return str(v).strip().lower().replace(" ", "_")
|
||||
|
||||
|
||||
def resolve_date_range(start_date, end_date, start_year: int) -> tuple:
|
||||
"""Apply the full-history default date range and check the order."""
|
||||
from datetime import date
|
||||
|
||||
start_date = start_date or date(start_year, 1, 1)
|
||||
end_date = end_date or date.today()
|
||||
if start_date > end_date:
|
||||
raise OpenBBError("The start_date must be on, or before, the end_date.")
|
||||
return start_date, end_date
|
||||
|
||||
|
||||
def check_oil_dates(start_date, end_date) -> None:
|
||||
"""Check the date range against the JODI-Oil data coverage."""
|
||||
from openbb_jodi.utils.constants import OIL_START_YEAR
|
||||
|
||||
if end_date.year < OIL_START_YEAR:
|
||||
raise OpenBBError(
|
||||
f"No JODI-Oil data is available between {start_date} and {end_date}."
|
||||
+ f" The data starts in {OIL_START_YEAR}."
|
||||
)
|
||||
|
||||
|
||||
def check_gas_dates(start_date, end_date) -> None:
|
||||
"""Check the date range against the JODI-Gas data coverage."""
|
||||
from openbb_jodi.utils.constants import GAS_START_YEAR
|
||||
|
||||
if end_date.year < GAS_START_YEAR:
|
||||
raise OpenBBError(
|
||||
f"No JODI-Gas data is available between {start_date} and {end_date}."
|
||||
+ f" The data starts in {GAS_START_YEAR}."
|
||||
)
|
||||
|
||||
|
||||
async def get_filtered_oil(
|
||||
tables: list[str],
|
||||
countries: set[str] | None,
|
||||
product_codes: set[str],
|
||||
flow_codes: set[str],
|
||||
unit_codes: set[str],
|
||||
start_date,
|
||||
end_date,
|
||||
use_cache: bool = True,
|
||||
) -> list[dict]:
|
||||
"""Get filtered JODI-Oil records for the requested view."""
|
||||
import asyncio # noqa
|
||||
from openbb_core.provider.utils.errors import EmptyDataError
|
||||
from pandas import concat
|
||||
|
||||
check_oil_dates(start_date, end_date)
|
||||
frames = await asyncio.gather(
|
||||
*(
|
||||
get_table_slice(
|
||||
f"oil_{table}",
|
||||
product_codes,
|
||||
flow_codes,
|
||||
use_cache,
|
||||
countries=countries,
|
||||
units=unit_codes,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
)
|
||||
for table in tables
|
||||
)
|
||||
)
|
||||
frames = [frame for frame in frames if not frame.empty] or frames[:1]
|
||||
df = frames[0] if len(frames) == 1 else concat(frames, ignore_index=True)
|
||||
df = apply_filters(
|
||||
df, countries, product_codes, flow_codes, unit_codes, start_date, end_date
|
||||
)
|
||||
if df.empty:
|
||||
raise EmptyDataError("No results were found with the given query parameters.")
|
||||
return df.to_dict(orient="records")
|
||||
|
||||
|
||||
async def get_filtered_gas(
|
||||
countries: set[str] | None,
|
||||
flow_codes: set[str],
|
||||
unit_codes: set[str],
|
||||
start_date,
|
||||
end_date,
|
||||
use_cache: bool = True,
|
||||
) -> list[dict]:
|
||||
"""Get filtered JODI-Gas records for the requested view."""
|
||||
from openbb_core.provider.utils.errors import EmptyDataError
|
||||
|
||||
check_gas_dates(start_date, end_date)
|
||||
df = await get_table_slice(
|
||||
"gas_world",
|
||||
None,
|
||||
flow_codes,
|
||||
use_cache,
|
||||
countries=countries,
|
||||
units=unit_codes,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
)
|
||||
df = apply_filters(
|
||||
df, countries, None, flow_codes, unit_codes, start_date, end_date
|
||||
)
|
||||
if df.empty:
|
||||
raise EmptyDataError("No results were found with the given query parameters.")
|
||||
return df.to_dict(orient="records")
|
||||
|
||||
|
||||
def _to_wide(df, columns: str, column_order: list[str]) -> list[dict]:
|
||||
"""Pivot filtered records into date rows with the given column order."""
|
||||
from datetime import date
|
||||
|
||||
wide = (
|
||||
df.pivot_table(
|
||||
index="TIME_PERIOD", columns=columns, values="OBS_VALUE", aggfunc="first"
|
||||
)
|
||||
.reindex(columns=[c for c in column_order if c in df[columns].unique()])
|
||||
.sort_index()
|
||||
.reset_index()
|
||||
)
|
||||
wide = wide.astype(object).where(wide.notna(), other=None)
|
||||
records = wide.to_dict(orient="records")
|
||||
for row in records:
|
||||
row["date"] = date.fromisoformat(f"{row.pop('TIME_PERIOD')}-01")
|
||||
return [{"date": row.pop("date"), **row} for row in records]
|
||||
|
||||
|
||||
def build_field_table(
|
||||
records: list[dict], code_column: str, field_map: dict[str, str]
|
||||
) -> list[dict]:
|
||||
"""Build date rows with one typed field per item code."""
|
||||
from pandas import DataFrame
|
||||
|
||||
df = DataFrame(records)
|
||||
df["field"] = df[code_column].map(field_map)
|
||||
return _to_wide(df, "field", list(dict.fromkeys(field_map.values())))
|
||||
|
||||
|
||||
def build_country_table(records: list[dict]) -> list[dict]:
|
||||
"""Build date rows with one field per country, alphabetical."""
|
||||
from pandas import DataFrame
|
||||
|
||||
from openbb_jodi.utils.constants import CODE_TO_COUNTRY
|
||||
|
||||
df = DataFrame(records)
|
||||
df["country"] = df["REF_AREA"].map(lambda c: CODE_TO_COUNTRY.get(c, c))
|
||||
return _to_wide(df, "country", sorted(df["country"].unique()))
|
||||
|
||||
|
||||
def build_assessments(
|
||||
records: list[dict], code_column: str, name_map: dict[str, str]
|
||||
) -> dict[str, str]:
|
||||
"""Get the most recent JODI assessment label for each series."""
|
||||
from openbb_jodi.utils.constants import ASSESSMENT_LABELS
|
||||
|
||||
assessments: dict[str, str] = {}
|
||||
for row in sorted(records, key=lambda r: str(r["TIME_PERIOD"])):
|
||||
key = name_map.get(row[code_column], row[code_column])
|
||||
label = ASSESSMENT_LABELS.get(str(row["ASSESSMENT_CODE"]))
|
||||
if label:
|
||||
assessments[key] = label
|
||||
return assessments
|
||||
91
openbb_platform/providers/jodi/pyproject.toml
vendored
Normal file
91
openbb_platform/providers/jodi/pyproject.toml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
[project]
|
||||
name = "openbb-jodi"
|
||||
version = "2.0.0"
|
||||
description = "Joint Organisations Data Initiative (JODI) data integration for OpenBB Platform."
|
||||
authors = [{ name = "OpenBB Team", email = "hello@openbb.co" }]
|
||||
license = { text = "AGPL-3.0-only" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<4"
|
||||
dependencies = [
|
||||
"openbb-core[pandas]>=2.0.0",
|
||||
"pyarrow>=17.0.0",
|
||||
]
|
||||
|
||||
[project.entry-points."openbb_provider_extension"]
|
||||
jodi = "openbb_jodi:jodi_provider"
|
||||
|
||||
[project.entry-points."openbb_core_extension"]
|
||||
jodi = "openbb_jodi.jodi_router:router"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://openbb.co"
|
||||
Repository = "https://github.com/OpenBB-finance/OpenBB"
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["openbb-devtools>=2.0.0"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling>=1.25"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["openbb_jodi"]
|
||||
|
||||
[tool.uv.sources]
|
||||
openbb-core = { path = "../../core", editable = true }
|
||||
openbb-devtools = { path = "../../extensions/devtools", editable = true }
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = "py310"
|
||||
fix = true
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["D", "E", "W", "F", "S", "UP", "I", "PLC", "PLE", "PLR", "PLW", "SIM", "T20"]
|
||||
ignore = [
|
||||
"PLR2004", "PLR0913", "PLR0915", "PLC0415",
|
||||
"E402", "E501",
|
||||
"D202", "D203", "D205", "D206", "D210", "D212", "D213", "D214",
|
||||
"D215", "D300", "D400", "D404", "D405", "D406", "D407", "D408",
|
||||
"D409", "D410", "D411", "D413", "D415", "D416", "D417",
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"openbb_jodi/models/*.py" = ["ARG002", "PLC0415"]
|
||||
"openbb_jodi/*_router.py" = ["ARG001", "PLC0415"]
|
||||
"openbb_jodi/utils/helpers.py" = ["PLR0911", "PLR0912"]
|
||||
"tests/**" = ["S101", "S108", "D", "SIM105", "SIM117"]
|
||||
"integration/**" = ["S101", "S108", "D", "SIM105", "SIM117", "PLC0415", "ARG001"]
|
||||
"**/__init__.py" = ["F401"]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
combine-as-imports = true
|
||||
force-wrap-aliases = true
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "numpy"
|
||||
|
||||
[tool.ty]
|
||||
[tool.ty.environment]
|
||||
python-version = "3.10"
|
||||
|
||||
[tool.ty.src]
|
||||
include = ["openbb_jodi"]
|
||||
exclude = ["tests"]
|
||||
|
||||
[tool.ty.rules]
|
||||
unresolved-import = "warn"
|
||||
# The platform-gated stdlib lock modules (msvcrt/fcntl) each need their
|
||||
# ty ignores on one platform, which read as unused on the other.
|
||||
unused-ignore-comment = "ignore"
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "raise NotImplementedError", "@overload"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
addopts = "-q --strict-markers"
|
||||
markers = [
|
||||
"record_http: VCR-backed HTTP cassette tests",
|
||||
"integration: end-to-end tests that hit live services (excluded from the coverage gate)",
|
||||
]
|
||||
1
openbb_platform/providers/jodi/tests/__init__.py
Normal file
1
openbb_platform/providers/jodi/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""OpenBB JODI Provider Tests."""
|
||||
203
openbb_platform/providers/jodi/tests/conftest.py
Normal file
203
openbb_platform/providers/jodi/tests/conftest.py
Normal file
@@ -0,0 +1,203 @@
|
||||
"""Shared fixtures for the openbb_jodi test suite."""
|
||||
|
||||
import json
|
||||
import zipfile
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
FIXTURES = Path(__file__).parent / "fixtures"
|
||||
|
||||
# The fetcher tests cannot use record_http: their HTTP interactions are the
|
||||
# bulk source archives (tens of MB), which must never be recorded. They use
|
||||
# the jodi_cassette fixture instead, which replays an in-memory cassette
|
||||
# synthesized from the trimmed fixtures. Each cassette holds exactly the
|
||||
# interactions its test performs; strict replay fails on anything else.
|
||||
CASSETTE_TESTS = {
|
||||
"test_jodi_oil_balance_fetcher": ("oil_primary",),
|
||||
"test_jodi_oil_production_fetcher": ("oil_primary",),
|
||||
"test_jodi_oil_demand_fetcher": ("oil_secondary",),
|
||||
"test_jodi_oil_demand_by_product_fetcher": ("oil_secondary",),
|
||||
"test_jodi_oil_imports_fetcher": ("oil_primary",),
|
||||
"test_jodi_oil_exports_fetcher": ("oil_primary",),
|
||||
"test_jodi_oil_stocks_fetcher": ("oil_primary",),
|
||||
"test_jodi_gas_balance_fetcher": ("gas_listing", "gas_archive"),
|
||||
"test_jodi_gas_production_fetcher": ("gas_listing", "gas_archive"),
|
||||
"test_jodi_gas_demand_fetcher": ("gas_listing", "gas_archive"),
|
||||
"test_jodi_gas_imports_fetcher": ("gas_listing", "gas_archive"),
|
||||
"test_jodi_gas_exports_fetcher": ("gas_listing", "gas_archive"),
|
||||
"test_jodi_gas_stocks_fetcher": ("gas_listing", "gas_archive"),
|
||||
}
|
||||
|
||||
|
||||
def zip_bytes(csv_path: Path) -> bytes:
|
||||
"""Wrap a CSV fixture in a single-member zip, like the JODI archives."""
|
||||
buffer = BytesIO()
|
||||
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||
archive.writestr(f"{csv_path.stem}.csv", csv_path.read_bytes())
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def _interaction(url: str, body, status: int, reason: str, headers: dict) -> dict:
|
||||
"""Build one vcr cassette interaction serving the given body."""
|
||||
return {
|
||||
"request": {"body": None, "headers": {}, "method": "GET", "uri": url},
|
||||
"response": {
|
||||
"body": {"string": body},
|
||||
"headers": {"Content-Length": [str(len(body))], **headers},
|
||||
"status": {"code": status, "message": reason},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _archive_interaction(url: str, body: bytes) -> dict:
|
||||
"""A served archive: binary body with the IIS cache validators."""
|
||||
return _interaction(
|
||||
url,
|
||||
body,
|
||||
200,
|
||||
"OK",
|
||||
{
|
||||
"Accept-Ranges": ["none"],
|
||||
"Content-Type": ["application/x-zip-compressed"],
|
||||
"ETag": [f'"{url.rsplit("/", 1)[-1]}"'],
|
||||
"Last-Modified": ["Wed, 24 Jun 2026 10:56:04 GMT"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _interaction_library() -> dict[str, dict]:
|
||||
"""Build every synthesized JODI source interaction, keyed for CASSETTE_TESTS."""
|
||||
from openbb_jodi.utils.constants import (
|
||||
GAS_DOWNLOAD_URL,
|
||||
GAS_FILES_URL,
|
||||
OIL_TABLE_URLS,
|
||||
)
|
||||
|
||||
listing_text = (FIXTURES / "files_gas.json").read_text(encoding="utf-8")
|
||||
listing = json.loads(listing_text)
|
||||
gas_file = next(
|
||||
f for f in listing["files"] if f.get("format") == "CSV" and not f.get("ignore")
|
||||
)
|
||||
gas_url = GAS_DOWNLOAD_URL.format(
|
||||
publication_id=listing["publicationId"], filename=gas_file["filename"]
|
||||
)
|
||||
return {
|
||||
"oil_primary": _archive_interaction(
|
||||
OIL_TABLE_URLS["primary"], zip_bytes(FIXTURES / "oil_primary_sample.csv")
|
||||
),
|
||||
"oil_secondary": _archive_interaction(
|
||||
OIL_TABLE_URLS["secondary"],
|
||||
zip_bytes(FIXTURES / "oil_secondary_sample.csv"),
|
||||
),
|
||||
"gas_listing": _interaction(
|
||||
GAS_FILES_URL,
|
||||
listing_text,
|
||||
200,
|
||||
"OK",
|
||||
{"Content-Type": ["application/json"]},
|
||||
),
|
||||
"gas_archive": _archive_interaction(
|
||||
gas_url, (FIXTURES / "gas_sample.zip").read_bytes()
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class MemoryPersister:
|
||||
"""A vcr persister that plays one in-memory cassette and never records."""
|
||||
|
||||
def __init__(self, document: str):
|
||||
self.document = document
|
||||
|
||||
def load_cassette(self, cassette_path, serializer):
|
||||
"""Deserialize the in-memory cassette; the path is only a label."""
|
||||
from vcr.serialize import deserialize
|
||||
|
||||
return deserialize(self.document, serializer)
|
||||
|
||||
@staticmethod
|
||||
def save_cassette(cassette_path, cassette_dict, serializer):
|
||||
"""Recording is disabled: the cassettes are synthesized from fixtures."""
|
||||
raise AssertionError(
|
||||
"The JODI source archives are never recorded;"
|
||||
" the cassettes are synthesized from the trimmed fixtures."
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def jodi_cassette(request, monkeypatch):
|
||||
"""Replay an in-memory cassette synthesized from the trimmed fixtures.
|
||||
|
||||
The full pipeline executes against vcr's aiohttp interception; nothing is
|
||||
downloaded, no cassette file is stored, and live sockets are refused.
|
||||
"""
|
||||
import socket
|
||||
|
||||
import vcr
|
||||
|
||||
def refuse(*args, **kwargs):
|
||||
raise AssertionError("jodi_cassette tests replay offline; no live sockets.")
|
||||
|
||||
monkeypatch.setattr(socket, "getaddrinfo", refuse)
|
||||
library = _interaction_library()
|
||||
document = yaml.safe_dump(
|
||||
{
|
||||
"interactions": [library[key] for key in CASSETTE_TESTS[request.node.name]],
|
||||
"version": 1,
|
||||
}
|
||||
)
|
||||
vcr_object = vcr.VCR(record_mode="none")
|
||||
vcr_object.register_persister(MemoryPersister(document))
|
||||
with vcr_object.use_cassette(request.node.name) as cassette:
|
||||
yield cassette
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolate_cache(tmp_path, monkeypatch):
|
||||
"""Point the JODI cache at a temporary path and return the original getter."""
|
||||
from openbb_jodi.utils import helpers
|
||||
|
||||
original = helpers.get_cache_directory
|
||||
monkeypatch.setattr(helpers, "get_cache_directory", lambda: tmp_path)
|
||||
state = (helpers._DOWNLOAD_LOCKS, helpers._PREFETCH_TASKS)
|
||||
for store in state:
|
||||
store.clear()
|
||||
yield original
|
||||
for store in state:
|
||||
store.clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_download(monkeypatch):
|
||||
"""Serve trimmed samples of the live JODI files instead of downloading."""
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
|
||||
from openbb_jodi.utils import helpers
|
||||
|
||||
files = {
|
||||
"world_Primary_CSV.zip": zip_bytes(FIXTURES / "oil_primary_sample.csv"),
|
||||
"world_Secondary_CSV.zip": zip_bytes(FIXTURES / "oil_secondary_sample.csv"),
|
||||
"web/files/gas": (FIXTURES / "files_gas.json").read_bytes(),
|
||||
"GAS_world_NewFormat.zip": (FIXTURES / "gas_sample.zip").read_bytes(),
|
||||
}
|
||||
calls: list[str] = []
|
||||
|
||||
async def fake_download(url: str, timeout: int = 600) -> tuple:
|
||||
import asyncio
|
||||
|
||||
await asyncio.sleep(0) # yield, so concurrent requests contend for the lock
|
||||
calls.append(url)
|
||||
for suffix, content in files.items():
|
||||
if url.endswith(suffix):
|
||||
meta = {
|
||||
"etag": f'"{suffix}"',
|
||||
"last_modified": "Wed, 24 Jun 2026 10:56:04 GMT",
|
||||
"size": len(content),
|
||||
}
|
||||
return content, meta
|
||||
raise OpenBBError(f"Failed to download {url} -> Status Code: 404")
|
||||
|
||||
monkeypatch.setattr(helpers, "download", fake_download)
|
||||
return calls
|
||||
1
openbb_platform/providers/jodi/tests/fixtures/files_gas.json
vendored
Normal file
1
openbb_platform/providers/jodi/tests/fixtures/files_gas.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"publicationId":23,"files":[{"filename":"GAS_world_NewFormat.zip","format":"CSV","ignore":false},{"filename":"ivt-merged.zip","format":"IVT Merged","ignore":false},{"filename":"ivt.zip","format":"IVT","ignore":false},{"filename":"jodi-data-availability-by-country.csv","format":"Availability","ignore":false}]}
|
||||
BIN
openbb_platform/providers/jodi/tests/fixtures/gas_sample.zip
vendored
Normal file
BIN
openbb_platform/providers/jodi/tests/fixtures/gas_sample.zip
vendored
Normal file
Binary file not shown.
1801
openbb_platform/providers/jodi/tests/fixtures/oil_primary_sample.csv
vendored
Normal file
1801
openbb_platform/providers/jodi/tests/fixtures/oil_primary_sample.csv
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4051
openbb_platform/providers/jodi/tests/fixtures/oil_secondary_sample.csv
vendored
Normal file
4051
openbb_platform/providers/jodi/tests/fixtures/oil_secondary_sample.csv
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Range:
|
||||
- bytes=0-0
|
||||
User-Agent:
|
||||
- Mozilla/5.0
|
||||
method: GET
|
||||
uri: https://www.jodidata.org/_resources/files/downloads/oil-data/annual-csv/primary/1990.csv
|
||||
response:
|
||||
body:
|
||||
string: "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\r\n<html
|
||||
xmlns=\"http://www.w3.org/1999/xhtml\">\r\n<head>\r\n<meta http-equiv=\"Content-Type\"
|
||||
content=\"text/html; charset=iso-8859-1\"/>\r\n<title>404 - File or directory
|
||||
not found.</title>\r\n<style type=\"text/css\">\r\n<!--\r\nbody{margin:0;font-size:.7em;font-family:Verdana,
|
||||
Arial, Helvetica, sans-serif;background:#EEEEEE;}\r\nfieldset{padding:0 15px
|
||||
10px 15px;} \r\nh1{font-size:2.4em;margin:0;color:#FFF;}\r\nh2{font-size:1.7em;margin:0;color:#CC0000;}
|
||||
\r\nh3{font-size:1.2em;margin:10px 0 0 0;color:#000000;} \r\n#header{width:96%;margin:0
|
||||
0 0 0;padding:6px 2% 6px 2%;font-family:\"trebuchet MS\", Verdana, sans-serif;color:#FFF;\r\nbackground-color:#555555;}\r\n#content{margin:0
|
||||
0 0 2%;position:relative;}\r\n.content-container{background:#FFF;width:96%;margin-top:8px;padding:10px;position:relative;}\r\n-->\r\n</style>\r\n</head>\r\n<body>\r\n<div
|
||||
id=\"header\"><h1>Server Error</h1></div>\r\n<div id=\"content\">\r\n <div
|
||||
class=\"content-container\"><fieldset>\r\n <h2>404 - File or directory not
|
||||
found.</h2>\r\n <h3>The resource you are looking for might have been removed,
|
||||
had its name changed, or is temporarily unavailable.</h3>\r\n </fieldset></div>\r\n</div>\r\n</body>\r\n</html>\r\n"
|
||||
headers:
|
||||
Content-Length:
|
||||
- '1245'
|
||||
Content-Type:
|
||||
- text/html
|
||||
Date:
|
||||
- Wed, 15 Jul 2026 17:01:33 GMT
|
||||
Server:
|
||||
- Microsoft-IIS/10.0
|
||||
X-Powered-By:
|
||||
- ASP.NET
|
||||
status:
|
||||
code: 404
|
||||
message: Not Found
|
||||
version: 1
|
||||
29
openbb_platform/providers/jodi/tests/record/http/test_helpers/test_download_urllib3_v2.yaml
vendored
Normal file
29
openbb_platform/providers/jodi/tests/record/http/test_helpers/test_download_urllib3_v2.yaml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Range:
|
||||
- bytes=0-0
|
||||
User-Agent:
|
||||
- Mozilla/5.0
|
||||
method: GET
|
||||
uri: https://api.publisher.jodidata.org/web/files/gas
|
||||
response:
|
||||
body:
|
||||
string: '{"publicationId":23,"files":[{"filename":"GAS_world_NewFormat.zip","format":"CSV","ignore":false},{"filename":"ivt-merged.zip","format":"IVT
|
||||
Merged","ignore":false},{"filename":"ivt.zip","format":"IVT","ignore":false},{"filename":"jodi-data-availability-by-country.csv","format":"Availability","ignore":false}]}'
|
||||
headers:
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Date:
|
||||
- Wed, 15 Jul 2026 17:01:32 GMT
|
||||
Server:
|
||||
- Microsoft-IIS/10.0
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Powered-By:
|
||||
- ASP.NET
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
1075
openbb_platform/providers/jodi/tests/test_helpers.py
Normal file
1075
openbb_platform/providers/jodi/tests/test_helpers.py
Normal file
File diff suppressed because it is too large
Load Diff
152
openbb_platform/providers/jodi/tests/test_jodi_fetchers.py
Normal file
152
openbb_platform/providers/jodi/tests/test_jodi_fetchers.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""Test the JODI fetchers against cassettes synthesized from trimmed fixtures.
|
||||
|
||||
Every test executes the full pipeline against a cold cache: the download
|
||||
replays offline from an in-memory cassette, then ingests and queries.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
from openbb_core.app.service.user_service import UserService
|
||||
|
||||
from openbb_jodi.models.gas_balance import JodiGasBalanceFetcher
|
||||
from openbb_jodi.models.gas_demand import JodiGasDemandFetcher
|
||||
from openbb_jodi.models.gas_exports import JodiGasExportsFetcher
|
||||
from openbb_jodi.models.gas_imports import JodiGasImportsFetcher
|
||||
from openbb_jodi.models.gas_production import JodiGasProductionFetcher
|
||||
from openbb_jodi.models.gas_stocks import JodiGasStocksFetcher
|
||||
from openbb_jodi.models.oil_balance import JodiOilBalanceFetcher
|
||||
from openbb_jodi.models.oil_demand import JodiOilDemandFetcher
|
||||
from openbb_jodi.models.oil_demand_by_product import JodiOilDemandByProductFetcher
|
||||
from openbb_jodi.models.oil_exports import JodiOilExportsFetcher
|
||||
from openbb_jodi.models.oil_imports import JodiOilImportsFetcher
|
||||
from openbb_jodi.models.oil_production import JodiOilProductionFetcher
|
||||
from openbb_jodi.models.oil_stocks import JodiOilStocksFetcher
|
||||
|
||||
test_credentials = UserService().default_user_settings.credentials.model_dump(
|
||||
mode="json"
|
||||
)
|
||||
|
||||
DATES = {"start_date": date(2024, 1, 1), "end_date": date(2024, 3, 31)}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("jodi_cassette")
|
||||
def test_jodi_oil_balance_fetcher(credentials=test_credentials):
|
||||
"""Test the JODI oil balance fetcher."""
|
||||
params = {
|
||||
"country": "united_states",
|
||||
"product": "crude_oil",
|
||||
"unit": "kbbl",
|
||||
**DATES,
|
||||
}
|
||||
fetcher = JodiOilBalanceFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("jodi_cassette")
|
||||
def test_jodi_oil_production_fetcher(credentials=test_credentials):
|
||||
"""Test the JODI oil production fetcher."""
|
||||
params = {"country": "united_states,saudi_arabia", "unit": "kbd", **DATES}
|
||||
fetcher = JodiOilProductionFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("jodi_cassette")
|
||||
def test_jodi_oil_demand_fetcher(credentials=test_credentials):
|
||||
"""Test the JODI oil demand fetcher."""
|
||||
params = {"country": "united_states", "product": "gasoline", **DATES}
|
||||
fetcher = JodiOilDemandFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("jodi_cassette")
|
||||
def test_jodi_oil_demand_by_product_fetcher(credentials=test_credentials):
|
||||
"""Test the JODI oil demand by product fetcher."""
|
||||
params = {"country": "united_states", "unit": "kbd", **DATES}
|
||||
fetcher = JodiOilDemandByProductFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("jodi_cassette")
|
||||
def test_jodi_oil_imports_fetcher(credentials=test_credentials):
|
||||
"""Test the JODI oil imports fetcher."""
|
||||
params = {"country": "united_states", "product": "crude_oil", **DATES}
|
||||
fetcher = JodiOilImportsFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("jodi_cassette")
|
||||
def test_jodi_oil_exports_fetcher(credentials=test_credentials):
|
||||
"""Test the JODI oil exports fetcher."""
|
||||
params = {"country": "saudi_arabia,united_states", "product": "crude_oil", **DATES}
|
||||
fetcher = JodiOilExportsFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("jodi_cassette")
|
||||
def test_jodi_oil_stocks_fetcher(credentials=test_credentials):
|
||||
"""Test the JODI oil stocks fetcher."""
|
||||
params = {"country": "united_states", "unit": "kbbl", **DATES}
|
||||
fetcher = JodiOilStocksFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("jodi_cassette")
|
||||
def test_jodi_gas_balance_fetcher(credentials=test_credentials):
|
||||
"""Test the JODI gas balance fetcher."""
|
||||
params = {"country": "united_states", "unit": "m3", **DATES}
|
||||
fetcher = JodiGasBalanceFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("jodi_cassette")
|
||||
def test_jodi_gas_production_fetcher(credentials=test_credentials):
|
||||
"""Test the JODI gas production fetcher."""
|
||||
params = {"country": "united_states,norway", "unit": "m3", **DATES}
|
||||
fetcher = JodiGasProductionFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("jodi_cassette")
|
||||
def test_jodi_gas_demand_fetcher(credentials=test_credentials):
|
||||
"""Test the JODI gas demand fetcher."""
|
||||
params = {"country": "united_states", "measure": "observed", **DATES}
|
||||
fetcher = JodiGasDemandFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("jodi_cassette")
|
||||
def test_jodi_gas_imports_fetcher(credentials=test_credentials):
|
||||
"""Test the JODI gas imports fetcher."""
|
||||
params = {"country": "united_states", "flow": "pipeline", **DATES}
|
||||
fetcher = JodiGasImportsFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("jodi_cassette")
|
||||
def test_jodi_gas_exports_fetcher(credentials=test_credentials):
|
||||
"""Test the JODI gas exports fetcher."""
|
||||
params = {"country": "qatar", "flow": "lng", **DATES}
|
||||
fetcher = JodiGasExportsFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("jodi_cassette")
|
||||
def test_jodi_gas_stocks_fetcher(credentials=test_credentials):
|
||||
"""Test the JODI gas stocks fetcher."""
|
||||
params = {"country": "united_states", "unit": "m3", **DATES}
|
||||
fetcher = JodiGasStocksFetcher()
|
||||
result = fetcher.test(params, credentials)
|
||||
assert result is None
|
||||
159
openbb_platform/providers/jodi/tests/test_jodi_router.py
Normal file
159
openbb_platform/providers/jodi/tests/test_jodi_router.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""Tests for the JODI router commands."""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from openbb_jodi import gas_router, oil_router
|
||||
|
||||
COMMANDS = [
|
||||
(oil_router, oil_router.balance),
|
||||
(oil_router, oil_router.production),
|
||||
(oil_router, oil_router.demand),
|
||||
(oil_router, oil_router.demand_by_product),
|
||||
(oil_router, oil_router.imports),
|
||||
(oil_router, oil_router.exports),
|
||||
(oil_router, oil_router.stocks),
|
||||
(gas_router, gas_router.balance),
|
||||
(gas_router, gas_router.production),
|
||||
(gas_router, gas_router.demand),
|
||||
(gas_router, gas_router.imports),
|
||||
(gas_router, gas_router.exports),
|
||||
(gas_router, gas_router.stocks),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("module, command", COMMANDS)
|
||||
def test_model_backed_commands(module, command):
|
||||
"""Each command delegates to OBBject.from_query(OpenBBQuery(...))."""
|
||||
sentinel = object()
|
||||
|
||||
with (
|
||||
patch.object(module, "OBBject") as mock_obbject,
|
||||
patch.object(module, "OpenBBQuery") as mock_query,
|
||||
):
|
||||
mock_obbject.from_query = AsyncMock(return_value=sentinel)
|
||||
|
||||
result = asyncio.run(
|
||||
command(
|
||||
cc=None,
|
||||
provider_choices=None,
|
||||
standard_params=None,
|
||||
extra_params=None,
|
||||
)
|
||||
)
|
||||
|
||||
assert result is sentinel
|
||||
assert mock_query.called
|
||||
mock_obbject.from_query.assert_awaited_once()
|
||||
|
||||
|
||||
WIDGET_IDS = {
|
||||
"jodi_oil_balance_jodi_obb",
|
||||
"jodi_oil_production_jodi_obb",
|
||||
"jodi_oil_demand_jodi_obb",
|
||||
"jodi_oil_demand_by_product_jodi_obb",
|
||||
"jodi_oil_imports_jodi_obb",
|
||||
"jodi_oil_exports_jodi_obb",
|
||||
"jodi_oil_stocks_jodi_obb",
|
||||
"jodi_gas_balance_jodi_obb",
|
||||
"jodi_gas_production_jodi_obb",
|
||||
"jodi_gas_demand_jodi_obb",
|
||||
"jodi_gas_imports_jodi_obb",
|
||||
"jodi_gas_exports_jodi_obb",
|
||||
"jodi_gas_stocks_jodi_obb",
|
||||
}
|
||||
|
||||
|
||||
def test_apps_json():
|
||||
"""The served app template is a valid Workspace apps.json fragment."""
|
||||
from openbb_jodi.jodi_router import jodi_apps
|
||||
|
||||
apps = asyncio.run(jodi_apps())
|
||||
assert isinstance(apps, list)
|
||||
for app in apps:
|
||||
assert app["name"]
|
||||
assert app["description"]
|
||||
assert app["img"]
|
||||
assert isinstance(app["allowCustomization"], bool)
|
||||
assert isinstance(app["groups"], list)
|
||||
assert isinstance(app["prompts"], list)
|
||||
laid_out: set[str] = set()
|
||||
for tab_id, tab in app["tabs"].items():
|
||||
assert tab["id"] == tab_id
|
||||
assert "name" in tab
|
||||
occupied: set[tuple[int, int]] = set()
|
||||
for item in tab["layout"]:
|
||||
assert item["i"] in WIDGET_IDS
|
||||
laid_out.add(item["i"])
|
||||
assert item["x"] >= 0 and item["x"] + item["w"] <= 40
|
||||
assert item["y"] >= 0 and item["h"] >= 4
|
||||
cells = {
|
||||
(col, row)
|
||||
for col in range(item["x"], item["x"] + item["w"])
|
||||
for row in range(item["y"], item["y"] + item["h"])
|
||||
}
|
||||
assert not cells & occupied, f"widgets overlap in tab {tab_id}"
|
||||
occupied |= cells
|
||||
assert laid_out == WIDGET_IDS # every view is on a dashboard
|
||||
for group in app["groups"]:
|
||||
assert group["name"].startswith("Group ")
|
||||
assert group["type"] in {"param", "endpointParam", "ticker"}
|
||||
for widget_id in group["widgetIds"]:
|
||||
assert widget_id in laid_out
|
||||
item = next(
|
||||
item
|
||||
for tab in app["tabs"].values()
|
||||
for item in tab["layout"]
|
||||
if item["i"] == widget_id
|
||||
)
|
||||
assert group["name"] in item.get("groups", [])
|
||||
|
||||
|
||||
def test_router_lifespan(monkeypatch):
|
||||
"""The router lifespan starts the cache prefetch at application startup."""
|
||||
from fastapi import FastAPI
|
||||
|
||||
from openbb_jodi.jodi_router import router
|
||||
from openbb_jodi.utils import helpers
|
||||
|
||||
prefetches: list[bool] = []
|
||||
monkeypatch.setattr(helpers, "start_prefetch", lambda: prefetches.append(True))
|
||||
app = FastAPI()
|
||||
# include_router merges the jodi lifespan into the application lifespan.
|
||||
app.include_router(router.api_router)
|
||||
|
||||
async def startup():
|
||||
async with app.router.lifespan_context(app):
|
||||
assert prefetches == [True]
|
||||
|
||||
asyncio.run(startup())
|
||||
|
||||
|
||||
def test_router_paths():
|
||||
"""Each view is mounted under its oil or gas sub-router."""
|
||||
from openbb_jodi.jodi_router import router
|
||||
|
||||
# The oil and gas sub-routers, plus the apps.json route.
|
||||
assert len(router.api_router.routes) == 3
|
||||
paths = {
|
||||
route.path
|
||||
for sub_router in (oil_router.router, gas_router.router)
|
||||
for route in sub_router.api_router.routes
|
||||
}
|
||||
assert paths == {
|
||||
"/oil/balance",
|
||||
"/oil/production",
|
||||
"/oil/demand",
|
||||
"/oil/demand_by_product",
|
||||
"/oil/imports",
|
||||
"/oil/exports",
|
||||
"/oil/stocks",
|
||||
"/gas/balance",
|
||||
"/gas/production",
|
||||
"/gas/demand",
|
||||
"/gas/imports",
|
||||
"/gas/exports",
|
||||
"/gas/stocks",
|
||||
}
|
||||
218
openbb_platform/providers/jodi/tests/test_models.py
Normal file
218
openbb_platform/providers/jodi/tests/test_models.py
Normal file
@@ -0,0 +1,218 @@
|
||||
"""Test the JODI model validators, views, and error branches."""
|
||||
|
||||
import asyncio
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.provider.utils.errors import EmptyDataError
|
||||
from pydantic import ValidationError
|
||||
|
||||
from openbb_jodi.models.gas_balance import JodiGasBalanceFetcher
|
||||
from openbb_jodi.models.gas_demand import JodiGasDemandFetcher, JodiGasDemandQueryParams
|
||||
from openbb_jodi.models.gas_imports import JodiGasImportsFetcher
|
||||
from openbb_jodi.models.oil_balance import (
|
||||
JodiOilBalanceFetcher,
|
||||
JodiOilBalanceQueryParams,
|
||||
)
|
||||
from openbb_jodi.models.oil_demand_by_product import JodiOilDemandByProductFetcher
|
||||
from openbb_jodi.models.oil_production import (
|
||||
JodiOilProductionFetcher,
|
||||
JodiOilProductionQueryParams,
|
||||
)
|
||||
|
||||
DATES = {"start_date": date(2024, 1, 1), "end_date": date(2024, 3, 31)}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, expected",
|
||||
[
|
||||
(None, "united_states"),
|
||||
("", "united_states"),
|
||||
("SA", "saudi_arabia"),
|
||||
("Saudi Arabia", "saudi_arabia"),
|
||||
],
|
||||
)
|
||||
def test_single_country_validator(value, expected):
|
||||
assert JodiOilBalanceQueryParams(country=value).country == expected
|
||||
|
||||
|
||||
def test_single_country_validator_rejects_multiple():
|
||||
with pytest.raises(OpenBBError, match="Only one country"):
|
||||
JodiOilBalanceQueryParams(country="united_states,saudi_arabia")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, expected",
|
||||
[
|
||||
(None, None),
|
||||
("", None),
|
||||
("US", "united_states"),
|
||||
(["US", "Saudi Arabia"], "united_states,saudi_arabia"),
|
||||
("united_states,us", "united_states"),
|
||||
],
|
||||
)
|
||||
def test_multi_country_validator(value, expected):
|
||||
assert JodiOilProductionQueryParams(country=value).country == expected
|
||||
|
||||
|
||||
def test_country_validator_invalid():
|
||||
with pytest.raises(OpenBBError, match="Invalid country: 'atlantis'"):
|
||||
JodiOilProductionQueryParams(country="atlantis")
|
||||
|
||||
|
||||
def test_token_normalization():
|
||||
params = JodiOilBalanceQueryParams(product="Crude Oil", unit="KBD")
|
||||
assert params.product == "crude_oil"
|
||||
assert params.unit == "kbd"
|
||||
params = JodiGasDemandQueryParams(measure=None, unit=None)
|
||||
assert params.measure == "observed"
|
||||
assert params.unit == "m3"
|
||||
|
||||
|
||||
def test_token_invalid():
|
||||
with pytest.raises(ValidationError):
|
||||
JodiOilBalanceQueryParams(unit="furlongs")
|
||||
with pytest.raises(ValidationError):
|
||||
JodiOilProductionQueryParams(product="gasoline") # secondary-only product
|
||||
|
||||
|
||||
def _fetch(fetcher, params: dict):
|
||||
return asyncio.run(fetcher.fetch_data(params, {}))
|
||||
|
||||
|
||||
def test_start_after_end():
|
||||
with pytest.raises(OpenBBError, match="start_date must be"):
|
||||
_fetch(
|
||||
JodiOilBalanceFetcher,
|
||||
{"start_date": date(2024, 2, 1), "end_date": date(2024, 1, 1)},
|
||||
)
|
||||
|
||||
|
||||
def test_oil_dates_before_data():
|
||||
with pytest.raises(OpenBBError, match="The data starts in 2002"):
|
||||
_fetch(
|
||||
JodiOilProductionFetcher,
|
||||
{"start_date": date(2000, 1, 1), "end_date": date(2001, 12, 31)},
|
||||
)
|
||||
|
||||
|
||||
def test_gas_dates_before_data():
|
||||
with pytest.raises(OpenBBError, match="The data starts in 2009"):
|
||||
_fetch(
|
||||
JodiGasBalanceFetcher,
|
||||
{"start_date": date(2007, 1, 1), "end_date": date(2008, 12, 31)},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_download")
|
||||
def test_empty_result():
|
||||
with pytest.raises(EmptyDataError):
|
||||
_fetch(
|
||||
JodiOilProductionFetcher,
|
||||
{"start_date": date(2024, 4, 1), "end_date": date(2024, 12, 31)},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_download")
|
||||
def test_oil_balance_view():
|
||||
result = _fetch(
|
||||
JodiOilBalanceFetcher,
|
||||
{"country": "united_states", "product": "crude_oil", "unit": "kbbl", **DATES},
|
||||
)
|
||||
assert len(result.result) == 3
|
||||
row = result.result[0]
|
||||
assert row.production is not None
|
||||
assert row.closing_stocks is not None
|
||||
assert row.refinery_output is None # secondary-only flow
|
||||
assert result.metadata["country"] == "United States"
|
||||
assert result.metadata["product"] == "Crude oil"
|
||||
assert result.metadata["unit"] == "Thousand barrels"
|
||||
assert result.metadata["assessments"]["production"] is not None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_download")
|
||||
def test_oil_balance_secondary_product():
|
||||
result = _fetch(
|
||||
JodiOilBalanceFetcher,
|
||||
{"country": "united_states", "product": "gasoline", "unit": "kbd", **DATES},
|
||||
)
|
||||
row = result.result[0]
|
||||
assert row.refinery_output is not None
|
||||
assert row.demand is not None
|
||||
assert row.refinery_intake is None # primary-only flow
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_download")
|
||||
def test_oil_production_comparison_view():
|
||||
result = _fetch(JodiOilProductionFetcher, {**DATES})
|
||||
row = result.result[0].model_dump(exclude_none=True)
|
||||
# Columns are countries, alphabetical, after the date.
|
||||
# Russia is in the sample but reports no kb/d production values.
|
||||
assert list(row) == ["date", "saudi_arabia", "united_states"]
|
||||
assert result.metadata["flow"] == "Production"
|
||||
assert result.metadata["assessments"]["saudi_arabia"] is not None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_download")
|
||||
def test_gas_empty_result():
|
||||
with pytest.raises(EmptyDataError):
|
||||
_fetch(
|
||||
JodiGasBalanceFetcher,
|
||||
{
|
||||
"country": "norway",
|
||||
"start_date": date(2024, 4, 1),
|
||||
"end_date": date(2024, 12, 31),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_download")
|
||||
def test_oil_demand_by_product_view():
|
||||
result = _fetch(
|
||||
JodiOilDemandByProductFetcher, {"country": "united_states", **DATES}
|
||||
)
|
||||
row = result.result[0]
|
||||
assert row.gasoline is not None
|
||||
assert row.total_products is not None
|
||||
assert result.metadata["flow"] == "Demand"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_download")
|
||||
def test_gas_balance_view():
|
||||
result = _fetch(JodiGasBalanceFetcher, {"country": "norway", **DATES})
|
||||
row = result.result[0]
|
||||
assert row.production is not None
|
||||
assert row.pipeline_exports is not None
|
||||
assert result.metadata["product"] == "Natural gas"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_download")
|
||||
def test_gas_demand_measures_differ():
|
||||
observed = _fetch(
|
||||
JodiGasDemandFetcher,
|
||||
{"country": "united_states", "measure": "observed", **DATES},
|
||||
)
|
||||
calculated = _fetch(
|
||||
JodiGasDemandFetcher,
|
||||
{"country": "united_states", "measure": "calculated", **DATES},
|
||||
)
|
||||
assert observed.metadata["flow"] == "Gross inland deliveries (observed)"
|
||||
assert calculated.metadata["flow"] == "Gross inland deliveries (calculated)"
|
||||
obs = observed.result[0].model_dump(exclude_none=True)["united_states"]
|
||||
calc = calculated.result[0].model_dump(exclude_none=True)["united_states"]
|
||||
assert obs != calc
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_download")
|
||||
def test_gas_imports_flow_selection():
|
||||
total = _fetch(JodiGasImportsFetcher, {"country": "united_states", **DATES})
|
||||
pipeline = _fetch(
|
||||
JodiGasImportsFetcher,
|
||||
{"country": "united_states", "flow": "pipeline", **DATES},
|
||||
)
|
||||
assert total.metadata["flow"] == "Imports"
|
||||
assert pipeline.metadata["flow"] == "Pipeline imports"
|
||||
total_value = total.result[0].model_dump(exclude_none=True)["united_states"]
|
||||
pipeline_value = pipeline.result[0].model_dump(exclude_none=True)["united_states"]
|
||||
assert pipeline_value <= total_value
|
||||
3581
openbb_platform/providers/jodi/uv.lock
generated
vendored
Normal file
3581
openbb_platform/providers/jodi/uv.lock
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user