feat(WP-07): Card State API endpoints (#72) #108
No reviewers
Labels
No Label
ai-changes-requested
ai-failed
ai-merged
ai-pr-opened
ai-reviewed
ai-reviewing
ai-reviewing
ai-working
bug
enhancement
evolution
performance
phase-0
phase-1a
phase-1b
phase-1c
phase-1d
security
tech-debt
todo
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: cal/paper-dynasty-database#108
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/wp-07-card-state-api"
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?
Closes #72
Summary
GET /api/v2/teams/{team_id}/evolutions— paginated list of a team's evolution card states with filters (card_type, tier, page, per_page)GET /api/v2/evolution/cards/{card_id}— single card state with card_id resolved to (player_id, team_id) via Card table joinnext_threshold(null if fully evolved)_build_card_state_response()shared helper avoids duplication between endpointstest_evolution_state_api.pyTest plan
🤖 Generated with Claude Code
Add two endpoints for reading EvolutionCardState: GET /api/v2/teams/{team_id}/evolutions - Optional filters: card_type, tier - Pagination: page / per_page (default 10, max 100) - Joins EvolutionTrack so card_type filter is a single query - Returns {count, items} with full card state + threshold context GET /api/v2/evolution/cards/{card_id} - Resolves card_id -> (player_id, team_id) via Card table - Duplicate cards for same player+team share one state row - Returns 404 when card missing or has no evolution state Both endpoints: - Require bearer token auth (valid_token dependency) - Embed the EvolutionTrack in each item (not just the FK id) - Compute next_threshold: threshold for tier above current (null at T4) - Share _build_card_state_response() helper in evolution.py Also cleans up 30 pre-existing ruff violations in teams.py that were blocking the pre-commit hook: F541 bare f-strings, E712 boolean comparisons (now noqa where Peewee ORM requires == False/True), and F841 unused variable assignments. Tests: tests/test_evolution_state_api.py — 10 integration tests that skip automatically without POSTGRES_HOST, following the same pattern as test_evolution_track_api.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>AI Code Review
Files Reviewed
app/routers_v2/evolution.py(modified — new_build_card_state_responsehelper andGET /evolution/cards/{card_id}endpoint)app/routers_v2/teams.py(modified — newGET /{team_id}/evolutionsendpoint + style cleanup)tests/test_evolution_state_api.py(added — 10 integration tests)Findings
Correctness
get_card_state: Card lookup viaCard.get_by_id(card_id)then EvolutionCardState JOIN is correct. Thecard.player_id/card.team_idraw FK accessor pattern is valid (Peewee auto-generates_idsuffix for FK fields) — same pattern verified in PR #106.list_team_evolutions: Peewee'squery.count()wraps the full query (including the JOIN and WHERE clauses) in a subquery, so thetotalcount correctly reflects all applied filters before slicing with.offset().limit()._NEXT_THRESHOLD_ATTRlookup: Handles all valid tiers (0-4) explicitly. For out-of-range tier values,dict.get()returnsNone(Python default), which is falsy, sogetattr(track, next_attr)is never called — silently returnsNone. Acceptable behaviour given the DB constraint on tier values._build_card_state_responseshared helper: Correctly called from both endpoints. The cross-router lazy import inteams.py(from ..routers_v2.evolution import _build_card_state_response) follows the lazy import pattern used throughout the codebase and avoids circular imports at module load time.lc_id/r_queryremovals:lc_idwas only referenced in a commented-outsheets.post_new_cardscall — safe to remove.r_query = Team.update(...)was an unused assignment;.execute()is called directly now — correct.Security
valid_token→ 401) is present on both new endpoints, consistent with all other endpoints in the router.[REDACTED]— no credential exposure. Pattern matches the project standard.card_idandteam_idare integer path/query params — FastAPI validates types before the handler runs. No injection vector.Style & Conventions
noqa: E712annotations on Peewee boolean comparisons (== True/== False) match the PostgreSQL-compatible pattern documented in the existing code and avoid ruff false positives.teams.py(no interpolation in the string) is correct and reduces noise.Suggestions
pg_connfixture is referenced in bothtest_evolution_state_api.pyand the pre-existingtest_evolution_track_api.pybut is not defined intests/conftest.py. This is a pre-existing gap (noted in PR #84 review). WhenPOSTGRES_HOSTis set, all integration tests in both files will fail withfixture 'pg_conn' not found. Thepg_connfixture (asession-scoped psycopg2 connection) should be added toconftest.py. This is a follow-up item, not a new regression introduced by this PR.list_team_evolutionsappliescard_typeandtierWHERE clauses after the base query object is built but beforecount()is called. The ordering is correct — filters are applied before count. Consider adding a comment to make the filter-then-count ordering explicit, since Peewee queries are lazy and the sequence matters.Verdict: APPROVED
Implementation is correct and follows all established patterns in this codebase. The shared
_build_card_state_responsehelper is a clean design that avoids duplication between the two endpoints. Auth, error handling, and response shapes are consistent with the rest of the evolution API. Thepg_connfixture gap is pre-existing and not introduced by this PR.Automated review by Claude PR Reviewer
AI Code Review
Files Reviewed
app/routers_v2/evolution.py(modified — new endpoints and helper)app/routers_v2/teams.py(modified — new list endpoint + incidental linting fixes)tests/test_evolution_state_api.py(added)Findings
Correctness
[BLOCKING]
pg_connfixture is undefined — tests will error, not skiptest_evolution_state_api.pyusespg_connas a fixture argument throughout (e.g.seeded_data(pg_conn),test_list_filter_by_card_type(client, seeded_data, pg_conn)), butpg_connis not defined intests/conftest.pyand is not defined in this file. The same gap exists intest_evolution_track_api.py(a pre-existing issue from WP-06), so this PR inherits and propagates a broken pattern.When
POSTGRES_HOSTis not set, tests are skipped via@_skip_no_pg, but when it is set, pytest will error withfixture 'pg_conn' not foundbefore any test body runs. This means the 10 integration tests cannot pass in CI. Apg_connfixture returning a psycopg2 connection needs to be added totests/conftest.py(scoped tosessionormodule, guarded byPOSTGRES_HOST).Suggested addition to
tests/conftest.py:This fix also retroactively resolves the same gap in
test_evolution_track_api.pyfrom WP-06.[MINOR] Out-of-range
current_tiersilently returnsnext_threshold: null_NEXT_THRESHOLD_ATTR.get(state.current_tier)returnsNonefor any value outside 0–4 (e.g. 5, -1). This makes a bad-data row indistinguishable from a legitimately fully-evolved card. The DB schema constrains tier to 0–4 in practice, but a defensive log warning would help diagnose corruption early.[CONFIRMED CORRECT]
card.player_idandcard.team_idattribute accessPeewee exposes the raw FK integer via
card.player_id(not requiring a join). The WHERE clause inget_card_stateis correct as written.[CONFIRMED CORRECT]
total = query.count()reflects filtered totalPeewee wraps the filtered query in a subquery for
.count(), socountin the paginated response reflects the total number of matching rows, not the page size. This is correct.[CONFIRMED CORRECT] Removed unused variables in
teams.pyThe removed
last_card/lc_idblock and ther_query =assignment were both unused after their assignment. Correct cleanup.Security
if not valid_token(token)— consistent with existing pattern.Style & Conventions
teams.py(from ..routers_v2.evolution import _build_card_state_response) is functional but creates coupling between two routers. Moving the helper to a shared module (e.g.app/routers_v2/evolution_helpers.py) would be cleaner at larger scale — non-blocking suggestion at current project size._NEXT_THRESHOLD_ATTRmodule-level dict is clean and readable.model_to_dict(track, recurse=False)consistent with rest of codebase.trackdict in the response correctly exposesid,name,card_type,formula, and the four threshold fields — matches spec.EvolutionCardState.player_idprovides stable pagination sinceplayer_idis a primary key. Correct.noqa: E712suppressions inteams.pyare appropriate for Peewee's ORM boolean filter pattern.Test Coverage
All 10 acceptance criteria from the PR description are covered:
card_typefiltertierfilternext_thresholdat multiple tier levels including tier=4 null casecard_idresolves via Card table joinThe test docstrings and module-level docstring clearly explain the object graph, test matrix, and rationale for each test. Good structure.
One gap (non-blocking): no test for
GET /teams/{team_id}/evolutionswhen the team has zero evolution states. Should return{"count": 0, "items": []}. Worth adding to prevent a future empty-list guard regression.One gap (non-blocking):
next_thresholdat tier=3 is not explicitly tested (should equalt4_threshold=896). Tier=2 and tier=4 are tested, but tier=3 is skipped.Verdict: REQUEST_CHANGES
The endpoint logic, response shape, auth enforcement, duplicate-card sharing, and test coverage are all correct and well-structured. The single blocking issue is the missing
pg_connfixture: without it, pytest will raisefixture 'pg_conn' not foundwhenPOSTGRES_HOSTis set, causing all 10 integration tests to error before running. The fix is a small, one-time addition totests/conftest.pythat will also retroactively fix the same gap from WP-06.Automated review by Claude PR Reviewer
374b82f362to503e570da5