API — practice-sessions router¶
backend/src/routers/practice_sessions.py (463 lines).
APIRouter(prefix="/practice-sessions", tags=["practice-sessions"])
(practice_sessions.py:50).
| Method | Path | Auth | Request | Response | Success | Errors |
|---|---|---|---|---|---|---|
| POST | /practice-sessions/ |
JWT | PracticeSessionCreate + optional Idempotency-Key header |
PracticeSessionResponse |
201 | 404 user_practice_not_found / 403 forbidden (inline split, practice_sessions.py:73-75); 403 stage_locked (:94); 400 chosen_option_key_required / chosen_option_key_not_in_catalog / mode_metadata_mismatch (:102,117,160); 404 practice_not_found / practice_session_not_found (:184,199); 409 idempotency_in_flight (:223) |
| GET | /practice-sessions/?user_practice_id= |
JWT + owned selection | PaginationParams |
Page[PracticeSessionResponse] or bare list |
200 | 404 / 403 |
| GET | /practice-sessions/insights |
JWT | — | PracticeInsightsResponse |
200 (with Cache-Control: private, max-age=60) |
— |
| GET | /practice-sessions/week-count |
JWT | — | WeekCountResponse |
200 | — |
Notes on create (practice_sessions.py:307-341):
- The ownership split is inlined (same order/exceptions as
require_owned_user_practice) becauseuser_practice_idarrives in the body — "FastAPI's DI cannot extract body fields into sub-dependencies … the IDOR matrix test sees the same 403 for cross-user calls" (practice_sessions.py:321-325). - Stage gating: assigning a practice to a future stage is allowed
for planning, but logging a real session there is 403
stage_locked, evaluated in the caller's timezone, "before any row or idempotency spend is written" (practice_sessions.py:327-330). - Idempotency (BUG-PRACTICE-007): with an
Idempotency-Key, a replay of the same(user_id, key)returns the recorded session without duplicating — backed bypracticesessionspend'sUNIQUE(user_id, idem_key), "so it holds across process restarts and workers — the database serialises the check-then-insert race" (practice_sessions.py:332-338; data-model/practice). - Mode metadata validation: the session's
mode_metadatamust match the resolved mode's discriminated-union schema and, for option-choice modes, name achosen_option_keypresent in the catalog config (practice_sessions.py:102-160).
Other endpoints:
- List runs the canonical 404→403 split before touching the sessions
table — cross-user calls "used to return an empty list (the
user_idfilter silently masked them)" (practice_sessions.py:350-359; envelope per BUG-INFRA-014). - Insights: one query fetches the last 60 days
(
_INSIGHTS_LOOKBACK_DAYS,practice_sessions.py:40), thendomain.practice_insights.build_insightsbuckets in memory; the private one-minuteCache-Controllets the frontend poll "without thrashing the DB" (practice_sessions.py:376-392; domain/practice-insights). - Week-count (BUG-PRACTICE-009): the week boundary derives from
User.timezone, "not the UTC equivalent" (practice_sessions.py:444-455).
DTOs: backend/src/schemas/practice.py,
schemas/practice_session_metadata.py.
Grounded in adepthood@fbc529d, 2026-07-31.