Ports and adapters
Your domain needs exchange rates, and a provider serves them over HTTP. Define a port: the interface the domain wants, in its own words. Then write adapters that plug into it, one that calls the real API and a fake for tests.
from decimal import Decimal
from typing import Protocol
class Rates(Protocol):
def rate(self, base: str,
quote: str) -> Decimal: ...
class FakeRates:
def __init__(self, table):
self.table = table
def rate(self, base, quote):
return self.table[(base, quote)]
def convert(amount, base, quote, rates):
return amount * rates.rate(base, quote)
table = {("EUR", "USD"): Decimal("1.1")}
print(convert(Decimal(10), "EUR", "USD",
FakeRates(table)))The real adapter takes an httpx.Client instead of creating one. A test can give it a client with an in-memory transport and exercise the real parsing without a network:
import httpx
def api(request):
return httpx.Response(
200, json={"rate": "1.1"})
client = httpx.Client(
base_url="https://rates.test",
transport=httpx.MockTransport(api))
print(client.get("/rates/EUR/USD").json())Your task
Write HttpRates(client) whose rate(base, quote) requests GET /rates/{base}/{quote}, raises for an error status, and returns the "rate" field as an exact Decimal, and FakeRates(table) reading a dict keyed by (base, quote). Write convert(amount, base, quote, rates) returning the amount converted and rounded to cents, or the amount unchanged, without asking for a rate, when the currencies match.
The explanation opens here once every check passes.