From adf4cc6491ac2921a47523c86bcb4ee527e55380 Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Mon, 22 Apr 2024 16:17:25 +0100 Subject: [PATCH 01/21] Added collection search extension --- stac_fastapi/api/stac_fastapi/api/config.py | 1 + .../stac_fastapi/extensions/core/__init__.py | 2 + .../core/collectionSearch/__init__.py | 6 + .../core/collectionSearch/collectionSearch.py | 109 ++++++++++++++++++ .../core/collectionSearch/request.py | 28 +++++ stac_fastapi/types/stac_fastapi/types/core.py | 77 ++++++++++++- .../types/stac_fastapi/types/search.py | 92 +++++++++++++++ 7 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/__init__.py create mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py create mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py diff --git a/stac_fastapi/api/stac_fastapi/api/config.py b/stac_fastapi/api/stac_fastapi/api/config.py index e6e4d882a..b4e2c8620 100644 --- a/stac_fastapi/api/stac_fastapi/api/config.py +++ b/stac_fastapi/api/stac_fastapi/api/config.py @@ -23,3 +23,4 @@ class AddOns(enum.Enum): """Enumeration of available third party add ons.""" bulk_transaction = "bulk-transaction" + collection_search = "collection-search" diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py index 96317fe4a..be608101c 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py @@ -6,6 +6,7 @@ from .query import QueryExtension from .sort import SortExtension from .transaction import TransactionExtension +from .collectionSearch import CollectionSearchExtension __all__ = ( "ContextExtension", @@ -16,4 +17,5 @@ "SortExtension", "TokenPaginationExtension", "TransactionExtension", + "CollectionSearchExtension", ) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/__init__.py new file mode 100644 index 000000000..21f35e8ca --- /dev/null +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/__init__.py @@ -0,0 +1,6 @@ +"""Filter extension module.""" + + +from .collectionSearch import CollectionSearchExtension + +__all__ = ["CollectionSearchExtension"] diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py new file mode 100644 index 000000000..91f4fc6d6 --- /dev/null +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py @@ -0,0 +1,109 @@ +# encoding: utf-8 +"""Collection Search Extension.""" +from enum import Enum +from typing import List, Type, Union + +import attr +from fastapi import APIRouter, FastAPI +from starlette.responses import Response +from stac_pydantic.api.collections import Collections + +from stac_fastapi.api.models import JSONSchemaResponse +from stac_fastapi.api.routes import create_async_endpoint +from stac_fastapi.types.core import AsyncCollectionSearchClient, CollectionSearchClient +from stac_fastapi.types.extension import ApiExtension +from stac_fastapi.types.search import BaseCollectionSearchGetRequest, BaseCollectionSearchPostRequest + +from .request import CollectionSearchExtensionGetRequest, CollectionSearchExtensionPostRequest + +class CollectionSearchConformanceClasses(str, Enum): + """Conformance classes for the Collection Search extension. + + See + https://github.com/stac-api-extensions/collection-search + """ + + CORE = "https://api.stacspec.org/v1.0.0-rc.1/core" + COLLECTION_SEARCH = "https://api.stacspec.org/v1.0.0-rc.1/collection-search" + SIMPLE_QUERY = "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query" + +@attr.s +class CollectionSearchExtension(ApiExtension): + """CollectionSearch Extension. + + The collection search extension adds two endpoints which allow searching of + collections via GET and POST: + GET /collection-search + POST /collection-search + + https://github.com/stac-api-extensions/collection-search + + Attributes: + search_get_request_model: Get request model for collection search + search_post_request_model: Post request model for collection search + client: Collection Search endpoint logic + conformance_classes: Conformance classes provided by the extension + """ + + GET = CollectionSearchExtensionGetRequest + POST = CollectionSearchExtensionPostRequest + + collection_search_get_request_model: Type[BaseCollectionSearchGetRequest] = attr.ib( + default=BaseCollectionSearchGetRequest + ) + collection_search_post_request_model: Type[BaseCollectionSearchPostRequest] = attr.ib( + default=BaseCollectionSearchPostRequest + ) + + client: Union[AsyncCollectionSearchClient, CollectionSearchClient] = attr.ib( + factory=CollectionSearchClient + ) + + conformance_classes: List[str] = attr.ib( + default=[ + CollectionSearchConformanceClasses.CORE, + CollectionSearchConformanceClasses.COLLECTION_SEARCH, + CollectionSearchConformanceClasses.SIMPLE_QUERY, + ] + ) + router: APIRouter = attr.ib(factory=APIRouter) + response_class: Type[Response] = attr.ib(default=JSONSchemaResponse) + + + 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="Collection Search", + path="/collection-search", + response_model=Collections, + response_class=self.response_class, + response_model_exclude_unset=True, + response_model_exclude_none=True, + methods=["GET"], + endpoint=create_async_endpoint( + self.client.get_collection_search, self.collection_search_get_request_model + ), + ) + + self.router.add_api_route( + name="Collection Search", + path="/collection-search", + response_model=Collections, + response_class=self.response_class, + response_model_exclude_unset=True, + response_model_exclude_none=True, + methods=["POST"], + endpoint=create_async_endpoint( + self.client.post_collection_search, self.collection_search_post_request_model + ), + ) + + app.include_router(self.router, tags=["Collection Search Extension"]) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py new file mode 100644 index 000000000..f159d6a87 --- /dev/null +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py @@ -0,0 +1,28 @@ +"""Collection Search extension request models.""" + +from enum import Enum +from typing import Any, Dict, Optional, List + +import attr +from pydantic import BaseModel, Field +from stac_pydantic.shared import BBox + +from stac_fastapi.types.search import APIRequest, Limit, str2bbox + +from stac_fastapi.types.rfc3339 import DateTimeType, str_to_interval + +@attr.s +class CollectionSearchExtensionGetRequest(APIRequest): + """Collection Search extension GET request model.""" + + bbox: Optional[BBox] = attr.ib(default=None, converter=str2bbox) + datetime: Optional[DateTimeType] = attr.ib(default=None, converter=str_to_interval) + limit: Optional[int] = attr.ib(default=10) + + +class CollectionSearchExtensionPostRequest(BaseModel): + """Collection Search extension POST request model.""" + + bbox: Optional[BBox] + datetime: Optional[DateTimeType] + limit: Optional[Limit] = Field(default=10) diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 77078ace3..e8d75b602 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -17,7 +17,7 @@ from stac_fastapi.types.extension import ApiExtension from stac_fastapi.types.requests import get_base_url from stac_fastapi.types.rfc3339 import DateTimeType -from stac_fastapi.types.search import BaseSearchPostRequest +from stac_fastapi.types.search import BaseSearchPostRequest, BaseCollectionSearchPostRequest from stac_fastapi.types.stac import Conformance NumType = Union[float, int] @@ -801,3 +801,78 @@ def get_queryables( "description": "Queryable names for the example STAC API Item Search filter.", "properties": {}, } + +@attr.s +class AsyncCollectionSearchClient(abc.ABC): + """Defines a pattern for implementing the STAC Collection Search extension.""" + + @abc.abstractmethod + async def post_collection_search( + self, search_request: BaseCollectionSearchPostRequest, **kwargs + ) -> stac_types.ItemCollection: + """Cross catalog search (POST) of collections. + + Called with `POST /collection-search`. + + Args: + search_request: search request parameters. + + Returns: + A tuple of (collections, next pagination token if any). + """ + ... + + @abc.abstractmethod + async def get_collection_search( + self, + bbox: Optional[BBox] = None, + datetime: Optional[DateTimeType] = None, + limit: Optional[int] = 10, + **kwargs, + ) -> stac_types.Collections: + """Cross catalog search (GET) of collections. + + Called with `GET /collection-search`. + + Returns: + A tuple of (collections, next pagination token if any). + """ + ... + + +@attr.s +class CollectionSearchClient(abc.ABC): + """Defines a pattern for implementing the STAC Collection Search extension.""" + + @abc.abstractmethod + def post_collection_search( + self, search_request: BaseCollectionSearchPostRequest, **kwargs + ) -> stac_types.Collections: + """Cross catalog search (POST) of collections. + + Called with `POST /collection-search`. + + Args: + search_request: search request parameters. + + Returns: + A tuple of (collections, next pagination token if any). + """ + ... + + @abc.abstractmethod + def get_collection_search( + self, + bbox: Optional[BBox] = None, + datetime: Optional[DateTimeType] = None, + limit: Optional[int] = 10, + **kwargs, + ) -> stac_types.Collections: + """Cross catalog search (GET) of collections. + + Called with `GET /collection-search`. + + Returns: + A tuple of (collections, next pagination token if any). + """ + ... \ No newline at end of file diff --git a/stac_fastapi/types/stac_fastapi/types/search.py b/stac_fastapi/types/stac_fastapi/types/search.py index 0851c1d30..46831c056 100644 --- a/stac_fastapi/types/stac_fastapi/types/search.py +++ b/stac_fastapi/types/stac_fastapi/types/search.py @@ -187,7 +187,99 @@ def validate_bbox(cls, v: Union[str, BBox]) -> BBox: # Validate against WGS84 if xmin < -180 or ymin < -90 or xmax > 180 or ymax > 90: raise ValueError("Bounding box must be within (-180, -90, 180, 90)") + return v + + @validator("datetime", pre=True) + def validate_datetime(cls, v: Union[str, DateTimeType]) -> DateTimeType: + """Parse datetime.""" + if type(v) == str: + v = str_to_interval(v) + return v + + @property + def spatial_filter(self) -> Optional[_GeometryBase]: + """Return a geojson-pydantic object representing the spatial filter for the search + request. + + Check for both because the ``bbox`` and ``intersects`` parameters are + mutually exclusive. + """ + if self.bbox: + return Polygon( + coordinates=[ + [ + [self.bbox[0], self.bbox[3]], + [self.bbox[2], self.bbox[3]], + [self.bbox[2], self.bbox[1]], + [self.bbox[0], self.bbox[1]], + [self.bbox[0], self.bbox[3]], + ] + ] + ) + if self.intersects: + return self.intersects + return + + +@attr.s +class BaseCollectionSearchGetRequest(APIRequest): + """Base arguments for Collection Search GET Request.""" + + bbox: Optional[BBox] = attr.ib(default=None, converter=str2bbox) + datetime: Optional[DateTimeType] = attr.ib(default=None, converter=str_to_interval) + limit: Optional[int] = attr.ib(default=10) + + +class BaseCollectionSearchPostRequest(BaseModel): + """Search model. + + Replace base model in STAC-pydantic as it includes additional fields, not in the core + model. + """ + bbox: Optional[BBox] + datetime: Optional[DateTimeType] + limit: Optional[Limit] = Field(default=10) + + @property + def start_date(self) -> Optional[datetime]: + """Extract the start date from the datetime string.""" + return self.datetime[0] if self.datetime else None + + @property + def end_date(self) -> Optional[datetime]: + """Extract the end date from the datetime string.""" + return self.datetime[1] if self.datetime else None + + @validator("bbox", pre=True) + def validate_bbox(cls, v: Union[str, BBox]) -> BBox: + """Check order of supplied bbox coordinates.""" + if v: + if type(v) == str: + v = str2bbox(v) + # Validate order + if len(v) == 4: + xmin, ymin, xmax, ymax = v + else: + xmin, ymin, min_elev, xmax, ymax, max_elev = v + if max_elev < min_elev: + raise ValueError( + "Maximum elevation must greater than minimum elevation" + ) + + if xmax < xmin: + raise ValueError( + "Maximum longitude must be greater than minimum longitude" + ) + + if ymax < ymin: + raise ValueError( + "Maximum longitude must be greater than minimum longitude" + ) + + # Validate against WGS84 + if xmin < -180 or ymin < -90 or xmax > 180 or ymax > 90: + raise ValueError("Bounding box must be within (-180, -90, 180, 90)") return v @validator("datetime", pre=True) From 16e2443c9bec42f68796bfc96bf6125773eb9773 Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Fri, 26 Apr 2024 16:26:25 +0100 Subject: [PATCH 02/21] Corrected collection search response class --- .../extensions/core/collectionSearch/collectionSearch.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py index 91f4fc6d6..cbf216bb4 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py @@ -5,7 +5,7 @@ import attr from fastapi import APIRouter, FastAPI -from starlette.responses import Response +from starlette.responses import JSONResponse, Response from stac_pydantic.api.collections import Collections from stac_fastapi.api.models import JSONSchemaResponse @@ -67,8 +67,7 @@ class CollectionSearchExtension(ApiExtension): ] ) router: APIRouter = attr.ib(factory=APIRouter) - response_class: Type[Response] = attr.ib(default=JSONSchemaResponse) - + response_class: Type[Response] = attr.ib(default=JSONResponse) def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. From 948ce1eb0f4b0e9c89ecd6d8e33e69827b083b99 Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Fri, 26 Apr 2024 16:30:25 +0100 Subject: [PATCH 03/21] Updating collection search response model --- .../extensions/core/collectionSearch/collectionSearch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py index cbf216bb4..defaa5702 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py @@ -82,7 +82,7 @@ def register(self, app: FastAPI) -> None: self.router.add_api_route( name="Collection Search", path="/collection-search", - response_model=Collections, + response_model=None, response_class=self.response_class, response_model_exclude_unset=True, response_model_exclude_none=True, @@ -95,7 +95,7 @@ def register(self, app: FastAPI) -> None: self.router.add_api_route( name="Collection Search", path="/collection-search", - response_model=Collections, + response_model=None, response_class=self.response_class, response_model_exclude_unset=True, response_model_exclude_none=True, From 6a125074ab234ea7eee23203458720e445623473 Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Fri, 26 Apr 2024 16:45:12 +0100 Subject: [PATCH 04/21] Updating response model for collection search --- .../extensions/core/collectionSearch/collectionSearch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py index defaa5702..cbf216bb4 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py @@ -82,7 +82,7 @@ def register(self, app: FastAPI) -> None: self.router.add_api_route( name="Collection Search", path="/collection-search", - response_model=None, + response_model=Collections, response_class=self.response_class, response_model_exclude_unset=True, response_model_exclude_none=True, @@ -95,7 +95,7 @@ def register(self, app: FastAPI) -> None: self.router.add_api_route( name="Collection Search", path="/collection-search", - response_model=None, + response_model=Collections, response_class=self.response_class, response_model_exclude_unset=True, response_model_exclude_none=True, From cb30fbb4311365fe615d99ab05783261025bd77e Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Fri, 26 Apr 2024 16:51:22 +0100 Subject: [PATCH 05/21] Updating response model for collection search --- .../extensions/core/collectionSearch/collectionSearch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py index cbf216bb4..defaa5702 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py @@ -82,7 +82,7 @@ def register(self, app: FastAPI) -> None: self.router.add_api_route( name="Collection Search", path="/collection-search", - response_model=Collections, + response_model=None, response_class=self.response_class, response_model_exclude_unset=True, response_model_exclude_none=True, @@ -95,7 +95,7 @@ def register(self, app: FastAPI) -> None: self.router.add_api_route( name="Collection Search", path="/collection-search", - response_model=Collections, + response_model=None, response_class=self.response_class, response_model_exclude_unset=True, response_model_exclude_none=True, From 50727293866efe70235dca3761546c2374ff1576 Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Thu, 9 May 2024 10:12:37 +0100 Subject: [PATCH 06/21] Add catalogs to stac fastapi and updated to maintain root link for collections and items --- .../types/stac_fastapi/types/links.py | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/links.py b/stac_fastapi/types/stac_fastapi/types/links.py index 28f05d6c0..05d3a5952 100644 --- a/stac_fastapi/types/stac_fastapi/types/links.py +++ b/stac_fastapi/types/stac_fastapi/types/links.py @@ -1,7 +1,7 @@ """Link helpers.""" from typing import Any, Dict, List -from urllib.parse import urljoin +from urllib.parse import urljoin, urlsplit import attr from stac_pydantic.links import Relations @@ -10,7 +10,8 @@ # These can be inferred from the item/collection so they aren't included in the database # Instead they are dynamically generated when querying the database using the # classes defined below -INFERRED_LINK_RELS = ["self", "item", "parent", "collection", "root", "items"] +#INFERRED_LINK_RELS = ["self", "item", "parent", "collection", "root", "items"] +INFERRED_LINK_RELS = ["self", "item", "parent", "collection", "items"] def filter_links(links: List[Dict]) -> List[Dict]: @@ -22,6 +23,10 @@ def resolve_links(links: list, base_url: str) -> List[Dict]: """Convert relative links to absolute links.""" filtered_links = filter_links(links) for link in filtered_links: + if "http://" in link["href"] or "https://" in link["href"]: + split_url = urlsplit(link["href"]) + link["href"] = split_url.path + #link["href"].replace(split_url.scheme, "").replace(split_url.netloc, "") link.update({"href": urljoin(base_url, link["href"])}) return filtered_links @@ -64,8 +69,41 @@ def items(self) -> Dict[str, Any]: def create_links(self) -> List[Dict[str, Any]]: """Return all inferred links.""" - return [self.self(), self.parent(), self.items(), self.root()] + # We use predefined root here to identify the catalog containing this dataset + return [self.self(), self.parent(), self.items()]#, self.root()] +@attr.s +class BaseCatalogLinks: + """Create inferred links common to collections and items.""" + + base_url: str = attr.ib() + catalog_id: str = attr.ib() + + def root(self) -> Dict[str, Any]: + """Return the catalog root.""" + return dict(rel=Relations.root, type=MimeTypes.json, href=self.base_url) + +@attr.s +class CatalogLinks(BaseCatalogLinks): + """Create inferred links specific to catalogs.""" + + def self(self) -> Dict[str, Any]: + """Create the `self` link.""" + return dict( + rel=Relations.self, + type=MimeTypes.json, + href=urljoin(self.base_url, f"collections/{self.catalog_id}"), + ) + + def parent(self) -> Dict[str, Any]: + """Create the `parent` link.""" + return dict(rel=Relations.parent, type=MimeTypes.json, href=self.base_url) + + + def create_links(self) -> List[Dict[str, Any]]: + """Return all inferred links.""" + # We use predefined root here to identify the catalog containing this dataset + return [self.self(), self.parent(), self.root()] @attr.s class ItemLinks(BaseLinks): From 4c0694f1e37477b0a59d36bcf2dfb03d51846b3e Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Thu, 9 May 2024 10:14:33 +0100 Subject: [PATCH 07/21] Added catalogs to stac fastapi --- stac_fastapi/api/stac_fastapi/api/app.py | 21 +++++++ stac_fastapi/api/tests/benchmarks.py | 11 ++++ stac_fastapi/api/tests/test_api.py | 3 + .../extensions/core/transaction.py | 18 +++++- .../extensions/tests/test_transaction.py | 6 ++ .../types/stac_fastapi/types/catalogs.py | 23 ++++++++ stac_fastapi/types/stac_fastapi/types/core.py | 59 +++++++++++++++++-- stac_fastapi/types/stac_fastapi/types/stac.py | 9 +++ 8 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 stac_fastapi/types/stac_fastapi/types/catalogs.py diff --git a/stac_fastapi/api/stac_fastapi/api/app.py b/stac_fastapi/api/stac_fastapi/api/app.py index 7ad0c96f5..b04f7ca06 100644 --- a/stac_fastapi/api/stac_fastapi/api/app.py +++ b/stac_fastapi/api/stac_fastapi/api/app.py @@ -10,6 +10,7 @@ from stac_pydantic import Collection, Item, ItemCollection from stac_pydantic.api import ConformanceClasses, LandingPage from stac_pydantic.api.collections import Collections +from stac_fastapi.types.stac import Catalogs from stac_pydantic.version import STAC_VERSION from starlette.responses import JSONResponse, Response @@ -247,6 +248,25 @@ def register_get_collections(self): endpoint=create_async_endpoint(self.client.all_collections, EmptyRequest), ) + def register_get_catalogs(self): + """Register get catalogs endpoint (GET /catalogs). + + Returns: + None + """ + self.router.add_api_route( + name="Get Catalogs", + path="/catalogs", + response_model=Catalogs + if self.settings.enable_response_models + else None, + response_class=self.response_class, + response_model_exclude_unset=True, + response_model_exclude_none=True, + methods=["GET"], + endpoint=create_async_endpoint(self.client.all_catalogs, EmptyRequest), + ) + def register_get_collection(self): """Register get collection endpoint (GET /collection/{collection_id}). @@ -316,6 +336,7 @@ def register_core(self): self.register_post_search() self.register_get_search() self.register_get_collections() + self.register_get_catalogs() self.register_get_collection() self.register_get_item_collection() diff --git a/stac_fastapi/api/tests/benchmarks.py b/stac_fastapi/api/tests/benchmarks.py index ad73d2424..9023def17 100644 --- a/stac_fastapi/api/tests/benchmarks.py +++ b/stac_fastapi/api/tests/benchmarks.py @@ -12,6 +12,7 @@ collection_links = link_factory.CollectionLinks("/", "test").create_links() item_links = link_factory.ItemLinks("/", "test", "test").create_links() +catalog_links = link_factory.CatalogLinks("/", "test").create_links() collections = [ @@ -74,6 +75,16 @@ def all_collections(self, **kwargs) -> stac_types.Collections: {"href": "test", "rel": "parent"}, ], ) + + def all_catalogs(self, **kwargs) -> stac_types.Catalogs: + return stac_types.Catalogs( + catalogs=catalogs, + links=[ + {"href": "test", "rel": "root"}, + {"href": "test", "rel": "self"}, + {"href": "test", "rel": "parent"}, + ], + ) def get_collection(self, collection_id: str, **kwargs) -> stac_types.Collection: return collections[0] diff --git a/stac_fastapi/api/tests/test_api.py b/stac_fastapi/api/tests/test_api.py index 91b50371e..fc5c3d050 100644 --- a/stac_fastapi/api/tests/test_api.py +++ b/stac_fastapi/api/tests/test_api.py @@ -119,6 +119,9 @@ def delete_item(self, *args, **kwargs): def create_collection(self, *args, **kwargs): return "dummy response" + + def create_catalog(self, *args, **kwargs): + return "dummy response" def update_collection(self, *args, **kwargs): return "dummy response" diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py index 0ebcc6194..6d755e296 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py @@ -4,7 +4,7 @@ import attr from fastapi import APIRouter, Body, FastAPI -from stac_pydantic import Collection, Item +from stac_pydantic import Collection, Item, Catalog from starlette.responses import JSONResponse, Response from stac_fastapi.api.models import CollectionUri, ItemUri @@ -117,6 +117,21 @@ def register_create_collection(self): ), ) + def register_create_catalog(self): + """Register create catalog endpoint (POST /catalogs).""" + self.router.add_api_route( + name="Create Catalog", + path="/catalogs", + response_model=Catalog if self.settings.enable_response_models else None, + response_class=self.response_class, + response_model_exclude_unset=True, + response_model_exclude_none=True, + methods=["POST"], + endpoint=create_async_endpoint( + self.client.create_catalog, stac_types.Catalog + ), + ) + def register_update_collection(self): """Register update collection endpoint (PUT /collections/{collection_id}).""" self.router.add_api_route( @@ -161,6 +176,7 @@ def register(self, app: FastAPI) -> None: self.register_update_item() self.register_delete_item() self.register_create_collection() + self.register_create_catalog() self.register_update_collection() self.register_delete_collection() app.include_router(self.router, tags=["Transaction Extension"]) diff --git a/stac_fastapi/extensions/tests/test_transaction.py b/stac_fastapi/extensions/tests/test_transaction.py index fc5acc2cf..c0bcaadae 100644 --- a/stac_fastapi/extensions/tests/test_transaction.py +++ b/stac_fastapi/extensions/tests/test_transaction.py @@ -14,6 +14,9 @@ class DummyCoreClient(BaseCoreClient): def all_collections(self, *args, **kwargs): raise NotImplementedError + + def all_catalogs(self, *args, **kwargs): + raise NotImplementedError def get_collection(self, *args, **kwargs): raise NotImplementedError @@ -45,6 +48,9 @@ def delete_item(self, *args, **kwargs): def create_collection(self, *args, **kwargs): raise NotImplementedError + + def create_catalog(self, *args, **kwargs): + raise NotImplementedError def update_collection(self, *args, **kwargs): raise NotImplementedError diff --git a/stac_fastapi/types/stac_fastapi/types/catalogs.py b/stac_fastapi/types/stac_fastapi/types/catalogs.py new file mode 100644 index 000000000..9caa19087 --- /dev/null +++ b/stac_fastapi/types/stac_fastapi/types/catalogs.py @@ -0,0 +1,23 @@ +# Cloned from stac_pydantic.api.collections v2.0 + +from typing import List + +from pydantic import BaseModel + +from stac_pydantic import Catalog +from stac_pydantic.api.links import Link + + +class Catalogs(BaseModel): + """ + http://docs.opengeospatial.org/is/17-069r3/17-069r3.html#_feature_collections_rootcollections + """ + + links: List[Link] + collections: List[Catalog] + + def to_dict(self, **kwargs): + return self.dict(by_alias=True, exclude_unset=True, **kwargs) + + def to_json(self, **kwargs): + return self.json(by_alias=True, exclude_unset=True, **kwargs) \ No newline at end of file diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index e8d75b602..0b5b94705 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -103,6 +103,22 @@ def create_collection( """ ... + @abc.abstractmethod + def create_catalog( + self, catalog: stac_types.Catalog, **kwargs + ) -> Optional[Union[stac_types.Catalog, Response]]: + """Create a new catalog. + + Called with `POST /catalogs`. + + Args: + catalog: the catalog + + Returns: + The catalog that was created. + """ + ... + @abc.abstractmethod def update_collection( self, collection_id: str, collection: stac_types.Collection, **kwargs @@ -500,6 +516,17 @@ def all_collections(self, **kwargs) -> stac_types.Collections: """ ... + @abc.abstractmethod + def all_catalogs(self, **kwargs) -> stac_types.Catalogs: + """Get all available catalogs. + + Called with `GET /catalogs`. + + Returns: + A list of catalogs. + """ + ... + @abc.abstractmethod def get_collection(self, collection_id: str, **kwargs) -> stac_types.Collection: """Get collection by id. @@ -599,14 +626,26 @@ async def landing_page(self, **kwargs) -> stac_types.LandingPage: ) # Add Collections links - collections = await self.all_collections(request=kwargs["request"]) - for collection in collections["collections"]: + # collections = await self.all_collections(request=kwargs["request"]) + # for collection in collections["collections"]: + # landing_page["links"].append( + # { + # "rel": Relations.child.value, + # "type": MimeTypes.json.value, + # "title": collection.get("title") or collection.get("id"), + # "href": urljoin(base_url, f"collections/{collection['id']}"), + # } + # ) + + # Add Collections links + catalogs = await self.all_catalogs(request=kwargs["request"]) + for catalog in catalogs["collections"]: landing_page["links"].append( { "rel": Relations.child.value, "type": MimeTypes.json.value, - "title": collection.get("title") or collection.get("id"), - "href": urljoin(base_url, f"collections/{collection['id']}"), + "title": catalog.get("title") or catalog.get("id"), + "href": urljoin(base_url, f"collections/{catalog['id']}"), } ) @@ -710,6 +749,18 @@ async def all_collections(self, **kwargs) -> stac_types.Collections: """ ... + @abc.abstractmethod + async def all_catalogs(self, **kwargs) -> stac_types.Catalogs: + """Get all available catalogs. + + Called with `GET /catalogs`. + + Returns: + A list of catalogs. + """ + ... + + @abc.abstractmethod async def get_collection( self, collection_id: str, **kwargs diff --git a/stac_fastapi/types/stac_fastapi/types/stac.py b/stac_fastapi/types/stac_fastapi/types/stac.py index 51bb6e652..a4152b72d 100644 --- a/stac_fastapi/types/stac_fastapi/types/stac.py +++ b/stac_fastapi/types/stac_fastapi/types/stac.py @@ -90,3 +90,12 @@ class Collections(TypedDict, total=False): collections: List[Collection] links: List[Dict[str, Any]] + +class Catalogs(TypedDict, total=False): + """All collections endpoint. + + https://github.com/radiantearth/stac-api-spec/tree/master/collections + """ + + catalogs: List[Catalog] + links: List[Dict[str, Any]] From 8b63cd1ec35b8428b9ee989fce9e6d67b3509c34 Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Thu, 9 May 2024 14:29:29 +0100 Subject: [PATCH 08/21] Moving collection-search to a core extension --- stac_fastapi/api/stac_fastapi/api/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stac_fastapi/api/stac_fastapi/api/config.py b/stac_fastapi/api/stac_fastapi/api/config.py index b4e2c8620..26834648e 100644 --- a/stac_fastapi/api/stac_fastapi/api/config.py +++ b/stac_fastapi/api/stac_fastapi/api/config.py @@ -17,10 +17,11 @@ class ApiExtensions(enum.Enum): query = "query" sort = "sort" transaction = "transaction" + collection_search = "collection-search" class AddOns(enum.Enum): """Enumeration of available third party add ons.""" bulk_transaction = "bulk-transaction" - collection_search = "collection-search" + From 8efbe921d85fb5f5fd611799ca719fbce3bde406 Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Thu, 9 May 2024 14:30:41 +0100 Subject: [PATCH 09/21] Adding discovery-level search for catalogue search --- .../core/discoverySearch/__init__.py | 6 ++ .../core/discoverySearch/discoverySearch.py | 93 +++++++++++++++++++ .../core/discoverySearch/request.py | 27 ++++++ 3 files changed, 126 insertions(+) create mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/__init__.py create mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/discoverySearch.py create mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/request.py diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/__init__.py new file mode 100644 index 000000000..c25d88d0b --- /dev/null +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/__init__.py @@ -0,0 +1,6 @@ +"""Filter extension module.""" + + +from .discoverySearch import DiscoverySearchExtension + +__all__ = ["DiscoverySearchExtension"] diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/discoverySearch.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/discoverySearch.py new file mode 100644 index 000000000..be1eefda4 --- /dev/null +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/discoverySearch.py @@ -0,0 +1,93 @@ +# encoding: utf-8 +"""Collection Search Extension.""" +from enum import Enum +from typing import List, Type, Union + +import attr +from fastapi import APIRouter, FastAPI +from starlette.responses import JSONResponse, Response +from stac_pydantic.api.collections import Collections + +from stac_fastapi.api.models import JSONSchemaResponse +from stac_fastapi.api.routes import create_async_endpoint +from stac_fastapi.types.core import AsyncDiscoverySearchClient, DiscoverySearchClient +from stac_fastapi.types.extension import ApiExtension +from stac_fastapi.types.search import BaseDiscoverySearchGetRequest, BaseDiscoverySearchPostRequest + +from .request import DiscoverySearchExtensionGetRequest, DiscoverySearchExtensionPostRequest + +@attr.s +class DiscoverySearchExtension(ApiExtension): + """DiscoverySearch Extension. + + The collection search extension adds two endpoints which allow searching of + collections via GET and POST: + GET /discovery-search + POST /discovery-search + + https://github.com/stac-api-extensions/collection-search + + Attributes: + search_get_request_model: Get request model for collection search + search_post_request_model: Post request model for collection search + client: Collection Search endpoint logic + conformance_classes: Conformance classes provided by the extension + """ + + GET = DiscoverySearchExtensionGetRequest + POST = DiscoverySearchExtensionPostRequest + + discovery_search_get_request_model: Type[BaseDiscoverySearchGetRequest] = attr.ib( + default=BaseDiscoverySearchGetRequest + ) + discovery_search_post_request_model: Type[BaseDiscoverySearchPostRequest] = attr.ib( + default=BaseDiscoverySearchPostRequest + ) + + client: Union[AsyncDiscoverySearchClient, DiscoverySearchClient] = attr.ib( + factory=DiscoverySearchClient + ) + + conformance_classes: List[str] = attr.ib( + default=[] + ) + router: APIRouter = attr.ib(factory=APIRouter) + response_class: Type[Response] = attr.ib(default=JSONResponse) + + 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="Discovery Search", + path="/discovery-search", + response_model=None, + response_class=self.response_class, + response_model_exclude_unset=True, + response_model_exclude_none=True, + methods=["GET"], + endpoint=create_async_endpoint( + self.client.get_discovery_search, self.discovery_search_get_request_model + ), + ) + + self.router.add_api_route( + name="Discovery Search", + path="/discovery-search", + response_model=None, + response_class=self.response_class, + response_model_exclude_unset=True, + response_model_exclude_none=True, + methods=["POST"], + endpoint=create_async_endpoint( + self.client.post_discovery_search, self.discovery_search_post_request_model + ), + ) + + app.include_router(self.router, tags=["Discovery Search Extension"]) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/request.py new file mode 100644 index 000000000..71942ef6e --- /dev/null +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/request.py @@ -0,0 +1,27 @@ +"""Discovery Search extension request models.""" + +from enum import Enum +from typing import Any, Dict, Optional, List + +import attr +from pydantic import BaseModel, Field +from stac_pydantic.shared import BBox + +from stac_fastapi.types.search import APIRequest, Limit, str2bbox + +from stac_fastapi.types.rfc3339 import DateTimeType, str_to_interval + +from ..filter.request import FilterLang + +@attr.s +class DiscoverySearchExtensionGetRequest(APIRequest): + """Discovery Search extension GET request model.""" + + q: Optional[str] = attr.ib(default=None) + limit: Optional[int] = attr.ib(default=10) + +class DiscoverySearchExtensionPostRequest(BaseModel): + """Discovery Search extension POST request model.""" + + q: Optional[str] + limit: Optional[Limit] = Field(default=10) From a27a7a48aed199ac2b8afc956bd7ced5da05cebb Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Thu, 9 May 2024 14:31:09 +0100 Subject: [PATCH 10/21] Adding discovery-search extension --- .../extensions/stac_fastapi/extensions/core/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py index be608101c..c4c320a6d 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py @@ -7,6 +7,7 @@ from .sort import SortExtension from .transaction import TransactionExtension from .collectionSearch import CollectionSearchExtension +from .discoverySearch import DiscoverySearchExtension __all__ = ( "ContextExtension", @@ -18,4 +19,5 @@ "TokenPaginationExtension", "TransactionExtension", "CollectionSearchExtension", + "DiscoverySearchExtension", ) From 569eb0cf3de859635c114cdebd82469e42431b82 Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Thu, 9 May 2024 14:31:31 +0100 Subject: [PATCH 11/21] Adding free-text search to collection-search --- .../extensions/core/collectionSearch/collectionSearch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py index defaa5702..538ab2f81 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py @@ -26,6 +26,7 @@ class CollectionSearchConformanceClasses(str, Enum): CORE = "https://api.stacspec.org/v1.0.0-rc.1/core" COLLECTION_SEARCH = "https://api.stacspec.org/v1.0.0-rc.1/collection-search" SIMPLE_QUERY = "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query" + COLLECTION_SEARCH_FREE_TEXT = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text" @attr.s class CollectionSearchExtension(ApiExtension): @@ -64,6 +65,7 @@ class CollectionSearchExtension(ApiExtension): CollectionSearchConformanceClasses.CORE, CollectionSearchConformanceClasses.COLLECTION_SEARCH, CollectionSearchConformanceClasses.SIMPLE_QUERY, + CollectionSearchConformanceClasses.COLLECTION_SEARCH_FREE_TEXT, ] ) router: APIRouter = attr.ib(factory=APIRouter) From 441a2529b8d8208b3e60791119d7b70466099395 Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Thu, 9 May 2024 14:32:16 +0100 Subject: [PATCH 12/21] Adding free-text search to collection-search --- .../stac_fastapi/extensions/core/collectionSearch/request.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py index f159d6a87..2bdf4b5e6 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py @@ -18,7 +18,7 @@ class CollectionSearchExtensionGetRequest(APIRequest): bbox: Optional[BBox] = attr.ib(default=None, converter=str2bbox) datetime: Optional[DateTimeType] = attr.ib(default=None, converter=str_to_interval) limit: Optional[int] = attr.ib(default=10) - + q: Optional[str] = attr.ib(default=None) class CollectionSearchExtensionPostRequest(BaseModel): """Collection Search extension POST request model.""" @@ -26,3 +26,4 @@ class CollectionSearchExtensionPostRequest(BaseModel): bbox: Optional[BBox] datetime: Optional[DateTimeType] limit: Optional[Limit] = Field(default=10) + q: Optional[str] From a84d63fe5db77a9b7c870d70c3e8551a25e97e65 Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Thu, 9 May 2024 14:33:03 +0100 Subject: [PATCH 13/21] Updates to support catalog search )discovery-level) via free-text --- .../types/stac_fastapi/types/catalogs.py | 2 - stac_fastapi/types/stac_fastapi/types/core.py | 81 ++++++++++++++++++- .../types/stac_fastapi/types/links.py | 4 +- .../types/stac_fastapi/types/search.py | 22 +++++ 4 files changed, 104 insertions(+), 5 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/catalogs.py b/stac_fastapi/types/stac_fastapi/types/catalogs.py index 9caa19087..3b48d6f64 100644 --- a/stac_fastapi/types/stac_fastapi/types/catalogs.py +++ b/stac_fastapi/types/stac_fastapi/types/catalogs.py @@ -1,5 +1,3 @@ -# Cloned from stac_pydantic.api.collections v2.0 - from typing import List from pydantic import BaseModel diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 0b5b94705..f0477dd09 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -17,7 +17,7 @@ from stac_fastapi.types.extension import ApiExtension from stac_fastapi.types.requests import get_base_url from stac_fastapi.types.rfc3339 import DateTimeType -from stac_fastapi.types.search import BaseSearchPostRequest, BaseCollectionSearchPostRequest +from stac_fastapi.types.search import BaseSearchPostRequest, BaseCollectionSearchPostRequest, BaseDiscoverySearchPostRequest, BaseDiscoverySearchPostRequest from stac_fastapi.types.stac import Conformance NumType = Union[float, int] @@ -857,6 +857,8 @@ def get_queryables( class AsyncCollectionSearchClient(abc.ABC): """Defines a pattern for implementing the STAC Collection Search extension.""" + extensions: List[ApiExtension] = attr.ib(default=attr.Factory(list)) + @abc.abstractmethod async def post_collection_search( self, search_request: BaseCollectionSearchPostRequest, **kwargs @@ -879,6 +881,7 @@ async def get_collection_search( bbox: Optional[BBox] = None, datetime: Optional[DateTimeType] = None, limit: Optional[int] = 10, + q: Optional[str] = None, **kwargs, ) -> stac_types.Collections: """Cross catalog search (GET) of collections. @@ -917,6 +920,82 @@ def get_collection_search( bbox: Optional[BBox] = None, datetime: Optional[DateTimeType] = None, limit: Optional[int] = 10, + q: Optional[str] = None, + **kwargs, + ) -> stac_types.Collections: + """Cross catalog search (GET) of collections. + + Called with `GET /collection-search`. + + Returns: + A tuple of (collections, next pagination token if any). + """ + ... + +@attr.s +class AsyncDiscoverySearchClient(abc.ABC): + """Defines a pattern for implementing the STAC Collection Search extension.""" + + extensions: List[ApiExtension] = attr.ib(default=attr.Factory(list)) + + @abc.abstractmethod + async def post_discovery_search( + self, search_request: BaseDiscoverySearchPostRequest, **kwargs + ) -> stac_types.ItemCollection: + """Cross catalog search (POST) of collections. + + Called with `POST /collection-search`. + + Args: + search_request: search request parameters. + + Returns: + A tuple of (collections, next pagination token if any). + """ + ... + + @abc.abstractmethod + async def get_discovery_search( + self, + q: Optional[str] = None, + limit: Optional[int] = 10, + **kwargs, + ) -> stac_types.Collections: + """Cross catalog search (GET) of collections. + + Called with `GET /collection-search`. + + Returns: + A tuple of (collections, next pagination token if any). + """ + ... + + +@attr.s +class DiscoverySearchClient(abc.ABC): + """Defines a pattern for implementing the STAC Collection Search extension.""" + + @abc.abstractmethod + def post_discovery_search( + self, search_request: BaseDiscoverySearchPostRequest, **kwargs + ) -> stac_types.Collections: + """Cross catalog search (POST) of collections. + + Called with `POST /collection-search`. + + Args: + search_request: search request parameters. + + Returns: + A tuple of (collections, next pagination token if any). + """ + ... + + @abc.abstractmethod + def get_discovery_search( + self, + q: Optional[str] = None, + limit: Optional[int] = 10, **kwargs, ) -> stac_types.Collections: """Cross catalog search (GET) of collections. diff --git a/stac_fastapi/types/stac_fastapi/types/links.py b/stac_fastapi/types/stac_fastapi/types/links.py index 05d3a5952..3855868b7 100644 --- a/stac_fastapi/types/stac_fastapi/types/links.py +++ b/stac_fastapi/types/stac_fastapi/types/links.py @@ -74,7 +74,7 @@ def create_links(self) -> List[Dict[str, Any]]: @attr.s class BaseCatalogLinks: - """Create inferred links common to collections and items.""" + """Create inferred links common to catalogs.""" base_url: str = attr.ib() catalog_id: str = attr.ib() @@ -92,7 +92,7 @@ def self(self) -> Dict[str, Any]: return dict( rel=Relations.self, type=MimeTypes.json, - href=urljoin(self.base_url, f"collections/{self.catalog_id}"), + href=urljoin(self.base_url, f"catalogs/{self.catalog_id}"), ) def parent(self) -> Dict[str, Any]: diff --git a/stac_fastapi/types/stac_fastapi/types/search.py b/stac_fastapi/types/stac_fastapi/types/search.py index 46831c056..f11ef896e 100644 --- a/stac_fastapi/types/stac_fastapi/types/search.py +++ b/stac_fastapi/types/stac_fastapi/types/search.py @@ -228,6 +228,7 @@ class BaseCollectionSearchGetRequest(APIRequest): bbox: Optional[BBox] = attr.ib(default=None, converter=str2bbox) datetime: Optional[DateTimeType] = attr.ib(default=None, converter=str_to_interval) limit: Optional[int] = attr.ib(default=10) + q: Optional[str] = attr.ib(default=None) class BaseCollectionSearchPostRequest(BaseModel): @@ -240,6 +241,7 @@ class BaseCollectionSearchPostRequest(BaseModel): bbox: Optional[BBox] datetime: Optional[DateTimeType] limit: Optional[Limit] = Field(default=10) + q: Optional[str] @property def start_date(self) -> Optional[datetime]: @@ -312,3 +314,23 @@ def spatial_filter(self) -> Optional[_GeometryBase]: if self.intersects: return self.intersects return + + +@attr.s +class BaseDiscoverySearchGetRequest(APIRequest): + """Base arguments for Collection Search GET Request.""" + + q: Optional[str] = attr.ib(default=None) + limit: Optional[int] = attr.ib(default=10) + + +class BaseDiscoverySearchPostRequest(BaseModel): + """Search model. + + Replace base model in STAC-pydantic as it includes additional fields, not in the core + model. + """ + + q: Optional[str] + limit: Optional[Limit] = Field(default=10) + From 5dab508388329f394c4011ba6f6bc3baeae85bb2 Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Wed, 15 May 2024 09:57:55 +0100 Subject: [PATCH 14/21] Added converter for datetime and updated cataloguri definition --- stac_fastapi/api/stac_fastapi/api/models.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/stac_fastapi/api/stac_fastapi/api/models.py b/stac_fastapi/api/stac_fastapi/api/models.py index 53f376aa0..a08b6c271 100644 --- a/stac_fastapi/api/stac_fastapi/api/models.py +++ b/stac_fastapi/api/stac_fastapi/api/models.py @@ -10,7 +10,7 @@ from stac_pydantic.shared import BBox from stac_fastapi.types.extension import ApiExtension -from stac_fastapi.types.rfc3339 import DateTimeType +from stac_fastapi.types.rfc3339 import DateTimeType, str_to_interval from stac_fastapi.types.search import ( APIRequest, BaseSearchGetRequest, @@ -49,9 +49,11 @@ def create_request_model( for k, v in model.__fields__.items(): field_info = v.field_info body = Body( - None - if isinstance(field_info.default, UndefinedType) - else field_info.default, + ( + None + if isinstance(field_info.default, UndefinedType) + else field_info.default + ), default_factory=field_info.default_factory, alias=field_info.alias, alias_priority=field_info.alias_priority, @@ -101,12 +103,17 @@ def create_post_request_model( @attr.s # type:ignore -class CollectionUri(APIRequest): +class CatalogUri(APIRequest): + """Delete catalog.""" + + catalog_id: str = attr.ib(default=Path(..., description="Catalog ID")) + +@attr.s # type:ignore +class CollectionUri(CatalogUri): """Delete collection.""" collection_id: str = attr.ib(default=Path(..., description="Collection ID")) - @attr.s class ItemUri(CollectionUri): """Delete item.""" @@ -127,7 +134,7 @@ class ItemCollectionUri(CollectionUri): limit: int = attr.ib(default=10) bbox: Optional[BBox] = attr.ib(default=None, converter=str2bbox) - datetime: Optional[DateTimeType] = attr.ib(default=None) + datetime: Optional[DateTimeType] = attr.ib(default=None, converter=str_to_interval) class POSTTokenPagination(BaseModel): From a22d035e8400719e26445392730c30856da69f53 Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Wed, 15 May 2024 10:02:49 +0100 Subject: [PATCH 15/21] Added support for catalogues and reformatting code --- stac_fastapi/api/stac_fastapi/api/app.py | 93 +++++++++++++----- stac_fastapi/api/stac_fastapi/api/config.py | 2 +- .../api/stac_fastapi/api/middleware.py | 1 + stac_fastapi/api/stac_fastapi/api/openapi.py | 7 +- stac_fastapi/api/stac_fastapi/api/version.py | 1 + stac_fastapi/api/tests/benchmarks.py | 12 ++- stac_fastapi/api/tests/test_api.py | 30 +++--- .../stac_fastapi/extensions/core/__init__.py | 1 + .../core/collectionSearch/__init__.py | 1 - .../core/collectionSearch/collectionSearch.py | 36 ++++--- .../core/collectionSearch/request.py | 4 + .../core/discoverySearch/__init__.py | 1 - .../core/discoverySearch/discoverySearch.py | 31 +++--- .../core/discoverySearch/request.py | 2 + .../extensions/core/fields/__init__.py | 1 - .../extensions/core/fields/fields.py | 1 + .../extensions/core/filter/__init__.py | 1 - .../extensions/core/query/query.py | 1 + .../stac_fastapi/extensions/core/sort/sort.py | 1 + .../extensions/core/transaction.py | 98 +++++++++++++++---- .../extensions/third_party/__init__.py | 1 + .../third_party/bulk_transactions.py | 9 +- .../stac_fastapi/extensions/version.py | 1 + .../extensions/tests/test_transaction.py | 17 +++- .../types/stac_fastapi/types/catalogs.py | 2 +- .../types/stac_fastapi/types/config.py | 1 + .../types/stac_fastapi/types/conformance.py | 1 + stac_fastapi/types/stac_fastapi/types/core.py | 92 +++++++++++++---- .../types/stac_fastapi/types/extension.py | 1 + .../types/stac_fastapi/types/links.py | 21 ++-- .../types/stac_fastapi/types/rfc3339.py | 1 + .../types/stac_fastapi/types/search.py | 15 +-- stac_fastapi/types/stac_fastapi/types/stac.py | 12 +++ .../types/stac_fastapi/types/version.py | 1 + 34 files changed, 361 insertions(+), 139 deletions(-) diff --git a/stac_fastapi/api/stac_fastapi/api/app.py b/stac_fastapi/api/stac_fastapi/api/app.py index b04f7ca06..405d76c8e 100644 --- a/stac_fastapi/api/stac_fastapi/api/app.py +++ b/stac_fastapi/api/stac_fastapi/api/app.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, FastAPI from fastapi.openapi.utils import get_openapi from fastapi.params import Depends -from stac_pydantic import Collection, Item, ItemCollection +from stac_pydantic import Collection, Item, ItemCollection, Catalog from stac_pydantic.api import ConformanceClasses, LandingPage from stac_pydantic.api.collections import Collections from stac_fastapi.types.stac import Catalogs @@ -22,6 +22,7 @@ GeoJSONResponse, ItemCollectionUri, ItemUri, + CatalogUri, create_request_model, ) from stac_fastapi.api.openapi import update_openapi @@ -139,9 +140,9 @@ def register_landing_page(self): self.router.add_api_route( name="Landing Page", path="/", - response_model=LandingPage - if self.settings.enable_response_models - else None, + response_model=( + LandingPage if self.settings.enable_response_models else None + ), response_class=self.response_class, response_model_exclude_unset=False, response_model_exclude_none=True, @@ -158,9 +159,9 @@ def register_conformance_classes(self): self.router.add_api_route( name="Conformance Classes", path="/conformance", - response_model=ConformanceClasses - if self.settings.enable_response_models - else None, + response_model=( + ConformanceClasses if self.settings.enable_response_models else None + ), response_class=self.response_class, response_model_exclude_unset=True, response_model_exclude_none=True, @@ -176,7 +177,7 @@ def register_get_item(self): """ self.router.add_api_route( name="Get Item", - path="/collections/{collection_id}/items/{item_id}", + path="/catalogs/{catalog_id}collections/{collection_id}/items/{item_id}", response_model=Item if self.settings.enable_response_models else None, response_class=GeoJSONResponse, response_model_exclude_unset=True, @@ -195,9 +196,11 @@ def register_post_search(self): self.router.add_api_route( name="Search", path="/search", - response_model=(ItemCollection if not fields_ext else None) - if self.settings.enable_response_models - else None, + response_model=( + (ItemCollection if not fields_ext else None) + if self.settings.enable_response_models + else None + ), response_class=GeoJSONResponse, response_model_exclude_unset=True, response_model_exclude_none=True, @@ -217,9 +220,11 @@ def register_get_search(self): self.router.add_api_route( name="Search", path="/search", - response_model=(ItemCollection if not fields_ext else None) - if self.settings.enable_response_models - else None, + response_model=( + (ItemCollection if not fields_ext else None) + if self.settings.enable_response_models + else None + ), response_class=GeoJSONResponse, response_model_exclude_unset=True, response_model_exclude_none=True, @@ -238,9 +243,9 @@ def register_get_collections(self): self.router.add_api_route( name="Get Collections", path="/collections", - response_model=Collections - if self.settings.enable_response_models - else None, + response_model=( + Collections if self.settings.enable_response_models else None + ), response_class=self.response_class, response_model_exclude_unset=True, response_model_exclude_none=True, @@ -257,9 +262,7 @@ def register_get_catalogs(self): self.router.add_api_route( name="Get Catalogs", path="/catalogs", - response_model=Catalogs - if self.settings.enable_response_models - else None, + response_model=Catalogs if self.settings.enable_response_models else None, response_class=self.response_class, response_model_exclude_unset=True, response_model_exclude_none=True, @@ -267,15 +270,34 @@ def register_get_catalogs(self): endpoint=create_async_endpoint(self.client.all_catalogs, EmptyRequest), ) + def register_get_catalog_collections(self): + """Register get catalogs endpoint (GET /catalogs). + + Returns: + None + """ + self.router.add_api_route( + name="Get CatalogCollections", + path="/catalogs/{catalog_id}/collections", + response_model=( + Collections if self.settings.enable_response_models else None + ), + response_class=self.response_class, + response_model_exclude_unset=True, + response_model_exclude_none=True, + methods=["GET"], + endpoint=create_async_endpoint(self.client.get_catalog_collections, CatalogUri), + ) + def register_get_collection(self): - """Register get collection endpoint (GET /collection/{collection_id}). + """Register get collection endpoint (GET /catalogues/{catalog_id}/collection/{collection_id}). Returns: None """ self.router.add_api_route( name="Get Collection", - path="/collections/{collection_id}", + path="/catalogues/{catalog_id}/collections/{collection_id}", response_model=Collection if self.settings.enable_response_models else None, response_class=self.response_class, response_model_exclude_unset=True, @@ -284,6 +306,23 @@ def register_get_collection(self): endpoint=create_async_endpoint(self.client.get_collection, CollectionUri), ) + def register_get_catalog(self): + """Register get catalog endpoint (GET /catalogs/{catalog_id}). + + Returns: + None + """ + self.router.add_api_route( + name="Get Catalog", + path="/catalogs/{catalog_id}", + response_model=Catalog if self.settings.enable_response_models else None, + response_class=self.response_class, + response_model_exclude_unset=True, + response_model_exclude_none=True, + methods=["GET"], + endpoint=create_async_endpoint(self.client.get_catalog, CatalogUri), + ) + def register_get_item_collection(self): """Register get item collection endpoint (GET /collection/{collection_id}/items). @@ -302,10 +341,10 @@ def register_get_item_collection(self): ) self.router.add_api_route( name="Get ItemCollection", - path="/collections/{collection_id}/items", - response_model=ItemCollection - if self.settings.enable_response_models - else None, + path="/catalogs/{catalog_id}/collections/{collection_id}/items", + response_model=( + ItemCollection if self.settings.enable_response_models else None + ), response_class=GeoJSONResponse, response_model_exclude_unset=True, response_model_exclude_none=True, @@ -338,7 +377,9 @@ def register_core(self): self.register_get_collections() self.register_get_catalogs() self.register_get_collection() + self.register_get_catalog() self.register_get_item_collection() + self.register_get_catalog_collections() def customize_openapi(self) -> Optional[Dict[str, Any]]: """Customize openapi schema.""" diff --git a/stac_fastapi/api/stac_fastapi/api/config.py b/stac_fastapi/api/stac_fastapi/api/config.py index 26834648e..81b148293 100644 --- a/stac_fastapi/api/stac_fastapi/api/config.py +++ b/stac_fastapi/api/stac_fastapi/api/config.py @@ -1,4 +1,5 @@ """Application settings.""" + import enum @@ -24,4 +25,3 @@ class AddOns(enum.Enum): """Enumeration of available third party add ons.""" bulk_transaction = "bulk-transaction" - diff --git a/stac_fastapi/api/stac_fastapi/api/middleware.py b/stac_fastapi/api/stac_fastapi/api/middleware.py index 3ed67d6c9..2ba3ef570 100644 --- a/stac_fastapi/api/stac_fastapi/api/middleware.py +++ b/stac_fastapi/api/stac_fastapi/api/middleware.py @@ -1,4 +1,5 @@ """Api middleware.""" + import re import typing from http.client import HTTP_PORT, HTTPS_PORT diff --git a/stac_fastapi/api/stac_fastapi/api/openapi.py b/stac_fastapi/api/stac_fastapi/api/openapi.py index a38a70bae..84d72dab1 100644 --- a/stac_fastapi/api/stac_fastapi/api/openapi.py +++ b/stac_fastapi/api/stac_fastapi/api/openapi.py @@ -1,4 +1,5 @@ """openapi.""" + import warnings from fastapi import FastAPI @@ -43,9 +44,9 @@ async def patched_openapi_endpoint(req: Request) -> Response: # Get the response from the old endpoint function response: JSONResponse = await old_endpoint(req) # Update the content type header in place - response.headers[ - "content-type" - ] = "application/vnd.oai.openapi+json;version=3.0" + response.headers["content-type"] = ( + "application/vnd.oai.openapi+json;version=3.0" + ) # Return the updated response return response diff --git a/stac_fastapi/api/stac_fastapi/api/version.py b/stac_fastapi/api/stac_fastapi/api/version.py index 1b1206cd3..863e3198e 100644 --- a/stac_fastapi/api/stac_fastapi/api/version.py +++ b/stac_fastapi/api/stac_fastapi/api/version.py @@ -1,2 +1,3 @@ """Library version.""" + __version__ = "2.5.2" diff --git a/stac_fastapi/api/tests/benchmarks.py b/stac_fastapi/api/tests/benchmarks.py index 9023def17..54a44a4c6 100644 --- a/stac_fastapi/api/tests/benchmarks.py +++ b/stac_fastapi/api/tests/benchmarks.py @@ -63,7 +63,7 @@ def get_search( ) -> stac_types.ItemCollection: raise NotImplementedError - def get_item(self, item_id: str, collection_id: str, **kwargs) -> stac_types.Item: + def get_item(self, item_id: str, collection_id: str, catalog_id: str, **kwargs) -> stac_types.Item: raise NotImplementedError def all_collections(self, **kwargs) -> stac_types.Collections: @@ -75,7 +75,7 @@ def all_collections(self, **kwargs) -> stac_types.Collections: {"href": "test", "rel": "parent"}, ], ) - + def all_catalogs(self, **kwargs) -> stac_types.Catalogs: return stac_types.Catalogs( catalogs=catalogs, @@ -86,8 +86,14 @@ def all_catalogs(self, **kwargs) -> stac_types.Catalogs: ], ) - def get_collection(self, collection_id: str, **kwargs) -> stac_types.Collection: + def get_collection(self, catalog_id: str, collection_id: str, **kwargs) -> stac_types.Collection: return collections[0] + + def get_catalog_collection(self, catalog_id: str, **kwargs) -> stac_types.Catalogs: + return collections[0] + + def get_catalog(self, catalog_id: str, **kwargs) -> stac_types.Catalog: + return catalogs[0] def item_collection( self, diff --git a/stac_fastapi/api/tests/test_api.py b/stac_fastapi/api/tests/test_api.py index fc5c3d050..8b9ee1aa0 100644 --- a/stac_fastapi/api/tests/test_api.py +++ b/stac_fastapi/api/tests/test_api.py @@ -86,23 +86,17 @@ def test_add_route_dependencies_after_building_api(self): class DummyCoreClient(core.BaseCoreClient): - def all_collections(self, *args, **kwargs): - ... + def all_collections(self, *args, **kwargs): ... - def get_collection(self, *args, **kwargs): - ... + def get_collection(self, *args, **kwargs): ... - def get_item(self, *args, **kwargs): - ... + def get_item(self, *args, **kwargs): ... - def get_search(self, *args, **kwargs): - ... + def get_search(self, *args, **kwargs): ... - def post_search(self, *args, **kwargs): - ... + def post_search(self, *args, **kwargs): ... - def item_collection(self, *args, **kwargs): - ... + def item_collection(self, *args, **kwargs): ... class DummyTransactionsClient(core.BaseTransactionsClient): @@ -119,15 +113,21 @@ def delete_item(self, *args, **kwargs): def create_collection(self, *args, **kwargs): return "dummy response" - - def create_catalog(self, *args, **kwargs): - return "dummy response" def update_collection(self, *args, **kwargs): return "dummy response" def delete_collection(self, *args, **kwargs): return "dummy response" + + def create_catalog(self, *args, **kwargs): + return "dummy response" + + def update_catalog(self, *args, **kwargs): + return "dummy response" + + def delete_catalog(self, *args, **kwargs): + return "dummy response" def must_be_bob( diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py index c4c320a6d..4374eb65c 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py @@ -1,4 +1,5 @@ """stac_api.extensions.core module.""" + from .context import ContextExtension from .fields import FieldsExtension from .filter import FilterExtension diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/__init__.py index 21f35e8ca..c1e0acb8f 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/__init__.py @@ -1,6 +1,5 @@ """Filter extension module.""" - from .collectionSearch import CollectionSearchExtension __all__ = ["CollectionSearchExtension"] diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py index 538ab2f81..103c877ad 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py @@ -12,9 +12,16 @@ from stac_fastapi.api.routes import create_async_endpoint from stac_fastapi.types.core import AsyncCollectionSearchClient, CollectionSearchClient from stac_fastapi.types.extension import ApiExtension -from stac_fastapi.types.search import BaseCollectionSearchGetRequest, BaseCollectionSearchPostRequest +from stac_fastapi.types.search import ( + BaseCollectionSearchGetRequest, + BaseCollectionSearchPostRequest, +) + +from .request import ( + CollectionSearchExtensionGetRequest, + CollectionSearchExtensionPostRequest, +) -from .request import CollectionSearchExtensionGetRequest, CollectionSearchExtensionPostRequest class CollectionSearchConformanceClasses(str, Enum): """Conformance classes for the Collection Search extension. @@ -26,13 +33,16 @@ class CollectionSearchConformanceClasses(str, Enum): CORE = "https://api.stacspec.org/v1.0.0-rc.1/core" COLLECTION_SEARCH = "https://api.stacspec.org/v1.0.0-rc.1/collection-search" SIMPLE_QUERY = "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query" - COLLECTION_SEARCH_FREE_TEXT = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text" + COLLECTION_SEARCH_FREE_TEXT = ( + "https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text" + ) + @attr.s class CollectionSearchExtension(ApiExtension): """CollectionSearch Extension. - The collection search extension adds two endpoints which allow searching of + The collection search extension adds two endpoints which allow searching of collections via GET and POST: GET /collection-search POST /collection-search @@ -45,21 +55,21 @@ class CollectionSearchExtension(ApiExtension): client: Collection Search endpoint logic conformance_classes: Conformance classes provided by the extension """ - + GET = CollectionSearchExtensionGetRequest POST = CollectionSearchExtensionPostRequest collection_search_get_request_model: Type[BaseCollectionSearchGetRequest] = attr.ib( default=BaseCollectionSearchGetRequest ) - collection_search_post_request_model: Type[BaseCollectionSearchPostRequest] = attr.ib( - default=BaseCollectionSearchPostRequest + collection_search_post_request_model: Type[BaseCollectionSearchPostRequest] = ( + attr.ib(default=BaseCollectionSearchPostRequest) ) - + client: Union[AsyncCollectionSearchClient, CollectionSearchClient] = attr.ib( factory=CollectionSearchClient ) - + conformance_classes: List[str] = attr.ib( default=[ CollectionSearchConformanceClasses.CORE, @@ -69,7 +79,7 @@ class CollectionSearchExtension(ApiExtension): ] ) router: APIRouter = attr.ib(factory=APIRouter) - response_class: Type[Response] = attr.ib(default=JSONResponse) + response_class: Type[Response] = attr.ib(default=JSONResponse) def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. @@ -90,7 +100,8 @@ def register(self, app: FastAPI) -> None: response_model_exclude_none=True, methods=["GET"], endpoint=create_async_endpoint( - self.client.get_collection_search, self.collection_search_get_request_model + self.client.get_collection_search, + self.collection_search_get_request_model, ), ) @@ -103,7 +114,8 @@ def register(self, app: FastAPI) -> None: response_model_exclude_none=True, methods=["POST"], endpoint=create_async_endpoint( - self.client.post_collection_search, self.collection_search_post_request_model + self.client.post_collection_search, + self.collection_search_post_request_model, ), ) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py index 2bdf4b5e6..7a65fdd72 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py @@ -11,6 +11,9 @@ from stac_fastapi.types.rfc3339 import DateTimeType, str_to_interval +from ..filter.request import FilterLang + + @attr.s class CollectionSearchExtensionGetRequest(APIRequest): """Collection Search extension GET request model.""" @@ -20,6 +23,7 @@ class CollectionSearchExtensionGetRequest(APIRequest): limit: Optional[int] = attr.ib(default=10) q: Optional[str] = attr.ib(default=None) + class CollectionSearchExtensionPostRequest(BaseModel): """Collection Search extension POST request model.""" diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/__init__.py index c25d88d0b..41eba4784 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/__init__.py @@ -1,6 +1,5 @@ """Filter extension module.""" - from .discoverySearch import DiscoverySearchExtension __all__ = ["DiscoverySearchExtension"] diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/discoverySearch.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/discoverySearch.py index be1eefda4..9c05d39cc 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/discoverySearch.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/discoverySearch.py @@ -12,15 +12,22 @@ from stac_fastapi.api.routes import create_async_endpoint from stac_fastapi.types.core import AsyncDiscoverySearchClient, DiscoverySearchClient from stac_fastapi.types.extension import ApiExtension -from stac_fastapi.types.search import BaseDiscoverySearchGetRequest, BaseDiscoverySearchPostRequest +from stac_fastapi.types.search import ( + BaseDiscoverySearchGetRequest, + BaseDiscoverySearchPostRequest, +) + +from .request import ( + DiscoverySearchExtensionGetRequest, + DiscoverySearchExtensionPostRequest, +) -from .request import DiscoverySearchExtensionGetRequest, DiscoverySearchExtensionPostRequest @attr.s class DiscoverySearchExtension(ApiExtension): """DiscoverySearch Extension. - The collection search extension adds two endpoints which allow searching of + The collection search extension adds two endpoints which allow searching of collections via GET and POST: GET /discovery-search POST /discovery-search @@ -33,7 +40,7 @@ class DiscoverySearchExtension(ApiExtension): client: Collection Search endpoint logic conformance_classes: Conformance classes provided by the extension """ - + GET = DiscoverySearchExtensionGetRequest POST = DiscoverySearchExtensionPostRequest @@ -43,16 +50,14 @@ class DiscoverySearchExtension(ApiExtension): discovery_search_post_request_model: Type[BaseDiscoverySearchPostRequest] = attr.ib( default=BaseDiscoverySearchPostRequest ) - + client: Union[AsyncDiscoverySearchClient, DiscoverySearchClient] = attr.ib( factory=DiscoverySearchClient ) - - conformance_classes: List[str] = attr.ib( - default=[] - ) + + conformance_classes: List[str] = attr.ib(default=[]) router: APIRouter = attr.ib(factory=APIRouter) - response_class: Type[Response] = attr.ib(default=JSONResponse) + response_class: Type[Response] = attr.ib(default=JSONResponse) def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. @@ -73,7 +78,8 @@ def register(self, app: FastAPI) -> None: response_model_exclude_none=True, methods=["GET"], endpoint=create_async_endpoint( - self.client.get_discovery_search, self.discovery_search_get_request_model + self.client.get_discovery_search, + self.discovery_search_get_request_model, ), ) @@ -86,7 +92,8 @@ def register(self, app: FastAPI) -> None: response_model_exclude_none=True, methods=["POST"], endpoint=create_async_endpoint( - self.client.post_discovery_search, self.discovery_search_post_request_model + self.client.post_discovery_search, + self.discovery_search_post_request_model, ), ) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/request.py index 71942ef6e..7e7f772f5 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/request.py @@ -13,6 +13,7 @@ from ..filter.request import FilterLang + @attr.s class DiscoverySearchExtensionGetRequest(APIRequest): """Discovery Search extension GET request model.""" @@ -20,6 +21,7 @@ class DiscoverySearchExtensionGetRequest(APIRequest): q: Optional[str] = attr.ib(default=None) limit: Optional[int] = attr.ib(default=10) + class DiscoverySearchExtensionPostRequest(BaseModel): """Discovery Search extension POST request model.""" 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 b9a246b63..087d01b7a 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/__init__.py @@ -1,6 +1,5 @@ """Fields extension module.""" - from .fields import FieldsExtension __all__ = ["FieldsExtension"] 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 df4cd44de..25b6fe252 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py @@ -1,4 +1,5 @@ """Fields extension.""" + from typing import List, Optional, Set import attr 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 78256bfd2..256f3e06e 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/__init__.py @@ -1,6 +1,5 @@ """Filter extension module.""" - from .filter import FilterExtension __all__ = ["FilterExtension"] 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 3e85b406d..dcb162060 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py @@ -1,4 +1,5 @@ """Query extension.""" + from typing import List, Optional import attr 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 5dd96cfa6..4b27d8d0e 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py @@ -1,4 +1,5 @@ """Sort extension.""" + from typing import List, Optional import attr diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py index 6d755e296..12162e391 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py @@ -7,7 +7,7 @@ from stac_pydantic import Collection, Item, Catalog from starlette.responses import JSONResponse, Response -from stac_fastapi.api.models import CollectionUri, ItemUri +from stac_fastapi.api.models import CollectionUri, ItemUri, CatalogUri from stac_fastapi.api.routes import create_async_endpoint from stac_fastapi.types import stac as stac_types from stac_fastapi.types.config import ApiSettings @@ -23,6 +23,30 @@ class PostItem(CollectionUri): default=Body(None) ) +@attr.s +class PostCatalog(CatalogUri): + """Create Item.""" + + catalog: Union[stac_types.Catalog] = attr.ib( + default=Body(None) + ) + +@attr.s +class PutCollection(CollectionUri): + """Update Collection.""" + + collection: Union[stac_types.Collection] = attr.ib( + default=Body(None) + ) + +@attr.s +class PostCollection(CatalogUri): + """Create Collection.""" + + collection: Union[stac_types.Collection] = attr.ib( + default=Body(None) + ) + @attr.s class PutItem(ItemUri): @@ -62,10 +86,10 @@ class TransactionExtension(ApiExtension): response_class: Type[Response] = attr.ib(default=JSONResponse) def register_create_item(self): - """Register create item endpoint (POST /collections/{collection_id}/items).""" + """Register create item endpoint (POST /catalogs/{catalog_id}/collections/{collection_id}/items).""" self.router.add_api_route( name="Create Item", - path="/collections/{collection_id}/items", + path="/catalogs/{catalog_id}/collections/{collection_id}/items", response_model=Item if self.settings.enable_response_models else None, response_class=self.response_class, response_model_exclude_unset=True, @@ -76,10 +100,10 @@ def register_create_item(self): def register_update_item(self): """Register update item endpoint (PUT - /collections/{collection_id}/items/{item_id}).""" + /catalogs/{catalog_id}/collections/{collection_id}/items/{item_id}).""" self.router.add_api_route( name="Update Item", - path="/collections/{collection_id}/items/{item_id}", + path="/catalogs/{catalog_id}/collections/{collection_id}/items/{item_id}", response_model=Item if self.settings.enable_response_models else None, response_class=self.response_class, response_model_exclude_unset=True, @@ -90,10 +114,10 @@ def register_update_item(self): def register_delete_item(self): """Register delete item endpoint (DELETE - /collections/{collection_id}/items/{item_id}).""" + /catalogs/{catalog_id}/collections/{collection_id}/items/{item_id}).""" self.router.add_api_route( name="Delete Item", - path="/collections/{collection_id}/items/{item_id}", + path="/catalogs/{catalog_id}/collections/{collection_id}/items/{item_id}", response_model=Item if self.settings.enable_response_models else None, response_class=self.response_class, response_model_exclude_unset=True, @@ -103,17 +127,47 @@ def register_delete_item(self): ) def register_create_collection(self): - """Register create collection endpoint (POST /collections).""" + """Register create collection endpoint (POST /catalogs/{catalog_id}/collections).""" self.router.add_api_route( name="Create Collection", - path="/collections", + path="/catalogs/{catalog_id}/collections/", response_model=Collection if self.settings.enable_response_models else None, response_class=self.response_class, response_model_exclude_unset=True, response_model_exclude_none=True, methods=["POST"], endpoint=create_async_endpoint( - self.client.create_collection, stac_types.Collection + self.client.create_collection, PostCollection + ), + ) + + def register_update_collection(self): + """Register update collection endpoint (PUT /catalogs/{catalog_id}/collections/{collection_id}).""" + self.router.add_api_route( + name="Update Collection", + path="/catalogs/{catalog_id}/collections/{collection_id}", + response_model=Collection if self.settings.enable_response_models else None, + response_class=self.response_class, + response_model_exclude_unset=True, + response_model_exclude_none=True, + methods=["PUT"], + endpoint=create_async_endpoint( + self.client.update_collection, PutCollection + ), + ) + + def register_delete_collection(self): + """Register delete collection endpoint (DELETE /catalogs/{catalog_id}/collections/{collection_id}).""" + self.router.add_api_route( + name="Delete Collection", + path="/catalogs/{catalog_id}/collections/{collection_id}", + response_model=Collection if self.settings.enable_response_models else None, + response_class=self.response_class, + response_model_exclude_unset=True, + response_model_exclude_none=True, + methods=["DELETE"], + endpoint=create_async_endpoint( + self.client.delete_collection, CollectionUri ), ) @@ -132,33 +186,33 @@ def register_create_catalog(self): ), ) - def register_update_collection(self): + def register_update_catalog(self): """Register update collection endpoint (PUT /collections/{collection_id}).""" self.router.add_api_route( - name="Update Collection", - path="/collections/{collection_id}", - response_model=Collection if self.settings.enable_response_models else None, + name="Update Catalog", + path="/catalogs/{catalog_id}", + response_model=Catalog if self.settings.enable_response_models else None, response_class=self.response_class, response_model_exclude_unset=True, response_model_exclude_none=True, methods=["PUT"], endpoint=create_async_endpoint( - self.client.update_collection, stac_types.Collection + self.client.update_catalog, PostCatalog ), ) - def register_delete_collection(self): - """Register delete collection endpoint (DELETE /collections/{collection_id}).""" + def register_delete_catalog(self): + """Register delete collection endpoint (DELETE /catalogs/{catalog_id}).""" self.router.add_api_route( - name="Delete Collection", - path="/collections/{collection_id}", - response_model=Collection if self.settings.enable_response_models else None, + name="Delete Catalog", + path="/catalogs/{catalog_id}", + response_model=Catalog if self.settings.enable_response_models else None, response_class=self.response_class, response_model_exclude_unset=True, response_model_exclude_none=True, methods=["DELETE"], endpoint=create_async_endpoint( - self.client.delete_collection, CollectionUri + self.client.delete_catalog, CatalogUri ), ) @@ -178,5 +232,7 @@ def register(self, app: FastAPI) -> None: self.register_create_collection() self.register_create_catalog() self.register_update_collection() + self.register_update_catalog() self.register_delete_collection() + self.register_delete_catalog() app.include_router(self.router, tags=["Transaction Extension"]) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/third_party/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/third_party/__init__.py index ab7349e60..d35c4c8f9 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/third_party/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/third_party/__init__.py @@ -1,4 +1,5 @@ """stac_api.extensions.third_party module.""" + from .bulk_transactions import BulkTransactionExtension __all__ = ("BulkTransactionExtension",) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/third_party/bulk_transactions.py b/stac_fastapi/extensions/stac_fastapi/extensions/third_party/bulk_transactions.py index 9fa96ff2b..cfe906afc 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/third_party/bulk_transactions.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/third_party/bulk_transactions.py @@ -1,4 +1,5 @@ """Bulk transactions extension.""" + import abc from enum import Enum from typing import Any, Dict, List, Optional, Union @@ -46,6 +47,7 @@ def _chunks(lst, n): @abc.abstractmethod def bulk_item_insert( self, + catalog_id: str, items: Items, chunk_size: Optional[int] = None, **kwargs, @@ -69,6 +71,7 @@ class AsyncBaseBulkTransactionsClient(abc.ABC): @abc.abstractmethod async def bulk_item_insert( self, + catalog_id: str, items: Items, **kwargs, ) -> str: @@ -109,9 +112,9 @@ class BulkTransactionExtension(ApiExtension): } """ - client: Union[ - AsyncBaseBulkTransactionsClient, BaseBulkTransactionsClient - ] = attr.ib() + client: Union[AsyncBaseBulkTransactionsClient, BaseBulkTransactionsClient] = ( + attr.ib() + ) conformance_classes: List[str] = attr.ib(default=list()) schema_href: Optional[str] = attr.ib(default=None) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/version.py b/stac_fastapi/extensions/stac_fastapi/extensions/version.py index 1b1206cd3..863e3198e 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/version.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/version.py @@ -1,2 +1,3 @@ """Library version.""" + __version__ = "2.5.2" diff --git a/stac_fastapi/extensions/tests/test_transaction.py b/stac_fastapi/extensions/tests/test_transaction.py index c0bcaadae..41633a737 100644 --- a/stac_fastapi/extensions/tests/test_transaction.py +++ b/stac_fastapi/extensions/tests/test_transaction.py @@ -14,12 +14,15 @@ class DummyCoreClient(BaseCoreClient): def all_collections(self, *args, **kwargs): raise NotImplementedError - + def all_catalogs(self, *args, **kwargs): raise NotImplementedError def get_collection(self, *args, **kwargs): raise NotImplementedError + + def get_catalog_collections(self, *args, **kwargs): + raise NotImplementedError def get_item(self, *args, **kwargs): raise NotImplementedError @@ -48,15 +51,21 @@ def delete_item(self, *args, **kwargs): def create_collection(self, *args, **kwargs): raise NotImplementedError - - def create_catalog(self, *args, **kwargs): - raise NotImplementedError def update_collection(self, *args, **kwargs): raise NotImplementedError def delete_collection(self, *args, **kwargs): raise NotImplementedError + + def create_catalog(self, *args, **kwargs): + raise NotImplementedError + + def update_catalog(self, *args, **kwargs): + raise NotImplementedError + + def delete_catalog(self, *args, **kwargs): + raise NotImplementedError def test_create_item(client: TestClient, item: Item) -> None: diff --git a/stac_fastapi/types/stac_fastapi/types/catalogs.py b/stac_fastapi/types/stac_fastapi/types/catalogs.py index 3b48d6f64..0d19db0eb 100644 --- a/stac_fastapi/types/stac_fastapi/types/catalogs.py +++ b/stac_fastapi/types/stac_fastapi/types/catalogs.py @@ -18,4 +18,4 @@ def to_dict(self, **kwargs): return self.dict(by_alias=True, exclude_unset=True, **kwargs) def to_json(self, **kwargs): - return self.json(by_alias=True, exclude_unset=True, **kwargs) \ No newline at end of file + return self.json(by_alias=True, exclude_unset=True, **kwargs) diff --git a/stac_fastapi/types/stac_fastapi/types/config.py b/stac_fastapi/types/stac_fastapi/types/config.py index 4b88c56a4..905f5ae55 100644 --- a/stac_fastapi/types/stac_fastapi/types/config.py +++ b/stac_fastapi/types/stac_fastapi/types/config.py @@ -1,4 +1,5 @@ """stac_fastapi.types.config module.""" + from typing import Optional, Set from pydantic import BaseSettings diff --git a/stac_fastapi/types/stac_fastapi/types/conformance.py b/stac_fastapi/types/stac_fastapi/types/conformance.py index 13836aaf5..840584c1b 100644 --- a/stac_fastapi/types/stac_fastapi/types/conformance.py +++ b/stac_fastapi/types/stac_fastapi/types/conformance.py @@ -1,4 +1,5 @@ """Conformance Classes.""" + from enum import Enum diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index f0477dd09..271eb1ed7 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -17,7 +17,12 @@ from stac_fastapi.types.extension import ApiExtension from stac_fastapi.types.requests import get_base_url from stac_fastapi.types.rfc3339 import DateTimeType -from stac_fastapi.types.search import BaseSearchPostRequest, BaseCollectionSearchPostRequest, BaseDiscoverySearchPostRequest, BaseDiscoverySearchPostRequest +from stac_fastapi.types.search import ( + BaseSearchPostRequest, + BaseCollectionSearchPostRequest, + BaseDiscoverySearchPostRequest, + BaseDiscoverySearchPostRequest, +) from stac_fastapi.types.stac import Conformance NumType = Union[float, int] @@ -52,7 +57,7 @@ def create_item( @abc.abstractmethod def update_item( - self, collection_id: str, item_id: str, item: stac_types.Item, **kwargs + self, catalog_id: str, collection_id: str, item_id: str, item: stac_types.Item, **kwargs ) -> Optional[Union[stac_types.Item, Response]]: """Perform a complete update on an existing item. @@ -72,7 +77,7 @@ def update_item( @abc.abstractmethod def delete_item( - self, item_id: str, collection_id: str, **kwargs + self, item_id: str, collection_id: str, catalog_id: str, **kwargs ) -> Optional[Union[stac_types.Item, Response]]: """Delete an item from a collection. @@ -89,7 +94,7 @@ def delete_item( @abc.abstractmethod def create_collection( - self, collection: stac_types.Collection, **kwargs + self, catalog_id: str, collection: stac_types.Collection, **kwargs ) -> Optional[Union[stac_types.Collection, Response]]: """Create a new collection. @@ -141,7 +146,7 @@ def update_collection( @abc.abstractmethod def delete_collection( - self, collection_id: str, **kwargs + self, catalog_id: str, collection_id: str, **kwargs ) -> Optional[Union[stac_types.Collection, Response]]: """Delete a collection. @@ -182,7 +187,7 @@ async def create_item( @abc.abstractmethod async def update_item( - self, collection_id: str, item_id: str, item: stac_types.Item, **kwargs + self, catalog_id: str, collection_id: str, item_id: str, item: stac_types.Item, **kwargs ) -> Optional[Union[stac_types.Item, Response]]: """Perform a complete update on an existing item. @@ -201,7 +206,7 @@ async def update_item( @abc.abstractmethod async def delete_item( - self, item_id: str, collection_id: str, **kwargs + self, item_id: str, collection_id: str, catalog_id: str, **kwargs ) -> Optional[Union[stac_types.Item, Response]]: """Delete an item from a collection. @@ -234,11 +239,11 @@ async def create_collection( @abc.abstractmethod async def update_collection( - self, collection_id: str, collection: stac_types.Collection, **kwargs + self, catalog_id: str, collection_id: str, collection: stac_types.Collection, **kwargs ) -> Optional[Union[stac_types.Collection, Response]]: """Perform a complete update on an existing collection. - Called with `PUT /collections/{collection_id}`. It is expected that this item + Called with `PUT /catalogs/{catalog_id}/collections/{collection_id}`. It is expected that this item already exists. The update should do a diff against the saved collection and perform any necessary updates. Partial updates are not supported by the transactions extension. @@ -254,11 +259,11 @@ async def update_collection( @abc.abstractmethod async def delete_collection( - self, collection_id: str, **kwargs + self, catalog_id: str, collection_id: str, **kwargs ) -> Optional[Union[stac_types.Collection, Response]]: """Delete a collection. - Called with `DELETE /collections/{collection_id}` + Called with `DELETE /catalogs/{catalog_id}/collections/{collection_id}` Args: collection_id: id of the collection. @@ -491,7 +496,7 @@ def get_search( ... @abc.abstractmethod - def get_item(self, item_id: str, collection_id: str, **kwargs) -> stac_types.Item: + def get_item(self, item_id: str, collection_id: str, catalog_id: str, **kwargs) -> stac_types.Item: """Get item by id. Called with `GET /collections/{collection_id}/items/{item_id}`. @@ -528,10 +533,10 @@ def all_catalogs(self, **kwargs) -> stac_types.Catalogs: ... @abc.abstractmethod - def get_collection(self, collection_id: str, **kwargs) -> stac_types.Collection: + def get_collection(self, catalog_id: str, collection_id: str, **kwargs) -> stac_types.Collection: """Get collection by id. - Called with `GET /collections/{collection_id}`. + Called with `GET /catalogs/{catalog_id}/collections/{collection_id}`. Args: collection_id: Id of the collection. @@ -541,6 +546,34 @@ def get_collection(self, collection_id: str, **kwargs) -> stac_types.Collection: """ ... + @abc.abstractmethod + def get_catalog(self, catalog_id: str, **kwargs) -> stac_types.Catalog: + """Get catalog by id. + + Called with `GET /catalogs/{catalog_id}`. + + Args: + catalog_id: Id of the catalog. + + Returns: + Catalog. + """ + ... + + @abc.abstractmethod + def get_catalog_collections(self, catalog_id: str, **kwargs) -> stac_types.Collections: + """Get collections by catalog id. + + Called with `GET /catalogs/{catalog_id}/collections`. + + Args: + catalog_id: Id of the catalog. + + Returns: + Collections. + """ + ... + @abc.abstractmethod def item_collection( self, @@ -723,11 +756,11 @@ async def get_search( @abc.abstractmethod async def get_item( - self, item_id: str, collection_id: str, **kwargs + self, item_id: str, collection_id: str, catalog_id: str, **kwargs ) -> stac_types.Item: """Get item by id. - Called with `GET /collections/{collection_id}/items/{item_id}`. + Called with `GET /catalogs/{catalog_id}/collections/{collection_id}/items/{item_id}`. Args: item_id: Id of the item. @@ -760,14 +793,13 @@ async def all_catalogs(self, **kwargs) -> stac_types.Catalogs: """ ... - @abc.abstractmethod async def get_collection( self, collection_id: str, **kwargs ) -> stac_types.Collection: """Get collection by id. - Called with `GET /collections/{collection_id}`. + Called with `GET /catalogs/{catalog_id}/collections/{collection_id}`. Args: collection_id: Id of the collection. @@ -777,6 +809,22 @@ async def get_collection( """ ... + @abc.abstractmethod + async def get_catalog( + self, catalog_id: str, **kwargs + ) -> stac_types.Catalog: + """Get catalog by id. + + Called with `GET /catalogs/{catalog_id}`. + + Args: + catalog_id: Id of the catalog. + + Returns: + Catalog. + """ + ... + @abc.abstractmethod async def item_collection( self, @@ -789,7 +837,7 @@ async def item_collection( ) -> stac_types.ItemCollection: """Get all items from a specific collection. - Called with `GET /collections/{collection_id}/items` + Called with `GET /catalogs/{catalog_id}/collections/{collection_id}/items` Args: collection_id: id of the collection. @@ -852,7 +900,8 @@ def get_queryables( "description": "Queryable names for the example STAC API Item Search filter.", "properties": {}, } - + + @attr.s class AsyncCollectionSearchClient(abc.ABC): """Defines a pattern for implementing the STAC Collection Search extension.""" @@ -932,6 +981,7 @@ def get_collection_search( """ ... + @attr.s class AsyncDiscoverySearchClient(abc.ABC): """Defines a pattern for implementing the STAC Collection Search extension.""" @@ -1005,4 +1055,4 @@ def get_discovery_search( Returns: A tuple of (collections, next pagination token if any). """ - ... \ No newline at end of file + ... diff --git a/stac_fastapi/types/stac_fastapi/types/extension.py b/stac_fastapi/types/stac_fastapi/types/extension.py index 732a907bf..55a4a123c 100644 --- a/stac_fastapi/types/stac_fastapi/types/extension.py +++ b/stac_fastapi/types/stac_fastapi/types/extension.py @@ -1,4 +1,5 @@ """Base api extension.""" + import abc from typing import List, Optional diff --git a/stac_fastapi/types/stac_fastapi/types/links.py b/stac_fastapi/types/stac_fastapi/types/links.py index 3855868b7..ee8ebd9a6 100644 --- a/stac_fastapi/types/stac_fastapi/types/links.py +++ b/stac_fastapi/types/stac_fastapi/types/links.py @@ -10,8 +10,9 @@ # These can be inferred from the item/collection so they aren't included in the database # Instead they are dynamically generated when querying the database using the # classes defined below -#INFERRED_LINK_RELS = ["self", "item", "parent", "collection", "root", "items"] -INFERRED_LINK_RELS = ["self", "item", "parent", "collection", "items"] +INFERRED_LINK_RELS = ["self", "item", "parent", "collection", "root", "items"] +# Attempt to maintain "root" links to later determine relationships to catalogues +#INFERRED_LINK_RELS = ["self", "item", "parent", "collection", "items"] def filter_links(links: List[Dict]) -> List[Dict]: @@ -26,7 +27,7 @@ def resolve_links(links: list, base_url: str) -> List[Dict]: if "http://" in link["href"] or "https://" in link["href"]: split_url = urlsplit(link["href"]) link["href"] = split_url.path - #link["href"].replace(split_url.scheme, "").replace(split_url.netloc, "") + # link["href"].replace(split_url.scheme, "").replace(split_url.netloc, "") link.update({"href": urljoin(base_url, link["href"])}) return filtered_links @@ -70,7 +71,13 @@ def items(self) -> Dict[str, Any]: def create_links(self) -> List[Dict[str, Any]]: """Return all inferred links.""" # We use predefined root here to identify the catalog containing this dataset - return [self.self(), self.parent(), self.items()]#, self.root()] + return [ + self.self(), + self.parent(), + self.items(), + self.root(), + ] # get rid of root here to remove generated value + @attr.s class BaseCatalogLinks: @@ -82,7 +89,8 @@ class BaseCatalogLinks: def root(self) -> Dict[str, Any]: """Return the catalog root.""" return dict(rel=Relations.root, type=MimeTypes.json, href=self.base_url) - + + @attr.s class CatalogLinks(BaseCatalogLinks): """Create inferred links specific to catalogs.""" @@ -99,12 +107,13 @@ def parent(self) -> Dict[str, Any]: """Create the `parent` link.""" return dict(rel=Relations.parent, type=MimeTypes.json, href=self.base_url) - def create_links(self) -> List[Dict[str, Any]]: """Return all inferred links.""" # We use predefined root here to identify the catalog containing this dataset + # No items for catalogues return [self.self(), self.parent(), self.root()] + @attr.s class ItemLinks(BaseLinks): """Create inferred links specific to items.""" diff --git a/stac_fastapi/types/stac_fastapi/types/rfc3339.py b/stac_fastapi/types/stac_fastapi/types/rfc3339.py index b1d40999e..1a8c2d0d6 100644 --- a/stac_fastapi/types/stac_fastapi/types/rfc3339.py +++ b/stac_fastapi/types/stac_fastapi/types/rfc3339.py @@ -1,4 +1,5 @@ """rfc3339.""" + import re from datetime import datetime, timezone from typing import Optional, Tuple, Union diff --git a/stac_fastapi/types/stac_fastapi/types/search.py b/stac_fastapi/types/stac_fastapi/types/search.py index f11ef896e..c060e838a 100644 --- a/stac_fastapi/types/stac_fastapi/types/search.py +++ b/stac_fastapi/types/stac_fastapi/types/search.py @@ -106,6 +106,7 @@ class BaseSearchGetRequest(APIRequest): """Base arguments for GET Request.""" collections: Optional[str] = attr.ib(default=None, converter=str2list) + catalogs: Optional[str] = attr.ib(default=None, converter=str2list) ids: Optional[str] = attr.ib(default=None, converter=str2list) bbox: Optional[BBox] = attr.ib(default=None, converter=str2bbox) intersects: Optional[str] = attr.ib(default=None, converter=str2list) @@ -125,6 +126,7 @@ class BaseSearchPostRequest(BaseModel): """ collections: Optional[List[str]] + catalogs: Optional[List[str]] ids: Optional[List[str]] bbox: Optional[BBox] intersects: Optional[ @@ -219,7 +221,7 @@ def spatial_filter(self) -> Optional[_GeometryBase]: if self.intersects: return self.intersects return - + @attr.s class BaseCollectionSearchGetRequest(APIRequest): @@ -229,8 +231,8 @@ class BaseCollectionSearchGetRequest(APIRequest): datetime: Optional[DateTimeType] = attr.ib(default=None, converter=str_to_interval) limit: Optional[int] = attr.ib(default=10) q: Optional[str] = attr.ib(default=None) - - + + class BaseCollectionSearchPostRequest(BaseModel): """Search model. @@ -314,7 +316,7 @@ def spatial_filter(self) -> Optional[_GeometryBase]: if self.intersects: return self.intersects return - + @attr.s class BaseDiscoverySearchGetRequest(APIRequest): @@ -322,8 +324,8 @@ class BaseDiscoverySearchGetRequest(APIRequest): q: Optional[str] = attr.ib(default=None) limit: Optional[int] = attr.ib(default=10) - - + + class BaseDiscoverySearchPostRequest(BaseModel): """Search model. @@ -333,4 +335,3 @@ class BaseDiscoverySearchPostRequest(BaseModel): q: Optional[str] limit: Optional[Limit] = Field(default=10) - diff --git a/stac_fastapi/types/stac_fastapi/types/stac.py b/stac_fastapi/types/stac_fastapi/types/stac.py index a4152b72d..d8c868acb 100644 --- a/stac_fastapi/types/stac_fastapi/types/stac.py +++ b/stac_fastapi/types/stac_fastapi/types/stac.py @@ -1,4 +1,5 @@ """STAC types.""" + import sys from typing import Any, Dict, List, Literal, Optional, Union @@ -56,6 +57,7 @@ class Collection(Catalog, total=False): extent: Dict[str, Any] summaries: Dict[str, Any] assets: Dict[str, Any] + # links: List[Dict[str, Any]] class Item(TypedDict, total=False): @@ -91,6 +93,7 @@ class Collections(TypedDict, total=False): collections: List[Collection] links: List[Dict[str, Any]] + class Catalogs(TypedDict, total=False): """All collections endpoint. @@ -99,3 +102,12 @@ class Catalogs(TypedDict, total=False): catalogs: List[Catalog] links: List[Dict[str, Any]] + + +class CatalogsAndCollections(TypedDict, total=False): + """All catalogues and collections endpoint. + + """ + + catalogs_and_collections: List[Union[Catalog, Collection]] + links: List[Dict[str, Any]] diff --git a/stac_fastapi/types/stac_fastapi/types/version.py b/stac_fastapi/types/stac_fastapi/types/version.py index 1b1206cd3..863e3198e 100644 --- a/stac_fastapi/types/stac_fastapi/types/version.py +++ b/stac_fastapi/types/stac_fastapi/types/version.py @@ -1,2 +1,3 @@ """Library version.""" + __version__ = "2.5.2" From 31fbeebb15edef2b3f6588c95bc4ac16d7f50a92 Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Wed, 15 May 2024 10:07:07 +0100 Subject: [PATCH 16/21] Code reformatting --- stac_fastapi/api/stac_fastapi/api/app.py | 13 ++++--- stac_fastapi/api/stac_fastapi/api/models.py | 2 + stac_fastapi/api/tests/benchmarks.py | 12 ++++-- stac_fastapi/api/tests/test_api.py | 4 +- .../stac_fastapi/extensions/core/__init__.py | 4 +- .../core/collectionSearch/collectionSearch.py | 2 - .../core/collectionSearch/request.py | 8 +--- .../core/discoverySearch/discoverySearch.py | 3 -- .../core/discoverySearch/request.py | 10 +---- .../extensions/core/transaction.py | 27 +++++-------- .../extensions/tests/test_transaction.py | 6 +-- .../types/stac_fastapi/types/catalogs.py | 1 - stac_fastapi/types/stac_fastapi/types/core.py | 39 +++++++++++++------ .../types/stac_fastapi/types/links.py | 2 +- stac_fastapi/types/stac_fastapi/types/stac.py | 4 +- 15 files changed, 69 insertions(+), 68 deletions(-) diff --git a/stac_fastapi/api/stac_fastapi/api/app.py b/stac_fastapi/api/stac_fastapi/api/app.py index 405d76c8e..654a3deb1 100644 --- a/stac_fastapi/api/stac_fastapi/api/app.py +++ b/stac_fastapi/api/stac_fastapi/api/app.py @@ -7,22 +7,21 @@ from fastapi import APIRouter, FastAPI from fastapi.openapi.utils import get_openapi from fastapi.params import Depends -from stac_pydantic import Collection, Item, ItemCollection, Catalog +from stac_pydantic import Catalog, Collection, Item, ItemCollection from stac_pydantic.api import ConformanceClasses, LandingPage from stac_pydantic.api.collections import Collections -from stac_fastapi.types.stac import Catalogs from stac_pydantic.version import STAC_VERSION from starlette.responses import JSONResponse, Response from stac_fastapi.api.errors import DEFAULT_STATUS_CODES, add_exception_handlers from stac_fastapi.api.middleware import CORSMiddleware, ProxyHeaderMiddleware from stac_fastapi.api.models import ( + CatalogUri, CollectionUri, EmptyRequest, GeoJSONResponse, ItemCollectionUri, ItemUri, - CatalogUri, create_request_model, ) from stac_fastapi.api.openapi import update_openapi @@ -34,6 +33,7 @@ from stac_fastapi.types.core import AsyncBaseCoreClient, BaseCoreClient from stac_fastapi.types.extension import ApiExtension from stac_fastapi.types.search import BaseSearchGetRequest, BaseSearchPostRequest +from stac_fastapi.types.stac import Catalogs @attr.s @@ -286,11 +286,14 @@ def register_get_catalog_collections(self): response_model_exclude_unset=True, response_model_exclude_none=True, methods=["GET"], - endpoint=create_async_endpoint(self.client.get_catalog_collections, CatalogUri), + endpoint=create_async_endpoint( + self.client.get_catalog_collections, CatalogUri + ), ) def register_get_collection(self): - """Register get collection endpoint (GET /catalogues/{catalog_id}/collection/{collection_id}). + """Register get collection endpoint + (GET /catalogues/{catalog_id}/collection/{collection_id}). Returns: None diff --git a/stac_fastapi/api/stac_fastapi/api/models.py b/stac_fastapi/api/stac_fastapi/api/models.py index a08b6c271..7186ba837 100644 --- a/stac_fastapi/api/stac_fastapi/api/models.py +++ b/stac_fastapi/api/stac_fastapi/api/models.py @@ -108,12 +108,14 @@ class CatalogUri(APIRequest): catalog_id: str = attr.ib(default=Path(..., description="Catalog ID")) + @attr.s # type:ignore class CollectionUri(CatalogUri): """Delete collection.""" collection_id: str = attr.ib(default=Path(..., description="Collection ID")) + @attr.s class ItemUri(CollectionUri): """Delete item.""" diff --git a/stac_fastapi/api/tests/benchmarks.py b/stac_fastapi/api/tests/benchmarks.py index 54a44a4c6..fe625700e 100644 --- a/stac_fastapi/api/tests/benchmarks.py +++ b/stac_fastapi/api/tests/benchmarks.py @@ -63,7 +63,9 @@ def get_search( ) -> stac_types.ItemCollection: raise NotImplementedError - def get_item(self, item_id: str, collection_id: str, catalog_id: str, **kwargs) -> stac_types.Item: + def get_item( + self, item_id: str, collection_id: str, catalog_id: str, **kwargs + ) -> stac_types.Item: raise NotImplementedError def all_collections(self, **kwargs) -> stac_types.Collections: @@ -86,12 +88,14 @@ def all_catalogs(self, **kwargs) -> stac_types.Catalogs: ], ) - def get_collection(self, catalog_id: str, collection_id: str, **kwargs) -> stac_types.Collection: + def get_collection( + self, catalog_id: str, collection_id: str, **kwargs + ) -> stac_types.Collection: return collections[0] - + def get_catalog_collection(self, catalog_id: str, **kwargs) -> stac_types.Catalogs: return collections[0] - + def get_catalog(self, catalog_id: str, **kwargs) -> stac_types.Catalog: return catalogs[0] diff --git a/stac_fastapi/api/tests/test_api.py b/stac_fastapi/api/tests/test_api.py index 8b9ee1aa0..4680d4660 100644 --- a/stac_fastapi/api/tests/test_api.py +++ b/stac_fastapi/api/tests/test_api.py @@ -119,10 +119,10 @@ def update_collection(self, *args, **kwargs): def delete_collection(self, *args, **kwargs): return "dummy response" - + def create_catalog(self, *args, **kwargs): return "dummy response" - + def update_catalog(self, *args, **kwargs): return "dummy response" diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py index 4374eb65c..8864d554b 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py @@ -1,14 +1,14 @@ """stac_api.extensions.core module.""" +from .collectionSearch import CollectionSearchExtension from .context import ContextExtension +from .discoverySearch import DiscoverySearchExtension from .fields import FieldsExtension from .filter import FilterExtension from .pagination import PaginationExtension, TokenPaginationExtension from .query import QueryExtension from .sort import SortExtension from .transaction import TransactionExtension -from .collectionSearch import CollectionSearchExtension -from .discoverySearch import DiscoverySearchExtension __all__ = ( "ContextExtension", diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py index 103c877ad..75de8388f 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/collectionSearch.py @@ -6,9 +6,7 @@ import attr from fastapi import APIRouter, FastAPI from starlette.responses import JSONResponse, Response -from stac_pydantic.api.collections import Collections -from stac_fastapi.api.models import JSONSchemaResponse from stac_fastapi.api.routes import create_async_endpoint from stac_fastapi.types.core import AsyncCollectionSearchClient, CollectionSearchClient from stac_fastapi.types.extension import ApiExtension diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py index 7a65fdd72..de48d90d6 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collectionSearch/request.py @@ -1,17 +1,13 @@ """Collection Search extension request models.""" -from enum import Enum -from typing import Any, Dict, Optional, List +from typing import Optional import attr from pydantic import BaseModel, Field from stac_pydantic.shared import BBox -from stac_fastapi.types.search import APIRequest, Limit, str2bbox - from stac_fastapi.types.rfc3339 import DateTimeType, str_to_interval - -from ..filter.request import FilterLang +from stac_fastapi.types.search import APIRequest, Limit, str2bbox @attr.s diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/discoverySearch.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/discoverySearch.py index 9c05d39cc..1688cbee8 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/discoverySearch.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/discoverySearch.py @@ -1,14 +1,11 @@ # encoding: utf-8 """Collection Search Extension.""" -from enum import Enum from typing import List, Type, Union import attr from fastapi import APIRouter, FastAPI from starlette.responses import JSONResponse, Response -from stac_pydantic.api.collections import Collections -from stac_fastapi.api.models import JSONSchemaResponse from stac_fastapi.api.routes import create_async_endpoint from stac_fastapi.types.core import AsyncDiscoverySearchClient, DiscoverySearchClient from stac_fastapi.types.extension import ApiExtension diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/request.py index 7e7f772f5..f70fc76a3 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/discoverySearch/request.py @@ -1,17 +1,11 @@ """Discovery Search extension request models.""" -from enum import Enum -from typing import Any, Dict, Optional, List +from typing import Optional import attr from pydantic import BaseModel, Field -from stac_pydantic.shared import BBox -from stac_fastapi.types.search import APIRequest, Limit, str2bbox - -from stac_fastapi.types.rfc3339 import DateTimeType, str_to_interval - -from ..filter.request import FilterLang +from stac_fastapi.types.search import APIRequest, Limit @attr.s diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py index 12162e391..a77fe5858 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py @@ -4,10 +4,10 @@ import attr from fastapi import APIRouter, Body, FastAPI -from stac_pydantic import Collection, Item, Catalog +from stac_pydantic import Catalog, Collection, Item from starlette.responses import JSONResponse, Response -from stac_fastapi.api.models import CollectionUri, ItemUri, CatalogUri +from stac_fastapi.api.models import CatalogUri, CollectionUri, ItemUri from stac_fastapi.api.routes import create_async_endpoint from stac_fastapi.types import stac as stac_types from stac_fastapi.types.config import ApiSettings @@ -23,29 +23,26 @@ class PostItem(CollectionUri): default=Body(None) ) + @attr.s class PostCatalog(CatalogUri): """Create Item.""" - catalog: Union[stac_types.Catalog] = attr.ib( - default=Body(None) - ) + catalog: Union[stac_types.Catalog] = attr.ib(default=Body(None)) + @attr.s class PutCollection(CollectionUri): """Update Collection.""" - collection: Union[stac_types.Collection] = attr.ib( - default=Body(None) - ) + collection: Union[stac_types.Collection] = attr.ib(default=Body(None)) + @attr.s class PostCollection(CatalogUri): """Create Collection.""" - collection: Union[stac_types.Collection] = attr.ib( - default=Body(None) - ) + collection: Union[stac_types.Collection] = attr.ib(default=Body(None)) @attr.s @@ -196,9 +193,7 @@ def register_update_catalog(self): response_model_exclude_unset=True, response_model_exclude_none=True, methods=["PUT"], - endpoint=create_async_endpoint( - self.client.update_catalog, PostCatalog - ), + endpoint=create_async_endpoint(self.client.update_catalog, PostCatalog), ) def register_delete_catalog(self): @@ -211,9 +206,7 @@ def register_delete_catalog(self): response_model_exclude_unset=True, response_model_exclude_none=True, methods=["DELETE"], - endpoint=create_async_endpoint( - self.client.delete_catalog, CatalogUri - ), + endpoint=create_async_endpoint(self.client.delete_catalog, CatalogUri), ) def register(self, app: FastAPI) -> None: diff --git a/stac_fastapi/extensions/tests/test_transaction.py b/stac_fastapi/extensions/tests/test_transaction.py index 41633a737..6ad3cf0f2 100644 --- a/stac_fastapi/extensions/tests/test_transaction.py +++ b/stac_fastapi/extensions/tests/test_transaction.py @@ -20,7 +20,7 @@ def all_catalogs(self, *args, **kwargs): def get_collection(self, *args, **kwargs): raise NotImplementedError - + def get_catalog_collections(self, *args, **kwargs): raise NotImplementedError @@ -57,10 +57,10 @@ def update_collection(self, *args, **kwargs): def delete_collection(self, *args, **kwargs): raise NotImplementedError - + def create_catalog(self, *args, **kwargs): raise NotImplementedError - + def update_catalog(self, *args, **kwargs): raise NotImplementedError diff --git a/stac_fastapi/types/stac_fastapi/types/catalogs.py b/stac_fastapi/types/stac_fastapi/types/catalogs.py index 0d19db0eb..421346707 100644 --- a/stac_fastapi/types/stac_fastapi/types/catalogs.py +++ b/stac_fastapi/types/stac_fastapi/types/catalogs.py @@ -1,7 +1,6 @@ from typing import List from pydantic import BaseModel - from stac_pydantic import Catalog from stac_pydantic.api.links import Link diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 271eb1ed7..fc610477d 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -18,10 +18,9 @@ from stac_fastapi.types.requests import get_base_url from stac_fastapi.types.rfc3339 import DateTimeType from stac_fastapi.types.search import ( - BaseSearchPostRequest, BaseCollectionSearchPostRequest, BaseDiscoverySearchPostRequest, - BaseDiscoverySearchPostRequest, + BaseSearchPostRequest, ) from stac_fastapi.types.stac import Conformance @@ -57,7 +56,12 @@ def create_item( @abc.abstractmethod def update_item( - self, catalog_id: str, collection_id: str, item_id: str, item: stac_types.Item, **kwargs + self, + catalog_id: str, + collection_id: str, + item_id: str, + item: stac_types.Item, + **kwargs, ) -> Optional[Union[stac_types.Item, Response]]: """Perform a complete update on an existing item. @@ -187,7 +191,12 @@ async def create_item( @abc.abstractmethod async def update_item( - self, catalog_id: str, collection_id: str, item_id: str, item: stac_types.Item, **kwargs + self, + catalog_id: str, + collection_id: str, + item_id: str, + item: stac_types.Item, + **kwargs, ) -> Optional[Union[stac_types.Item, Response]]: """Perform a complete update on an existing item. @@ -239,7 +248,11 @@ async def create_collection( @abc.abstractmethod async def update_collection( - self, catalog_id: str, collection_id: str, collection: stac_types.Collection, **kwargs + self, + catalog_id: str, + collection_id: str, + collection: stac_types.Collection, + **kwargs, ) -> Optional[Union[stac_types.Collection, Response]]: """Perform a complete update on an existing collection. @@ -496,7 +509,9 @@ def get_search( ... @abc.abstractmethod - def get_item(self, item_id: str, collection_id: str, catalog_id: str, **kwargs) -> stac_types.Item: + def get_item( + self, item_id: str, collection_id: str, catalog_id: str, **kwargs + ) -> stac_types.Item: """Get item by id. Called with `GET /collections/{collection_id}/items/{item_id}`. @@ -533,7 +548,9 @@ def all_catalogs(self, **kwargs) -> stac_types.Catalogs: ... @abc.abstractmethod - def get_collection(self, catalog_id: str, collection_id: str, **kwargs) -> stac_types.Collection: + def get_collection( + self, catalog_id: str, collection_id: str, **kwargs + ) -> stac_types.Collection: """Get collection by id. Called with `GET /catalogs/{catalog_id}/collections/{collection_id}`. @@ -561,7 +578,9 @@ def get_catalog(self, catalog_id: str, **kwargs) -> stac_types.Catalog: ... @abc.abstractmethod - def get_catalog_collections(self, catalog_id: str, **kwargs) -> stac_types.Collections: + def get_catalog_collections( + self, catalog_id: str, **kwargs + ) -> stac_types.Collections: """Get collections by catalog id. Called with `GET /catalogs/{catalog_id}/collections`. @@ -810,9 +829,7 @@ async def get_collection( ... @abc.abstractmethod - async def get_catalog( - self, catalog_id: str, **kwargs - ) -> stac_types.Catalog: + async def get_catalog(self, catalog_id: str, **kwargs) -> stac_types.Catalog: """Get catalog by id. Called with `GET /catalogs/{catalog_id}`. diff --git a/stac_fastapi/types/stac_fastapi/types/links.py b/stac_fastapi/types/stac_fastapi/types/links.py index ee8ebd9a6..4cb8ed21a 100644 --- a/stac_fastapi/types/stac_fastapi/types/links.py +++ b/stac_fastapi/types/stac_fastapi/types/links.py @@ -12,7 +12,7 @@ # classes defined below INFERRED_LINK_RELS = ["self", "item", "parent", "collection", "root", "items"] # Attempt to maintain "root" links to later determine relationships to catalogues -#INFERRED_LINK_RELS = ["self", "item", "parent", "collection", "items"] +# INFERRED_LINK_RELS = ["self", "item", "parent", "collection", "items"] def filter_links(links: List[Dict]) -> List[Dict]: diff --git a/stac_fastapi/types/stac_fastapi/types/stac.py b/stac_fastapi/types/stac_fastapi/types/stac.py index d8c868acb..21895ef6b 100644 --- a/stac_fastapi/types/stac_fastapi/types/stac.py +++ b/stac_fastapi/types/stac_fastapi/types/stac.py @@ -105,9 +105,7 @@ class Catalogs(TypedDict, total=False): class CatalogsAndCollections(TypedDict, total=False): - """All catalogues and collections endpoint. - - """ + """All catalogues and collections endpoint.""" catalogs_and_collections: List[Union[Catalog, Collection]] links: List[Dict[str, Any]] From d2b2863c45a3f583c1a2db2658893b598356d0ae Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Wed, 15 May 2024 10:51:56 +0100 Subject: [PATCH 17/21] Corrected catalogs endpoint spelling --- stac_fastapi/api/stac_fastapi/api/app.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/stac_fastapi/api/stac_fastapi/api/app.py b/stac_fastapi/api/stac_fastapi/api/app.py index 654a3deb1..f9834c82d 100644 --- a/stac_fastapi/api/stac_fastapi/api/app.py +++ b/stac_fastapi/api/stac_fastapi/api/app.py @@ -271,7 +271,7 @@ def register_get_catalogs(self): ) def register_get_catalog_collections(self): - """Register get catalogs endpoint (GET /catalogs). + """Register get catalogs collections endpoint (GET /catalogs/{catalog_id}/collections). Returns: None @@ -292,15 +292,14 @@ def register_get_catalog_collections(self): ) def register_get_collection(self): - """Register get collection endpoint - (GET /catalogues/{catalog_id}/collection/{collection_id}). + """Register get collection endpoint (GET /catalogs/{catalog_id}/collection/{collection_id}). Returns: None """ self.router.add_api_route( name="Get Collection", - path="/catalogues/{catalog_id}/collections/{collection_id}", + path="/catalogs/{catalog_id}/collections/{collection_id}", response_model=Collection if self.settings.enable_response_models else None, response_class=self.response_class, response_model_exclude_unset=True, From 61118e4ed47d160c4e83af243dca5c0b864ac12c Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Wed, 15 May 2024 10:52:03 +0100 Subject: [PATCH 18/21] Corrected class name --- .../extensions/stac_fastapi/extensions/core/transaction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py index a77fe5858..c6e5ecc3e 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py @@ -39,7 +39,7 @@ class PutCollection(CollectionUri): @attr.s -class PostCollection(CatalogUri): +class PostNewCollection(CatalogUri): """Create Collection.""" collection: Union[stac_types.Collection] = attr.ib(default=Body(None)) @@ -134,7 +134,7 @@ def register_create_collection(self): response_model_exclude_none=True, methods=["POST"], endpoint=create_async_endpoint( - self.client.create_collection, PostCollection + self.client.create_collection, PostNewCollection ), ) From d8700c854cb0c3eccde85d4ced26fee36e36c73f Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Wed, 15 May 2024 10:52:56 +0100 Subject: [PATCH 19/21] Corrected link handling --- .../types/stac_fastapi/types/links.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/links.py b/stac_fastapi/types/stac_fastapi/types/links.py index 4cb8ed21a..426ce1189 100644 --- a/stac_fastapi/types/stac_fastapi/types/links.py +++ b/stac_fastapi/types/stac_fastapi/types/links.py @@ -36,6 +36,7 @@ def resolve_links(links: list, base_url: str) -> List[Dict]: class BaseLinks: """Create inferred links common to collections and items.""" + catalog_id: str = attr.ib() collection_id: str = attr.ib() base_url: str = attr.ib() @@ -53,7 +54,10 @@ def self(self) -> Dict[str, Any]: return dict( rel=Relations.self, type=MimeTypes.json, - href=urljoin(self.base_url, f"collections/{self.collection_id}"), + href=urljoin( + self.base_url, + f"catalogs/{self.catalog_id}/collections/{self.collection_id}", + ), ) def parent(self) -> Dict[str, Any]: @@ -65,7 +69,10 @@ def items(self) -> Dict[str, Any]: return dict( rel="items", type=MimeTypes.geojson, - href=urljoin(self.base_url, f"collections/{self.collection_id}/items"), + href=urljoin( + self.base_url, + f"catalogs/{self.catalog_id}/collections/{self.collection_id}/items", + ), ) def create_links(self) -> List[Dict[str, Any]]: @@ -127,7 +134,7 @@ def self(self) -> Dict[str, Any]: type=MimeTypes.geojson, href=urljoin( self.base_url, - f"collections/{self.collection_id}/items/{self.item_id}", + f"catalogs/{self.catalog_id}/collections/{self.collection_id}/items/{self.item_id}", ), ) @@ -136,7 +143,10 @@ def parent(self) -> Dict[str, Any]: return dict( rel=Relations.parent, type=MimeTypes.json, - href=urljoin(self.base_url, f"collections/{self.collection_id}"), + href=urljoin( + self.base_url, + f"catalogs/{self.catalog_id}/collections/{self.collection_id}", + ), ) def collection(self) -> Dict[str, Any]: @@ -144,7 +154,10 @@ def collection(self) -> Dict[str, Any]: return dict( rel=Relations.collection, type=MimeTypes.json, - href=urljoin(self.base_url, f"collections/{self.collection_id}"), + href=urljoin( + self.base_url, + f"catalogs/{self.catalog_id}/collections/{self.collection_id}", + ), ) def create_links(self) -> List[Dict[str, Any]]: From 43005fd9bdb793e9fe62aa5d511327443f819a02 Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Wed, 15 May 2024 10:53:06 +0100 Subject: [PATCH 20/21] Removed old link comment --- stac_fastapi/types/stac_fastapi/types/stac.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stac_fastapi/types/stac_fastapi/types/stac.py b/stac_fastapi/types/stac_fastapi/types/stac.py index 21895ef6b..aaaa0b4ca 100644 --- a/stac_fastapi/types/stac_fastapi/types/stac.py +++ b/stac_fastapi/types/stac_fastapi/types/stac.py @@ -105,7 +105,10 @@ class Catalogs(TypedDict, total=False): class CatalogsAndCollections(TypedDict, total=False): - """All catalogues and collections endpoint.""" + """All catalogues and collections endpoint. + + https://github.com/radiantearth/stac-api-spec/tree/master/collections + """ catalogs_and_collections: List[Union[Catalog, Collection]] links: List[Dict[str, Any]] From bcd5c7219d65f22883aa4be536dee53368d844e2 Mon Sep 17 00:00:00 2001 From: tjellicoe Date: Wed, 15 May 2024 13:46:34 +0100 Subject: [PATCH 21/21] Corrected endpoint path for create collection --- .../extensions/stac_fastapi/extensions/core/transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py index c6e5ecc3e..346984411 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py @@ -127,7 +127,7 @@ def register_create_collection(self): """Register create collection endpoint (POST /catalogs/{catalog_id}/collections).""" self.router.add_api_route( name="Create Collection", - path="/catalogs/{catalog_id}/collections/", + path="/catalogs/{catalog_id}/collections", response_model=Collection if self.settings.enable_response_models else None, response_class=self.response_class, response_model_exclude_unset=True,