Spaces:
Sleeping
Sleeping
|
Download docs/contract.md from pardeep-singh/reading-list: direct link, hf CLI and curl.
- Browser
- Download file 9 kB
-
https://huggingface.co/spaces/pardeep-singh/reading-list/resolve/main/docs/contract.md
- Command line
-
hf download hf://spaces/pardeep-singh/reading-list/docs/contract.md
-
curl -L -o contract.md https://huggingface.co/spaces/pardeep-singh/reading-list/resolve/main/docs/contract.md
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) | |
| ```json | |
| {"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) | |
| ```json | |
| {"status": "done"} | |
| ``` | |
| `status` is required and must be a Status value. | |
| ### BookStats | |
| ```json | |
| {"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: | |
| ```json | |
| {"title": " Dune ", "author": "Frank Herbert", "status": "reading"} | |
| ``` | |
| Response **201**: | |
| ```json | |
| {"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. | |
| ```json | |
| [{"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: | |
| ```json | |
| {"status": "done"} | |
| ``` | |
| Response **200**: the updated Book. | |
| ```json | |
| {"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): | |
| ```json | |
| {"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**: | |
| ```json | |
| {"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"> | |
| ``` | |