Add the core labeling data model (label sets, ad-hoc Main->Sub class hierarchy, frames, bounding-box labels, per-frame/per-set label status) behind a FastAPI app, with SQLite as the default swappable DATABASE_URL. Multi-user support is attribution-only for now (get-or-create by username, no login flow yet). Dataset versioning/promotion is intentionally deferred - it needs its own design pass around snapshot semantics. Each test gets a fully isolated app+DB via create_app(database_url=...) rather than relying on process-global state. 13/13 tests pass; also verified live end-to-end against a running uvicorn instance. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
76 lines
2.8 KiB
Python
76 lines
2.8 KiB
Python
def _make_set_with_subclass(client, name="Enemy", sub_name="Bat"):
|
|
set_id = client.post("/sets", json={"name": name}).json()["id"]
|
|
main_class_id = client.post(f"/sets/{set_id}/main-classes", json={"name": name}).json()["id"]
|
|
sub_class_id = client.post(f"/main-classes/{main_class_id}/sub-classes", json={"name": sub_name}).json()["id"]
|
|
return set_id, sub_class_id
|
|
|
|
|
|
def _make_frame(client, session_name="run01", frame_index=0):
|
|
payload = {"session_name": session_name, "frame_index": frame_index, "image_path": "x.png", "width": 1280, "height": 720}
|
|
return client.post("/frames", json=payload).json()["id"]
|
|
|
|
|
|
def test_create_list_update_delete_label(client):
|
|
set_id, sub_class_id = _make_set_with_subclass(client)
|
|
frame_id = _make_frame(client)
|
|
|
|
resp = client.post(
|
|
f"/frames/{frame_id}/sets/{set_id}/labels",
|
|
json={"sub_class_id": sub_class_id, "x": 10, "y": 20, "width": 30, "height": 40, "created_by": "jonas"},
|
|
)
|
|
assert resp.status_code == 201
|
|
label_id = resp.json()["id"]
|
|
|
|
resp = client.get(f"/frames/{frame_id}/sets/{set_id}/labels")
|
|
assert len(resp.json()) == 1
|
|
|
|
resp = client.patch(f"/labels/{label_id}", json={"x": 15})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["x"] == 15
|
|
|
|
resp = client.delete(f"/labels/{label_id}")
|
|
assert resp.status_code == 204
|
|
|
|
resp = client.get(f"/frames/{frame_id}/sets/{set_id}/labels")
|
|
assert resp.json() == []
|
|
|
|
|
|
def test_label_rejects_sub_class_from_another_set(client):
|
|
_, sub_class_id = _make_set_with_subclass(client, name="Enemy", sub_name="Bat")
|
|
other_set_id, _ = _make_set_with_subclass(client, name="Items", sub_name="Gold")
|
|
frame_id = _make_frame(client)
|
|
|
|
resp = client.post(
|
|
f"/frames/{frame_id}/sets/{other_set_id}/labels",
|
|
json={"sub_class_id": sub_class_id, "x": 0, "y": 0, "width": 1, "height": 1},
|
|
)
|
|
assert resp.status_code == 422
|
|
|
|
|
|
def test_labels_require_existing_frame_and_set(client):
|
|
_, sub_class_id = _make_set_with_subclass(client)
|
|
resp = client.post(
|
|
"/frames/999/sets/999/labels",
|
|
json={"sub_class_id": sub_class_id, "x": 0, "y": 0, "width": 1, "height": 1},
|
|
)
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_frame_set_status_defaults_and_updates(client):
|
|
set_id, _ = _make_set_with_subclass(client)
|
|
frame_id = _make_frame(client)
|
|
|
|
resp = client.get(f"/frames/{frame_id}/sets/{set_id}/status")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["status"] == "unlabeled"
|
|
|
|
resp = client.put(
|
|
f"/frames/{frame_id}/sets/{set_id}/status",
|
|
json={"status": "reviewed", "updated_by": "jonas"},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["status"] == "reviewed"
|
|
|
|
resp = client.get(f"/frames/{frame_id}/sets/{set_id}/status")
|
|
assert resp.json()["status"] == "reviewed"
|