Add a thin async ESI client (httpx, descriptive User-Agent, uniform errors, mockable transport) plus name<->id resolution. New read-only tools: - universe: resolve_names, get_item, get_system, get_station, get_region, get_route - market: list_hubs, get_market_price, get_market_orders, compare_hubs Trade-hub IDs (Jita/Amarr/Dodixie/Rens/Hek) are hardcoded constants. Market aggregation (best buy/sell, spread, order book) lives in pure helpers. 30 unit tests with mocked ESI; coverage 99%. SDE bulk-load deferred to a later phase. Closes #2 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
50 lines
1.3 KiB
Python
50 lines
1.3 KiB
Python
"""Shared test helpers: a mock ESI client backed by httpx.MockTransport."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from familiar import esi
|
|
|
|
Handler = Callable[[httpx.Request], httpx.Response]
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_esi():
|
|
"""Install a mock ESI client driven by a request handler.
|
|
|
|
Usage::
|
|
|
|
def test_x(mock_esi):
|
|
def handler(request):
|
|
return httpx.Response(200, json={...})
|
|
mock_esi(handler)
|
|
...
|
|
|
|
The shared client is reset after the test.
|
|
"""
|
|
|
|
def _make(handler: Handler) -> esi.EsiClient:
|
|
client = esi.EsiClient(transport=httpx.MockTransport(handler))
|
|
esi.set_client(client)
|
|
return client
|
|
|
|
yield _make
|
|
esi.set_client(None)
|
|
|
|
|
|
def route(request: httpx.Request, fixtures: dict[str, object]) -> httpx.Response:
|
|
"""Match a request against ``{substring_in_path: json_body}`` and respond.
|
|
|
|
The special key ``"/universe/ids/"`` is matched for the name-resolution POST.
|
|
A missing match returns 404 so tests fail loudly.
|
|
"""
|
|
path = request.url.path
|
|
for needle, body in fixtures.items():
|
|
if needle in path:
|
|
return httpx.Response(200, json=body)
|
|
return httpx.Response(404, json={"error": f"no fixture for {path}"})
|