diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 242e6a9c..5316e547 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: "v0.4.4" + rev: "v0.4.5" hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/CHANGELOG.md b/CHANGELOG.md index f3824554..f14f3964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- Support for "Feature" type `intersects` dictionaries [#696](https://github.com/stac-utils/pystac-client/pull/696) + +## [v0.8.1] - 2024-05-23 + +### Fixed + +- Use singular `include` and `exclude` Field extension key names [#690](https://github.com/stac-utils/pystac-client/pull/690) + ## [v0.8.0] - 2024-05-17 ### Fixed @@ -366,7 +376,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. Initial release. -[Unreleased]: https://github.com/stac-utils/pystac-client/compare/v0.8.0...main +[Unreleased]: https://github.com/stac-utils/pystac-client/compare/v0.8.1...main +[v0.8.1]: https://github.com/stac-utils/pystac-client/compare/v0.8.0...v0.8.1 [v0.8.0]: https://github.com/stac-utils/pystac-client/compare/v0.7.7...v0.8.0 [v0.7.7]: https://github.com/stac-utils/pystac-client/compare/v0.7.6...v0.7.7 [v0.7.6]: https://github.com/stac-utils/pystac-client/compare/v0.7.5...v0.7.6 diff --git a/docs/index.rst b/docs/index.rst index 198f7ac7..a27ac16c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,6 +19,8 @@ STAC Versions +---------------+-----------+-----------------------------+ | pystac-client | STAC spec | STAC API Spec | +===============+===========+=============================+ +| 0.8.x | 1.0.x | 1.0.0-beta.1 - 1.0.0 | ++ --------------+-----------+-----------------------------+ | 0.7.x | 1.0.x | 1.0.0-beta.1 - 1.0.0 | +---------------+-----------+-----------------------------+ | 0.6.x | 1.0.x | 1.0.0-beta.1 - 1.0.0-rc.2 | diff --git a/pyproject.toml b/pyproject.toml index dd38815b..bac841cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ stac-client = "pystac_client.cli:cli" [project.optional-dependencies] dev = [ "black~=24.0", - "codespell~=2.2.4", + "codespell~=2.3.0", "coverage~=7.2", "doc8~=1.1.1", "importlib-metadata~=7.0", @@ -53,7 +53,7 @@ dev = [ "pytest~=8.0", "recommonmark~=0.7.1", "requests-mock~=1.12", - "ruff==0.4.4", + "ruff==0.4.6", "tomli~=2.0; python_version<'3.11'", "types-python-dateutil>=2.8.19,<2.10.0", "types-requests~=2.31.0", diff --git a/pystac_client/item_search.py b/pystac_client/item_search.py index de9b51c5..fe1bb14d 100644 --- a/pystac_client/item_search.py +++ b/pystac_client/item_search.py @@ -179,11 +179,10 @@ class ItemSearch: bbox: A list, tuple, or iterator representing a bounding box of 2D or 3D coordinates. Results will be filtered to only those intersecting the bounding box. - 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. + intersects: A string or dictionary representing a GeoJSON geometry or feature, + 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. datetime: Either a single datetime or datetime range used to filter results. You may express a single datetime using a :class:`datetime.datetime` instance, a `RFC 3339-compliant `__ @@ -633,12 +632,12 @@ def _fields_to_dict(fields: List[str]) -> Fields: includes.append(field[1:]) else: includes.append(field) - return {"includes": includes, "excludes": excludes} + return {"include": includes, "exclude": excludes} @staticmethod def _fields_dict_to_str(fields: Fields) -> str: - includes = [f"+{x}" for x in fields.get("includes", [])] - excludes = [f"-{x}" for x in fields.get("excludes", [])] + includes = [f"+{x}" for x in fields.get("include", [])] + excludes = [f"-{x}" for x in fields.get("exclude", [])] return ",".join(chain(includes, excludes)) @staticmethod @@ -646,7 +645,10 @@ def _format_intersects(value: Optional[IntersectsLike]) -> Optional[Intersects]: if value is None: return None if isinstance(value, dict): - return deepcopy(value) + if value.get("type") == "Feature": + return deepcopy(value.get("geometry")) + else: + return deepcopy(value) if isinstance(value, str): return dict(json.loads(value)) if hasattr(value, "__geo_interface__"): diff --git a/pystac_client/version.py b/pystac_client/version.py index 777f190d..8088f751 100644 --- a/pystac_client/version.py +++ b/pystac_client/version.py @@ -1 +1 @@ -__version__ = "0.8.0" +__version__ = "0.8.1" diff --git a/tests/cassettes/test_item_search/test_fields.yaml b/tests/cassettes/test_item_search/test_fields.yaml new file mode 100644 index 00000000..49e3a2b8 --- /dev/null +++ b/tests/cassettes/test_item_search/test_fields.yaml @@ -0,0 +1,59 @@ +interactions: +- request: + body: '{"collections": ["sentinel-2-c1-l2a"], "intersects": {"type": "Point", + "coordinates": [-105.1019, 40.1672]}, "fields": {"include": [], "exclude": ["geometry", + "assets", "links"]}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '179' + Content-Type: + - application/json + User-Agent: + - python-requests/2.31.0 + method: POST + uri: https://earth-search.aws.element84.com/v1/search + response: + body: + string: '{"type":"FeatureCollection","stac_version":"1.0.0","stac_extensions":[],"context":{"limit":10,"matched":572,"returned":10},"numberMatched":572,"numberReturned":10,"features":[{"stac_version":"1.0.0","bbox":[-105.349918,39.661089,-104.884569,40.650836],"id":"S2A_T13TDE_20240522T174948_L2A","collection":"sentinel-2-c1-l2a","type":"Feature","properties":{"datetime":"2024-05-22T17:52:58.479000Z"}},{"stac_version":"1.0.0","bbox":[-106.183161,39.655762,-104.884569,40.65079],"id":"S2B_T13TDE_20240520T175430_L2A","collection":"sentinel-2-c1-l2a","type":"Feature","properties":{"datetime":"2024-05-20T18:02:54.683000Z"}},{"stac_version":"1.0.0","bbox":[-105.348402,39.661093,-104.884569,40.650836],"id":"S2B_T13TDE_20240517T174143_L2A","collection":"sentinel-2-c1-l2a","type":"Feature","properties":{"datetime":"2024-05-17T17:52:57.296000Z"}},{"stac_version":"1.0.0","bbox":[-106.183161,39.655762,-104.884569,40.65079],"id":"S2A_T13TDE_20240515T175302_L2A","collection":"sentinel-2-c1-l2a","type":"Feature","properties":{"datetime":"2024-05-15T18:02:58.179000Z"}},{"stac_version":"1.0.0","bbox":[-105.368102,39.661033,-104.884569,40.650826],"id":"S2A_T13TDE_20240512T174554_L2A","collection":"sentinel-2-c1-l2a","type":"Feature","properties":{"datetime":"2024-05-12T17:53:02.216000Z"}},{"stac_version":"1.0.0","bbox":[-106.183161,39.655762,-104.884569,40.65079],"id":"S2B_T13TDE_20240510T175043_L2A","collection":"sentinel-2-c1-l2a","type":"Feature","properties":{"datetime":"2024-05-10T18:02:56.145000Z"}},{"stac_version":"1.0.0","bbox":[-105.361809,39.661052,-104.884569,40.650829],"id":"S2B_T13TDE_20240507T174341_L2A","collection":"sentinel-2-c1-l2a","type":"Feature","properties":{"datetime":"2024-05-07T17:52:59.873000Z"}},{"stac_version":"1.0.0","bbox":[-106.183161,39.655762,-104.884569,40.65079],"id":"S2A_T13TDE_20240505T175610_L2A","collection":"sentinel-2-c1-l2a","type":"Feature","properties":{"datetime":"2024-05-05T18:02:58.175000Z"}},{"stac_version":"1.0.0","bbox":[-105.368337,39.661032,-104.884569,40.650825],"id":"S2A_T13TDE_20240502T175028_L2A","collection":"sentinel-2-c1-l2a","type":"Feature","properties":{"datetime":"2024-05-02T17:53:01.904000Z"}},{"stac_version":"1.0.0","bbox":[-106.183161,39.655762,-104.884569,40.65079],"id":"S2B_T13TDE_20240430T175332_L2A","collection":"sentinel-2-c1-l2a","type":"Feature","properties":{"datetime":"2024-04-30T18:02:55.400000Z"}}],"links":[{"rel":"next","title":"Next + page of Items","method":"POST","type":"application/geo+json","href":"https://earth-search.aws.element84.com/v1/search","merge":false,"body":{"intersects":{"type":"Point","coordinates":[-105.1019,40.1672]},"fields":{"include":[],"exclude":["geometry","assets","links"]},"collections":["sentinel-2-c1-l2a"],"next":"2024-04-30T18:02:55.400000Z,S2B_T13TDE_20240430T175332_L2A,sentinel-2-c1-l2a"}},{"rel":"root","type":"application/json","href":"https://earth-search.aws.element84.com/v1"}]}' + headers: + Connection: + - keep-alive + Content-Length: + - '2914' + Content-Type: + - application/geo+json; charset=utf-8 + Date: + - Thu, 23 May 2024 13:55:07 GMT + Via: + - 1.1 584febef1233840787d98d1cd03f82c0.cloudfront.net (CloudFront) + X-Amz-Cf-Id: + - SvrAu9sInbz45xXsZLnU9fYdDuyPI4l5_IVhZTXZaoplQEKQ9CUNKw== + X-Amz-Cf-Pop: + - DEN52-C1 + X-Amzn-Trace-Id: + - Root=1-664f4abb-67c82c1178b7ad8c4adf165a;Parent=07f14554667b6d8c;Sampled=0;lineage=9e2884e9:0 + X-Cache: + - Miss from cloudfront + access-control-allow-origin: + - '*' + etag: + - W/"b62-IhsIfKrcigmqt63/hc9yHpeCjD8" + x-amz-apigw-id: + - YOidRFT8vHcEj-w= + x-amzn-Remapped-content-length: + - '2914' + x-amzn-RequestId: + - d105b93a-e1f1-4d32-b4ef-86a4b3e10525 + x-powered-by: + - Express + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_item_search.py b/tests/test_item_search.py index a198181e..ca7704d2 100644 --- a/tests/test_item_search.py +++ b/tests/test_item_search.py @@ -468,23 +468,23 @@ def test_fields(self) -> None: search = ItemSearch(url=SEARCH_URL, fields="id,collection,+foo,-bar") assert search.get_parameters()["fields"] == { - "excludes": ["bar"], - "includes": ["id", "collection", "foo"], + "exclude": ["bar"], + "include": ["id", "collection", "foo"], } search = ItemSearch(url=SEARCH_URL, fields=["id", "collection", "+foo", "-bar"]) assert search.get_parameters()["fields"] == { - "excludes": ["bar"], - "includes": ["id", "collection", "foo"], + "exclude": ["bar"], + "include": ["id", "collection", "foo"], } search = ItemSearch( url=SEARCH_URL, - fields={"excludes": ["bar"], "includes": ["id", "collection"]}, + fields={"exclude": ["bar"], "include": ["id", "collection"]}, ) assert search.get_parameters()["fields"] == { - "excludes": ["bar"], - "includes": ["id", "collection"], + "exclude": ["bar"], + "include": ["id", "collection"], } search = ItemSearch( @@ -500,7 +500,7 @@ def test_fields(self) -> None: search = ItemSearch( url=SEARCH_URL, method="GET", - fields={"excludes": ["bar"], "includes": ["id", "collection"]}, + fields={"exclude": ["bar"], "include": ["id", "collection"]}, ) assert search.get_parameters()["fields"] == "+id,+collection,-bar" @@ -835,3 +835,32 @@ def test_naive_datetime() -> None: method="POST", ) assert search.get_parameters()["datetime"] == "2024-05-14T04:25:42Z" + + +@pytest.mark.vcr +def test_fields() -> None: + search = ItemSearch( + url="https://earth-search.aws.element84.com/v1/search", + collections=["sentinel-2-c1-l2a"], + intersects={"type": "Point", "coordinates": [-105.1019, 40.1672]}, + max_items=1, + fields=["-geometry", "-assets", "-links"], + ) + item = next(search.items_as_dicts()) + assert "geometry" not in item + assert "assets" not in item + assert "links" not in item + + +def test_feature() -> None: + search = ItemSearch( + url="https://earth-search.aws.element84.com/v1/search", + intersects={ + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [-105.1019, 40.1672]}, + }, + ) + assert search.get_parameters()["intersects"] == { + "type": "Point", + "coordinates": [-105.1019, 40.1672], + }