From aac7b7a66e394759f411aaf0c63e6c350985e50c Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Wed, 21 Aug 2024 15:02:10 +0200 Subject: [PATCH 1/5] add from_extensions class method to create CollectionSearch extensions classes --- CHANGES.md | 4 + .../collection_search/collection_search.py | 102 ++++++++++++++- .../tests/test_collection_search.py | 119 +++++++++++++++++- 3 files changed, 223 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 992d23c37..5609bd815 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +* Add `from_extensions()` method to `CollectionSearchExtension` and `CollectionSearchPostExtension` extensions to build the class based on a list of available extensions. + ## [3.0.0] - 2024-07-29 Full changelog: https://stac-utils.github.io/stac-fastapi/migrations/v3.0.0/#changelog 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 2927cd822..2a5acbce3 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 @@ -1,5 +1,6 @@ """Collection-Search extension.""" +import warnings from enum import Enum from typing import List, Optional, Union @@ -8,7 +9,7 @@ from stac_pydantic.api.collections import Collections from stac_pydantic.shared import MimeTypes -from stac_fastapi.api.models import GeoJSONResponse +from stac_fastapi.api.models import GeoJSONResponse, create_request_model from stac_fastapi.api.routes import create_async_endpoint from stac_fastapi.types.config import ApiSettings from stac_fastapi.types.extension import ApiExtension @@ -71,6 +72,48 @@ def register(self, app: FastAPI) -> None: """ pass + @classmethod + def from_extensions( + cls, + extensions: List[ApiExtension], + schema_href: Optional[str] = None, + ) -> "CollectionSearchExtension": + """Create CollectionSearchExtension object from extensions.""" + supported_extension = { + "FreeTextExtension": ConformanceClasses.FREETEXT, + "FreeTextAdvancedExtension": ConformanceClasses.FREETEXT, + "QueryExtension": ConformanceClasses.QUERY, + "SortExtension": ConformanceClasses.SORT, + "FieldsExtension": ConformanceClasses.FIELDS, + "FilterExtension": ConformanceClasses.FILTER, + } + conformance_classes = [ + ConformanceClasses.COLLECTIONSEARCH, + ConformanceClasses.BASIS, + ] + for ext in extensions: + conf = supported_extension.get(ext.__class__.__name__, None) + if not conf: + warnings.warn( + f"{ext.__class__.__name__} extension not supported in `CollectionSearchExtension.from_extensions` method.", # noqa: E501 + UserWarning, + ) + else: + conformance_classes.append(supported_extension[ext.__class__.__name__]) + + get_request_model = create_request_model( + model_name="CollectionsGetRequest", + base_model=BaseCollectionSearchGetRequest, + extensions=extensions, + request_type="GET", + ) + + return cls( + GET=get_request_model, + conformance_classes=conformance_classes, + schema_href=schema_href, + ) + @attr.s class CollectionSearchPostExtension(CollectionSearchExtension): @@ -132,3 +175,60 @@ def register(self, app: FastAPI) -> None: endpoint=create_async_endpoint(self.client.post_all_collections, self.POST), ) app.include_router(self.router) + + @classmethod + def from_extensions( + cls, + extensions: List[ApiExtension], + *, + client: Union[AsyncBaseCollectionSearchClient, BaseCollectionSearchClient], + settings: ApiSettings, + schema_href: Optional[str] = None, + router: Optional[APIRouter] = None, + ) -> "CollectionSearchPostExtension": + """Create CollectionSearchPostExtension object from extensions.""" + supported_extension = { + "FreeTextExtension": ConformanceClasses.FREETEXT, + "FreeTextAdvancedExtension": ConformanceClasses.FREETEXT, + "QueryExtension": ConformanceClasses.QUERY, + "SortExtension": ConformanceClasses.SORT, + "FieldsExtension": ConformanceClasses.FIELDS, + "FilterExtension": ConformanceClasses.FILTER, + } + conformance_classes = [ + ConformanceClasses.COLLECTIONSEARCH, + ConformanceClasses.BASIS, + ] + for ext in extensions: + conf = supported_extension.get(ext.__class__.__name__, None) + if not conf: + warnings.warn( + f"{ext.__class__.__name__} extension not supported in `CollectionSearchExtension.from_extensions` method.", # noqa: E501 + UserWarning, + ) + else: + conformance_classes.append(supported_extension[ext.__class__.__name__]) + + get_request_model = create_request_model( + model_name="CollectionsGetRequest", + base_model=BaseCollectionSearchGetRequest, + extensions=extensions, + request_type="GET", + ) + + post_request_model = create_request_model( + model_name="CollectionsPostRequest", + base_model=BaseCollectionSearchPostRequest, + extensions=extensions, + request_type="POST", + ) + + return cls( + client=client, + settings=settings, + GET=get_request_model, + POST=post_request_model, + conformance_classes=conformance_classes, + router=router or APIRouter(), + schema_href=schema_href, + ) diff --git a/stac_fastapi/extensions/tests/test_collection_search.py b/stac_fastapi/extensions/tests/test_collection_search.py index edc292210..b23219956 100644 --- a/stac_fastapi/extensions/tests/test_collection_search.py +++ b/stac_fastapi/extensions/tests/test_collection_search.py @@ -2,13 +2,21 @@ from urllib.parse import quote_plus import attr +import pytest from starlette.testclient import TestClient from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import create_request_model from stac_fastapi.extensions.core import ( + AggregationExtension, CollectionSearchExtension, CollectionSearchPostExtension, + FieldsExtension, + FilterExtension, + FreeTextAdvancedExtension, + FreeTextExtension, + QueryExtension, + SortExtension, ) from stac_fastapi.extensions.core.collection_search import ConformanceClasses from stac_fastapi.extensions.core.collection_search.client import ( @@ -302,8 +310,8 @@ def test_collection_search_extension_post_models(): client=DummyCoreClient(), extensions=[ CollectionSearchPostExtension( - settings=settings, client=DummyPostClient(), + settings=settings, GET=get_request_model, POST=post_request_model, conformance_classes=[ @@ -392,3 +400,112 @@ def test_collection_search_extension_post_models(): assert response_dict["query"] assert response_dict["sortby"] assert response_dict["fields"] + + +@pytest.mark.parametrize( + "extensions", + [ + # with FreeTextExtension + [ + FieldsExtension(), + FilterExtension(), + FreeTextExtension(), + QueryExtension(), + SortExtension(), + ], + # with FreeTextAdvancedExtension + [ + FieldsExtension(), + FilterExtension(), + FreeTextAdvancedExtension(), + QueryExtension(), + SortExtension(), + ], + ], +) +def test_from_extensions_methods(extensions): + """ + Make sure `from_extensions` create the correct + models and adds desired conformances classes. + """ + ext = CollectionSearchExtension.from_extensions( + extensions, + ) + collection_search = ext.GET() + assert collection_search.__class__.__name__ == "CollectionsGetRequest" + assert hasattr(collection_search, "bbox") + assert hasattr(collection_search, "datetime") + assert hasattr(collection_search, "limit") + assert hasattr(collection_search, "fields") + assert hasattr(collection_search, "q") + assert hasattr(collection_search, "sortby") + assert hasattr(collection_search, "filter") + assert ext.conformance_classes == [ + ConformanceClasses.COLLECTIONSEARCH, + ConformanceClasses.BASIS, + ConformanceClasses.FIELDS, + ConformanceClasses.FILTER, + ConformanceClasses.FREETEXT, + ConformanceClasses.QUERY, + ConformanceClasses.SORT, + ] + + ext = CollectionSearchPostExtension.from_extensions( + extensions, + client=DummyPostClient(), + settings=ApiSettings(), + ) + collection_search = ext.POST() + assert collection_search.__class__.__name__ == "CollectionsPostRequest" + assert hasattr(collection_search, "bbox") + assert hasattr(collection_search, "datetime") + assert hasattr(collection_search, "limit") + assert hasattr(collection_search, "fields") + assert hasattr(collection_search, "q") + assert hasattr(collection_search, "sortby") + assert hasattr(collection_search, "filter") + assert ext.conformance_classes == [ + ConformanceClasses.COLLECTIONSEARCH, + ConformanceClasses.BASIS, + ConformanceClasses.FIELDS, + ConformanceClasses.FILTER, + ConformanceClasses.FREETEXT, + ConformanceClasses.QUERY, + ConformanceClasses.SORT, + ] + + +def test_from_extensions_methods_invalid(): + """Should raise warnings for invalid extensions.""" + extensions = [ + AggregationExtension(), + ] + with pytest.warns((UserWarning)): + ext = CollectionSearchExtension.from_extensions( + extensions, + ) + collection_search = ext.GET() + assert collection_search.__class__.__name__ == "CollectionsGetRequest" + assert hasattr(collection_search, "bbox") + assert hasattr(collection_search, "datetime") + assert hasattr(collection_search, "limit") + assert ext.conformance_classes == [ + ConformanceClasses.COLLECTIONSEARCH, + ConformanceClasses.BASIS, + ] + + with pytest.warns((UserWarning)): + ext = CollectionSearchPostExtension.from_extensions( + extensions, + client=DummyPostClient(), + settings=ApiSettings(), + ) + collection_search = ext.POST() + assert collection_search.__class__.__name__ == "CollectionsPostRequest" + assert hasattr(collection_search, "bbox") + assert hasattr(collection_search, "datetime") + assert hasattr(collection_search, "limit") + assert ext.conformance_classes == [ + ConformanceClasses.COLLECTIONSEARCH, + ConformanceClasses.BASIS, + ] From ccfab4c579f95984c60779bcda0ed11fd90541fa Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Wed, 21 Aug 2024 15:07:33 +0200 Subject: [PATCH 2/5] Apply suggestions from code review --- .../core/collection_search/collection_search.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 2a5acbce3..d06812fa2 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 @@ -79,7 +79,7 @@ def from_extensions( schema_href: Optional[str] = None, ) -> "CollectionSearchExtension": """Create CollectionSearchExtension object from extensions.""" - supported_extension = { + supported_extensions = { "FreeTextExtension": ConformanceClasses.FREETEXT, "FreeTextAdvancedExtension": ConformanceClasses.FREETEXT, "QueryExtension": ConformanceClasses.QUERY, @@ -92,7 +92,7 @@ def from_extensions( ConformanceClasses.BASIS, ] for ext in extensions: - conf = supported_extension.get(ext.__class__.__name__, None) + conf = supported_extensions.get(ext.__class__.__name__, None) if not conf: warnings.warn( f"{ext.__class__.__name__} extension not supported in `CollectionSearchExtension.from_extensions` method.", # noqa: E501 @@ -187,7 +187,7 @@ def from_extensions( router: Optional[APIRouter] = None, ) -> "CollectionSearchPostExtension": """Create CollectionSearchPostExtension object from extensions.""" - supported_extension = { + supported_extensions = { "FreeTextExtension": ConformanceClasses.FREETEXT, "FreeTextAdvancedExtension": ConformanceClasses.FREETEXT, "QueryExtension": ConformanceClasses.QUERY, @@ -200,7 +200,7 @@ def from_extensions( ConformanceClasses.BASIS, ] for ext in extensions: - conf = supported_extension.get(ext.__class__.__name__, None) + conf = supported_extensions.get(ext.__class__.__name__, None) if not conf: warnings.warn( f"{ext.__class__.__name__} extension not supported in `CollectionSearchExtension.from_extensions` method.", # noqa: E501 From f9d6f735a717f636d96c2e5687aa7c69a0c2da15 Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Wed, 21 Aug 2024 15:09:05 +0200 Subject: [PATCH 3/5] Apply suggestions from code review --- .../extensions/core/collection_search/collection_search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 d06812fa2..f7f452c95 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 @@ -95,7 +95,7 @@ def from_extensions( conf = supported_extensions.get(ext.__class__.__name__, None) if not conf: warnings.warn( - f"{ext.__class__.__name__} extension not supported in `CollectionSearchExtension.from_extensions` method.", # noqa: E501 + f"Conformance class for `{ext.__class__.__name__}` extension not found.", UserWarning, ) else: @@ -203,7 +203,7 @@ def from_extensions( conf = supported_extensions.get(ext.__class__.__name__, None) if not conf: warnings.warn( - f"{ext.__class__.__name__} extension not supported in `CollectionSearchExtension.from_extensions` method.", # noqa: E501 + f"Conformance class for `{ext.__class__.__name__}` extension not found.", UserWarning, ) else: From cc80d09a48d21c3e815246231daeb7ea5109eba9 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Wed, 21 Aug 2024 15:11:15 +0200 Subject: [PATCH 4/5] fix --- .../core/collection_search/collection_search.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 f7f452c95..2a5f7cf4d 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 @@ -95,11 +95,11 @@ def from_extensions( conf = supported_extensions.get(ext.__class__.__name__, None) if not conf: warnings.warn( - f"Conformance class for `{ext.__class__.__name__}` extension not found.", + f"Conformance class for `{ext.__class__.__name__}` extension not found.", # noqa: E501 UserWarning, ) else: - conformance_classes.append(supported_extension[ext.__class__.__name__]) + conformance_classes.append(conf) get_request_model = create_request_model( model_name="CollectionsGetRequest", @@ -203,11 +203,11 @@ def from_extensions( conf = supported_extensions.get(ext.__class__.__name__, None) if not conf: warnings.warn( - f"Conformance class for `{ext.__class__.__name__}` extension not found.", + f"Conformance class for `{ext.__class__.__name__}` extension not found.", # noqa: E501 UserWarning, ) else: - conformance_classes.append(supported_extension[ext.__class__.__name__]) + conformance_classes.append(conf) get_request_model = create_request_model( model_name="CollectionsGetRequest", From a959cd8a9b6e646d47e0cddc5a1da4db1fd57cd8 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Wed, 21 Aug 2024 15:14:40 +0200 Subject: [PATCH 5/5] update makefile --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index eef5dae35..a835bb52a 100644 --- a/Makefile +++ b/Makefile @@ -11,12 +11,12 @@ install: .PHONY: docs-image docs-image: - docker-compose -f docker-compose.docs.yml \ + docker compose -f docker-compose.docs.yml \ build .PHONY: docs docs: docs-image - docker-compose -f docker-compose.docs.yml \ + docker compose -f docker-compose.docs.yml \ run docs .PHONY: test