Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow default route dependencies #705

Merged
merged 6 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 29 additions & 17 deletions 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,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]
jonhealy1 marked this conversation as resolved.
Show resolved Hide resolved

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"):
rhysrevans3 marked this conversation as resolved.
Show resolved Hide resolved
# 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)
290 changes: 278 additions & 12 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,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):
Expand Down
Loading