Skip to content

Commit b4d3533

Browse files
committed
fix: don't strip slash from root href
Some servers require it (per #373 (comment)).
1 parent 80f5323 commit b4d3533

File tree

2 files changed

+121
-6
lines changed

2 files changed

+121
-6
lines changed

pystac_client/client.py

+42-5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
)
1313

1414
import pystac
15+
import pystac.utils
1516
import pystac.validation
1617
from pystac import CatalogType, Collection
1718
from requests import Request
@@ -149,7 +150,6 @@ def open(
149150
Return:
150151
catalog : A :class:`Client` instance for this Catalog/API
151152
"""
152-
url = url.rstrip("/")
153153
client: Client = cls.from_file(
154154
url,
155155
headers=headers,
@@ -254,7 +254,7 @@ def get_collection(self, collection_id: str) -> Optional[Collection]:
254254
CollectionClient: A STAC Collection
255255
"""
256256
if self._supports_collections() and self._stac_io:
257-
url = f"{self.get_self_href()}/collections/{collection_id}"
257+
url = self._get_collections_href(collection_id)
258258
collection = CollectionClient.from_dict(
259259
self._stac_io.read_json(url),
260260
root=self,
@@ -281,9 +281,9 @@ def get_collections(self) -> Iterator[Collection]:
281281
"""
282282
collection: Union[Collection, CollectionClient]
283283

284-
if self._supports_collections() and self.get_self_href() is not None:
285-
url = f"{self.get_self_href()}/collections"
286-
for page in self._stac_io.get_pages(url): # type: ignore
284+
if self._supports_collections() and self._stac_io:
285+
url = self._get_collections_href()
286+
for page in self._stac_io.get_pages(url):
287287
if "collections" not in page:
288288
raise APIError("Invalid response from /collections")
289289
for col in page["collections"]:
@@ -504,3 +504,40 @@ def get_search_link(self) -> Optional[pystac.Link]:
504504
),
505505
None,
506506
)
507+
508+
def _get_collections_href(self, id: Optional[str] = None) -> str:
509+
self_href = self.get_self_href()
510+
if self_href is None:
511+
data_link = self.get_single_link("data")
512+
if data_link is None:
513+
raise ValueError(
514+
"cannot build a collections href without a self href or a data link"
515+
)
516+
else:
517+
collections_href = data_link.href
518+
elif self_href.endswith("/"):
519+
collections_href = f"{self_href}collections"
520+
else:
521+
collections_href = f"{self_href}/collections"
522+
523+
if not pystac.utils.is_absolute_href(collections_href):
524+
collections_href = self._make_absolute_href(collections_href)
525+
526+
if id is None:
527+
return collections_href
528+
elif collections_href.endswith("/"):
529+
return f"{collections_href}{id}"
530+
else:
531+
return f"{collections_href}/{id}"
532+
533+
def _make_absolute_href(self, href: str) -> str:
534+
self_link = self.get_single_link("self")
535+
if self_link is None:
536+
raise ValueError("cannot build an absolute href without a self link")
537+
elif not pystac.utils.is_absolute_href(self_link.href):
538+
raise ValueError(
539+
"cannot build an absolute href from "
540+
f"a relative self link: {self_link.href}"
541+
)
542+
else:
543+
return pystac.utils.make_absolute_href(href, self_link.href)

tests/test_client.py

+79-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import warnings
44
from datetime import datetime
55
from tempfile import TemporaryDirectory
6-
from typing import Any
6+
from typing import Any, Dict
77
from urllib.parse import parse_qs, urlsplit
88

99
import pystac
@@ -146,6 +146,84 @@ def test_get_collections_single_slash(self, requests_mock: Mocker) -> None:
146146
assert len(history) == 2
147147
assert history[1].url == f"{root_url}collections"
148148

149+
def test_keep_trailing_slash_on_root(self, requests_mock: Mocker) -> None:
150+
pc_root_text = read_data_file("planetary-computer-root.json")
151+
root_url = "http://pystac-client.test/"
152+
requests_mock.get(root_url, status_code=200, text=pc_root_text)
153+
client = Client.open(root_url)
154+
self_href = client.get_self_href()
155+
assert self_href
156+
assert self_href.endswith("/")
157+
158+
def test_fall_back_to_data_link_for_collections(
159+
self, requests_mock: Mocker
160+
) -> None:
161+
pc_root_text = read_data_file("planetary-computer-root.json")
162+
root_url = "http://pystac-client.test/"
163+
requests_mock.get(root_url, status_code=200, text=pc_root_text)
164+
api = Client.open(root_url)
165+
api.set_self_href(None)
166+
pc_collection_dict = read_data_file(
167+
"planetary-computer-aster-l1t-collection.json", parse_json=True
168+
)
169+
requests_mock.get(
170+
# the href of the data link
171+
"https://planetarycomputer.microsoft.com/api/stac/v1/collections",
172+
status_code=200,
173+
json={"collections": [pc_collection_dict], "links": []},
174+
)
175+
_ = next(api.get_collections())
176+
history = requests_mock.request_history
177+
assert len(history) == 2
178+
assert (
179+
history[1].url
180+
== "https://planetarycomputer.microsoft.com/api/stac/v1/collections"
181+
)
182+
183+
def test_build_absolute_href_from_data_link(self, requests_mock: Mocker) -> None:
184+
pc_root = read_data_file("planetary-computer-root.json", parse_json=True)
185+
assert isinstance(pc_root, Dict)
186+
for link in pc_root["links"]:
187+
if link["rel"] == "data":
188+
link["href"] = "./collections"
189+
root_url = "http://pystac-client.test/"
190+
requests_mock.get(root_url, status_code=200, text=json.dumps(pc_root))
191+
api = Client.open(root_url)
192+
api.set_self_href(None)
193+
api.add_link(
194+
pystac.Link(
195+
rel="self",
196+
target="https://planetarycomputer.microsoft.com/api/stac/v1/",
197+
)
198+
)
199+
pc_collection_dict = read_data_file(
200+
"planetary-computer-aster-l1t-collection.json", parse_json=True
201+
)
202+
requests_mock.get(
203+
# the href of the data link
204+
"https://planetarycomputer.microsoft.com/api/stac/v1/collections",
205+
status_code=200,
206+
json={"collections": [pc_collection_dict], "links": []},
207+
)
208+
_ = next(api.get_collections())
209+
history = requests_mock.request_history
210+
assert len(history) == 2
211+
assert (
212+
history[1].url
213+
== "https://planetarycomputer.microsoft.com/api/stac/v1/collections"
214+
)
215+
216+
def test_error_if_no_self_href_or_data_link(self, requests_mock: Mocker) -> None:
217+
pc_root = read_data_file("planetary-computer-root.json", parse_json=True)
218+
assert isinstance(pc_root, Dict)
219+
pc_root["links"] = [link for link in pc_root["links"] if link["rel"] != "data"]
220+
root_url = "http://pystac-client.test/"
221+
requests_mock.get(root_url, status_code=200, text=json.dumps(pc_root))
222+
api = Client.open(root_url)
223+
api.set_self_href(None)
224+
with pytest.raises(ValueError):
225+
_ = api.get_collection("an-id")
226+
149227
def test_custom_request_parameters(self, requests_mock: Mocker) -> None:
150228
pc_root_text = read_data_file("planetary-computer-root.json")
151229
pc_collection_dict = read_data_file(

0 commit comments

Comments
 (0)