You build the FastAPI app that scene traces: POST /ask (JSON),
GET /stream (SSE from a stub model), /healthz that never touches
the model and /readyz that gates on it, all wired through an
X-Request-Id middleware that puts the id in a ContextVar — the
same idea as py-09's
request_scope, now scoped to one asyncio task per HTTP request instead of one
thread per CLI call. This exact service is what goes to Fargate on day 16 and becomes the
RAG API later. It runs on your Mac rather than in the browser because Pyodide has no
FastAPI, no real ASGI server, and no Docker.
reqctx.request_scope(request_id) / current_request_id() — a
ContextVar holding one id per asyncio task, "-" outside any
request.RequestIdMiddleware — a raw ASGI middleware, not
@app.middleware("http"): it reads an incoming X-Request-Id (or
mints a uuid.uuid4()), wraps the entire downstream call in
request_scope(...), and patches the response header by wrapping
send.create_app(model) — POST /ask (pydantic body, 422 on a missing
prompt); GET /stream (five event: token SSE frames then
event: done, real text/event-stream); GET /healthz
(always 200, never calls model); GET /readyz (503 until
model.loaded, 200 after).The tests are ordinary pytest and ship in the public folder with the starter — read them first; the names below are the check list. Solutions are not published.
Needs git. uv installs the right Python itself, so nothing else is required.
# once, anywhere on your machine
git clone https://github.com/theDocWho/ai-ml-roadmap.git
cd ai-ml-roadmap
No git? Download the ZIP, unzip it, and cd into the unzipped folder instead.
From the repo root:
# one-time: uv (https://docs.astral.sh/uv/) manages the venv and pins Python ≥ 3.12 cd exercises/py-10-fastapi-service && uv sync && uv run pytest -q # once the tests pass: talk to it for real uv run uvicorn askapi.main:app --port 8000 & curl -s -X POST localhost:8000/ask -H 'content-type: application/json' -d '{"prompt": "hi"}' curl -sN localhost:8000/stream?prompt=hi curl -si localhost:8000/readyz # 503 for the first ~3 s — main.py load()s the stub on a timer, then 200 # the same bar the reference solution clears uv run ruff check . && uv run mypy src
Done when uv run pytest -q prints 9 passed — or
8 passed, 1 skipped without a Docker daemon running (the one Docker test skips itself
rather than failing when it can't reach one). A skip elsewhere is not a pass: only the
Docker-build check gets that exception. The untouched starter fails all nine. The line also
says 3 deselected: the live checks in integration/, which
cloud-07 runs against the deployed service with
BASE_URL=http://<alb-dns> uv run pytest -q -m integration — not part of the nine.
test_ask_returns_200_with_answer_and_request_id — a valid prompt gets a 200
with a non-empty answer and a request_id.test_ask_missing_prompt_returns_422 — an empty JSON body 422s.test_stream_sets_event_stream_content_type_and_five_tokens_then_done —
content-type starts text/event-stream; exactly five
event: token frames, then event: done.test_stream_delivers_chunks_incrementally_not_buffered — the stub's
emitted counter is still under 5 the moment the app sends its first body chunk.test_healthz_ok_and_readyz_503_until_loaded — /healthz stays
200 throughout; /readyz is 503 before model.load(), 200 after.test_incoming_request_id_is_echoed_in_response — a caller-supplied
X-Request-Id comes back unchanged, header and body both.test_generated_request_id_is_valid_uuid4 — with no incoming header, the
minted id matches a uuid4 shape, and the header matches the body.test_log_line_carries_same_request_id_as_response — caplog
catches a log record whose request_id equals the response's.test_dockerfile_builds_and_serves_healthz (slow, needs docker) — the image
builds and the running container answers /healthz with 200.This is self-attestation — the site cannot see your terminal, so the box and the button are you telling The Path the suite went green on your machine.
request_id comes back as "-" —
the ContextVar was never set while the route ran: the
await self.app(...) call sits outside the with request_scope(rid):
block. Everything downstream — the route, its log line, every streamed chunk — runs inside
that one await, so it goes inside the with.test_stream_delivers_chunks_incrementally_not_buffered is red — look
for a list comprehension or list(...) around model.stream(...)
before the yields start; that exhausts the generator up front./readyz never 503s — a handler that wants to change the status code
needs response: Response as a parameter and
response.status_code = 503; returning a dict alone always answers 200.X-Request-Id isn't echoed — scope["headers"] is a list
of (bytes, bytes) pairs with lower-cased keys; compare against
b"x-request-id", not the string.uv run pytest -q -x --tb=short stops at the first
failure and shows the assertion that tripped; the message names the behaviour, not the fix.