reading-list / docs /contract.md
pardeep-singh's picture
Deploy polished warm-library UI from main b09a3f3
eab7052 verified
|
Raw History Blame Contribute Delete
9 kB

Reading List — Contract

Source of truth for all workers. Only the lead edits this file. If something here is ambiguous or you need a file you don't own: STOP and report it in your HANDOFF.

Stack

  • Python 3.12, FastAPI, uvicorn. Tooling: uv only (uv sync, uv run ...).
  • Tests: pytest + FastAPI TestClient (backed by httpx2). Fixture client in tests/conftest.py gives a fresh app, and therefore a fresh empty store, per test.
  • In-memory storage, no auth, no frontend frameworks. No new dependencies without the lead.
  • Deploy: Hugging Face Docker Space, port 7860, python:3.12-slim base.

Data

Status

Exactly these three string values (app.models.Status, a StrEnum):

Enum member Wire value
Status.TO_READ "to-read"
Status.READING "reading"
Status.DONE "done"

Anything else is rejected with 422, in request bodies and in the status query parameter.

Book (response)

{"id": 1, "title": "Dune", "author": "Frank Herbert", "status": "reading"}
  • id: int, auto-incremented from 1 per store, never reused.
  • title: string, stored after trimming.
  • author: string or null.
  • status: a Status value.

BookCreate (POST body) — validation owned by add

Field Required Rules
title yes Must be a string. Leading/trailing whitespace is trimmed; after trimming it must be 1–200 characters (so "" and " " are rejected).
author no String or null. Trimmed; empty after trimming → stored as null. Max 200 characters after trimming.
status no One of the Status values. Defaults to "to-read".

Unknown fields are ignored (FastAPI default). Duplicate titles are allowed.

BookStatusUpdate (PATCH body)

{"status": "done"}

status is required and must be a Status value.

BookStats

{"total": 3, "by_status": {"to-read": 1, "reading": 0, "done": 2}}

All three by_status keys are always present, including zero counts. total equals the sum.

Endpoints

Routers declare full paths (e.g. @router.post("/api/books")); app/main.py includes them with no prefix. Routers get the store via Depends(get_store) from app/store.py.

POST /api/books — owner: add

Request:

{"title": "  Dune ", "author": "Frank Herbert", "status": "reading"}

Response 201:

{"id": 1, "title": "Dune", "author": "Frank Herbert", "status": "reading"}

Minimal request {"title": "Dune"} → 201 {"id": 1, "title": "Dune", "author": null, "status": "to-read"}.

Errors: 422 for missing/blank/too-long/non-string title, too-long author, invalid status, or a malformed JSON body.

GET /api/books — owner: query

Query parameter: status (optional, a Status value).

  • GET /api/books → 200, all books.
  • GET /api/books?status=reading → 200, only books with that status.

Response 200: always a JSON array in insertion order (ascending id); [] when empty.

[{"id": 1, "title": "Dune", "author": "Frank Herbert", "status": "reading"}]

Errors: 422 if status is not a Status value.

PATCH /api/books/{id} — owner: query

Request:

{"status": "done"}

Response 200: the updated Book.

{"id": 1, "title": "Dune", "author": "Frank Herbert", "status": "done"}

Errors: 404 {"detail": "Book not found"} if no book has that id; 422 if the body is missing status, status is invalid, or id is not an integer.

GET /api/stats — owner: query

Response 200: BookStats (see above). An empty store gives {"total": 0, "by_status": {"to-read": 0, "reading": 0, "done": 0}}.

GET / — owner: ui

Response 200, Content-Type: text/html, the contents of app/static/index.html (served with FileResponse).

Error formats

FastAPI defaults, no custom exception handlers.

422 (validation):

{"detail": [{"type": "string_too_short", "loc": ["body", "title"], "msg": "String should have at least 1 character", "input": ""}]}

Clients should rely on detail being a list with loc and msg; exact type/msg text is not part of the contract.

404:

{"detail": "Book not found"}

UI rule: if detail is a list, show detail[0].msg (optionally prefixed by the last loc element); if it's a string, show it as-is.

Store API (app/store.py)

One BookStore per app, created in create_app() and stored on app.state.store. Every public method must do its work inside with self._lock:. State: self._books: dict[int, Book], self._next_id: int (starts at 1).

Method Owner Behaviour
add(data: BookCreate) -> Book add Assigns the next id, stores and returns the Book. data is already validated.
list_books(status: Status | None = None) -> list[Book] query All books, or only those with status, ascending id.
update_status(book_id: int, status: Status) -> Book | None query Updates and returns the Book; returns None if the id is missing (router maps to 404).
counts() -> BookStats query Total and per-status counts, all three keys present.

UI (app/static/index.html)

One page, inline CSS and JS, no frameworks, no build step. Uses only the endpoints above.

  • Add form: title (required), author (optional), status select (default "to-read").
  • Book list, with a status filter (All / to-read / reading / done).
  • Per-row status select that sends PATCH /api/books/{id} on change.
  • Stats line from GET /api/stats, refreshed after every add/update.
  • Feedback area with aria-live="polite": a success message after add/update, and an error message (per the UI rule above) on failure. Labels on all form controls.
  • Display labels are presentation only: the UI may show "To read" / "Reading" / "Done", but always sends and filters by the exact Status wire values.
  • The status filter may be a select or an accessible radio-group of tabs; filter counts come from GET /api/stats (totals, independent of the active filter).
  • The add form's status control may likewise be a select or an accessible radio group (name="status", wire values, default to-read).

File ownership

Owner Files
lead docs/*, CLAUDE.md, pyproject.toml, uv.lock, .python-version, app/__init__.py, app/api/__init__.py, app/main.py, app/models.py (except BookCreate validation), app/store.py (__init__, get_store), tests/conftest.py, tests/test_boot.py
add BookCreate validation in app/models.py, BookStore.add in app/store.py, app/api/add.py, tests/test_add.py
query BookStore.list_books, BookStore.update_status, BookStore.counts in app/store.py, app/api/query.py, tests/test_query.py
ui app/static/index.html, app/api/ui.py, tests/test_ui.py
tests tests/test_contract.py
deploy Dockerfile, .dockerignore, README.md, scripts/smoke.sh

Notes:

  • add and query both edit app/store.py, but only their own method bodies. Don't reformat or reorder anything else; the lead reconciles at merge.
  • tests: tests/test_contract.py is black-box (HTTP only, via the client fixture) and covers this whole document. On the skeleton it is expected to fail (routes return 404/405 until add and query are merged). Commit the red tests and paste the failing output in the HANDOFF; do not implement features to make them pass. This is the project's red→green demonstration.
  • deploy: README.md must start with HF Space frontmatter (sdk: docker, app_port: 7860). The Dockerfile uses uv (uv sync --frozen --no-dev), runs as a non-root user with uid 1000, and starts uvicorn app.main:app --host 0.0.0.0 --port 7860. scripts/smoke.sh takes BASE_URL (default http://localhost:7860), exercises every endpoint including one 422 and one 404, and exits non-zero on any failure. Deploy must not push anywhere; only the lead touches Hugging Face or git remotes.

Ports

Run locally with uv run uvicorn app.main:app --port <N> (app.main exposes app):

Worker Port
add 8001
query 8002
ui 8003
deploy 7860 (Docker)

Workflow

  • Each worker works in its own git worktree on branch worker/<name>, branched from main. Run uv sync first.
  • TDD: failing test first, run it and show the expected failure, minimal implementation, then uv run pytest -q. Commit after each red→green. Small conventional commits.
  • Merge order (lead): tests → add → query → ui → deploy.

HANDOFF format

Every worker ends with exactly this:

## HANDOFF — <worker> @ worker/<worker> <short-sha>
Files changed: <list>
Provides: <what now works: endpoints / functions>
Assumptions: <any interpretation of this contract>
Test command + output: `uv run pytest -q`
<pasted output>
Gaps / blockers: <ambiguities, files needed but not owned, or "none">