From 1e68f7e3d51497de0c387d74dbfa49d0a090c76c Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Mon, 10 Jun 2024 11:03:54 +0100 Subject: [PATCH] Allowing for default route dependencies. --- CHANGES.md | 1 + stac_fastapi/api/stac_fastapi/api/routes.py | 46 ++-- stac_fastapi/api/tests/test_api.py | 290 +++++++++++++++++++- 3 files changed, 308 insertions(+), 29 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a75f1da8a..666bc173d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ ### Changed +* Added option for default route dependencies `*` can be used for `path` or `method` to match all allowed route. * moved `AsyncBaseFiltersClient` and `BaseFiltersClient` classes in `stac_fastapi.extensions.core.filter.client` submodule ([#704](https://github.com/stac-utils/stac-fastapi/pull/704)) ## [3.0.0a2] - 2024-05-31 diff --git a/stac_fastapi/api/stac_fastapi/api/routes.py b/stac_fastapi/api/stac_fastapi/api/routes.py index df4a136eb..ec5c0e057 100644 --- a/stac_fastapi/api/stac_fastapi/api/routes.py +++ b/stac_fastapi/api/stac_fastapi/api/routes.py @@ -1,5 +1,6 @@ """Route factories.""" +import copy import functools import inspect import warnings @@ -100,28 +101,39 @@ def add_route_dependencies( Allows a developer to add dependencies to a route after the route has been defined. + "*" can be used for path or method to match all allowed routes. + Returns: None """ for scope in scopes: + _scope = copy.deepcopy(scope) for route in routes: - match, _ = route.matches({"type": "http", **scope}) + + if scope["path"] == "*": + _scope["path"] = route.path + + if scope["method"] == "*": + _scope["method"] = list(route.methods)[0] + + match, _ = route.matches({"type": "http", **_scope}) if match != Match.FULL: continue - # Mimicking how APIRoute handles dependencies: - # https://github.com/tiangolo/fastapi/blob/1760da0efa55585c19835d81afa8ca386036c325/fastapi/routing.py#L408-L412 - for depends in dependencies[::-1]: - route.dependant.dependencies.insert( - 0, - get_parameterless_sub_dependant( - depends=depends, path=route.path_format - ), - ) - - # Register dependencies directly on route so that they aren't ignored if - # the routes are later associated with an app (e.g. - # app.include_router(router)) - # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/applications.py#L337-L360 - # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/routing.py#L677-L678 - route.dependencies.extend(dependencies) + if hasattr(route, "dependant"): + # Mimicking how APIRoute handles dependencies: + # https://github.com/tiangolo/fastapi/blob/1760da0efa55585c19835d81afa8ca386036c325/fastapi/routing.py#L408-L412 + for depends in dependencies[::-1]: + route.dependant.dependencies.insert( + 0, + get_parameterless_sub_dependant( + depends=depends, path=route.path_format + ), + ) + + # Register dependencies directly on route so that they aren't ignored if + # the routes are later associated with an app (e.g. + # app.include_router(router)) + # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/applications.py#L337-L360 + # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/routing.py#L677-L678 + route.dependencies.extend(dependencies) diff --git a/stac_fastapi/api/tests/test_api.py b/stac_fastapi/api/tests/test_api.py index f2d51f1db..9ed2aec9c 100644 --- a/stac_fastapi/api/tests/test_api.py +++ b/stac_fastapi/api/tests/test_api.py @@ -49,6 +49,24 @@ def _assert_dependency_applied(api, routes): ), "Authenticated requests should be accepted" assert response.json() == "dummy response" + @staticmethod + def _assert_dependency_not_applied(api, routes): + with TestClient(api.app) as client: + for route in routes: + path = route["path"].format( + collectionId="test_collection", itemId="test_item" + ) + response = client.request( + method=route["method"].lower(), + url=path, + content=route["payload"], + headers={"content-type": "application/json"}, + ) + assert ( + 200 <= response.status_code < 300 + ), "Authenticated requests should be accepted" + assert response.json() == "dummy response" + def test_openapi_content_type(self): api = self._build_api() with TestClient(api.app) as client: @@ -116,25 +134,273 @@ def test_add_route_dependencies_after_building_api(self, collection, item): api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)]) self._assert_dependency_applied(api, routes) + def test_build_api_with_default_route_dependencies(self, collection, item): + routes = [{"path": "*", "method": "*"}] + test_routes = [ + {"path": "/collections", "method": "POST", "payload": collection}, + { + "path": "/collections/{collectionId}", + "method": "PUT", + "payload": collection, + }, + {"path": "/collections/{collectionId}", "method": "DELETE", "payload": ""}, + { + "path": "/collections/{collectionId}/items", + "method": "POST", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "PUT", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "DELETE", + "payload": "", + }, + ] + dependencies = [Depends(must_be_bob)] + api = self._build_api(route_dependencies=[(routes, dependencies)]) + self._assert_dependency_applied(api, test_routes) + + def test_build_api_with_default_path_route_dependencies(self, collection, item): + routes = [{"path": "*", "method": "POST"}] + test_routes = [ + { + "path": "/collections", + "method": "POST", + "payload": collection, + }, + { + "path": "/collections/{collectionId}/items", + "method": "POST", + "payload": item, + }, + ] + test_not_routes = [ + { + "path": "/collections/{collectionId}", + "method": "PUT", + "payload": collection, + }, + { + "path": "/collections/{collectionId}", + "method": "DELETE", + "payload": "", + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "PUT", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "DELETE", + "payload": "", + }, + ] + dependencies = [Depends(must_be_bob)] + api = self._build_api(route_dependencies=[(routes, dependencies)]) + self._assert_dependency_applied(api, test_routes) + self._assert_dependency_not_applied(api, test_not_routes) + + def test_build_api_with_default_method_route_dependencies(self, collection, item): + routes = [ + { + "path": "/collections/{collectionId}", + "method": "*", + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "*", + }, + ] + test_routes = [ + { + "path": "/collections/{collectionId}", + "method": "PUT", + "payload": collection, + }, + { + "path": "/collections/{collectionId}", + "method": "DELETE", + "payload": "", + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "PUT", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "DELETE", + "payload": "", + }, + ] + test_not_routes = [ + { + "path": "/collections", + "method": "POST", + "payload": collection, + }, + { + "path": "/collections/{collectionId}/items", + "method": "POST", + "payload": item, + }, + ] + dependencies = [Depends(must_be_bob)] + api = self._build_api(route_dependencies=[(routes, dependencies)]) + self._assert_dependency_applied(api, test_routes) + self._assert_dependency_not_applied(api, test_not_routes) + + def test_add_default_route_dependencies_after_building_api(self, collection, item): + routes = [{"path": "*", "method": "*"}] + test_routes = [ + { + "path": "/collections", + "method": "POST", + "payload": collection, + }, + { + "path": "/collections/{collectionId}", + "method": "PUT", + "payload": collection, + }, + { + "path": "/collections/{collectionId}", + "method": "DELETE", + "payload": "", + }, + { + "path": "/collections/{collectionId}/items", + "method": "POST", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "PUT", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "DELETE", + "payload": "", + }, + ] + api = self._build_api() + api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)]) + self._assert_dependency_applied(api, test_routes) + + def test_add_default_path_route_dependencies_after_building_api( + self, collection, item + ): + routes = [{"path": "*", "method": "POST"}] + test_routes = [ + { + "path": "/collections", + "method": "POST", + "payload": collection, + }, + { + "path": "/collections/{collectionId}/items", + "method": "POST", + "payload": item, + }, + ] + test_not_routes = [ + { + "path": "/collections/{collectionId}", + "method": "PUT", + "payload": collection, + }, + { + "path": "/collections/{collectionId}", + "method": "DELETE", + "payload": "", + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "PUT", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "DELETE", + "payload": "", + }, + ] + api = self._build_api() + api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)]) + self._assert_dependency_applied(api, test_routes) + self._assert_dependency_not_applied(api, test_not_routes) + + def test_add_default_method_route_dependencies_after_building_api( + self, collection, item + ): + routes = [ + { + "path": "/collections/{collectionId}", + "method": "*", + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "*", + }, + ] + test_routes = [ + { + "path": "/collections/{collectionId}", + "method": "PUT", + "payload": collection, + }, + { + "path": "/collections/{collectionId}", + "method": "DELETE", + "payload": "", + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "PUT", + "payload": item, + }, + { + "path": "/collections/{collectionId}/items/{itemId}", + "method": "DELETE", + "payload": "", + }, + ] + test_not_routes = [ + { + "path": "/collections", + "method": "POST", + "payload": collection, + }, + { + "path": "/collections/{collectionId}/items", + "method": "POST", + "payload": item, + }, + ] + api = self._build_api() + api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)]) + self._assert_dependency_applied(api, test_routes) + self._assert_dependency_not_applied(api, test_not_routes) + 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):