PythiloLesson 11 of 15 · progress kept in this browser

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.

This lesson uses httpx, which Python in this browser does not include. Read it here, and run and check it in the Pythilo app on iPhone or iPad.