Skip to content

Commit

Permalink
Allow default route dependencies (#705)
Browse files Browse the repository at this point in the history
* Allowing for default route dependencies.

* Running precommit hooks.

* Adding pull request to CHANGELOG.

* Update stac_fastapi/api/stac_fastapi/api/routes.py

Co-authored-by: Anthony Lukach <[email protected]>

* Fixing indenting.

---------

Co-authored-by: Jonathan Healy <[email protected]>
Co-authored-by: Anthony Lukach <[email protected]>
  • Loading branch information
3 people authored Jun 12, 2024
1 parent 8075fc9 commit 9a8ab84
Show file tree
Hide file tree
Showing 3 changed files with 288 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Changed

* Added option for default route dependencies `*` can be used for `path` or `method` to match all allowed route. ([#705](https://github.com/stac-utils/stac-fastapi/pull/705))
* 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
Expand Down
16 changes: 15 additions & 1 deletion stac_fastapi/api/stac_fastapi/api/routes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Route factories."""

import copy
import functools
import inspect
import warnings
Expand Down Expand Up @@ -100,15 +101,28 @@ 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

# Ignore paths without dependants, e.g. /api, /api.html, /docs/oauth2-redirect
if not hasattr(route, "dependant"):
continue

# Mimicking how APIRoute handles dependencies:
# https://github.com/tiangolo/fastapi/blob/1760da0efa55585c19835d81afa8ca386036c325/fastapi/routing.py#L408-L412
for depends in dependencies[::-1]:
Expand Down
272 changes: 272 additions & 0 deletions stac_fastapi/api/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -116,6 +134,260 @@ 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):
Expand Down

0 comments on commit 9a8ab84

Please sign in to comment.