mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 04:31:13 +00:00
* add the JODI oil and gas database as a provider/router extension * lint fixes * add grouping to apps.json --------- Co-authored-by: deeleeramone <>
204 lines
7.1 KiB
Python
204 lines
7.1 KiB
Python
"""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
|