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

12 files changed

Lines changed: 107 additions & 43 deletions

File tree

‎.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

0 commit comments

Comments
 (0)