From c2c9b5279427af9383d702f02cd26631de306ea9 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Wed, 10 Aug 2022 22:21:55 -0400 Subject: [PATCH 1/5] Add handle_ prefix to any route handler methods --- stac_fastapi/api/app.py | 16 ++--- stac_fastapi/extensions/core/transaction.py | 12 ++-- .../third_party/bulk_transactions.py | 4 +- stac_fastapi/types/core.py | 60 +++++++++---------- tests/api/test_api.py | 24 ++++---- 5 files changed, 58 insertions(+), 58 deletions(-) diff --git a/stac_fastapi/api/app.py b/stac_fastapi/api/app.py index d18844e5c..24b9e4913 100644 --- a/stac_fastapi/api/app.py +++ b/stac_fastapi/api/app.py @@ -143,7 +143,7 @@ def register_landing_page(self): response_model_exclude_none=True, methods=["GET"], endpoint=self._create_endpoint( - self.client.landing_page, EmptyRequest, self.response_class + self.client.handle_landing_page, EmptyRequest, self.response_class ), ) @@ -164,7 +164,7 @@ def register_conformance_classes(self): response_model_exclude_none=True, methods=["GET"], endpoint=self._create_endpoint( - self.client.conformance, EmptyRequest, self.response_class + self.client.handle_conformance, EmptyRequest, self.response_class ), ) @@ -183,7 +183,7 @@ def register_get_item(self): response_model_exclude_none=True, methods=["GET"], endpoint=self._create_endpoint( - self.client.get_item, ItemUri, self.response_class + self.client.handle_get_item, ItemUri, self.response_class ), ) @@ -205,7 +205,7 @@ def register_post_search(self): response_model_exclude_none=True, methods=["POST"], endpoint=self._create_endpoint( - self.client.post_search, self.search_post_request_model, GeoJSONResponse + self.client.handle_post_search, self.search_post_request_model, GeoJSONResponse ), ) @@ -227,7 +227,7 @@ def register_get_search(self): response_model_exclude_none=True, methods=["GET"], endpoint=self._create_endpoint( - self.client.get_search, self.search_get_request_model, GeoJSONResponse + self.client.handle_get_search, self.search_get_request_model, GeoJSONResponse ), ) @@ -248,7 +248,7 @@ def register_get_collections(self): response_model_exclude_none=True, methods=["GET"], endpoint=self._create_endpoint( - self.client.all_collections, EmptyRequest, self.response_class + self.client.handle_all_collections, EmptyRequest, self.response_class ), ) @@ -267,7 +267,7 @@ def register_get_collection(self): response_model_exclude_none=True, methods=["GET"], endpoint=self._create_endpoint( - self.client.get_collection, CollectionUri, self.response_class + self.client.handle_get_collection, CollectionUri, self.response_class ), ) @@ -298,7 +298,7 @@ def register_get_item_collection(self): response_model_exclude_none=True, methods=["GET"], endpoint=self._create_endpoint( - self.client.item_collection, request_model, self.response_class + self.client.handle_collection_items, request_model, self.response_class ), ) diff --git a/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/core/transaction.py index 5967e7128..b93cccd14 100644 --- a/stac_fastapi/extensions/core/transaction.py +++ b/stac_fastapi/extensions/core/transaction.py @@ -91,7 +91,7 @@ def register_create_item(self): response_model_exclude_unset=True, response_model_exclude_none=True, methods=["POST"], - endpoint=self._create_endpoint(self.client.create_item, PostItem), + endpoint=self._create_endpoint(self.client.handle_create_item, PostItem), ) def register_update_item(self): @@ -104,7 +104,7 @@ def register_update_item(self): response_model_exclude_unset=True, response_model_exclude_none=True, methods=["PUT"], - endpoint=self._create_endpoint(self.client.update_item, PutItem), + endpoint=self._create_endpoint(self.client.handle_update_item, PutItem), ) def register_delete_item(self): @@ -117,7 +117,7 @@ def register_delete_item(self): response_model_exclude_unset=True, response_model_exclude_none=True, methods=["DELETE"], - endpoint=self._create_endpoint(self.client.delete_item, ItemUri), + endpoint=self._create_endpoint(self.client.handle_delete_item, ItemUri), ) def register_create_collection(self): @@ -131,7 +131,7 @@ def register_create_collection(self): response_model_exclude_none=True, methods=["POST"], endpoint=self._create_endpoint( - self.client.create_collection, stac_types.Collection + self.client.handle_create_collection, stac_types.Collection ), ) @@ -146,7 +146,7 @@ def register_update_collection(self): response_model_exclude_none=True, methods=["PUT"], endpoint=self._create_endpoint( - self.client.update_collection, stac_types.Collection + self.client.handle_update_collection, stac_types.Collection ), ) @@ -161,7 +161,7 @@ def register_delete_collection(self): response_model_exclude_none=True, methods=["DELETE"], endpoint=self._create_endpoint( - self.client.delete_collection, CollectionUri + self.client.handle_delete_collection, CollectionUri ), ) diff --git a/stac_fastapi/extensions/third_party/bulk_transactions.py b/stac_fastapi/extensions/third_party/bulk_transactions.py index 3fe25c9d1..1f66075d8 100644 --- a/stac_fastapi/extensions/third_party/bulk_transactions.py +++ b/stac_fastapi/extensions/third_party/bulk_transactions.py @@ -36,7 +36,7 @@ def _chunks(lst, n): yield lst[i : i + n] @abc.abstractmethod - def bulk_item_insert( + def handle_bulk_item_insert( self, items: Items, chunk_size: Optional[int] = None, **kwargs ) -> str: """Bulk creation of items. @@ -125,7 +125,7 @@ def register(self, app: FastAPI) -> None: response_model_exclude_none=True, methods=["POST"], endpoint=self._create_endpoint( - self.client.bulk_item_insert, items_request_model + self.client.handle_bulk_item_insert, items_request_model ), ) app.include_router(router, tags=["Bulk Transaction Extension"]) diff --git a/stac_fastapi/types/core.py b/stac_fastapi/types/core.py index bce7ca2a2..f3c898692 100644 --- a/stac_fastapi/types/core.py +++ b/stac_fastapi/types/core.py @@ -27,7 +27,7 @@ class BaseTransactionsClient(abc.ABC): """Defines a pattern for implementing the STAC API Transaction Extension.""" @abc.abstractmethod - def create_item( + def handle_create_item( self, collection_id: str, item: stac_types.Item, **kwargs ) -> Optional[Union[stac_types.Item, Response]]: """Create a new item. @@ -45,7 +45,7 @@ def create_item( ... @abc.abstractmethod - def update_item( + def handle_update_item( self, 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. @@ -64,7 +64,7 @@ def update_item( ... @abc.abstractmethod - def delete_item( + def handle_delete_item( self, item_id: str, collection_id: str, **kwargs ) -> Optional[Union[stac_types.Item, Response]]: """Delete an item from a collection. @@ -81,7 +81,7 @@ def delete_item( ... @abc.abstractmethod - def create_collection( + def handle_create_collection( self, collection: stac_types.Collection, **kwargs ) -> Optional[Union[stac_types.Collection, Response]]: """Create a new collection. @@ -97,7 +97,7 @@ def create_collection( ... @abc.abstractmethod - def update_collection( + def handle_update_collection( self, collection: stac_types.Collection, **kwargs ) -> Optional[Union[stac_types.Collection, Response]]: """Perform a complete update on an existing collection. @@ -116,7 +116,7 @@ def update_collection( ... @abc.abstractmethod - def delete_collection( + def handle_delete_collection( self, collection_id: str, **kwargs ) -> Optional[Union[stac_types.Collection, Response]]: """Delete a collection. @@ -137,7 +137,7 @@ class AsyncBaseTransactionsClient(abc.ABC): """Defines a pattern for implementing the STAC transaction extension.""" @abc.abstractmethod - async def create_item( + async def handle_create_item( self, collection_id: str, item: stac_types.Item, **kwargs ) -> Optional[Union[stac_types.Item, Response]]: """Create a new item. @@ -154,7 +154,7 @@ async def create_item( ... @abc.abstractmethod - async def update_item( + async def handle_update_item( self, 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. @@ -172,7 +172,7 @@ async def update_item( ... @abc.abstractmethod - async def delete_item( + async def handle_delete_item( self, item_id: str, collection_id: str, **kwargs ) -> Optional[Union[stac_types.Item, Response]]: """Delete an item from a collection. @@ -189,7 +189,7 @@ async def delete_item( ... @abc.abstractmethod - async def create_collection( + async def handle_create_collection( self, collection: stac_types.Collection, **kwargs ) -> Optional[Union[stac_types.Collection, Response]]: """Create a new collection. @@ -205,7 +205,7 @@ async def create_collection( ... @abc.abstractmethod - async def update_collection( + async def handle_update_collection( self, collection: stac_types.Collection, **kwargs ) -> Optional[Union[stac_types.Collection, Response]]: """Perform a complete update on an existing collection. @@ -223,7 +223,7 @@ async def update_collection( ... @abc.abstractmethod - async def delete_collection( + async def handle_delete_collection( self, collection_id: str, **kwargs ) -> Optional[Union[stac_types.Collection, Response]]: """Delete a collection. @@ -341,7 +341,7 @@ def list_conformance_classes(self): return base_conformance - def landing_page(self, **kwargs) -> stac_types.LandingPage: + def handle_landing_page(self, **kwargs) -> stac_types.LandingPage: """Landing page. Called with `GET /`. @@ -361,7 +361,7 @@ def landing_page(self, **kwargs) -> stac_types.LandingPage: ) # Add Collections links - collections = self.all_collections(request=kwargs["request"]) + collections = self.handle_all_collections(request=kwargs["request"]) for collection in collections["collections"]: landing_page["links"].append( { @@ -398,7 +398,7 @@ def landing_page(self, **kwargs) -> stac_types.LandingPage: return landing_page - def conformance(self, **kwargs) -> stac_types.Conformance: + def handle_conformance(self, **kwargs) -> stac_types.Conformance: """Conformance classes. Called with `GET /conformance`. @@ -409,7 +409,7 @@ def conformance(self, **kwargs) -> stac_types.Conformance: return Conformance(conformsTo=self.conformance_classes()) @abc.abstractmethod - def post_search( + def handle_post_search( self, search_request: BaseSearchPostRequest, **kwargs ) -> stac_types.ItemCollection: """Cross catalog search (POST). @@ -425,7 +425,7 @@ def post_search( ... @abc.abstractmethod - def get_search( + def handle_get_search( self, collections: Optional[List[str]] = None, ids: Optional[List[str]] = None, @@ -448,7 +448,7 @@ def get_search( ... @abc.abstractmethod - def get_item(self, item_id: str, collection_id: str, **kwargs) -> stac_types.Item: + def handle_get_item(self, item_id: str, collection_id: str, **kwargs) -> stac_types.Item: """Get item by id. Called with `GET /collections/{collection_id}/items/{item_id}`. @@ -463,7 +463,7 @@ def get_item(self, item_id: str, collection_id: str, **kwargs) -> stac_types.Ite ... @abc.abstractmethod - def all_collections(self, **kwargs) -> stac_types.Collections: + def handle_all_collections(self, **kwargs) -> stac_types.Collections: """Get all available collections. Called with `GET /collections`. @@ -474,7 +474,7 @@ def all_collections(self, **kwargs) -> stac_types.Collections: ... @abc.abstractmethod - def get_collection(self, collection_id: str, **kwargs) -> stac_types.Collection: + def handle_get_collection(self, collection_id: str, **kwargs) -> stac_types.Collection: """Get collection by id. Called with `GET /collections/{collection_id}`. @@ -488,7 +488,7 @@ def get_collection(self, collection_id: str, **kwargs) -> stac_types.Collection: ... @abc.abstractmethod - def item_collection( + def handle_collection_items( self, collection_id: str, limit: int = 10, token: str = None, **kwargs ) -> stac_types.ItemCollection: """Get all items from a specific collection. @@ -534,7 +534,7 @@ def extension_is_enabled(self, extension: str) -> bool: """Check if an api extension is enabled.""" return any([type(ext).__name__ == extension for ext in self.extensions]) - async def landing_page(self, **kwargs) -> stac_types.LandingPage: + async def handle_landing_page(self, **kwargs) -> stac_types.LandingPage: """Landing page. Called with `GET /`. @@ -552,7 +552,7 @@ async def landing_page(self, **kwargs) -> stac_types.LandingPage: conformance_classes=self.conformance_classes(), extension_schemas=extension_schemas, ) - collections = await self.all_collections(request=kwargs["request"]) + collections = await self.handle_all_collections(request=kwargs["request"]) for collection in collections["collections"]: landing_page["links"].append( { @@ -589,7 +589,7 @@ async def landing_page(self, **kwargs) -> stac_types.LandingPage: return landing_page - async def conformance(self, **kwargs) -> stac_types.Conformance: + async def handle_conformance(self, **kwargs) -> stac_types.Conformance: """Conformance classes. Called with `GET /conformance`. @@ -600,7 +600,7 @@ async def conformance(self, **kwargs) -> stac_types.Conformance: return Conformance(conformsTo=self.conformance_classes()) @abc.abstractmethod - async def post_search( + async def handle_post_search( self, search_request: BaseSearchPostRequest, **kwargs ) -> stac_types.ItemCollection: """Cross catalog search (POST). @@ -616,7 +616,7 @@ async def post_search( ... @abc.abstractmethod - async def get_search( + async def handle_get_search( self, collections: Optional[List[str]] = None, ids: Optional[List[str]] = None, @@ -639,7 +639,7 @@ async def get_search( ... @abc.abstractmethod - async def get_item( + async def handle_get_item( self, item_id: str, collection_id: str, **kwargs ) -> stac_types.Item: """Get item by id. @@ -656,7 +656,7 @@ async def get_item( ... @abc.abstractmethod - async def all_collections(self, **kwargs) -> stac_types.Collections: + async def handle_all_collections(self, **kwargs) -> stac_types.Collections: """Get all available collections. Called with `GET /collections`. @@ -667,7 +667,7 @@ async def all_collections(self, **kwargs) -> stac_types.Collections: ... @abc.abstractmethod - async def get_collection( + async def handle_get_collection( self, collection_id: str, **kwargs ) -> stac_types.Collection: """Get collection by id. @@ -683,7 +683,7 @@ async def get_collection( ... @abc.abstractmethod - async def item_collection( + async def handle_collection_items( self, collection_id: str, limit: int = 10, token: str = None, **kwargs ) -> stac_types.ItemCollection: """Get all items from a specific collection. diff --git a/tests/api/test_api.py b/tests/api/test_api.py index ab5a304d4..4d2b99ca9 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -77,44 +77,44 @@ def test_add_route_dependencies_after_building_api(self): class DummyCoreClient(core.BaseCoreClient): - def all_collections(self, *args, **kwargs): + def handle_all_collections(self, *args, **kwargs): ... - def get_collection(self, *args, **kwargs): + def handle_get_collection(self, *args, **kwargs): ... - def get_item(self, *args, **kwargs): + def handle_get_item(self, *args, **kwargs): ... - def get_search(self, *args, **kwargs): + def handle_get_search(self, *args, **kwargs): ... - def post_search(self, *args, **kwargs): + def handle_post_search(self, *args, **kwargs): ... - def item_collection(self, *args, **kwargs): + def handle_collection_items(self, *args, **kwargs): ... class DummyTransactionsClient(core.BaseTransactionsClient): """Defines a pattern for implementing the STAC transaction extension.""" - def create_item(self, *args, **kwargs): + def handle_create_item(self, *args, **kwargs): return "dummy response" - def update_item(self, *args, **kwargs): + def handle_update_item(self, *args, **kwargs): return "dummy response" - def delete_item(self, *args, **kwargs): + def handle_delete_item(self, *args, **kwargs): return "dummy response" - def create_collection(self, *args, **kwargs): + def handle_create_collection(self, *args, **kwargs): return "dummy response" - def update_collection(self, *args, **kwargs): + def handle_update_collection(self, *args, **kwargs): return "dummy response" - def delete_collection(self, *args, **kwargs): + def handle_delete_collection(self, *args, **kwargs): return "dummy response" From f55464cdc485ac95d6dcfdac067a5e9f6ee4fd4a Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Wed, 10 Aug 2022 23:49:17 -0400 Subject: [PATCH 2/5] Separate handler from backend logic for GET /collections --- stac_fastapi/api/app.py | 8 +- .../third_party/bulk_transactions.py | 2 +- stac_fastapi/types/core.py | 107 ++++++++++++++++-- stac_fastapi/types/links.py | 36 +++++- 4 files changed, 141 insertions(+), 12 deletions(-) diff --git a/stac_fastapi/api/app.py b/stac_fastapi/api/app.py index 24b9e4913..70ec259d1 100644 --- a/stac_fastapi/api/app.py +++ b/stac_fastapi/api/app.py @@ -205,7 +205,9 @@ def register_post_search(self): response_model_exclude_none=True, methods=["POST"], endpoint=self._create_endpoint( - self.client.handle_post_search, self.search_post_request_model, GeoJSONResponse + self.client.handle_post_search, + self.search_post_request_model, + GeoJSONResponse, ), ) @@ -227,7 +229,9 @@ def register_get_search(self): response_model_exclude_none=True, methods=["GET"], endpoint=self._create_endpoint( - self.client.handle_get_search, self.search_get_request_model, GeoJSONResponse + self.client.handle_get_search, + self.search_get_request_model, + GeoJSONResponse, ), ) diff --git a/stac_fastapi/extensions/third_party/bulk_transactions.py b/stac_fastapi/extensions/third_party/bulk_transactions.py index 1f66075d8..74e79168e 100644 --- a/stac_fastapi/extensions/third_party/bulk_transactions.py +++ b/stac_fastapi/extensions/third_party/bulk_transactions.py @@ -57,7 +57,7 @@ class AsyncBaseBulkTransactionsClient(abc.ABC): """BulkTransactionsClient.""" @abc.abstractmethod - async def bulk_item_insert(self, items: Items, **kwargs) -> str: + async def handle_bulk_item_insert(self, items: Items, **kwargs) -> str: """Bulk creation of items. Args: diff --git a/stac_fastapi/types/core.py b/stac_fastapi/types/core.py index f3c898692..8d8664215 100644 --- a/stac_fastapi/types/core.py +++ b/stac_fastapi/types/core.py @@ -14,6 +14,7 @@ from stac_fastapi.types import stac as stac_types from stac_fastapi.types.conformance import BASE_CONFORMANCE_CLASSES from stac_fastapi.types.extension import ApiExtension +from stac_fastapi.types.links import CollectionLinks from stac_fastapi.types.requests import get_base_url from stac_fastapi.types.search import BaseSearchPostRequest from stac_fastapi.types.stac import Conformance @@ -341,6 +342,17 @@ def list_conformance_classes(self): return base_conformance + @abc.abstractmethod + def list_all_collections(self) -> List[stac_types.Collection]: + """Return a list of all available collections. + + This method MUST be defined in the backend implementation. + + Returns: + List of STAC Collection-like dictionaries + """ + ... + def handle_landing_page(self, **kwargs) -> stac_types.LandingPage: """Landing page. @@ -361,8 +373,8 @@ def handle_landing_page(self, **kwargs) -> stac_types.LandingPage: ) # Add Collections links - collections = self.handle_all_collections(request=kwargs["request"]) - for collection in collections["collections"]: + collections = self.list_all_collections() + for collection in collections: landing_page["links"].append( { "rel": Relations.child.value, @@ -448,7 +460,9 @@ def handle_get_search( ... @abc.abstractmethod - def handle_get_item(self, item_id: str, collection_id: str, **kwargs) -> stac_types.Item: + def handle_get_item( + self, item_id: str, collection_id: str, **kwargs + ) -> stac_types.Item: """Get item by id. Called with `GET /collections/{collection_id}/items/{item_id}`. @@ -462,7 +476,6 @@ def handle_get_item(self, item_id: str, collection_id: str, **kwargs) -> stac_ty """ ... - @abc.abstractmethod def handle_all_collections(self, **kwargs) -> stac_types.Collections: """Get all available collections. @@ -471,10 +484,45 @@ def handle_all_collections(self, **kwargs) -> stac_types.Collections: Returns: A list of collections. """ - ... + request: Request = kwargs["request"] + base_url = get_base_url(request) + collections = self.list_all_collections() + linked_collections: List[stac_types.Collection] = [] + if collections is not None and len(collections) > 0: + for c in collections: + coll = stac_types.Collection(**c) + coll["links"] = CollectionLinks( + collection_id=coll["id"], request=request + ).get_links(extra_links=coll.get("links")) + + linked_collections.append(coll) + + links = [ + { + "rel": Relations.root.value, + "type": MimeTypes.json, + "href": base_url, + }, + { + "rel": Relations.parent.value, + "type": MimeTypes.json, + "href": base_url, + }, + { + "rel": Relations.self.value, + "type": MimeTypes.json, + "href": urljoin(base_url, "collections"), + }, + ] + collection_list = stac_types.Collections( + collections=linked_collections or [], links=links + ) + return collection_list @abc.abstractmethod - def handle_get_collection(self, collection_id: str, **kwargs) -> stac_types.Collection: + def handle_get_collection( + self, collection_id: str, **kwargs + ) -> stac_types.Collection: """Get collection by id. Called with `GET /collections/{collection_id}`. @@ -534,6 +582,17 @@ def extension_is_enabled(self, extension: str) -> bool: """Check if an api extension is enabled.""" return any([type(ext).__name__ == extension for ext in self.extensions]) + @abc.abstractmethod + async def list_all_collections(self) -> List[stac_types.Collection]: + """Return a list of all available collections. + + This method MUST be defined in the backend implementation. + + Returns: + List of STAC Collection-like dictionaries + """ + ... + async def handle_landing_page(self, **kwargs) -> stac_types.LandingPage: """Landing page. @@ -655,7 +714,6 @@ async def handle_get_item( """ ... - @abc.abstractmethod async def handle_all_collections(self, **kwargs) -> stac_types.Collections: """Get all available collections. @@ -664,7 +722,40 @@ async def handle_all_collections(self, **kwargs) -> stac_types.Collections: Returns: A list of collections. """ - ... + request: Request = kwargs["request"] + base_url = get_base_url(request) + collections = await self.list_all_collections(request) + linked_collections: List[stac_types.Collection] = [] + if collections is not None and len(collections) > 0: + for c in collections: + coll = stac_types.Collection(**c) + coll["links"] = CollectionLinks( + collection_id=coll["id"], base_url=base_url + ).get_links(extra_links=coll.get("links")) + + linked_collections.append(coll) + + links = [ + { + "rel": Relations.root.value, + "type": MimeTypes.json, + "href": base_url, + }, + { + "rel": Relations.parent.value, + "type": MimeTypes.json, + "href": base_url, + }, + { + "rel": Relations.self.value, + "type": MimeTypes.json, + "href": urljoin(base_url, "collections"), + }, + ] + collection_list = stac_types.Collections( + collections=linked_collections or [], links=links + ) + return collection_list @abc.abstractmethod async def handle_get_collection( diff --git a/stac_fastapi/types/links.py b/stac_fastapi/types/links.py index 0349984b1..3aa1225af 100644 --- a/stac_fastapi/types/links.py +++ b/stac_fastapi/types/links.py @@ -1,6 +1,6 @@ """link helpers.""" -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from urllib.parse import urljoin import attr @@ -36,6 +36,10 @@ def root(self) -> Dict[str, Any]: """Return the catalog root.""" return dict(rel=Relations.root, type=MimeTypes.json, href=self.base_url) + def resolve(self, url): + """Resolve url to the current request url.""" + return urljoin(str(self.base_url), str(url)) + @attr.s class CollectionLinks(BaseLinks): @@ -65,6 +69,36 @@ def create_links(self) -> List[Dict[str, Any]]: """Return all inferred links.""" return [self.self(), self.parent(), self.items(), self.root()] + def get_links( + self, extra_links: Optional[List[Dict[str, Any]]] = None + ) -> List[Dict[str, Any]]: + """ + Generate all the links. + + Get the links object for a stac resource by iterating through + available methods on this class that start with link_. + """ + # join passed in links with generated links + # and update relative paths + links = self.create_links() + + if extra_links: + # For extra links passed in, + # add links modified with a resolved href. + # Drop any links that are dynamically + # determined by the server (e.g. self, parent, etc.) + # Resolving the href allows for relative paths + # to be stored in pgstac and for the hrefs in the + # links of response STAC objects to be resolved + # to the request url. + links += [ + {**link, "href": self.resolve(link["href"])} + for link in extra_links + if link["rel"] not in INFERRED_LINK_RELS + ] + + return links + @attr.s class ItemLinks(BaseLinks): From a2e55bd3d17ff9c7f955d2aa3aaa52bda1a7f3f7 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Wed, 10 Aug 2022 23:53:34 -0400 Subject: [PATCH 3/5] Fix DummyCoreClient in tests --- tests/api/test_api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/api/test_api.py b/tests/api/test_api.py index 4d2b99ca9..de5edf6da 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -1,9 +1,11 @@ -from fastapi import Depends, HTTPException, security, status +from typing import List + +from fastapi import Depends, HTTPException, security, status, Request from starlette.testclient import TestClient from stac_fastapi.api.app import StacApi from stac_fastapi.extensions.core import TokenPaginationExtension, TransactionExtension -from stac_fastapi.types import config, core +from stac_fastapi.types import config, core, stac as stac_types class TestRouteDependencies: @@ -77,7 +79,7 @@ def test_add_route_dependencies_after_building_api(self): class DummyCoreClient(core.BaseCoreClient): - def handle_all_collections(self, *args, **kwargs): + def list_all_collections(self, request: Request) -> List[stac_types.Collection]: ... def handle_get_collection(self, *args, **kwargs): From 64cab60ad6836b8a0c994f1faec89d65ab018c85 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 11 Aug 2022 10:50:35 -0400 Subject: [PATCH 4/5] Do not rebuild image on change in tests --- .dockerignore | 1 + docker-compose.yml | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.dockerignore b/.dockerignore index 5173d0657..bdd66ba7a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ +tests docker-compose.yml **/__pycache__ *.pyc diff --git a/docker-compose.yml b/docker-compose.yml index e41968aaa..fc716165c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,9 +9,9 @@ services: dockerfile: Dockerfile platform: linux/amd64 volumes: - - ./stac_fastapi:/app/stac_fastapi - - ./scripts:/app/scripts - - ./tests:/app/tests + - ./stac_fastapi:/opt/src/stac_fastapi + - ./scripts:/opt/src/scripts + - ./tests:/opt/src/tests command: - bash - -c From c4b738ea3bf8f50473db35b7b505edf1a46d2b06 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 11 Aug 2022 10:51:22 -0400 Subject: [PATCH 5/5] Separate handler from backend logic for GET /collections/{collection_id} --- stac_fastapi/types/core.py | 36 +++++++++++++++++++++++++++++++----- tests/api/test_api.py | 5 +++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/stac_fastapi/types/core.py b/stac_fastapi/types/core.py index 8d8664215..fc932f9b1 100644 --- a/stac_fastapi/types/core.py +++ b/stac_fastapi/types/core.py @@ -11,13 +11,14 @@ from stac_pydantic.version import STAC_VERSION from starlette.responses import Response +from stac_fastapi.api.errors import NotFoundError from stac_fastapi.types import stac as stac_types from stac_fastapi.types.conformance import BASE_CONFORMANCE_CLASSES from stac_fastapi.types.extension import ApiExtension from stac_fastapi.types.links import CollectionLinks from stac_fastapi.types.requests import get_base_url from stac_fastapi.types.search import BaseSearchPostRequest -from stac_fastapi.types.stac import Conformance +from stac_fastapi.types.stac import Collection, Conformance NumType = Union[float, int] StacType = Dict[str, Any] @@ -583,7 +584,9 @@ def extension_is_enabled(self, extension: str) -> bool: return any([type(ext).__name__ == extension for ext in self.extensions]) @abc.abstractmethod - async def list_all_collections(self) -> List[stac_types.Collection]: + async def fetch_all_collections( + self, request: Request + ) -> List[stac_types.Collection]: """Return a list of all available collections. This method MUST be defined in the backend implementation. @@ -593,6 +596,19 @@ async def list_all_collections(self) -> List[stac_types.Collection]: """ ... + @abc.abstractclassmethod + async def fetch_collection( + self, collection_id: str, request: Request + ) -> Optional[stac_types.Collection]: + """Return the STAC Collection with the given ID, or `None` if the Collection does not exist. + + This method MUST be defined in the backend implementation. + + Returns: + Dictionary representing the STAC Collection, or `None` if no Collection with the given ID is found + """ + ... + async def handle_landing_page(self, **kwargs) -> stac_types.LandingPage: """Landing page. @@ -724,7 +740,7 @@ async def handle_all_collections(self, **kwargs) -> stac_types.Collections: """ request: Request = kwargs["request"] base_url = get_base_url(request) - collections = await self.list_all_collections(request) + collections = await self.fetch_all_collections(request) linked_collections: List[stac_types.Collection] = [] if collections is not None and len(collections) > 0: for c in collections: @@ -757,7 +773,6 @@ async def handle_all_collections(self, **kwargs) -> stac_types.Collections: ) return collection_list - @abc.abstractmethod async def handle_get_collection( self, collection_id: str, **kwargs ) -> stac_types.Collection: @@ -771,7 +786,18 @@ async def handle_get_collection( Returns: Collection. """ - ... + request: Request = kwargs["request"] + base_url = get_base_url(request) + + collection = await self.fetch_collection(collection_id, request) + if collection is None: + raise NotFoundError(f"Collection {collection_id} does not exist.") + + collection["links"] = CollectionLinks( + collection_id=collection_id, base_url=base_url + ).get_links(extra_links=collection.get("links")) + + return Collection(**collection) @abc.abstractmethod async def handle_collection_items( diff --git a/tests/api/test_api.py b/tests/api/test_api.py index de5edf6da..36e6495ba 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -1,11 +1,12 @@ from typing import List -from fastapi import Depends, HTTPException, security, status, Request +from fastapi import Depends, HTTPException, Request, security, status from starlette.testclient import TestClient from stac_fastapi.api.app import StacApi from stac_fastapi.extensions.core import TokenPaginationExtension, TransactionExtension -from stac_fastapi.types import config, core, stac as stac_types +from stac_fastapi.types import config, core +from stac_fastapi.types import stac as stac_types class TestRouteDependencies: