feat: FastAPI scaffold + GroupMe webhook handler #10
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "4-fastapi-scaffold"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Scaffolds the entire FastAPI application with a GroupMe webhook callback handler, health endpoints, pydantic-settings config, multi-stage Dockerfile, Woodpecker CI pipeline, and 8 passing tests. The callback validates group allowlist, ignores bot messages, logs user messages, and returns 200. No AI logic yet -- that is wired in ticket #6.
Changes
app/__init__.py-- empty package initapp/config.py-- pydantic BaseSettings with all env vars (GroupMe, Anthropic, Keycloak, basketball-api)app/main.py-- FastAPI app with lifespan, includes health and groupme routersapp/groupme.py-- POST /groupme/callback: parse body, validate group_id against allowlist, ignore bot sender_type, log sender_name + textapp/health.py-- GET /healthz (liveness, always 200), GET /health/ready (readiness, checks ANTHROPIC_API_KEY)Dockerfile-- multi-stage python:3.12-slim, gunicorn+uvicorn workers, EXPOSE 8000.woodpecker.yaml-- test on push/PR, build+push to Harbor on main, kustomize tag updaterequirements.txt-- fastapi, uvicorn, gunicorn, httpx, anthropic, pydantic-settings, pytest, pytest-asynciotests/__init__.py-- empty package inittests/test_groupme.py-- 8 tests covering valid message, bot ignore, wrong group, malformed payload, invalid JSON, healthz, readiness with/without keyTest Plan
pytest tests/ -v-- all 8 tests passdocker build .Review Checklist
Related
Related Notes
project-westside-ai-assistant-- parent projectarch-domain-westside-ai-assistant-- A1: GroupMe Webhook HandlerQA Review -- PR #10 (Parent Issue #4)
Spec Compliance
Checked every file target and acceptance criterion from issue #4:
POST /groupme/callbackvalid payload returns 200, logs messageGET /healthzreturns 200GET /health/readyreturns 200 when config loadedFindings
Nit 1: Unused
import osin tests/test_groupme.py (line 3)import osis never used. Should be removed.Nit 2: Readiness probe returns 200 even when not ready
GET /health/readyreturns HTTP 200 with{"status": "not_ready"}when ANTHROPIC_API_KEY is missing. Kubernetes readiness probes typically expect a non-2xx status (e.g., 503) to mark the pod as not ready. The current implementation means k8s will consider the pod ready even when config is incomplete. Consider returning 503 for the not-ready case. This is a nit for this ticket since the issue spec says "check that config loaded" without specifying the HTTP status for the failure path -- but it will need to be addressed before k8s deployment.Verification
pytest tests/ -v-- 8/8 passeddocker build .-- image built successfullyVERDICT: APPROVED with 2 nits
PR #10 Review
DOMAIN REVIEW
Tech stack: Python 3.12 / FastAPI / Pydantic Settings / Gunicorn+Uvicorn / Docker multi-stage / Woodpecker CI
app/config.py -- Clean pydantic-settings usage.
allowed_group_idsproperty correctly parses comma-separated env var with whitespace stripping and empty-string filtering. No hardcoded secrets; all sensitive values sourced from environment.app/groupme.py -- Webhook handler correctly returns 200 on all code paths (GroupMe expects this -- non-200 causes retries and eventual callback deregistration). Manual
request.json()parse with blanketexceptis appropriate here since we must never crash on malformed GroupMe payloads. Group allowlist validation and bot-message filtering are clean.app/health.py -- Standard liveness/readiness probe pattern. Liveness always 200, readiness gates on
ANTHROPIC_API_KEYpresence. Correct use ofJSONResponsewith explicit 503 for not-ready state.app/main.py -- Lifespan context manager, structured logging config, router includes. The
settingsimport withnoqa: F401ensures config validation runs at startup (fails fast on bad env). Clean.Dockerfile -- Multi-stage build correctly separates dependency install from runtime. Copies
site-packagesandbinfrom builder stage. Gunicorn with UvicornWorker class and 2 workers is a solid production default.tests/test_groupme.py -- 8 tests covering: valid message, bot ignore, wrong group, empty payload, invalid JSON, liveness, readiness (with and without key). The
importlib.reloadpattern in theclientfixture is necessary to pick up monkeypatched env vars sincesettingsis a module-level singleton. Coverage is solid for a scaffold..woodpecker.yaml -- Clone uses internal Forgejo service URL. Test step installs deps and runs pytest. Build uses kaniko with Harbor secrets from Woodpecker secret store. Kustomize tag update downloads the shared script from pal-e-platform with retry logic. Pipeline structure follows established patterns.
BLOCKERS
None.
NITS
Test/prod dependencies mixed (
requirements.txtL7-8):pytestandpytest-asyncioare bundled with production dependencies. The Docker image ships with test frameworks installed. Consider splitting intorequirements.txt(prod) andrequirements-dev.txt(test) so the runtime image stays lean.Permissive-by-default allowlist (
app/groupme.pyL33): WhenGROUPME_ALLOWED_GROUP_IDSis empty (the default),allowed_group_idsreturns[], and theif allowed and group_id not in allowedcheck passes all groups through. This is likely intentional for dev convenience, but a code comment documenting "empty allowlist = accept all" would prevent future confusion.basketball_api_urldefault port conflict (app/config.pyL9): Defaults tohttp://localhost:8000, which is the same port gunicorn binds to. Not a runtime issue (will be overridden in k8s), but the default is misleading for local development.Readiness probe scope (
app/health.pyL20): Only gates onANTHROPIC_API_KEY. Other required config for the app to function (e.g.,GROUPME_BOT_ID,GROUPME_ALLOWED_GROUP_IDS) is not checked. Acceptable for scaffold, but as more features land (ticket #6, #7), readiness should validate all critical config.Module-level singleton reload fragility (
tests/test_groupme.py): Theimportlib.reloadchain (config -> groupme -> health -> main) is necessary but order-sensitive. If a future module importssettingsat the top level, it must be added to the reload chain or tests will use stale config. A brief docstring on theclientfixture explaining the reload order requirement would help future contributors.SOP COMPLIANCE
4-fastapi-scaffoldfollows{issue-number}-{kebab-case-purpose}project-westside-ai-assistant,arch-domain-westside-ai-assistantfrom_secretPROCESS OBSERVATIONS
importlib.reloadtest pattern works but will accumulate friction as the module graph grows. Worth considering a fixture that constructsSettingsdirectly (bypassing the singleton) for future test files.VERDICT: APPROVED