Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/routes/specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from fastapi import APIRouter, HTTPException, Query

from app import specs as spec_store
from app.db import get_db
from app.models import Spec

router = APIRouter(prefix="/specs", tags=["specs"])
Expand All @@ -32,12 +33,23 @@ def _active_round_ids() -> set[str]:
return active


async def _claimed_spec_ids() -> set[str]:
"""Return spec IDs that have at least one passing submission (SOTA claimed)."""
async with get_db() as db:
async with db.execute(
"SELECT DISTINCT spec_id FROM submissions WHERE passed = 1"
) as cur:
rows = await cur.fetchall()
return {row["spec_id"] for row in rows}


@router.get("", response_model=list[Spec])
async def list_specs(
tier: _TIERS | None = Query(None, description="Filter by difficulty tier (easy/medium/hard)"),
round_id: str | None = Query(None, description="Filter by round ID (e.g. round_001)"),
material: str | None = Query(None, description="Filter by material (e.g. pla, petg, aluminum_6061, stainless_316)"),
active: bool | None = Query(None, description="When true, return only specs in currently active rounds"),
unclaimed: bool | None = Query(None, description="When true, return only specs with no passing submission (no SOTA set yet)"),
):
specs = spec_store.load_all()
if active:
Expand All @@ -49,6 +61,12 @@ async def list_specs(
specs = [s for s in specs if s.round_id == round_id]
if material is not None:
specs = [s for s in specs if s.material == material]
if unclaimed is not None:
claimed = await _claimed_spec_ids()
if unclaimed:
specs = [s for s in specs if s.id not in claimed]
else:
specs = [s for s in specs if s.id in claimed]
return specs


Expand Down
28 changes: 28 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,34 @@ def test_get_spec_missing(client: TestClient):
assert r.status_code == 404


def test_specs_unclaimed_filter_no_submissions(client: TestClient):
"""?unclaimed=true returns all specs when there are no passing submissions."""
r = client.get("/specs?unclaimed=true")
assert r.status_code == 200
assert len(r.json()) == 1 # fixture has one spec; no submissions yet


def test_specs_claimed_filter_no_submissions(client: TestClient):
"""?unclaimed=false returns empty list when there are no passing submissions."""
r = client.get("/specs?unclaimed=false")
assert r.status_code == 200
assert len(r.json()) == 0 # nothing claimed yet


def test_specs_unclaimed_filter_after_submission(client: TestClient):
"""After a passing submission, ?unclaimed=true excludes the spec."""
# Create a passing submission for 001_bracket
client.post("/submissions", json=GOOD_SUBMISSION)
# Now 001_bracket is claimed
r_unclaimed = client.get("/specs?unclaimed=true")
assert r_unclaimed.status_code == 200
assert len(r_unclaimed.json()) == 0 # no unclaimed specs
# ?unclaimed=false should return it
r_claimed = client.get("/specs?unclaimed=false")
assert r_claimed.status_code == 200
assert len(r_claimed.json()) == 1


def test_create_submission(client: TestClient):
r = client.post("/submissions", json=GOOD_SUBMISSION)
assert r.status_code == 201
Expand Down
Loading