Skip to content

Add simple version of get_queryables #477

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

Merged
merged 11 commits into from
Apr 21, 2023
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 @@ -21,6 +21,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Three tests that were false positives due to out-of-date cassettes [#491](https://github.com/stac-utils/pystac-client/pull/491)
- Max items checks when paging [#492](https://github.com/stac-utils/pystac-client/pull/492)

### Added

- Support for fetching catalog queryables [#477](https://github.com/stac-utils/pystac-client/pull/477)

## [v0.6.1] - 2023-03-14

### Changed
Expand Down
9 changes: 6 additions & 3 deletions pystac_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@
QueryLike,
SortbyLike,
)
from pystac_client.mixins import QueryablesMixin
from pystac_client.stac_api_io import StacApiIO

if TYPE_CHECKING:
from pystac.item import Item as Item_Type


class Client(pystac.Catalog):
class Client(pystac.Catalog, QueryablesMixin):
"""A Client for interacting with the root of a STAC Catalog or API

Instances of the ``Client`` class inherit from :class:`pystac.Catalog`
Expand Down Expand Up @@ -236,7 +237,9 @@ def from_dict(
return result

@lru_cache()
def get_collection(self, collection_id: str) -> Optional[Collection]:
def get_collection(
self, collection_id: str
) -> Optional[Union[Collection, CollectionClient]]:
"""Get a single collection from this Catalog/API

Args:
Expand All @@ -262,7 +265,7 @@ def get_collection(self, collection_id: str) -> Optional[Collection]:

return None

def get_collections(self) -> Iterator[Collection]:
def get_collections(self) -> Iterator[Union[Collection, CollectionClient]]:
"""Get Collections in this Catalog

Gets the collections from the /collections endpoint if supported,
Expand Down
92 changes: 45 additions & 47 deletions pystac_client/collection_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
from pystac_client.conformance import ConformanceClasses
from pystac_client.exceptions import APIError
from pystac_client.item_search import ItemSearch
from pystac_client.mixins import QueryablesMixin
from pystac_client.stac_api_io import StacApiIO

if TYPE_CHECKING:
from pystac.item import Item as Item_Type


class CollectionClient(pystac.Collection):
class CollectionClient(pystac.Collection, QueryablesMixin):
modifier: Callable[[Modifiable], None]
_stac_io: Optional[StacApiIO]

Expand Down Expand Up @@ -70,6 +71,15 @@ def from_dict(
setattr(result, "modifier", modifier)
return result

def set_root(self, root: Optional[pystac.Catalog]) -> None:
# hook in to set_root and use it for setting _stac_io
super().set_root(root=root)
if root is not None and root._stac_io is not None:
if not isinstance(root._stac_io, StacApiIO):
raise ValueError("Root should be a Client object")
else:
self._stac_io = root._stac_io

def __repr__(self) -> str:
return "<CollectionClient id={}>".format(self.id)

Expand All @@ -85,19 +95,11 @@ def get_items(self) -> Iterator["Item_Type"]:
"""

link = self.get_single_link("items")
root = self.get_root()
if link is not None and root is not None:
# error: Argument "stac_io" to "ItemSearch" has incompatible type
# "Optional[StacIO]"; expected "Optional[StacApiIO]" [arg-type]
# so we add these asserts
stac_io = root._stac_io
assert stac_io
assert isinstance(stac_io, StacApiIO)

if link is not None and self._stac_io is not None:
search = ItemSearch(
url=link.href,
method="GET",
stac_io=stac_io,
stac_io=self._stac_io,
modifier=self.modifier,
)
yield from search.items()
Expand Down Expand Up @@ -128,43 +130,39 @@ def get_item(self, id: str, recursive: bool = False) -> Optional["Item_Type"]:
"""
if not recursive:
root = self.get_root()
assert root
stac_io = root._stac_io
assert stac_io
assert isinstance(stac_io, StacApiIO)
items_link = self.get_single_link("items")
if root:
if root and self._stac_io:
items_link = self.get_single_link("items")
search_link = root.get_single_link("search")
else:
search_link = None
if (
stac_io.conforms_to(ConformanceClasses.FEATURES)
and items_link is not None
):
url = f"{items_link.href}/{id}"
try:
obj = stac_io.read_stac_object(url, root=self)
item = cast(Optional[pystac.Item], obj)
except APIError as err:
if err.status_code and err.status_code == 404:
return None
else:
raise err
assert isinstance(item, pystac.Item)
elif (
stac_io.conforms_to(ConformanceClasses.ITEM_SEARCH)
and search_link
and search_link.href
):
item_search = ItemSearch(
url=search_link.href,
method="GET",
stac_io=self._stac_io,
ids=[id],
collections=[self.id],
modifier=self.modifier,
)
item = next(item_search.items(), None)
if (
self._stac_io.conforms_to(ConformanceClasses.FEATURES)
and items_link is not None
):
url = f"{items_link.href}/{id}"
try:
obj = self._stac_io.read_stac_object(url, root=self)
item = cast(Optional[pystac.Item], obj)
except APIError as err:
if err.status_code and err.status_code == 404:
return None
else:
raise err
assert isinstance(item, pystac.Item)
elif (
self._stac_io.conforms_to(ConformanceClasses.ITEM_SEARCH)
and search_link
and search_link.href
):
item_search = ItemSearch(
url=search_link.href,
method="GET",
stac_io=self._stac_io,
ids=[id],
collections=[self.id],
modifier=self.modifier,
)
item = next(item_search.items(), None)
else:
item = super().get_item(id, recursive=False)
else:
item = super().get_item(id, recursive=False)
else:
Expand Down
51 changes: 51 additions & 0 deletions pystac_client/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from typing import Optional, Dict, Any

import pystac

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

QUERYABLES_REL = "http://www.opengis.net/def/rel/ogc/1.0/queryables"
QUERYABLES_ENDPOINT = "/queryables"


class StacAPIObject(pystac.STACObject):
_stac_io: Optional[StacApiIO]


class QueryablesMixin(StacAPIObject):
"""Mixin for adding support for /queryables endpoint"""

def get_queryables(self) -> Dict[str, Any]:
"""Return all queryables.

Output is a dictionary that can be used in ``jsonshema.validate``

Return:
Dict[str, Any]: Dictionary containing queryable fields
"""
if self._stac_io is None:
raise APIError("API access is not properly configured")

self._stac_io.assert_conforms_to(ConformanceClasses.FILTER)
url = self._get_queryables_href()

result = self._stac_io.read_json(url)
if "properties" not in result:
raise APIError(
f"Invalid response from {QUERYABLES_ENDPOINT}: "
"expected 'properties' attribute"
)

return result

def _get_queryables_href(self) -> str:
link = self.get_single_link(QUERYABLES_REL)
if link is not None:
url = link.href
else:
# The queryables link should be defined at the root, but if it is not
# try to guess the url
url = f"{self.self_href.rstrip('/')}{QUERYABLES_ENDPOINT}"
return url
130 changes: 130 additions & 0 deletions tests/cassettes/test_client/TestQueryables.test_get_queryables.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.28.2
method: GET
uri: https://planetarycomputer.microsoft.com/api/stac/v1
response:
body:
string: !!binary |
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
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Origin:
- '*'
Content-Encoding:
- gzip
Content-Length:
- '2906'
Content-Type:
- application/json
Date:
- Wed, 12 Apr 2023 18:24:59 GMT
Strict-Transport-Security:
- max-age=15724800; includeSubDomains
Vary:
- Accept-Encoding
X-Azure-Ref:
- 0fPc2ZAAAAADutojn4rB+S6IokbBhkP27RVdSMzBFREdFMDUxMAA5MjdhYmZhNi0xOWY2LTRhZjEtYTA5ZC1jOTU5ZDlhMWU2NDQ=
X-Cache:
- CONFIG_NOCACHE
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
User-Agent:
- python-requests/2.28.2
method: GET
uri: https://planetarycomputer.microsoft.com/api/stac/v1/queryables
response:
body:
string: '{"$schema":"http://json-schema.org/draft-07/schema#","$id":"https://example.org/queryables","type":"object","title":"","properties":{"id":{"title":"Item
ID","description":"Item identifier","$ref":"https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id"}}}'
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Origin:
- '*'
Content-Length:
- '308'
Content-Type:
- application/schema+json
Date:
- Wed, 12 Apr 2023 18:25:04 GMT
Strict-Transport-Security:
- max-age=15724800; includeSubDomains
X-Azure-Ref:
- 0fPc2ZAAAAAD8HYLLizYsQorHSY/+cLX5RVdSMzBFREdFMDUxMAA5MjdhYmZhNi0xOWY2LTRhZjEtYTA5ZC1jOTU5ZDlhMWU2NDQ=
X-Cache:
- CONFIG_NOCACHE
status:
code: 200
message: OK
version: 1
Loading