Skip to content

Commit c25745e

Browse files
committed
docs: move docs into api-docs, out of readme
1 parent 2dfa845 commit c25745e

12 files changed

Lines changed: 572 additions & 628 deletions

File tree

README.md

Lines changed: 17 additions & 401 deletions
Large diffs are not rendered by default.
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
"""Integration test: full platform/acquisition_type lifecycle against the production server.
2+
3+
Steps
4+
-----
5+
1. POST /acquisition-types — register (platform="test", acquisition_type=<unique>)
6+
2. POST /acquisition-types — re-post the same pair → still 200, dedupes (idempotent)
7+
3. GET /acquisition-types — verify the pair is present
8+
4. POST /scheduled-acquisitions with an unregistered acquisition_type → expect 400
9+
5. POST /scheduled-acquisitions with a missing field → expect 422 (typed body)
10+
6. POST /scheduled-acquisitions, date=today, our acquisition_type → 200, get uuid
11+
7. GET /scheduled-acquisitions/{uuid} — verify subject_id, date, platform="test", acquisition_type
12+
8. GET /scheduled-acquisitions/{made-up-uuid} → expect 404
13+
9. GET /scheduled-acquisitions (default, future-only) — today's uuid is present
14+
10. POST /scheduled-acquisitions, date=yesterday (past) — second uuid
15+
11. GET /scheduled-acquisitions (default, future-only) — past uuid is excluded
16+
12. GET /scheduled-acquisitions?include_past=true — both uuids present
17+
18+
Usage
19+
-----
20+
python scripts/test_acquisitions_integration.py
21+
"""
22+
23+
import sys
24+
import time
25+
from datetime import date, timedelta
26+
27+
import requests
28+
29+
BASE_URL = "https://metadata-portal.allenneuraldynamics.org"
30+
PLATFORM = "test"
31+
ACQUISITION_TYPE = f"integration-test-type-{int(time.time())}"
32+
SUBJECT_ID = f"integration-test-subject-{int(time.time())}"
33+
34+
ACQUISITION_TYPES_URL = f"{BASE_URL}/acquisition-types"
35+
SCHEDULED_ACQUISITIONS_URL = f"{BASE_URL}/scheduled-acquisitions"
36+
37+
print(f"Testing against: {BASE_URL}")
38+
print(f"platform={PLATFORM!r} acquisition_type={ACQUISITION_TYPE!r}")
39+
print("=" * 60)
40+
41+
42+
def sep(title):
43+
print(f"\n{'─' * 60}")
44+
print(f" {title}")
45+
print(f"{'─' * 60}")
46+
47+
48+
def check(response, expected_status):
49+
if response.status_code != expected_status:
50+
print(f" FAIL status={response.status_code} body={response.text[:400]}")
51+
sys.exit(1)
52+
print(f" OK status={response.status_code}")
53+
try:
54+
return response.json()
55+
except Exception:
56+
return {}
57+
58+
59+
# ---------------------------------------------------------------------------
60+
# Step 1: POST /acquisition-types — register platform="test" + acquisition_type
61+
# ---------------------------------------------------------------------------
62+
63+
sep("Step 1: POST /acquisition-types (register platform:test pair)")
64+
r = requests.post(ACQUISITION_TYPES_URL, json={"platform": PLATFORM, "acquisition_type": ACQUISITION_TYPE})
65+
entry = check(r, 200)
66+
assert entry == {"platform": PLATFORM, "acquisition_type": ACQUISITION_TYPE}, f"unexpected entry: {entry}"
67+
print(f" entry: {entry}")
68+
69+
# ---------------------------------------------------------------------------
70+
# Step 2: POST again — dedupes, still 200
71+
# ---------------------------------------------------------------------------
72+
73+
sep("Step 2: POST /acquisition-types again (expect 200, idempotent dedupe)")
74+
r = requests.post(ACQUISITION_TYPES_URL, json={"platform": PLATFORM, "acquisition_type": ACQUISITION_TYPE})
75+
check(r, 200)
76+
77+
# ---------------------------------------------------------------------------
78+
# Step 3: GET /acquisition-types — verify the pair is present exactly once
79+
# ---------------------------------------------------------------------------
80+
81+
sep("Step 3: GET /acquisition-types (verify pair present)")
82+
r = requests.get(ACQUISITION_TYPES_URL)
83+
entries = check(r, 200)
84+
matches = [e for e in entries if e == {"platform": PLATFORM, "acquisition_type": ACQUISITION_TYPE}]
85+
assert len(matches) == 1, f"expected exactly 1 match, found {len(matches)} in {len(entries)} entries"
86+
print(f" found {len(matches)} matching entry among {len(entries)} total")
87+
88+
# ---------------------------------------------------------------------------
89+
# Step 4: POST /scheduled-acquisitions with an unregistered acquisition_type → 400
90+
# ---------------------------------------------------------------------------
91+
92+
sep("Step 4: POST /scheduled-acquisitions with unknown acquisition_type (expect 400)")
93+
r = requests.post(
94+
SCHEDULED_ACQUISITIONS_URL,
95+
json={
96+
"subject_id": SUBJECT_ID,
97+
"date": date.today().isoformat(),
98+
"acquisition_type": f"not-a-real-type-{int(time.time())}",
99+
},
100+
)
101+
check(r, 400)
102+
103+
# ---------------------------------------------------------------------------
104+
# Step 5: POST /scheduled-acquisitions with a missing field → 422 (typed body)
105+
# ---------------------------------------------------------------------------
106+
107+
sep("Step 5: POST /scheduled-acquisitions missing acquisition_type (expect 422)")
108+
r = requests.post(
109+
SCHEDULED_ACQUISITIONS_URL,
110+
json={"subject_id": SUBJECT_ID, "date": date.today().isoformat()},
111+
)
112+
check(r, 422)
113+
114+
# ---------------------------------------------------------------------------
115+
# Step 6: POST /scheduled-acquisitions, date=today, our acquisition_type → 200
116+
# ---------------------------------------------------------------------------
117+
118+
sep("Step 6: POST /scheduled-acquisitions (date=today, expect 200 + uuid)")
119+
r = requests.post(
120+
SCHEDULED_ACQUISITIONS_URL,
121+
json={"subject_id": SUBJECT_ID, "date": date.today().isoformat(), "acquisition_type": ACQUISITION_TYPE},
122+
)
123+
created = check(r, 200)
124+
assert "uuid" in created, f"missing uuid in response: {created}"
125+
today_uuid = created["uuid"]
126+
print(f" uuid: {today_uuid}")
127+
128+
# ---------------------------------------------------------------------------
129+
# Step 7: GET /scheduled-acquisitions/{uuid} — verify full record
130+
# ---------------------------------------------------------------------------
131+
132+
sep("Step 7: GET /scheduled-acquisitions/{uuid} (verify record)")
133+
r = requests.get(f"{SCHEDULED_ACQUISITIONS_URL}/{today_uuid}")
134+
record = check(r, 200)
135+
assert record["subject_id"] == SUBJECT_ID, f"unexpected subject_id: {record}"
136+
assert record["date"] == date.today().isoformat(), f"unexpected date: {record}"
137+
assert record["platform"] == PLATFORM, f"expected platform resolved to 'test', got {record}"
138+
assert record["acquisition_type"] == ACQUISITION_TYPE, f"unexpected acquisition_type: {record}"
139+
print(f" record: {record}")
140+
141+
# ---------------------------------------------------------------------------
142+
# Step 8: GET /scheduled-acquisitions/{made-up-uuid} → 404
143+
# ---------------------------------------------------------------------------
144+
145+
sep("Step 8: GET /scheduled-acquisitions/{made-up-uuid} (expect 404)")
146+
r = requests.get(f"{SCHEDULED_ACQUISITIONS_URL}/not-a-real-uuid-{int(time.time())}")
147+
check(r, 404)
148+
149+
# ---------------------------------------------------------------------------
150+
# Step 9: GET /scheduled-acquisitions (default, future-only) — today's uuid present
151+
# ---------------------------------------------------------------------------
152+
153+
sep("Step 9: GET /scheduled-acquisitions (future-only, today's uuid should appear)")
154+
r = requests.get(SCHEDULED_ACQUISITIONS_URL)
155+
future_records = check(r, 200)
156+
future_uuids = {rec["uuid"] for rec in future_records}
157+
assert today_uuid in future_uuids, f"today's uuid {today_uuid} missing from future-only list"
158+
print(f" today's uuid present among {len(future_records)} future records")
159+
160+
# ---------------------------------------------------------------------------
161+
# Step 10: POST /scheduled-acquisitions, date=yesterday (past) — second uuid
162+
# ---------------------------------------------------------------------------
163+
164+
sep("Step 10: POST /scheduled-acquisitions (date=yesterday, expect 200 + uuid)")
165+
yesterday = (date.today() - timedelta(days=1)).isoformat()
166+
r = requests.post(
167+
SCHEDULED_ACQUISITIONS_URL,
168+
json={"subject_id": SUBJECT_ID, "date": yesterday, "acquisition_type": ACQUISITION_TYPE},
169+
)
170+
created_past = check(r, 200)
171+
past_uuid = created_past["uuid"]
172+
print(f" uuid: {past_uuid}")
173+
174+
# ---------------------------------------------------------------------------
175+
# Step 11: GET /scheduled-acquisitions (default, future-only) — past uuid excluded
176+
# ---------------------------------------------------------------------------
177+
178+
sep("Step 11: GET /scheduled-acquisitions (future-only, past uuid should be excluded)")
179+
r = requests.get(SCHEDULED_ACQUISITIONS_URL)
180+
future_records = check(r, 200)
181+
future_uuids = {rec["uuid"] for rec in future_records}
182+
assert past_uuid not in future_uuids, f"past uuid {past_uuid} unexpectedly present in future-only list"
183+
assert today_uuid in future_uuids, "today's uuid should still be present"
184+
print(f" past uuid correctly excluded; today's uuid still present ({len(future_records)} future records)")
185+
186+
# ---------------------------------------------------------------------------
187+
# Step 12: GET /scheduled-acquisitions?include_past=true — both uuids present
188+
# ---------------------------------------------------------------------------
189+
190+
sep("Step 12: GET /scheduled-acquisitions?include_past=true (both uuids should appear)")
191+
r = requests.get(SCHEDULED_ACQUISITIONS_URL, params={"include_past": "true"})
192+
all_records = check(r, 200)
193+
all_uuids = {rec["uuid"] for rec in all_records}
194+
assert today_uuid in all_uuids, "today's uuid missing from include_past=true list"
195+
assert past_uuid in all_uuids, "past uuid missing from include_past=true list"
196+
print(f" both uuids present among {len(all_records)} total records")
197+
198+
print("\n" + "=" * 60)
199+
print(" ALL STEPS PASSED")
200+
print("=" * 60)
Lines changed: 69 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,21 @@
11
"""FastAPI router for the scheduled-acquisitions REST endpoints.
22
3-
Routes
4-
------
5-
POST /acquisition-types
6-
Body: {"platform": str, "acquisition_type": str}
7-
Adds a new allowed (platform, acquisition_type) pair.
8-
9-
GET /acquisition-types
10-
Returns all allowed (platform, acquisition_type) pairs.
11-
12-
POST /scheduled-acquisitions
13-
Body: {"subject_id": str, "date": "YYYY-MM-DD", "acquisition_type": str}
14-
Validates acquisition_type against the allowed types, stores the record,
15-
and returns {"uuid": "<uuid>"}.
16-
17-
GET /scheduled-acquisitions?include_past=false
18-
Returns all scheduled acquisitions. By default only acquisitions scheduled
19-
for today or later are returned; pass include_past=true to include all.
20-
21-
GET /scheduled-acquisitions/{acquisition_uuid}
22-
Returns {"subject_id", "date", "platform", "acquisition_type"} for a
23-
single scheduled acquisition.
3+
See /docs (Swagger UI) for full request/response schemas.
244
"""
255

266
import logging
7+
from typing import List
278

28-
from fastapi import APIRouter, Request
9+
from fastapi import APIRouter, Query
2910
from fastapi.responses import JSONResponse
3011

12+
from .models import (
13+
AcquisitionTypeEntry,
14+
ScheduledAcquisition,
15+
ScheduledAcquisitionCreate,
16+
ScheduledAcquisitionCreated,
17+
ScheduledAcquisitionDetail,
18+
)
3119
from .store import (
3220
add_acquisition_type,
3321
add_scheduled_acquisition,
@@ -38,76 +26,95 @@
3826

3927
_logger = logging.getLogger(__name__)
4028

41-
acquisitions_router = APIRouter()
42-
29+
acquisitions_router = APIRouter(tags=["acquisitions"])
4330

44-
@acquisitions_router.post("/acquisition-types")
45-
async def acquisition_types_post(request: Request):
46-
body = await request.json()
47-
platform = body.get("platform")
48-
acquisition_type = body.get("acquisition_type")
49-
if not platform or not acquisition_type:
50-
return JSONResponse(
51-
status_code=400,
52-
content={"error": "platform and acquisition_type are required"},
53-
)
5431

32+
@acquisitions_router.post(
33+
"/acquisition-types",
34+
response_model=AcquisitionTypeEntry,
35+
summary="Register an allowed acquisition type",
36+
description=(
37+
"Adds a new allowed `(platform, acquisition_type)` pair. Deduplicates on the exact pair "
38+
"— posting the same pair twice is a no-op. If `ALLOWED_PLATFORMS` has been populated, "
39+
"`platform` must be one of those values."
40+
),
41+
)
42+
async def acquisition_types_post(body: AcquisitionTypeEntry):
5543
try:
56-
entry = add_acquisition_type(platform, acquisition_type)
44+
entry = add_acquisition_type(body.platform, body.acquisition_type)
5745
except ValueError as e:
5846
return JSONResponse(status_code=400, content={"error": str(e)})
5947
except Exception as e:
60-
_logger.exception("POST /acquisition-types platform=%s acquisition_type=%s", platform, acquisition_type)
48+
_logger.exception(
49+
"POST /acquisition-types platform=%s acquisition_type=%s", body.platform, body.acquisition_type
50+
)
6151
return JSONResponse(status_code=500, content={"error": str(e)})
6252

63-
return JSONResponse(content=entry)
53+
return entry
6454

6555

66-
@acquisitions_router.get("/acquisition-types")
56+
@acquisitions_router.get(
57+
"/acquisition-types",
58+
response_model=List[AcquisitionTypeEntry],
59+
summary="List allowed acquisition types and platforms",
60+
description="Returns every allowed `(platform, acquisition_type)` pair registered so far.",
61+
)
6762
async def acquisition_types_get():
6863
try:
6964
entries = get_allowed_types()
7065
except Exception as e:
7166
_logger.exception("GET /acquisition-types")
7267
return JSONResponse(status_code=500, content={"error": str(e)})
73-
return JSONResponse(content=entries)
74-
75-
76-
@acquisitions_router.post("/scheduled-acquisitions")
77-
async def scheduled_acquisitions_post(request: Request):
78-
body = await request.json()
79-
subject_id = body.get("subject_id")
80-
acquisition_date = body.get("date")
81-
acquisition_type = body.get("acquisition_type")
82-
if not subject_id or not acquisition_date or not acquisition_type:
83-
return JSONResponse(
84-
status_code=400,
85-
content={"error": "subject_id, date, and acquisition_type are required"},
86-
)
87-
68+
return entries
69+
70+
71+
@acquisitions_router.post(
72+
"/scheduled-acquisitions",
73+
response_model=ScheduledAcquisitionCreated,
74+
summary="Schedule an acquisition",
75+
description=(
76+
"Validates `acquisition_type` against the allowed types registered via "
77+
"POST /acquisition-types (resolving `platform` automatically), stores the record, and "
78+
"returns a `uuid` that identifies it. 400 if `acquisition_type` isn't an allowed type."
79+
),
80+
)
81+
async def scheduled_acquisitions_post(body: ScheduledAcquisitionCreate):
8882
try:
89-
acquisition_uuid = add_scheduled_acquisition(subject_id, acquisition_date, acquisition_type)
83+
acquisition_uuid = add_scheduled_acquisition(body.subject_id, body.date, body.acquisition_type)
9084
except ValueError as e:
9185
return JSONResponse(status_code=400, content={"error": str(e)})
9286
except Exception as e:
93-
_logger.exception("POST /scheduled-acquisitions subject_id=%s", subject_id)
87+
_logger.exception("POST /scheduled-acquisitions subject_id=%s", body.subject_id)
9488
return JSONResponse(status_code=500, content={"error": str(e)})
9589

96-
return JSONResponse(content={"uuid": acquisition_uuid})
90+
return ScheduledAcquisitionCreated(uuid=acquisition_uuid)
9791

9892

99-
@acquisitions_router.get("/scheduled-acquisitions")
100-
async def scheduled_acquisitions_get(request: Request):
101-
include_past = request.query_params.get("include_past", "false").lower() == "true"
93+
@acquisitions_router.get(
94+
"/scheduled-acquisitions",
95+
response_model=List[ScheduledAcquisition],
96+
summary="List scheduled acquisitions",
97+
description="Returns all scheduled acquisitions. By default only acquisitions scheduled for "
98+
"today or later are included; pass `include_past=true` to include past acquisitions too.",
99+
)
100+
async def scheduled_acquisitions_get(
101+
include_past: bool = Query(default=False, description="Include acquisitions scheduled before today"),
102+
):
102103
try:
103104
records = get_scheduled_acquisitions(include_past=include_past)
104105
except Exception as e:
105106
_logger.exception("GET /scheduled-acquisitions include_past=%s", include_past)
106107
return JSONResponse(status_code=500, content={"error": str(e)})
107-
return JSONResponse(content=records)
108+
return records
108109

109110

110-
@acquisitions_router.get("/scheduled-acquisitions/{acquisition_uuid}")
111+
@acquisitions_router.get(
112+
"/scheduled-acquisitions/{acquisition_uuid}",
113+
response_model=ScheduledAcquisitionDetail,
114+
summary="Get a scheduled acquisition by uuid",
115+
description="Returns the subject_id, date, platform, and acquisition_type for a single "
116+
"scheduled acquisition. 404 if the uuid isn't found.",
117+
)
111118
async def scheduled_acquisition_get(acquisition_uuid: str):
112119
try:
113120
record = get_scheduled_acquisition(acquisition_uuid)
@@ -121,4 +128,4 @@ async def scheduled_acquisition_get(acquisition_uuid: str):
121128
content={"error": f"No scheduled acquisition found for uuid '{acquisition_uuid}'"},
122129
)
123130

124-
return JSONResponse(content=record)
131+
return record

0 commit comments

Comments
 (0)