Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use ogciapi-features for get_item #166

Merged
merged 2 commits into from
May 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [v0.3.4] - 2022-05-18

### Added

- Direct item GET via ogcapi-features, if conformant [#166](https://github.com/stac-utils/pystac-client/pull/166)

### Changed

- Relaxed media type requirement for search links [#160](https://github.com/stac-utils/pystac-client/pull/160), [#165](https://github.com/stac-utils/pystac-client/pull/165)
Expand Down
53 changes: 52 additions & 1 deletion pystac_client/collection_client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from typing import TYPE_CHECKING, Iterable
from typing import TYPE_CHECKING, Iterable, Optional, cast

import pystac

from pystac_client.conformance import ConformanceClasses
from pystac_client.exceptions import APIError
from pystac_client.item_search import ItemSearch
from pystac_client.stac_api_io import StacApiIO

if TYPE_CHECKING:
from pystac.item import Item as Item_Type
Expand Down Expand Up @@ -30,3 +33,51 @@ def get_items(self) -> Iterable["Item_Type"]:
yield from search.get_items()
else:
yield from super().get_items()

def get_item(self, id: str, recursive: bool = False) -> Optional["Item_Type"]:
"""Returns an item with a given ID.

If the collection conforms to
[ogcapi-features](https://github.com/radiantearth/stac-api-spec/blob/738f4837ac6bea041dc226219e6d13b2c577fb19/ogcapi-features/README.md),
this will use the `/collections/{collectionId}/items/{featureId}`.
Otherwise, the default PySTAC behavior is used.

Args:
id : The ID of the item to find.
recursive : If True, search this catalog and all children for the
item; otherwise, only search the items of this catalog. Defaults
to False.

Return:
Item or None: The item with the given ID, or None if not found.
"""
if not recursive:
root = self.get_root()
assert root
stac_io = root._stac_io
assert stac_io
assert isinstance(stac_io, StacApiIO)
link = self.get_single_link("items")
if (
stac_io.conforms_to(ConformanceClasses.OGCAPI_FEATURES)
and link is not None
):
url = f"{link.href}/{id}"
try:
item = stac_io.read_stac_object(url, root=self)
except APIError as err:
if err.status_code and err.status_code == 404:
return None
else:
raise err
assert isinstance(item, pystac.Item)
return item
else:
return super().get_item(id, recursive=False)
else:
for root, _, _ in self.walk():
item = cast(pystac.Item, root.get_item(id, recursive=False))
if item is not None:
assert isinstance(item, pystac.Item)
return item
return None
1 change: 1 addition & 0 deletions pystac_client/conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ConformanceClasses(Enum):
COLLECTIONS = re.escape(
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30"
)
OGCAPI_FEATURES = rf"{stac_prefix}(.*){re.escape('/ogcapi-features')}"


CONFORMANCE_URIS = {c.name: c.value for c in ConformanceClasses}
13 changes: 13 additions & 0 deletions pystac_client/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
from typing import Optional

from requests import Response


class APIError(Exception):
"""Raised when unexpected server error."""

status_code: Optional[int]

@classmethod
def from_response(cls, response: Response) -> "APIError":
error = cls(response.text)
error.status_code = response.status_code
return error


class ParametersError(Exception):
"""Raised when invalid parameters are used in a query"""
7 changes: 5 additions & 2 deletions pystac_client/stac_api_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,11 @@ def request(
msg += f" Payload: {json.dumps(request.json)}"
logger.debug(msg)
resp = self.session.send(prepped)
if resp.status_code != 200:
raise APIError(resp.text)
except Exception as err:
raise APIError(str(err))
if resp.status_code != 200:
raise APIError.from_response(resp)
try:
return resp.content.decode("utf-8")
except Exception as err:
raise APIError(str(err))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.25.1
method: GET
uri: https://planetarycomputer.microsoft.com/api/stac/v1
response:
body:
string: !!binary |
H4sIAJbRh2IC/72ZbW/bNhDHv8rBA4YWK/0QD0XrYS8U23E8xIlhueiAoRhoipbYUaJK0k69ot99
Rz0k7hAUAU33TYxQ4t1PJO/uT/JLxx5K3hl1xtRSqdLOq45I8N9cMK2M2lpSMmyzwkr31qJthqWk
BbdUH2Cs8nJnuYZ4HY0hWs7x/YQbpkVphSqwV8ypZhndSA6mpNhoeV4qTSXkaCJBz1B32IgihSnV
NgPDBC8YB/fUcGsgU8byBDYHsBmH74Ggf2Mp+3vPtakBBt1+t4/NTBVbpXOzVp3RX53M2nLU693f
33dVyYtUmC5a6pmSs55KGS0F2XJqd5obMuihjZ7rj380R1uevVOuPhqE8jagqBn2m+4G++N7Xfe5
rl9X6bS3r76WbHBQur8e0z7jbYEzQ0w1XV6dftoKiTMw2lAjGGGf5ElWsD85Giw/I/XPSSaOmsgJ
5npoj8vEeHb+tOP64NnXKG2f3/V/q6/z4VVHiuIfg0HzpaO5xIgyXG5dXqiTBy1LKZiL7KLXTpjm
W3zSeizbKGVNkHYfMkwXmxxRzxEhSa/z9VXrRitlf4Abl2TO5YYpKTlzxsyRxyYTUUxx33XcJl6X
W3vvr+IhHPUEJqkx3IDIS8lzXjwkSGHAcL2vF+op7I+Uj+wP+eEJbExvvzyBDo85xZ/mwQZWjUy5
KjWbrp/J9eOYlnfxMRTLhEyexRTF6+kKbgbrYOutR7FkaiIH1gvohhaJoRbeYGVtbcIF3PA9l+Qi
HKWs/ZA3hF0QeeHFer2aRiO4FmkGK26U3FWwU+dDCybsASLGuDHhqNEM9UK9jebLEdxWz1ACRSkC
7qTLtDDPaYpJHpZapZrm4WALKkov2NndNCYrGEu1S+BnWChhjknDEaYKZQ7LhRflhB4w/mBChTzA
rXL6Mco5jisNB5hUPkjifJCCnsIZFcUOZ36JBd0qWAmmgmPSygUptRdnjKVEFC7K22iPwhGaB+MY
7L4hFEeT6QKuZ2vY9/uDkIFiaMJzL6qFupyPYEFLUFu4FCoR1Q4Ec8/cbXdsU+wDkeZq4xcsmG+S
xTRglXEGcc15way51nQsRU5twLGxziprrPpgvYthzAuzC1gvdpjeapNeeViqDaaMb1dVLc6qSndF
mZCu8cXscn71MuDkbsTWWz2gcEBYFw5jWuBGP2CcakYL4pQDcy48J3kWwwzD9RE14LhRv4J7hft1
Y3Fu95gmFWoBxxahUDgYEXA1boVf5v1jNYZmLcY7vaW4CXlPbciB+6gZSc29F93UaAGDPllwdx51
tAJfYGO1WQoYGUIRuZPsdMFyTh1QyxVPGfAN5TW9p0KcCTA7SfctVGGzsyu/vPZyovZrWc856S3p
adPeqNTzDmqjU8OM6ZmWaDucpy3SZjjPxNiMoyfiWJVcF7gNNeBk9ezmjrzth2NkqiQoqkkqlbMb
iHB4JsJh319NDCfTJcSc5pKbijRgyR4mvHQHupVtvxOmm7sY3istEwTF7wx4wEClMsR343Q9uxxh
YGgMNPEvT+BR7OZYsgFVLlz0BwGnO0s3fju89WU8culGoDRzd1VrzYvEgCjgcqcLnHkUG6jCA+7y
7MZvrseZRETTDmLAUKkNk01j2Gs3s1zAfDFdzQLq7TInAotUSrLMr+zV947TzyyjRcrbRViLn4m6
LwyjEhfnUquPjVN4cTv9k8wmkyUZL+bL1y/DnkOQgn8maZKU7lysfO31UYN+3laeSg6/M/wbXfz2
XLKYNJb9jihuo3U8u4NYCTcDlm4ochNYuzvkgGs5Lag1qSK2thsSdVWdvZ+BVTeGj+899F4wTtwd
+tPM+yLpKiqqe2a0X93Q/NZcj/8+rK7G2++5w1ei5Rwao3B8k3/StzS+u9X4PQGvjtgt/2x7mc3l
98AU27krL3o6GprC8fzw9T/RbRmhDSEAAA==
headers:
Accept-Ranges:
- bytes
Connection:
- keep-alive
Content-Length:
- '1450'
Content-Type:
- application/json
Date:
- Fri, 20 May 2022 17:36:22 GMT
Strict-Transport-Security:
- max-age=15724800; includeSubDomains
X-Cache:
- CONFIG_NOCACHE
content-encoding:
- gzip
vary:
- Accept-Encoding
x-azure-ref:
- 20220520T173621Z-2vtb9nr461753e6runuwrkw02s00000001kg00000002wkr9
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.25.1
method: GET
uri: https://planetarycomputer.microsoft.com/api/stac/v1/collections/aster-l1t
response:
body:
string: !!binary |
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
headers:
Accept-Ranges:
- bytes
Connection:
- keep-alive
Content-Length:
- '1754'
Content-Type:
- application/json
Date:
- Fri, 20 May 2022 17:36:22 GMT
Strict-Transport-Security:
- max-age=15724800; includeSubDomains
X-Cache:
- CONFIG_NOCACHE
content-encoding:
- gzip
vary:
- Accept-Encoding
x-azure-ref:
- 20220520T173622Z-2vtb9nr461753e6runuwrkw02s00000001kg00000002wkvh
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.25.1
method: GET
uri: https://planetarycomputer.microsoft.com/api/stac/v1/collections/aster-l1t/items/AST_L1T_00312272006020322_20150518201805
response:
body:
string: !!binary |
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
GQAA
headers:
Accept-Ranges:
- bytes
Connection:
- keep-alive
Content-Length:
- '1599'
Content-Type:
- application/json
Date:
- Fri, 20 May 2022 17:36:22 GMT
Strict-Transport-Security:
- max-age=15724800; includeSubDomains
X-Cache:
- CONFIG_NOCACHE
content-encoding:
- gzip
vary:
- Accept-Encoding
x-azure-ref:
- 20220520T173622Z-2vtb9nr461753e6runuwrkw02s00000001kg00000002wkx4
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.25.1
method: GET
uri: https://planetarycomputer.microsoft.com/api/stac/v1/collections/aster-l1t/items/for-sure-not-a-real-id
response:
body:
string: '{"detail":"Item for-sure-not-a-real-id in Collection aster-l1t does
not exist."}'
headers:
Connection:
- keep-alive
Content-Length:
- '80'
Content-Type:
- application/json
Date:
- Fri, 20 May 2022 17:37:35 GMT
Strict-Transport-Security:
- max-age=15724800; includeSubDomains
X-Cache:
- CONFIG_NOCACHE
x-azure-ref:
- 20220520T173734Z-5gkzkf8b0d2ex948vs77f03zgw00000001m000000000cmza
status:
code: 404
message: Not Found
version: 1
11 changes: 11 additions & 0 deletions tests/test_collection_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ def test_get_items(self):
for item in collection.get_items():
assert item.collection_id == collection.id
return

@pytest.mark.vcr
def test_get_item(self):
client = Client.open(STAC_URLS["PLANETARY-COMPUTER"])
collection = client.get_collection("aster-l1t")
item = collection.get_item("AST_L1T_00312272006020322_20150518201805")
assert item
assert item.id == "AST_L1T_00312272006020322_20150518201805"

item = collection.get_item("for-sure-not-a-real-id")
assert item is None