From 81a2ce0e708bce1e166fb0c6be88f611242001e9 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Fri, 24 Jan 2025 10:51:43 +0100 Subject: [PATCH 1/2] refactored conformance classes for extensions --- CHANGES.md | 13 ++ .../stac_fastapi/extensions/core/__init__.py | 10 +- .../extensions/core/aggregation/__init__.py | 4 +- .../core/collection_search/__init__.py | 4 +- .../collection_search/collection_search.py | 48 +++---- .../extensions/core/fields/__init__.py | 4 +- .../extensions/core/fields/fields.py | 17 ++- .../extensions/core/filter/__init__.py | 16 ++- .../extensions/core/filter/filter.py | 122 ++++++++++++++++- .../extensions/core/free_text/free_text.py | 12 +- .../extensions/core/query/__init__.py | 4 +- .../extensions/core/query/query.py | 16 ++- .../extensions/core/sort/__init__.py | 4 +- .../stac_fastapi/extensions/core/sort/sort.py | 17 ++- .../extensions/core/transaction.py | 16 ++- .../tests/test_collection_search.py | 127 +++++++++++------- stac_fastapi/extensions/tests/test_filter.py | 74 +++++++++- 17 files changed, 388 insertions(+), 120 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index aa3a1c83b..a2e8d8546 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,19 @@ ## [Unreleased] +- refactored conformance classes for extensions + + - renamed `collection_search.ConformanceClasses` -> `collection_search.CollectionSearchConformanceClasses` + - `collection_search.CollectionSearchPostExtension.from_extension(ext)` method will now use the conformance classes from the input extensions to derived the output conformance classes. + - added `fields.FieldsConformanceClasses` Enum + - renamed `filter.FilterConformanceClasses.FEATURES_FILTER` -> `filter.FilterConformanceClasses.ITEMS` + - renamed `filter.FilterConformanceClasses.ITEM_SEARCH_FILTER` -> `filter.FilterConformanceClasses.SEARCH` + - added `filter.FilterConformanceClasses.COLLECTIONS` + - added `filter.SearchFilterExtension`, `filter.ItemCollectionFilterExtension` and `filter.CollectionSearchFilterExtension` endpoint specific extensions + - removed `FreeTextConformanceClasses.COLLECTIONS` and `FreeTextConformanceClasses.ITEMS` in `FreeTextExtension` and `FreeTextAdvancedExtension` default conformances classes + - added `query.QueryConformanceClasses` Enum + - added `SortConformanceClasses` Enum + ## [4.0.1] - 2025-01-23 ### Changed diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py index 77e45d0ab..d6b5f7589 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py @@ -3,7 +3,12 @@ from .aggregation import AggregationExtension from .collection_search import CollectionSearchExtension, CollectionSearchPostExtension from .fields import FieldsExtension -from .filter import FilterExtension +from .filter import ( + CollectionSearchFilterExtension, + FilterExtension, + ItemCollectionFilterExtension, + SearchFilterExtension, +) from .free_text import FreeTextAdvancedExtension, FreeTextExtension from .pagination import ( OffsetPaginationExtension, @@ -28,4 +33,7 @@ "TransactionExtension", "CollectionSearchExtension", "CollectionSearchPostExtension", + "SearchFilterExtension", + "ItemCollectionFilterExtension", + "CollectionSearchFilterExtension", ) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/__init__.py index 2a7fc7a71..c7dceafa5 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/__init__.py @@ -1,5 +1,5 @@ """Aggregation extension module.""" -from .aggregation import AggregationExtension +from .aggregation import AggregationConformanceClasses, AggregationExtension -__all__ = ["AggregationExtension"] +__all__ = ["AggregationExtension", "AggregationConformanceClasses"] diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/__init__.py index eed6d5020..2565bec9c 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/__init__.py @@ -1,13 +1,13 @@ """Collection-Search extension module.""" from .collection_search import ( + CollectionSearchConformanceClasses, CollectionSearchExtension, CollectionSearchPostExtension, - ConformanceClasses, ) __all__ = [ "CollectionSearchExtension", "CollectionSearchPostExtension", - "ConformanceClasses", + "CollectionSearchConformanceClasses", ] diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/collection_search.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/collection_search.py index 2bd7ac285..e5ab77b50 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/collection_search.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/collection_search.py @@ -17,7 +17,7 @@ from .request import BaseCollectionSearchGetRequest, BaseCollectionSearchPostRequest -class ConformanceClasses(str, Enum): +class CollectionSearchConformanceClasses(str, Enum): """Conformance classes for the Collection-Search extension. See @@ -26,11 +26,6 @@ class ConformanceClasses(str, Enum): COLLECTIONSEARCH = "https://api.stacspec.org/v1.0.0-rc.1/collection-search" BASIS = "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query" - FREETEXT = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text" - FILTER = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter" - QUERY = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#query" - SORT = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort" - FIELDS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#fields" @attr.s @@ -56,7 +51,10 @@ class CollectionSearchExtension(ApiExtension): POST = None conformance_classes: List[str] = attr.ib( - default=[ConformanceClasses.COLLECTIONSEARCH, ConformanceClasses.BASIS] + default=[ + CollectionSearchConformanceClasses.COLLECTIONSEARCH, + CollectionSearchConformanceClasses.BASIS, + ] ) schema_href: Optional[str] = attr.ib(default=None) @@ -78,21 +76,13 @@ def from_extensions( schema_href: Optional[str] = None, ) -> "CollectionSearchExtension": """Create CollectionSearchExtension object from extensions.""" - known_extension_conformances = { - "FreeTextExtension": ConformanceClasses.FREETEXT, - "FreeTextAdvancedExtension": ConformanceClasses.FREETEXT, - "QueryExtension": ConformanceClasses.QUERY, - "SortExtension": ConformanceClasses.SORT, - "FieldsExtension": ConformanceClasses.FIELDS, - "FilterExtension": ConformanceClasses.FILTER, - } + conformance_classes = [ - ConformanceClasses.COLLECTIONSEARCH, - ConformanceClasses.BASIS, + CollectionSearchConformanceClasses.COLLECTIONSEARCH, + CollectionSearchConformanceClasses.BASIS, ] for ext in extensions: - if conf := known_extension_conformances.get(ext.__class__.__name__, None): - conformance_classes.append(conf) + conformance_classes.extend(ext.conformance_classes) get_request_model = create_request_model( model_name="CollectionsGetRequest", @@ -128,7 +118,10 @@ class CollectionSearchPostExtension(CollectionSearchExtension): client: Union[AsyncBaseCollectionSearchClient, BaseCollectionSearchClient] = attr.ib() settings: ApiSettings = attr.ib() conformance_classes: List[str] = attr.ib( - default=[ConformanceClasses.COLLECTIONSEARCH, ConformanceClasses.BASIS] + default=[ + CollectionSearchConformanceClasses.COLLECTIONSEARCH, + CollectionSearchConformanceClasses.BASIS, + ] ) schema_href: Optional[str] = attr.ib(default=None) router: APIRouter = attr.ib(factory=APIRouter) @@ -180,21 +173,12 @@ def from_extensions( router: Optional[APIRouter] = None, ) -> "CollectionSearchPostExtension": """Create CollectionSearchPostExtension object from extensions.""" - known_extension_conformances = { - "FreeTextExtension": ConformanceClasses.FREETEXT, - "FreeTextAdvancedExtension": ConformanceClasses.FREETEXT, - "QueryExtension": ConformanceClasses.QUERY, - "SortExtension": ConformanceClasses.SORT, - "FieldsExtension": ConformanceClasses.FIELDS, - "FilterExtension": ConformanceClasses.FILTER, - } conformance_classes = [ - ConformanceClasses.COLLECTIONSEARCH, - ConformanceClasses.BASIS, + CollectionSearchConformanceClasses.COLLECTIONSEARCH, + CollectionSearchConformanceClasses.BASIS, ] for ext in extensions: - if conf := known_extension_conformances.get(ext.__class__.__name__, None): - conformance_classes.append(conf) + conformance_classes.extend(ext.conformance_classes) get_request_model = create_request_model( model_name="CollectionsGetRequest", diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/__init__.py index 087d01b7a..da67b145f 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/__init__.py @@ -1,5 +1,5 @@ """Fields extension module.""" -from .fields import FieldsExtension +from .fields import FieldsConformanceClasses, FieldsExtension -__all__ = ["FieldsExtension"] +__all__ = ["FieldsExtension", "FieldsConformanceClasses"] diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py index 90b4b2697..0b6e4177d 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py @@ -1,5 +1,6 @@ """Fields extension.""" +from enum import Enum from typing import List, Optional import attr @@ -10,6 +11,18 @@ from .request import FieldsExtensionGetRequest, FieldsExtensionPostRequest +class FieldsConformanceClasses(str, Enum): + """Conformance classes for the Fields extension. + + See https://github.com/stac-api-extensions/fields + + """ + + SEARCH = "https://api.stacspec.org/v1.0.0/item-search#fields" + ITEMS = "https://api.stacspec.org/v1.0.0/ogcapi-features#fields" + COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#fields" + + @attr.s class FieldsExtension(ApiExtension): """Fields Extension. @@ -33,7 +46,9 @@ class FieldsExtension(ApiExtension): POST = FieldsExtensionPostRequest conformance_classes: List[str] = attr.ib( - factory=lambda: ["https://api.stacspec.org/v1.0.0/item-search#fields"] + factory=lambda: [ + FieldsConformanceClasses.SEARCH, + ] ) schema_href: Optional[str] = attr.ib(default=None) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/__init__.py index 256f3e06e..dbbf2b1e2 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/__init__.py @@ -1,5 +1,17 @@ """Filter extension module.""" -from .filter import FilterExtension +from .filter import ( + CollectionSearchFilterExtension, + FilterConformanceClasses, + FilterExtension, + ItemCollectionFilterExtension, + SearchFilterExtension, +) -__all__ = ["FilterExtension"] +__all__ = [ + "FilterConformanceClasses", + "FilterExtension", + "SearchFilterExtension", + "ItemCollectionFilterExtension", + "CollectionSearchFilterExtension", +] diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py index cd9463ec6..d182dcf37 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py @@ -23,10 +23,11 @@ class FilterConformanceClasses(str, Enum): """ FILTER = "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/filter" - FEATURES_FILTER = ( - "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter" - ) - ITEM_SEARCH_FILTER = "https://api.stacspec.org/v1.0.0-rc.2/item-search#filter" + + SEARCH = "https://api.stacspec.org/v1.0.0-rc.2/item-search#filter" + ITEMS = "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter" + COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter" + CQL2_TEXT = "http://www.opengis.net/spec/cql2/1.0/conf/cql2-text" CQL2_JSON = "http://www.opengis.net/spec/cql2/1.0/conf/cql2-json" BASIC_CQL2 = "http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2" @@ -73,8 +74,8 @@ class FilterExtension(ApiExtension): conformance_classes: List[str] = attr.ib( default=[ FilterConformanceClasses.FILTER, - FilterConformanceClasses.FEATURES_FILTER, - FilterConformanceClasses.ITEM_SEARCH_FILTER, + FilterConformanceClasses.SEARCH, + FilterConformanceClasses.ITEMS, FilterConformanceClasses.BASIC_CQL2, FilterConformanceClasses.CQL2_JSON, FilterConformanceClasses.CQL2_TEXT, @@ -124,3 +125,112 @@ def register(self, app: FastAPI) -> None: endpoint=create_async_endpoint(self.client.get_queryables, CollectionUri), ) app.include_router(self.router, tags=["Filter Extension"]) + + +@attr.s +class SearchFilterExtension(FilterExtension): + """Item Search Filter Extension.""" + + conformance_classes: List[str] = attr.ib( + default=[ + FilterConformanceClasses.FILTER, + FilterConformanceClasses.SEARCH, + FilterConformanceClasses.BASIC_CQL2, + FilterConformanceClasses.CQL2_JSON, + FilterConformanceClasses.CQL2_TEXT, + ] + ) + + def register(self, app: FastAPI) -> None: + """Register the extension with a FastAPI application. + + Args: + app: target FastAPI application. + + Returns: + None + """ + self.router.prefix = app.state.router_prefix + self.router.add_api_route( + name="Queryables", + path="/queryables", + methods=["GET"], + responses={ + 200: { + "content": { + "application/schema+json": {}, + }, + # TODO: add output model in stac-pydantic + }, + }, + response_class=self.response_class, + endpoint=create_async_endpoint(self.client.get_queryables, EmptyRequest), + ) + app.include_router(self.router, tags=["Filter Extension"]) + + +@attr.s +class ItemCollectionFilterExtension(FilterExtension): + """Item Collection Filter Extension.""" + + conformance_classes: List[str] = attr.ib( + default=[ + FilterConformanceClasses.FILTER, + FilterConformanceClasses.ITEMS, + FilterConformanceClasses.BASIC_CQL2, + FilterConformanceClasses.CQL2_JSON, + FilterConformanceClasses.CQL2_TEXT, + ] + ) + + def register(self, app: FastAPI) -> None: + """Register the extension with a FastAPI application. + + Args: + app: target FastAPI application. + + Returns: + None + """ + self.router.add_api_route( + name="Collection Queryables", + path="/collections/{collection_id}/queryables", + methods=["GET"], + responses={ + 200: { + "content": { + "application/schema+json": {}, + }, + # TODO: add output model in stac-pydantic + }, + }, + response_class=self.response_class, + endpoint=create_async_endpoint(self.client.get_queryables, CollectionUri), + ) + app.include_router(self.router, tags=["Filter Extension"]) + + +@attr.s +class CollectionSearchFilterExtension(FilterExtension): + """Collection Search Filter Extension.""" + + conformance_classes: List[str] = attr.ib( + default=[ + FilterConformanceClasses.FILTER, + FilterConformanceClasses.COLLECTIONS, + FilterConformanceClasses.BASIC_CQL2, + FilterConformanceClasses.CQL2_JSON, + FilterConformanceClasses.CQL2_TEXT, + ] + ) + + def register(self, app: FastAPI) -> None: + """Register the extension with a FastAPI application. + + Args: + app: target FastAPI application. + + Returns: + None + """ + pass diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/free_text.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/free_text.py index 8b61b32df..67aaa2b27 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/free_text.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/free_text.py @@ -25,19 +25,19 @@ class FreeTextConformanceClasses(str, Enum): # https://github.com/stac-api-extensions/freetext-search?tab=readme-ov-file#basic SEARCH = "https://api.stacspec.org/v1.0.0-rc.1/item-search#free-text" - COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text" ITEMS = "https://api.stacspec.org/v1.0.0-rc.1/ogcapi-features#free-text" + COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text" # https://github.com/stac-api-extensions/freetext-search?tab=readme-ov-file#advanced SEARCH_ADVANCED = ( "https://api.stacspec.org/v1.0.0-rc.1/item-search#advanced-free-text" ) - COLLECTIONS_ADVANCED = ( - "https://api.stacspec.org/v1.0.0-rc.1/collection-search#advanced-free-text" - ) ITEMS_ADVANCED = ( "https://api.stacspec.org/v1.0.0-rc.1/ogcapi-features#advanced-free-text" ) + COLLECTIONS_ADVANCED = ( + "https://api.stacspec.org/v1.0.0-rc.1/collection-search#advanced-free-text" + ) @attr.s @@ -57,8 +57,6 @@ class FreeTextExtension(ApiExtension): conformance_classes: List[str] = attr.ib( default=[ FreeTextConformanceClasses.SEARCH, - FreeTextConformanceClasses.COLLECTIONS, - FreeTextConformanceClasses.ITEMS, ] ) schema_href: Optional[str] = attr.ib(default=None) @@ -92,8 +90,6 @@ class FreeTextAdvancedExtension(ApiExtension): conformance_classes: List[str] = attr.ib( default=[ FreeTextConformanceClasses.SEARCH_ADVANCED, - FreeTextConformanceClasses.COLLECTIONS_ADVANCED, - FreeTextConformanceClasses.ITEMS_ADVANCED, ] ) schema_href: Optional[str] = attr.ib(default=None) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/__init__.py index 5bbe70595..1be29a545 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/__init__.py @@ -1,5 +1,5 @@ """Query extension module.""" -from .query import QueryExtension +from .query import QueryConformanceClasses, QueryExtension -__all__ = ["QueryExtension"] +__all__ = ["QueryExtension", "QueryConformanceClasses"] diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py index 472c385b4..9f4c8cb0c 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py @@ -1,5 +1,6 @@ """Query extension.""" +from enum import Enum from typing import List, Optional import attr @@ -10,6 +11,17 @@ from .request import QueryExtensionGetRequest, QueryExtensionPostRequest +class QueryConformanceClasses(str, Enum): + """Conformance classes for the Query extension. + + See https://github.com/stac-api-extensions/query + """ + + SEARCH = "https://api.stacspec.org/v1.0.0/item-search#query" + ITEMS = "https://api.stacspec.org/v1.0.0/ogcapi-features#query" + COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#query" + + @attr.s class QueryExtension(ApiExtension): """Query Extension. @@ -24,7 +36,9 @@ class QueryExtension(ApiExtension): POST = QueryExtensionPostRequest conformance_classes: List[str] = attr.ib( - factory=lambda: ["https://api.stacspec.org/v1.0.0/item-search#query"] + factory=lambda: [ + QueryConformanceClasses.SEARCH, + ] ) schema_href: Optional[str] = attr.ib(default=None) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/__init__.py index b6996b018..561dbf464 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/__init__.py @@ -1,5 +1,5 @@ """Sort extension module.""" -from .sort import SortExtension +from .sort import SortConformanceClasses, SortExtension -__all__ = ["SortExtension"] +__all__ = ["SortExtension", "SortConformanceClasses"] diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py index 4b27d8d0e..77984719f 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py @@ -1,5 +1,6 @@ """Sort extension.""" +from enum import Enum from typing import List, Optional import attr @@ -10,6 +11,18 @@ from .request import SortExtensionGetRequest, SortExtensionPostRequest +class SortConformanceClasses(str, Enum): + """Conformance classes for the Sort extension. + + See https://github.com/stac-api-extensions/sort + + """ + + SEARCH = "https://api.stacspec.org/v1.0.0/item-search#sort" + ITEMS = "https://api.stacspec.org/v1.0.0/ogcapi-features#sort" + COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort" + + @attr.s class SortExtension(ApiExtension): """Sort Extension. @@ -23,7 +36,9 @@ class SortExtension(ApiExtension): POST = SortExtensionPostRequest conformance_classes: List[str] = attr.ib( - factory=lambda: ["https://api.stacspec.org/v1.0.0/item-search#sort"] + factory=lambda: [ + SortConformanceClasses.SEARCH, + ] ) schema_href: Optional[str] = attr.ib(default=None) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py index 4e940a0ea..246be296c 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py @@ -1,5 +1,6 @@ """Transaction extension.""" +from enum import Enum from typing import List, Optional, Type, Union import attr @@ -16,6 +17,17 @@ from stac_fastapi.types.extension import ApiExtension +class TransactionConformanceClasses(str, Enum): + """Conformance classes for the Transaction extension. + + See https://github.com/stac-api-extensions/transaction + + """ + + ITEMS = "https://api.stacspec.org/v1.0.0/ogcapi-features/extensions/transaction" + COLLECTIONS = "https://api.stacspec.org/v1.0.0/collections/extensions/transaction" + + @attr.s class PostItem(CollectionUri): """Create Item.""" @@ -62,8 +74,8 @@ class TransactionExtension(ApiExtension): settings: ApiSettings = attr.ib() conformance_classes: List[str] = attr.ib( factory=lambda: [ - "https://api.stacspec.org/v1.0.0/ogcapi-features/extensions/transaction", - "https://api.stacspec.org/v1.0.0/collections/extensions/transaction", + TransactionConformanceClasses.ITEMS, + TransactionConformanceClasses.COLLECTIONS, ] ) schema_href: Optional[str] = attr.ib(default=None) diff --git a/stac_fastapi/extensions/tests/test_collection_search.py b/stac_fastapi/extensions/tests/test_collection_search.py index 83de85eba..4c5f641ad 100644 --- a/stac_fastapi/extensions/tests/test_collection_search.py +++ b/stac_fastapi/extensions/tests/test_collection_search.py @@ -10,15 +10,17 @@ from stac_fastapi.extensions.core import ( AggregationExtension, CollectionSearchExtension, + CollectionSearchFilterExtension, CollectionSearchPostExtension, FieldsExtension, - FilterExtension, FreeTextAdvancedExtension, FreeTextExtension, QueryExtension, SortExtension, ) -from stac_fastapi.extensions.core.collection_search import ConformanceClasses +from stac_fastapi.extensions.core.collection_search import ( + CollectionSearchConformanceClasses, +) from stac_fastapi.extensions.core.collection_search.client import ( BaseCollectionSearchClient, ) @@ -26,22 +28,27 @@ BaseCollectionSearchGetRequest, BaseCollectionSearchPostRequest, ) +from stac_fastapi.extensions.core.fields import FieldsConformanceClasses from stac_fastapi.extensions.core.fields.request import ( FieldsExtensionGetRequest, FieldsExtensionPostRequest, ) +from stac_fastapi.extensions.core.filter import FilterConformanceClasses from stac_fastapi.extensions.core.filter.request import ( FilterExtensionGetRequest, FilterExtensionPostRequest, ) +from stac_fastapi.extensions.core.free_text import FreeTextConformanceClasses from stac_fastapi.extensions.core.free_text.request import ( FreeTextExtensionGetRequest, FreeTextExtensionPostRequest, ) +from stac_fastapi.extensions.core.query import QueryConformanceClasses from stac_fastapi.extensions.core.query.request import ( QueryExtensionGetRequest, QueryExtensionPostRequest, ) +from stac_fastapi.extensions.core.sort import SortConformanceClasses from stac_fastapi.extensions.core.sort.request import ( SortExtensionGetRequest, SortExtensionPostRequest, @@ -148,13 +155,13 @@ def test_collection_search_extension_models(): CollectionSearchExtension( GET=collections_get_request_model, conformance_classes=[ - ConformanceClasses.COLLECTIONSEARCH, - ConformanceClasses.BASIS, - ConformanceClasses.FREETEXT, - ConformanceClasses.FILTER, - ConformanceClasses.QUERY, - ConformanceClasses.SORT, - ConformanceClasses.FIELDS, + CollectionSearchConformanceClasses.COLLECTIONSEARCH, + CollectionSearchConformanceClasses.BASIS, + FieldsConformanceClasses.COLLECTIONS, + FilterConformanceClasses.COLLECTIONS, + FreeTextConformanceClasses.COLLECTIONS, + QueryConformanceClasses.COLLECTIONS, + SortConformanceClasses.COLLECTIONS, ], ) ], @@ -311,13 +318,13 @@ def test_collection_search_extension_post_models(): GET=get_request_model, POST=post_request_model, conformance_classes=[ - ConformanceClasses.COLLECTIONSEARCH, - ConformanceClasses.BASIS, - ConformanceClasses.FREETEXT, - ConformanceClasses.FILTER, - ConformanceClasses.QUERY, - ConformanceClasses.SORT, - ConformanceClasses.FIELDS, + CollectionSearchConformanceClasses.COLLECTIONSEARCH, + CollectionSearchConformanceClasses.BASIS, + FieldsConformanceClasses.COLLECTIONS, + FilterConformanceClasses.COLLECTIONS, + FreeTextConformanceClasses.COLLECTIONS, + QueryConformanceClasses.COLLECTIONS, + SortConformanceClasses.COLLECTIONS, ], ) ], @@ -403,19 +410,23 @@ def test_collection_search_extension_post_models(): [ # with FreeTextExtension [ - FieldsExtension(), - FilterExtension(), - FreeTextExtension(), - QueryExtension(), - SortExtension(), + FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]), + CollectionSearchFilterExtension(), + FreeTextExtension( + conformance_classes=[FreeTextConformanceClasses.COLLECTIONS] + ), + QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]), + SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]), ], # with FreeTextAdvancedExtension [ - FieldsExtension(), - FilterExtension(), - FreeTextAdvancedExtension(), - QueryExtension(), - SortExtension(), + FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]), + CollectionSearchFilterExtension(), + FreeTextAdvancedExtension( + conformance_classes=[FreeTextConformanceClasses.COLLECTIONS] + ), + QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]), + SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]), ], ], ) @@ -436,15 +447,20 @@ def test_from_extensions_methods(extensions): assert hasattr(collection_search, "q") assert hasattr(collection_search, "sortby") assert hasattr(collection_search, "filter_expr") - assert ext.conformance_classes == [ - ConformanceClasses.COLLECTIONSEARCH, - ConformanceClasses.BASIS, - ConformanceClasses.FIELDS, - ConformanceClasses.FILTER, - ConformanceClasses.FREETEXT, - ConformanceClasses.QUERY, - ConformanceClasses.SORT, - ] + for conf in [ + CollectionSearchConformanceClasses.COLLECTIONSEARCH, + CollectionSearchConformanceClasses.BASIS, + FieldsConformanceClasses.COLLECTIONS, + FilterConformanceClasses.COLLECTIONS, + FilterConformanceClasses.FILTER, + FilterConformanceClasses.BASIC_CQL2, + FilterConformanceClasses.CQL2_JSON, + FilterConformanceClasses.CQL2_TEXT, + FreeTextConformanceClasses.COLLECTIONS, + QueryConformanceClasses.COLLECTIONS, + SortConformanceClasses.COLLECTIONS, + ]: + assert conf in ext.conformance_classes ext = CollectionSearchPostExtension.from_extensions( extensions, @@ -460,15 +476,20 @@ def test_from_extensions_methods(extensions): assert hasattr(collection_search, "q") assert hasattr(collection_search, "sortby") assert hasattr(collection_search, "filter_expr") - assert ext.conformance_classes == [ - ConformanceClasses.COLLECTIONSEARCH, - ConformanceClasses.BASIS, - ConformanceClasses.FIELDS, - ConformanceClasses.FILTER, - ConformanceClasses.FREETEXT, - ConformanceClasses.QUERY, - ConformanceClasses.SORT, - ] + for conf in [ + CollectionSearchConformanceClasses.COLLECTIONSEARCH, + CollectionSearchConformanceClasses.BASIS, + FieldsConformanceClasses.COLLECTIONS, + FilterConformanceClasses.COLLECTIONS, + FilterConformanceClasses.FILTER, + FilterConformanceClasses.BASIC_CQL2, + FilterConformanceClasses.CQL2_JSON, + FilterConformanceClasses.CQL2_TEXT, + FreeTextConformanceClasses.COLLECTIONS, + QueryConformanceClasses.COLLECTIONS, + SortConformanceClasses.COLLECTIONS, + ]: + assert conf in ext.conformance_classes def test_from_extensions_methods_invalid(): @@ -486,10 +507,11 @@ def test_from_extensions_methods_invalid(): assert hasattr(collection_search, "datetime") assert hasattr(collection_search, "limit") assert hasattr(collection_search, "aggregations") - assert ext.conformance_classes == [ - ConformanceClasses.COLLECTIONSEARCH, - ConformanceClasses.BASIS, - ] + for conf in [ + CollectionSearchConformanceClasses.COLLECTIONSEARCH, + CollectionSearchConformanceClasses.BASIS, + ]: + assert conf in ext.conformance_classes ext = CollectionSearchPostExtension.from_extensions( extensions, @@ -502,7 +524,8 @@ def test_from_extensions_methods_invalid(): assert hasattr(collection_search, "datetime") assert hasattr(collection_search, "limit") assert hasattr(collection_search, "aggregations") - assert ext.conformance_classes == [ - ConformanceClasses.COLLECTIONSEARCH, - ConformanceClasses.BASIS, - ] + for conf in [ + CollectionSearchConformanceClasses.COLLECTIONSEARCH, + CollectionSearchConformanceClasses.BASIS, + ]: + assert conf in ext.conformance_classes diff --git a/stac_fastapi/extensions/tests/test_filter.py b/stac_fastapi/extensions/tests/test_filter.py index 32152c432..bc8b902e3 100644 --- a/stac_fastapi/extensions/tests/test_filter.py +++ b/stac_fastapi/extensions/tests/test_filter.py @@ -6,6 +6,11 @@ from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import create_get_request_model, create_post_request_model from stac_fastapi.extensions.core import FilterExtension +from stac_fastapi.extensions.core.filter import ( + CollectionSearchFilterExtension, + ItemCollectionFilterExtension, + SearchFilterExtension, +) from stac_fastapi.types.config import ApiSettings from stac_fastapi.types.core import BaseCoreClient @@ -31,7 +36,7 @@ def item_collection(self, *args, **kwargs): raise NotImplementedError -@pytest.fixture +@pytest.fixture(autouse=True) def client() -> Iterator[TestClient]: settings = ApiSettings() extensions = [FilterExtension()] @@ -46,8 +51,63 @@ def client() -> Iterator[TestClient]: yield client -def test_search_filter_post_filter_lang_default(client: TestClient): +@pytest.fixture(autouse=True) +def client_multit_ext() -> Iterator[TestClient]: + settings = ApiSettings() + extensions = [ + SearchFilterExtension(), + ItemCollectionFilterExtension(), + # Technically `CollectionSearchFilterExtension` + # shouldn't be registered to the application but to the collection-search class + CollectionSearchFilterExtension(), + ] + + api = StacApi( + settings=settings, + client=DummyCoreClient(), + extensions=extensions, + search_get_request_model=create_get_request_model([SearchFilterExtension()]), + search_post_request_model=create_post_request_model([SearchFilterExtension()]), + ) + with TestClient(api.app) as client: + yield client + + +@pytest.mark.parametrize("client_name", ["client", "client_multit_ext"]) +def test_filter_endpoints_conformances(client_name, request): + """Make sure conformances classes are set.""" + client = request.getfixturevalue(client_name) + + response = client.get("/conformance") + assert response.is_success, response.json() + response_dict = response.json() + conf = response_dict["conformsTo"] + assert ( + "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter" in conf + ) + assert "https://api.stacspec.org/v1.0.0-rc.2/item-search#filter" in conf + assert client.get("/queryables").is_success + assert client.get("/collections/collection_id/queryables").is_success + + +def test_filter_conformances_collection_search(client_multit_ext): + """Make sure conformances classes are set.""" + response = client_multit_ext.get("/conformance") + assert response.is_success, response.json() + response_dict = response.json() + conf = response_dict["conformsTo"] + assert ( + "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter" in conf + ) + assert "https://api.stacspec.org/v1.0.0-rc.2/item-search#filter" in conf + assert "https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter" in conf + + +@pytest.mark.parametrize("client_name", ["client", "client_multit_ext"]) +def test_search_filter_post_filter_lang_default(client_name, request): """Test search POST endpoint with filter ext.""" + client = request.getfixturevalue(client_name) + response = client.post( "/search", json={ @@ -61,8 +121,11 @@ def test_search_filter_post_filter_lang_default(client: TestClient): assert response_dict["filter_lang"] == "cql2-json" -def test_search_filter_post_filter_lang_non_default(client: TestClient): +@pytest.mark.parametrize("client_name", ["client", "client_multit_ext"]) +def test_search_filter_post_filter_lang_non_default(client_name, request): """Test search POST endpoint with filter ext.""" + client = request.getfixturevalue(client_name) + filter_lang_value = "cql-json" response = client.post( "/search", @@ -78,8 +141,11 @@ def test_search_filter_post_filter_lang_non_default(client: TestClient): assert response_dict["filter_lang"] == filter_lang_value -def test_search_filter_get(client: TestClient): +@pytest.mark.parametrize("client_name", ["client", "client_multit_ext"]) +def test_search_filter_get(client_name, request): """Test search GET endpoint with filter ext.""" + client = request.getfixturevalue(client_name) + response = client.get( "/search", params={ From 317e011b76161e31eabe7d4288b83dc685589701 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Fri, 24 Jan 2025 11:07:48 +0100 Subject: [PATCH 2/2] update changelog --- CHANGES.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a2e8d8546..3aee38ea8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,10 +2,13 @@ ## [Unreleased] +### Changed + - refactored conformance classes for extensions - renamed `collection_search.ConformanceClasses` -> `collection_search.CollectionSearchConformanceClasses` - - `collection_search.CollectionSearchPostExtension.from_extension(ext)` method will now use the conformance classes from the input extensions to derived the output conformance classes. + - removed `FREETEXT`, `FILTER`, `QUERY`, `SORT` and `FIELDS` entries from the `CollectionSearchConformanceClasses` Enum (and moved to each extension's Enum) + - changed `collection_search.CollectionSearchPostExtension.from_extension(ext)` to use the conformance classes from the input extensions to derive the output conformance classes. - added `fields.FieldsConformanceClasses` Enum - renamed `filter.FilterConformanceClasses.FEATURES_FILTER` -> `filter.FilterConformanceClasses.ITEMS` - renamed `filter.FilterConformanceClasses.ITEM_SEARCH_FILTER` -> `filter.FilterConformanceClasses.SEARCH`