Skip to content

Commit 31778fe

Browse files
authored
Return existing links in pgstac (#282)
* Add venv to ignore files * Add run-joplin-pgstac make command * Modify ingest logic to update if exists * Add license to collections; test license link * Fix bug where existing links were not returned * Update CHANGES * Use get_links instead of new _extended_links Explain why we resolve the href for extra_links in get_links * Test item extra_links with relative href
1 parent 8222b40 commit 31778fe

File tree

12 files changed

+107
-43
lines changed

12 files changed

+107
-43
lines changed

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ coverage.xml
1010
*.log
1111
.git
1212
.envrc
13+
14+
venv

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,6 @@ docs/api/*
126126

127127
# Direnv
128128
.envrc
129+
130+
# Virtualenv
131+
venv

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
### Fixed
1212

13+
* Links stored with Collections and Items (e.g. license links) are now returned with those STAC objects ([#282](https://github.com/stac-utils/stac-fastapi/pull/282))
14+
1315
## [2.2.0]
1416

1517
### Added

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ run-database:
5050
run-joplin-sqlalchemy:
5151
docker-compose run --rm loadjoplin-sqlalchemy
5252

53+
.PHONY: run-joplin-pgstac
54+
run-joplin-pgstac:
55+
docker-compose run --rm loadjoplin-pgstac
56+
5357
.PHONY: test
5458
test: test-sqlalchemy test-pgstac
5559

scripts/ingest_joplin.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,33 @@
1515
raise Exception("You must include full path/port to stac instance")
1616

1717

18+
def post_or_put(url: str, data: dict):
19+
"""Post or put data to url."""
20+
r = requests.post(url, json=data)
21+
if r.status_code == 409:
22+
# Exists, so update
23+
r = requests.put(url, json=data)
24+
# Unchanged may throw a 404
25+
if not r.status_code == 404:
26+
r.raise_for_status()
27+
else:
28+
r.raise_for_status()
29+
30+
1831
def ingest_joplin_data(app_host: str = app_host, data_dir: Path = joplindata):
1932
"""ingest data."""
2033

2134
with open(data_dir / "collection.json") as f:
2235
collection = json.load(f)
2336

24-
r = requests.post(urljoin(app_host, "collections"), json=collection)
25-
if r.status_code not in (200, 409):
26-
r.raise_for_status()
37+
post_or_put(urljoin(app_host, "/collections"), collection)
2738

2839
with open(data_dir / "index.geojson") as f:
2940
index = json.load(f)
3041

3142
for feat in index["features"]:
3243
del feat["stac_extensions"]
33-
r = requests.post(
34-
urljoin(app_host, f"collections/{collection['id']}/items"), json=feat
35-
)
36-
if r.status_code == 409:
37-
continue
38-
r.raise_for_status()
44+
post_or_put(urljoin(app_host, f"collections/{collection['id']}/items"), feat)
3945

4046

4147
if __name__ == "__main__":

stac_fastapi/pgstac/stac_fastapi/pgstac/core.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ async def all_collections(self, **kwargs) -> Collections:
4747
coll = Collection(**c)
4848
coll["links"] = await CollectionLinks(
4949
collection_id=coll["id"], request=request
50-
).get_links()
50+
).get_links(extra_links=coll.get("links"))
51+
5152
linked_collections.append(coll)
53+
5254
links = [
5355
{
5456
"rel": Relations.root.value,
@@ -94,8 +96,11 @@ async def get_collection(self, id: str, **kwargs) -> Collection:
9496
collection = await conn.fetchval(q, *p)
9597
if collection is None:
9698
raise NotFoundError(f"Collection {id} does not exist.")
97-
links = await CollectionLinks(collection_id=id, request=request).get_links()
98-
collection["links"] = links
99+
100+
collection["links"] = await CollectionLinks(
101+
collection_id=id, request=request
102+
).get_links(extra_links=collection.get("links"))
103+
99104
return Collection(**collection)
100105

101106
async def _search_base(
@@ -147,12 +152,12 @@ async def _search_base(
147152
# TODO: feature.collection is not always included
148153
# This code fails if it's left outside of the fields expression
149154
# I've fields extension updated test cases to always include feature.collection
150-
links = await ItemLinks(
155+
feature["links"] = await ItemLinks(
151156
collection_id=feature["collection"],
152157
item_id=feature["id"],
153158
request=request,
154-
).get_links()
155-
feature["links"] = links
159+
).get_links(extra_links=feature.get("links"))
160+
156161
exclude = search_request.fields.exclude
157162
if exclude and len(exclude) == 0:
158163
exclude = None

stac_fastapi/pgstac/stac_fastapi/pgstac/models/links.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def link_root(self) -> Dict:
6666
rel=Relations.root.value, type=MimeTypes.json.value, href=self.base_url
6767
)
6868

69-
def create_links(self) -> List[Dict]:
69+
def create_links(self) -> List[Dict[str, Any]]:
7070
"""Return all inferred links."""
7171
links = []
7272
for name in dir(self):
@@ -77,7 +77,7 @@ def create_links(self) -> List[Dict]:
7777
return links
7878

7979
async def get_links(
80-
self, extra_links: List[Dict[str, Any]] = []
80+
self, extra_links: Optional[List[Dict[str, Any]]] = None
8181
) -> List[Dict[str, Any]]:
8282
"""
8383
Generate all the links.
@@ -91,11 +91,22 @@ async def get_links(
9191
# join passed in links with generated links
9292
# and update relative paths
9393
links = self.create_links()
94-
if extra_links is not None and len(extra_links) >= 1:
95-
for link in extra_links:
96-
if link["rel"] not in INFERRED_LINK_RELS:
97-
link["href"] = self.resolve(link["href"])
98-
links.append(link)
94+
95+
if extra_links:
96+
# For extra links passed in,
97+
# add links modified with a resolved href.
98+
# Drop any links that are dynamically
99+
# determined by the server (e.g. self, parent, etc.)
100+
# Resolving the href allows for relative paths
101+
# to be stored in pgstac and for the hrefs in the
102+
# links of response STAC objects to be resolved
103+
# to the request url.
104+
links += [
105+
{**link, "href": self.resolve(link["href"])}
106+
for link in extra_links
107+
if link["rel"] not in INFERRED_LINK_RELS
108+
]
109+
99110
return links
100111

101112

stac_fastapi/pgstac/tests/data/test_collection.json

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -100,24 +100,9 @@
100100
},
101101
"links": [
102102
{
103-
"href": "http://localhost:8081/collections/landsat-8-l1",
104-
"rel": "self",
105-
"type": "application/json"
106-
},
107-
{
108-
"href": "http://localhost:8081/",
109-
"rel": "parent",
110-
"type": "application/json"
111-
},
112-
{
113-
"href": "http://localhost:8081/collections/landsat-8-l1/items",
114-
"rel": "item",
115-
"type": "application/geo+json"
116-
},
117-
{
118-
"href": "http://localhost:8081/",
119-
"rel": "root",
120-
"type": "application/json"
103+
"rel": "license",
104+
"href": "https://creativecommons.org/licenses/publicdomain/",
105+
"title": "public domain"
121106
}
122107
],
123108
"title": "Landsat 8 L1",

stac_fastapi/pgstac/tests/data/test_item.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,11 @@
500500
"href": "http://localhost:8081/",
501501
"rel": "root",
502502
"type": "application/json"
503+
},
504+
{
505+
"href": "preview.html",
506+
"rel": "preview",
507+
"type": "application/html"
503508
}
504509
]
505510
}

stac_fastapi/pgstac/tests/resources/test_collection.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,14 @@ async def test_returns_valid_links_in_collections(app_client, load_test_data):
164164
for i in collection.to_dict()["links"]
165165
if i not in single_coll_mocked_link.to_dict()["links"]
166166
] == []
167+
168+
169+
@pytest.mark.asyncio
170+
async def test_returns_license_link(app_client, load_test_collection):
171+
coll = load_test_collection
172+
173+
resp = await app_client.get(f"/collections/{coll.id}")
174+
assert resp.status_code == 200
175+
resp_json = resp.json()
176+
link_rel_types = [link["rel"] for link in resp_json["links"]]
177+
assert "license" in link_rel_types

stac_fastapi/pgstac/tests/resources/test_item.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
import uuid
33
from datetime import datetime, timedelta
44
from typing import Callable
5-
from urllib.parse import parse_qs, urlparse
5+
from urllib.parse import parse_qs, urljoin, urlparse
66

77
import pystac
88
import pytest
9+
from httpx import AsyncClient
910
from shapely.geometry import Polygon
1011
from stac_pydantic import Collection, Item
1112
from stac_pydantic.shared import DATETIME_RFC339
@@ -67,7 +68,7 @@ async def test_create_item(app_client, load_test_data: Callable, load_test_colle
6768
in_json = load_test_data("test_item.json")
6869
in_item = Item.parse_obj(in_json)
6970
resp = await app_client.post(
70-
"/collections/{coll.id}/items",
71+
f"/collections/{coll.id}/items",
7172
json=in_json,
7273
)
7374
assert resp.status_code == 200
@@ -1005,3 +1006,26 @@ async def test_search_bbox_errors(app_client):
10051006
params = {"bbox": "100.0,0.0,0.0,105.0"}
10061007
resp = await app_client.get("/search", params=params)
10071008
assert resp.status_code == 400
1009+
1010+
1011+
@pytest.mark.asyncio
1012+
async def test_preserves_extra_link(
1013+
app_client: AsyncClient, load_test_data, load_test_collection
1014+
):
1015+
coll = load_test_collection
1016+
test_item = load_test_data("test_item.json")
1017+
expected_href = urljoin(str(app_client.base_url), "preview.html")
1018+
1019+
resp = await app_client.post(f"/collections/{coll.id}/items", json=test_item)
1020+
assert resp.status_code == 200
1021+
1022+
response_item = await app_client.get(
1023+
f"/collections/{coll.id}/items/{test_item['id']}",
1024+
params={"limit": 1},
1025+
)
1026+
assert response_item.status_code == 200
1027+
item = response_item.json()
1028+
1029+
extra_link = [link for link in item["links"] if link["rel"] == "preview"]
1030+
assert extra_link
1031+
assert extra_link[0]["href"] == expected_href

stac_fastapi/testdata/joplin/collection.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
"description": "This imagery was acquired by the NOAA Remote Sensing Division to support NOAA national security and emergency response requirements. In addition, it will be used for ongoing research efforts for testing and developing standards for airborne digital imagery. Individual images have been combined into a larger mosaic and tiled for distribution. The approximate ground sample distance (GSD) for each pixel is 35 cm (1.14 feet).",
44
"stac_version": "1.0.0",
55
"license": "public-domain",
6-
"links": [],
6+
"links": [
7+
{
8+
"rel": "license",
9+
"href": "https://creativecommons.org/licenses/publicdomain/",
10+
"title": "public domain"
11+
}
12+
],
713
"type": "collection",
814
"extent": {
915
"spatial": {

0 commit comments

Comments
 (0)