Skip to content
This repository was archived by the owner on Jul 21, 2025. It is now read-only.

Commit f782512

Browse files
authored
fix: set item links correctly (#61)
- Closes #58
1 parent a8d1a2f commit f782512

File tree

7 files changed

+179
-158
lines changed

7 files changed

+179
-158
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@
1010
"config": {
1111
"MD013": false
1212
}
13-
}
14-
}
13+
},
14+
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
15+
}

pyproject.toml

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [
88
"attr>=0.3.2",
9-
"fastapi>=0.115.6",
9+
"fastapi>=0.115.8",
1010
"geojson-pydantic>=1.2.0",
1111
"pydantic>=2.10.4",
12-
"stac-fastapi-api>=3.0.5",
13-
"stac-fastapi-extensions>=3.0.5",
14-
"stac-fastapi-types>=3.0.5",
12+
"stac-fastapi-api>=5.0.2",
13+
"stac-fastapi-extensions>=5.0.2",
14+
"stac-fastapi-types>=5.0.2",
1515
"stacrs>=0.4.0",
1616
]
1717

@@ -22,31 +22,34 @@ lambda = ["mangum==0.19.0"]
2222
dev = [
2323
"fastapi[standard]>=0.115.6",
2424
"httpx>=0.28.1",
25-
"mkdocs-material>=9.5.50",
26-
"mypy>=1.14.1",
27-
"pygithub>=2.5.0",
28-
"pystac[validation]>=1.11.0",
25+
"mkdocs-material>=9.6.5",
26+
"mypy>=1.15.0",
27+
"pygithub>=2.6.1",
28+
"pystac[validation]>=1.12.2",
2929
"pytest>=8.3.4",
30-
"pytest-asyncio>=0.25.1",
31-
"ruff>=0.9.1",
30+
"pytest-asyncio>=0.25.3",
31+
"ruff>=0.9.7",
3232
"stac-api-validator>=0.6.3",
3333
]
3434
validate = ["stac-api-validator>=0.6.3"]
3535
deploy = [
36-
"aws-cdk-lib==2.175.1",
36+
"aws-cdk-lib==2.181.0",
3737
"constructs>=10.4.2",
3838
"pydantic>=2.10.4",
39-
"pydantic-settings>=2.7.1",
39+
"pydantic-settings>=2.8.0",
4040
]
4141

42+
[tool.uv]
43+
default-groups = ["dev", "validate", "deploy"]
44+
4245
[tool.mypy]
4346
strict = true
4447
files = ["src/**/*.py", "infrastructure/**/*.py"]
4548
exclude = ["infrastructure/aws/cdk.out/.*"]
4649
plugins = "pydantic.mypy"
4750

4851
[[tool.mypy.overrides]]
49-
module = ["stac_fastapi.*", "config"]
52+
module = ["stac_fastapi.*", "config", "magnum"]
5053
ignore_missing_imports = true
5154

5255
[tool.pytest.ini_options]

scripts/validate

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ result=$?
2020
set -e
2121

2222
kill $(pgrep -P $uvicorn_pid)
23-
exit $?
23+
exit $result

src/stac_fastapi/geoparquet/client.py

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,18 @@ async def get_collection(
5050
async def get_item(
5151
self, *, request: Request, item_id: str, collection_id: str, **kwargs: Any
5252
) -> Item:
53-
client = cast(DuckdbClient, request.state.client)
54-
hrefs = cast(dict[str, str], request.state.hrefs)
55-
if href := hrefs.get(collection_id):
56-
item_collection = client.search(
57-
href, ids=[item_id], collections=[collection_id], **kwargs
58-
)
59-
if len(item_collection["features"]) == 1:
60-
return Item(**item_collection["features"][0])
61-
else:
62-
raise NotFoundError(
63-
f"Item does not exist: {item_id} in collection {collection_id}"
64-
)
53+
item_collection = await self.get_search(
54+
request=request,
55+
ids=[item_id],
56+
collections=[collection_id],
57+
**kwargs,
58+
)
59+
if len(item_collection["features"]) == 1:
60+
return Item(**item_collection["features"][0])
6561
else:
66-
raise NotFoundError(f"Collection does not exist: {collection_id}")
62+
raise NotFoundError(
63+
f"Item does not exist: {item_id} in collection {collection_id}"
64+
)
6765

6866
async def get_search(
6967
self,
@@ -189,6 +187,9 @@ async def search(
189187
href,
190188
**search_dict,
191189
)
190+
item_collection["features"] = [
191+
self.item_with_links(item, request) for item in item_collection["features"]
192+
]
192193
num_items = len(item_collection["features"])
193194
limit = int(search_dict.get("limit", None) or num_items)
194195
offset = int(search_dict.get("offset", None) or 0)
@@ -249,6 +250,40 @@ async def search(
249250
item_collection["links"] = links
250251
return ItemCollection(**item_collection)
251252

253+
def item_with_links(self, item: dict[str, Any], request: Request) -> dict[str, Any]:
254+
links = [
255+
{
256+
"href": str(request.url_for("Landing Page")),
257+
"rel": "root",
258+
"type": "application/json",
259+
},
260+
]
261+
if collection_id := item.get("collection"):
262+
href = str(request.url_for("Get Collection", collection_id=collection_id))
263+
links.append(
264+
{"href": href, "rel": "collection", "type": "application/json"}
265+
)
266+
links.append({"href": href, "rel": "parent", "type": "application/json"})
267+
if item_id := item.get("id"):
268+
links.append(
269+
{
270+
"href": str(
271+
request.url_for(
272+
"Get Item",
273+
collection_id=collection_id,
274+
item_id=item_id,
275+
)
276+
),
277+
"rel": "self",
278+
"type": "application/geo+json",
279+
}
280+
)
281+
for link in item.get("links", []):
282+
if link["rel"] not in ("root", "parent", "collection", "self"):
283+
links.append(link)
284+
item["links"] = links
285+
return item
286+
252287

253288
def collection_with_links(
254289
collection: dict[str, Any], request: Request

tests/test_get_item.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from fastapi.testclient import TestClient
2+
3+
4+
async def test_get_search(client: TestClient) -> None:
5+
response = client.get("/collections/naip/items/ne_m_4110264_sw_13_060_20220827")
6+
assert response.status_code == 200, response.text

tests/test_search.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,16 @@ async def test_paging(client: TestClient) -> None:
2626
response = client.get("/search", params=url.query)
2727
assert response.status_code == 200
2828
assert response.json()["features"][0]["id"] == "ne_m_4110263_sw_13_060_20220820"
29+
30+
31+
async def test_collection_link(client: TestClient) -> None:
32+
# https://github.com/developmentseed/labs-375-stac-geoparquet-backend/issues/58
33+
response = client.get("/search", params={"limit": 1})
34+
response.raise_for_status()
35+
data = response.json()
36+
link = next(
37+
(link for link in data["features"][0]["links"] if link["rel"] == "collection")
38+
)
39+
assert link["href"].startswith(str(client.base_url)), (
40+
link["href"] + " does not start with the test client base url"
41+
)

0 commit comments

Comments
 (0)