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

clarify type requirements for search intersects parameter, update some documentation #174

Merged
merged 11 commits into from
May 26, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Bumped PySTAC dependency to >= 1.4.0 [#147](https://github.com/stac-utils/pystac-client/pull/147)
- Search `filter-lang` defaults to `cql2-json` instead of `cql-json`
- Search `filter-lang` will be set to `cql2-json` if the `filter` is a dict, or `cql2-text` if it is a string
- Search parameter `intersects` is now typed to only accept a str, dict, or object that implements `__geo_interface__`

## Removed

Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
STAC Client
===============
# STAC Client <!-- omit in toc -->
gadomski marked this conversation as resolved.
Show resolved Hide resolved

[![CI](https://github.com/stac-utils/pystac-client/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/stac-utils/pystac-client/actions/workflows/continuous-integration.yml)
[![Release](https://github.com/stac-utils/pystac-client/actions/workflows/release.yml/badge.svg)](https://github.com/stac-utils/pystac-client/actions/workflows/release.yml)
Expand Down
4 changes: 2 additions & 2 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ The :meth:`pystac_client.Client.search` method provides an interface for making
.. code-block:: python

>>> from pystac_client import API
>>> api = API.from_file('https://eod-catalog-svc-prod.astraea.earth')
>>> api = API.from_file('https://planetarycomputer.microsoft.com/api/stac/v1')
>>> results = api.search(
... bbox=[-73.21, 43.99, -73.12, 44.05],
... datetime=['2019-01-01T00:00:00Z', '2019-01-02T00:00:00Z'],
Expand Down Expand Up @@ -169,7 +169,7 @@ implementation of this ``"next"`` link parsing assumes that the link follows the
described in the `STAC API - Item Search: Paging <https://github.com/radiantearth/stac-api-spec/tree/master/item-search#paging>`__
section. See the :mod:`Paging <pystac_client.paging>` docs for details on how to customize this behavior.

Query Filter
Query Extension
------------

If the Catalog supports the `Query
Expand Down
30 changes: 22 additions & 8 deletions pystac_client/item_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from collections.abc import Iterable, Mapping
from copy import deepcopy
from datetime import timezone, datetime as datetime_
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Tuple, Union
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Tuple, Union, Protocol
import warnings

from pystac import Collection, Item, ItemCollection
Expand All @@ -22,6 +22,12 @@
r"(?P<remainder>(T|t)\d{2}:\d{2}:\d{2}(\.\d+)?"
r"(?P<tz_info>Z|([-+])(\d{2}):(\d{2}))?)?)?)?")


class GeoInterface(Protocol):
def __geo_interface__(self) -> dict:
...


DatetimeOrTimestamp = Optional[Union[datetime_, str]]
Datetime = Union[Tuple[str], Tuple[str, str]]
DatetimeLike = Union[DatetimeOrTimestamp, Tuple[DatetimeOrTimestamp, DatetimeOrTimestamp],
Expand All @@ -37,7 +43,7 @@
IDsLike = Union[IDs, str, List[str], Iterator[str]]

Intersects = dict
IntersectsLike = Union[str, Intersects, object]
IntersectsLike = Union[str, Intersects, GeoInterface]

Query = dict
QueryLike = Union[Query, List[str]]
Expand Down Expand Up @@ -132,7 +138,9 @@ class ItemSearch:
- ``2017/2018`` expands to ``2017-01-01T00:00:00Z/2018-12-31T23:59:59Z``
- ``2017-06/2017-07`` expands to ``2017-06-01T00:00:00Z/2017-07-31T23:59:59Z``
- ``2017-06-10/2017-06-11`` expands to ``2017-06-10T00:00:00Z/2017-06-11T23:59:59Z``
intersects: A GeoJSON-like dictionary or JSON string. Results filtered to only those intersecting the geometry
intersects: A string or dictionary representing a GeoJSON geometry, or an object that implements a
``__geo_interface__`` property as supported by several libraries including Shapely, ArcPy, PySAL, and
geojson. Results filtered to only those intersecting the geometry.
ids: List of Item ids to return. All other filter parameters that further restrict the number of search results
(except ``limit``) are ignored.
collections: List of one or more Collection IDs or :class:`pystac.Collection` instances. Only Items in one
Expand All @@ -142,11 +150,11 @@ class ItemSearch:
filter_lang: Language variant used in the filter body. If `filter` is a dictionary or not provided, defaults
to 'cql2-json'. If `filter` is a string, defaults to `cql2-text`.
sortby: A single field or list of fields to sort the response by
fields: A list of fields to return in the response. Note this may result in invalid JSON.
Use `get_all_items_as_dict` to avoid errors
max_items: The maximum number of items to get, even if there are more matched items
fields: A list of fields to include in the response. Note this may result in invalid STAC objects, as they
may not have required fields. Use `get_all_items_as_dict` to avoid object unmarshalling errors.
max_items: The maximum number of items to get, even if there are more matched items.
method: The http method, 'GET' or 'POST'
stac_io: An instance of of StacIO for retrieving results. Normally comes from the Client that returns this ItemSearch
stac_io: An instance of StacIO for retrieving results. Normally comes from the Client that returns this ItemSearch
client: An instance of a root Client used to set the root on resulting Items
"""
def __init__(self,
Expand Down Expand Up @@ -409,9 +417,15 @@ def _format_fields(self, value: Optional[FieldsLike]) -> Optional[Fields]:
def _format_intersects(value: Optional[IntersectsLike]) -> Optional[Intersects]:
if value is None:
return None
if isinstance(value, dict):
return deepcopy(value)
if isinstance(value, str):
return json.loads(value)
return deepcopy(getattr(value, '__geo_interface__', value))
if hasattr(value, '__geo_interface__'):
gadomski marked this conversation as resolved.
Show resolved Hide resolved
return deepcopy(getattr(value, '__geo_interface__'))
raise Exception(
"intersects must be of type None, str, dict, or an object that implements __geo_interface__"
)

@lru_cache(1)
def matched(self) -> int:
Expand Down
8 changes: 7 additions & 1 deletion tests/test_item_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,10 @@ def test_intersects_json_string(self):
search = ItemSearch(url=SEARCH_URL, intersects=json.dumps(INTERSECTS_EXAMPLE))
assert search._parameters['intersects'] == INTERSECTS_EXAMPLE

def test_intersects_non_geo_interface_object(self):
with pytest.raises(Exception):
ItemSearch(url=SEARCH_URL, intersects=object())

def test_filter_lang_default_for_dict(self):
search = ItemSearch(url=SEARCH_URL, filter={})
assert search._parameters['filter-lang'] == 'cql2-json'
Expand Down Expand Up @@ -377,7 +381,9 @@ def test_intersects_results(self):

# Geo-interface object
class MockGeoObject:
__geo_interface__ = intersects_dict
gadomski marked this conversation as resolved.
Show resolved Hide resolved
@property
def __geo_interface__(self):
return intersects_dict

intersects_obj = MockGeoObject()
search = ItemSearch(url=SEARCH_URL, intersects=intersects_obj, collections='naip')
Expand Down