## Doof Backend (aka Munch Ease) 🍽️ FastAPI backend for collaborative meal planning, recipe wrangling, and grocery shopping. Household-scoped, JWT-secured, SQLite-fast. Bring your recipes, we’ll do the rest. ## Why you’ll love it - πŸš€ Fast and modern API with FastAPI + Pydantic v2 - 🏠 Household-scoped everything (recipes, meals, shopping) for clean multi-user isolation - πŸ” JWT access tokens + HttpOnly refresh cookie (Argon2 password hashing) - πŸ§ͺ 100% test-friendly: ephemeral SQLite, deterministic APIs, RFC7807 errors - 🧠 Ingredient NLP parsing with product matching - πŸ›’ One-click shopping lists: request meals or individual ingredients, dedup done for you - 🧾 OpenAPI on tap for your frontend and SDKs - πŸ”— Built-in recipe parsing from URLs (with BeautifulSoup + httpx) ## Stack - Runtime: Python 3.11+ - Web: FastAPI, Starlette - Data models: Pydantic v2 (camelCase JSON via custom `ApiModel`) - Database: SQLite (aiosqlite), schema bootstrapped in each `repository.py` - Auth: Custom HMAC-SHA256 JWTs + Argon2 password hashing - Parsing/Scraping: `ingredient-parser-nlp`, `beautifulsoup4`, `httpx` - Tooling: ruff (lint+format), mypy (typecheck), pytest, pre-commit, uvicorn ## Features at a glance - πŸ”‘ Auth: Register, login, refresh, logout; access bearer token + HttpOnly refresh cookie - 🏑 Households: Create, list, invite members; accept invitations via shareable links - 🧾 Recipes: Create, list, paginate, delete (hide) with actor attribution; parse-from-url helper - πŸ§ͺ Ingredients: NLP parse one or many lines; product matching baked in - 🍽️ Meals: Plan, get, update, delete, mark consumed; validate participants and recipes - πŸ›οΈ Shopping: Request meals and ingredients, view current list, purchase to a list, fetch past lists - 🩺 Health: `GET /healthz` returns a tiny β€œok” model for probes - πŸ“œ Errors: RFC7807 Problem Details everywhere, with tidy camelCase payloads ## Project structure - `main.py` β€” FastAPI app factory, routers, exception handlers, health, frontend proxy/static - `settings.py` β€” Environment-driven runtime settings (no external deps) - `security.py` β€” Minimal JWT utilities (HS256) + helpers - `db.py` β€” aiosqlite connect + `create()` bootstraps all domain tables - `api/` β€” HTTP surface (versioned under `/api/v1`) - STRICTLY NO SQL AT THIS LAYER! - `auth.py` β€” register, login, refresh, logout - `households.py` β€” create/list, members, invitations, scoped routes - `ingredients.py` β€” household-scoped NLP parsing - `recipes.py` β€” household-scoped list/get/create/delete, public utilities - `meals.py` β€” household-scoped CRUD + mark consumed - `shopping.py` β€” household-scoped current list, purchase, request/unrequest - `openapi.py` β€” OpenAPI augmentation (cookie auth, problem+json) - `deps.py` β€” DB/session, auth, household scoping, error helpers - Domain packages (models + repository + sql mutation + helpers): - `persons/` - `users/`, `households/`, `ingredients/`, `recipes/` (incl. `scraping.py`), `meals/`, `products/` (Coles/Woolworths helpers), `shopping/` - Strive to be useful as a fairly portal package independant of the http layer - `scripts/` β€” Utility scripts - `export_openapi.py` β€” writes `openapi.json` from the live app - `manual_parse_recipes.py` β€” Test parsing against a variety of sources - `tests/` β€” API and domain tests with fixtures and sample files ## Quickstart First time setup: ```bash make install ``` Run the dev server: ```bash make dev ``` Run tests: ```bash make test ``` Run all checks (lint, typecheck, tests, format check, OpenAPI export): ```bash make all-checks ``` ## Environment These are read from the environment (see `settings.py`): - `DOOF_DB` β€” SQLite file path (default `./data/doof.sqlite`) - `DOOF_PROD` β€” `true/false` controls frontend proxy vs. static serving (default `false`) - `FRONTEND_DEV_URL` β€” dev server to reverse-proxy in non-prod (default `http://localhost:8000/`) - `DOOF_JWT_ISSUER`, `DOOF_JWT_AUDIENCE` β€” JWT claims - `DOOF_JWT_ACCESS_TTL`, `DOOF_JWT_REFRESH_TTL` β€” TTLs in seconds (default 900/2592000) - `DOOF_JWT_ACCESS_SECRET_B64`, `DOOF_JWT_REFRESH_SECRET_B64` β€” base64 secrets (use in prod!) Dev convenience: if secrets aren’t provided, deterministic dev secrets are used. Don’t ship those. ## API surface (v1) - Base: `/api/v1` - Auth: `/auth/register`, `/auth/login`, `/auth/refresh`, `/auth/logout` - Households: `/households`, `/users/me/households`, `/households/{slug}/members`, `/households/{slug}/whoami`, invitations create/accept - Ingredients: `/households/{slug}/ingredients/parse` (single or batch parsing) - Recipes: `/households/{slug}/recipes` (list/paged, create), `/{id}` (get/delete), `/parse-from-url` - Meals: `/households/{slug}/meals` (create/update/delete/get/upcoming/mark-consumed) - Shopping: `/households/{slug}/shopping/current`, `/{listId}`, request/unrequest meals and ingredients, purchase lists - Health: `/healthz` Errors are consistent Problem Details (`application/problem+json`). Models serialize in camelCase. ## Make targets - `make install` β€” Create venv and install dependencies - `make dev` β€” Run dev server (`uvicorn main:app --reload`) - `make test` β€” Run tests (pytest -q) - `make format` β€” Format with ruff - `make lint` β€” Lint with ruff - `make typecheck` β€” Type check with mypy - `make openapi` β€” Export OpenAPI to `openapi.json` - `make all-checks` β€” Lint + typecheck + tests + format check + OpenAPI export - `make clean` β€” Remove venv and caches ## Development notes - Schema bootstrap: `db.create(conn)` calls each feature’s `repository.create` to make tables - Frontend integration: - Dev: requests for non-`/api/*` are reverse-proxied to `FRONTEND_DEV_URL` - Prod: static files served from `./front-dist` - Security: Argon2 password hashing; HS256 JWTs signed with your secrets; refresh token stored as HttpOnly cookie - DX niceties: camelCase JSON by default, strict validation, helpful error messages ## OpenAPI Generate the spec file used by the frontend and CI: ```bash make openapi ``` This writes `openapi.json` at the repo root. --- Built with love and leftovers. Hungry for issues and PRs. πŸ§‘β€πŸ³