Skip to content

Commit f27f4dd

Browse files
ircwavesgadomski
andauthored
Aggregation of queryables across select collections (#511)
* Aggregation of queryables across select collections * Update get_queryables docstring Co-authored-by: Pete Gadomski <[email protected]> * ruff reordered imports * update: get_collection no longer returns Optional Previously Client.get_collection returned None if the requested collection was not found. Now it has been updated to raise an exception if the requested collection does not exist. * move Client-specific get_queryables implementation to Client class * update CHANGELOG.md * remove orjson dep * docs: fix wording --------- Co-authored-by: Pete Gadomski <[email protected]>
1 parent 7b74de7 commit f27f4dd

File tree

5 files changed

+703
-25
lines changed

5 files changed

+703
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1212
- Timeout option added to `Client.open` [#463](https://github.com/stac-utils/pystac-client/pull/463)
1313
- Support for fetching catalog queryables [#477](https://github.com/stac-utils/pystac-client/pull/477)
1414
- PySTAC Client specific warnings [#480](https://github.com/stac-utils/pystac-client/pull/480)
15+
- Support for fetching and merging a selection of queryables [#511](https://github.com/stac-utils/pystac-client/pull/511)
1516

1617
### Changed
1718

pystac_client/client.py

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
from functools import lru_cache
21
import re
2+
import warnings
3+
from functools import lru_cache
34
from typing import (
45
TYPE_CHECKING,
56
Any,
67
Callable,
7-
cast,
88
Dict,
99
Iterator,
1010
List,
1111
Optional,
1212
Union,
13+
cast,
1314
)
14-
import warnings
1515

1616
import pystac
1717
import pystac.utils
@@ -22,7 +22,6 @@
2222
from pystac_client._utils import Modifiable, call_modifier
2323
from pystac_client.collection_client import CollectionClient
2424
from pystac_client.conformance import ConformanceClasses
25-
2625
from pystac_client.errors import ClientTypeError
2726
from pystac_client.exceptions import APIError
2827
from pystac_client.item_search import (
@@ -39,13 +38,9 @@
3938
QueryLike,
4039
SortbyLike,
4140
)
42-
from pystac_client.mixins import QueryablesMixin
41+
from pystac_client.mixins import QUERYABLES_ENDPOINT, QueryablesMixin
4342
from pystac_client.stac_api_io import StacApiIO, Timeout
44-
from pystac_client.warnings import (
45-
DoesNotConformTo,
46-
FallbackToPystac,
47-
NoConformsTo,
48-
)
43+
from pystac_client.warnings import DoesNotConformTo, FallbackToPystac, NoConformsTo
4944

5045
if TYPE_CHECKING:
5146
from pystac.item import Item as Item_Type
@@ -340,17 +335,60 @@ def _warn_about_fallback(self, *args: str) -> None:
340335
warnings.warn(DoesNotConformTo(*args), stacklevel=2)
341336
warnings.warn(FallbackToPystac(), stacklevel=2)
342337

338+
def get_merged_queryables(self, collections: List[str]) -> Dict[str, Any]:
339+
"""Return the set of queryables in common to the specified collections.
340+
341+
Queryables from multiple collections are unioned together, except in the case
342+
when the same queryable key has a different definition, in which case that key
343+
is dropped.
344+
345+
Output is a dictionary that can be used in ``jsonshema.validate``
346+
347+
Args:
348+
collections List[str]: The IDs of the collections to inspect.
349+
350+
Return:
351+
Dict[str, Any]: Dictionary containing queryable fields
352+
"""
353+
if not collections:
354+
raise ValueError("cannot get_merged_queryables from empty Iterable")
355+
356+
if not self.conforms_to(ConformanceClasses.FILTER):
357+
raise DoesNotConformTo(ConformanceClasses.FILTER.name)
358+
response = self.get_queryables_from(
359+
self._get_collection_queryables_href(collections[0])
360+
)
361+
response.pop("$id")
362+
addl_props = response.get("additionalProperties", False)
363+
for collection in collections[1:]:
364+
resp = self.get_queryables_from(
365+
self._get_collection_queryables_href(collection)
366+
)
367+
368+
# additionalProperties is false if any collection doesn't support additional
369+
# properties
370+
addl_props &= resp.get("additionalProperties", False)
371+
372+
# drop queryables if their keys match, but the descriptions differ
373+
for k in set(resp["properties"]).intersection(response["properties"]):
374+
if resp["properties"][k] != response["properties"][k]:
375+
resp["properties"].pop(k)
376+
response["properties"].pop(k)
377+
response["properties"].update(resp["properties"])
378+
return response
379+
343380
@lru_cache()
344-
def get_collection(
345-
self, collection_id: str
346-
) -> Optional[Union[Collection, CollectionClient]]:
381+
def get_collection(self, collection_id: str) -> Union[Collection, CollectionClient]:
347382
"""Get a single collection from this Catalog/API
348383
349384
Args:
350385
collection_id: The Collection ID to get
351386
352387
Returns:
353388
Union[Collection, CollectionClient]: A STAC Collection
389+
390+
Raises:
391+
NotFoundError if collection_id does not exist.
354392
"""
355393
collection: Union[Collection, CollectionClient]
356394

@@ -602,3 +640,9 @@ def _collections_href(self, collection_id: Optional[str] = None) -> str:
602640
if collection_id is not None:
603641
return f"{href.rstrip('/')}/{collection_id}"
604642
return href
643+
644+
def _get_collection_queryables_href(
645+
self, collection_id: Optional[str] = None
646+
) -> str:
647+
href = self._collections_href(collection_id)
648+
return f"{href.rstrip('/')}/{QUERYABLES_ENDPOINT}"

pystac_client/mixins.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from typing import Optional, Dict, Any, Union
21
import warnings
2+
from typing import Any, Dict, Optional, Union
33

44
import pystac
55

6-
from pystac_client.exceptions import APIError
76
from pystac_client.conformance import ConformanceClasses
7+
from pystac_client.exceptions import APIError
88
from pystac_client.stac_api_io import StacApiIO
99
from pystac_client.warnings import DoesNotConformTo, MissingLink
1010

@@ -32,22 +32,21 @@ def _get_href(self, rel: str, link: Optional[pystac.Link], endpoint: str) -> str
3232
class QueryablesMixin(BaseMixin):
3333
"""Mixin for adding support for /queryables endpoint"""
3434

35-
def get_queryables(self) -> Dict[str, Any]:
35+
def get_queryables_from(self, url: str) -> Dict[str, Any]:
3636
"""Return all queryables.
3737
3838
Output is a dictionary that can be used in ``jsonshema.validate``
3939
40+
Args:
41+
url: a queryables url
42+
4043
Return:
4144
Dict[str, Any]: Dictionary containing queryable fields
4245
"""
46+
4347
if self._stac_io is None:
4448
raise APIError("API access is not properly configured")
4549

46-
if not self.conforms_to(ConformanceClasses.FILTER):
47-
raise DoesNotConformTo(ConformanceClasses.FILTER.name)
48-
49-
url = self._get_queryables_href()
50-
5150
result = self._stac_io.read_json(url)
5251
if "properties" not in result:
5352
raise APIError(
@@ -57,7 +56,14 @@ def get_queryables(self) -> Dict[str, Any]:
5756

5857
return result
5958

59+
def get_queryables(self) -> Dict[str, Any]:
60+
url = self._get_queryables_href()
61+
return self.get_queryables_from(url)
62+
6063
def _get_queryables_href(self) -> str:
64+
if not self.conforms_to(ConformanceClasses.FILTER):
65+
raise DoesNotConformTo(ConformanceClasses.FILTER.name)
66+
6167
link = self.get_single_link(QUERYABLES_REL)
6268
href = self._get_href(QUERYABLES_REL, link, QUERYABLES_ENDPOINT)
6369
return href

0 commit comments

Comments
 (0)