httpx MockTransport: test a Sume poll loop with no network
Pass httpx.MockTransport to httpx.Client to feed a Sume status poller canned replies, assert it stops on terminal and honors next_poll_after_seconds.

Short answer
Build the client with httpx.Client(transport=httpx.MockTransport(handler)), where handler takes a request and returns httpx.Response(200, json=...), as shown on the HTTPX transports page. Your poller runs unchanged, the test controls every reply, and no request ever leaves the machine.
Why this seam is enough
A Sume job poller has three behaviors worth testing: it keeps going while the job is not terminal, it waits as long as the server asked, and it stops the moment terminal is true. All three depend only on the JSON the status route returns, so a mock transport covers them.
Inject the sleep function rather than patching time. That makes the test instant and lets you assert on the exact waits.
| Field | Poller behavior |
|---|---|
| terminal | Stop when true |
| next_poll_after_seconds | Sleep this long before the next read, when present |
| sume_status | One of queued, processing, completed, failed, canceled |
| result_ready | Fetch /result only when true |
The test
The handler below returns one non-terminal reply and then a completed one. The assertion on waits proves the poller used the server's hint, not a hard-coded delay. The base_url means the poller can call relative paths, which is how it will run in production against api.sume.com.
import httpx
def poll(client, job_id, sleep):
while True:
s = client.get(f"/v1/jobs/{job_id}/status").json()
if s["terminal"]:
return s
sleep(s.get("next_poll_after_seconds") or 3)
def test_poll_stops_on_terminal():
replies = iter([
{"terminal": False, "next_poll_after_seconds": 2},
{"terminal": True, "sume_status": "completed"},
])
waits = []
transport = httpx.MockTransport(
lambda request: httpx.Response(200, json=next(replies)))
with httpx.Client(transport=transport,
base_url="https://api.sume.com") as client:
out = poll(client, "job_123", waits.append)
assert out["sume_status"] == "completed"
assert waits == [2]
test_poll_stops_on_terminal()
print("ok")
Failure cases to add
Once the happy path passes, add replies that mirror real errors. A handler can inspect request.url and return a 404 for a job the key does not own, since job reads are scoped to the member whose key created the job. It can return 429 with a retry-after header, and your retry layer should wait that long. It can return a failed terminal reply, and your code should read the error from the job record rather than ask for /result, which answers 409 job_not_completed for anything not completed.
Also test the stop condition you did not intend: a reply with no next_poll_after_seconds. The fallback delay then applies, and the loop must still end on terminal.
What this does not prove
A mock transport proves your logic, not the service. Keep one small live check for the contract, such as the unauthenticated GET /v1/health route, and let the mocked tests carry everything that would otherwise cost credits. Check your replies against the live OpenAPI document that Sume publishes, so that a field rename shows up as a failing comparison.
For the async variant, httpx also accepts a mock transport on its async client, and the same handler shape applies. The async submit and poll post shows the production loop that this test exercises.
Making the test strict
A canned iterator hides one mistake: if the poller reads more often than expected, next() raises StopIteration and the test fails with an unhelpful message. Prefer a handler that records each request and asserts on the count and the path. Also assert the header your client sends. The API accepts either Authorization or x-api-key, but a request with both is rejected with 401 unauthorized, so a test that inspects request.headers can prove your code sets exactly one.
Keep each test about one behavior. A test named for stopping on terminal should not also check retry-after handling; split them, and the failing name tells you what broke.
Running it in CI
Because the transport is in process, the suite needs no secrets and no outbound network. That means it can run on every pull request, including from forks, where secrets are normally withheld. Store the real key only in the job that runs your live smoke check, and read it from an environment variable there. The mocked tests should pass with the variable unset, which is itself a useful guard against code that reads the key at import time.
Sources
Related posts
More in Developers
- Idempotency-Key over 255 characters: hash long business keys
Sume accepts Idempotency-Key values up to 255 characters. Keep readable keys when short and fall back to a prefixed SHA-256 for long ones. Runnable Python.
- Idempotency-Key for a SaaS: customer, order and version
Derive a Format run's Idempotency-Key from customer id, order id, Format slug and a version you bump on purpose, so double clicks never make a second paid run.
- Ideogram 4 download: Hugging Face gate, login and first image
To run Ideogram 4 locally: accept the gate on Hugging Face, log in with hf, pip install the repo, run run_inference.py. The flags and the nf4 or fp8 choice.
- Image batch in Python: read ratelimit headers and retry-after
Sume can send ratelimit-limit, ratelimit-remaining, ratelimit-reset and retry-after on image calls. Back off on 429 in Python, and treat queue_full separately.
Written by Sume