Skip to content

Commit 9a8ab84

Browse files
rhysrevans3jonhealy1alukach
authored
Allow default route dependencies (#705)
* 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]>
1 parent 8075fc9 commit 9a8ab84

File tree

3 files changed

+288
-1
lines changed

3 files changed

+288
-1
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
### Changed
1010

11+
* 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))
1112
* moved `AsyncBaseFiltersClient` and `BaseFiltersClient` classes in `stac_fastapi.extensions.core.filter.client` submodule ([#704](https://github.com/stac-utils/stac-fastapi/pull/704))
1213

1314
## [3.0.0a2] - 2024-05-31

stac_fastapi/api/stac_fastapi/api/routes.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Route factories."""
22

3+
import copy
34
import functools
45
import inspect
56
import warnings
@@ -100,15 +101,28 @@ def add_route_dependencies(
100101
Allows a developer to add dependencies to a route after the route has been
101102
defined.
102103
104+
"*" can be used for path or method to match all allowed routes.
105+
103106
Returns:
104107
None
105108
"""
106109
for scope in scopes:
110+
_scope = copy.deepcopy(scope)
107111
for route in routes:
108-
match, _ = route.matches({"type": "http", **scope})
112+
if scope["path"] == "*":
113+
_scope["path"] = route.path
114+
115+
if scope["method"] == "*":
116+
_scope["method"] = list(route.methods)[0]
117+
118+
match, _ = route.matches({"type": "http", **_scope})
109119
if match != Match.FULL:
110120
continue
111121

122+
# Ignore paths without dependants, e.g. /api, /api.html, /docs/oauth2-redirect
123+
if not hasattr(route, "dependant"):
124+
continue
125+
112126
# Mimicking how APIRoute handles dependencies:
113127
# https://github.com/tiangolo/fastapi/blob/1760da0efa55585c19835d81afa8ca386036c325/fastapi/routing.py#L408-L412
114128
for depends in dependencies[::-1]:

stac_fastapi/api/tests/test_api.py

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,24 @@ def _assert_dependency_applied(api, routes):
4949
), "Authenticated requests should be accepted"
5050
assert response.json() == "dummy response"
5151

52+
@staticmethod
53+
def _assert_dependency_not_applied(api, routes):
54+
with TestClient(api.app) as client:
55+
for route in routes:
56+
path = route["path"].format(
57+
collectionId="test_collection", itemId="test_item"
58+
)
59+
response = client.request(
60+
method=route["method"].lower(),
61+
url=path,
62+
content=route["payload"],
63+
headers={"content-type": "application/json"},
64+
)
65+
assert (
66+
200 <= response.status_code < 300
67+
), "Authenticated requests should be accepted"
68+
assert response.json() == "dummy response"
69+
5270
def test_openapi_content_type(self):
5371
api = self._build_api()
5472
with TestClient(api.app) as client:
@@ -116,6 +134,260 @@ def test_add_route_dependencies_after_building_api(self, collection, item):
116134
api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)])
117135
self._assert_dependency_applied(api, routes)
118136

137+
def test_build_api_with_default_route_dependencies(self, collection, item):
138+
routes = [{"path": "*", "method": "*"}]
139+
test_routes = [
140+
{"path": "/collections", "method": "POST", "payload": collection},
141+
{
142+
"path": "/collections/{collectionId}",
143+
"method": "PUT",
144+
"payload": collection,
145+
},
146+
{"path": "/collections/{collectionId}", "method": "DELETE", "payload": ""},
147+
{
148+
"path": "/collections/{collectionId}/items",
149+
"method": "POST",
150+
"payload": item,
151+
},
152+
{
153+
"path": "/collections/{collectionId}/items/{itemId}",
154+
"method": "PUT",
155+
"payload": item,
156+
},
157+
{
158+
"path": "/collections/{collectionId}/items/{itemId}",
159+
"method": "DELETE",
160+
"payload": "",
161+
},
162+
]
163+
dependencies = [Depends(must_be_bob)]
164+
api = self._build_api(route_dependencies=[(routes, dependencies)])
165+
self._assert_dependency_applied(api, test_routes)
166+
167+
def test_build_api_with_default_path_route_dependencies(self, collection, item):
168+
routes = [{"path": "*", "method": "POST"}]
169+
test_routes = [
170+
{
171+
"path": "/collections",
172+
"method": "POST",
173+
"payload": collection,
174+
},
175+
{
176+
"path": "/collections/{collectionId}/items",
177+
"method": "POST",
178+
"payload": item,
179+
},
180+
]
181+
test_not_routes = [
182+
{
183+
"path": "/collections/{collectionId}",
184+
"method": "PUT",
185+
"payload": collection,
186+
},
187+
{
188+
"path": "/collections/{collectionId}",
189+
"method": "DELETE",
190+
"payload": "",
191+
},
192+
{
193+
"path": "/collections/{collectionId}/items/{itemId}",
194+
"method": "PUT",
195+
"payload": item,
196+
},
197+
{
198+
"path": "/collections/{collectionId}/items/{itemId}",
199+
"method": "DELETE",
200+
"payload": "",
201+
},
202+
]
203+
dependencies = [Depends(must_be_bob)]
204+
api = self._build_api(route_dependencies=[(routes, dependencies)])
205+
self._assert_dependency_applied(api, test_routes)
206+
self._assert_dependency_not_applied(api, test_not_routes)
207+
208+
def test_build_api_with_default_method_route_dependencies(self, collection, item):
209+
routes = [
210+
{
211+
"path": "/collections/{collectionId}",
212+
"method": "*",
213+
},
214+
{
215+
"path": "/collections/{collectionId}/items/{itemId}",
216+
"method": "*",
217+
},
218+
]
219+
test_routes = [
220+
{
221+
"path": "/collections/{collectionId}",
222+
"method": "PUT",
223+
"payload": collection,
224+
},
225+
{
226+
"path": "/collections/{collectionId}",
227+
"method": "DELETE",
228+
"payload": "",
229+
},
230+
{
231+
"path": "/collections/{collectionId}/items/{itemId}",
232+
"method": "PUT",
233+
"payload": item,
234+
},
235+
{
236+
"path": "/collections/{collectionId}/items/{itemId}",
237+
"method": "DELETE",
238+
"payload": "",
239+
},
240+
]
241+
test_not_routes = [
242+
{
243+
"path": "/collections",
244+
"method": "POST",
245+
"payload": collection,
246+
},
247+
{
248+
"path": "/collections/{collectionId}/items",
249+
"method": "POST",
250+
"payload": item,
251+
},
252+
]
253+
dependencies = [Depends(must_be_bob)]
254+
api = self._build_api(route_dependencies=[(routes, dependencies)])
255+
self._assert_dependency_applied(api, test_routes)
256+
self._assert_dependency_not_applied(api, test_not_routes)
257+
258+
def test_add_default_route_dependencies_after_building_api(self, collection, item):
259+
routes = [{"path": "*", "method": "*"}]
260+
test_routes = [
261+
{
262+
"path": "/collections",
263+
"method": "POST",
264+
"payload": collection,
265+
},
266+
{
267+
"path": "/collections/{collectionId}",
268+
"method": "PUT",
269+
"payload": collection,
270+
},
271+
{
272+
"path": "/collections/{collectionId}",
273+
"method": "DELETE",
274+
"payload": "",
275+
},
276+
{
277+
"path": "/collections/{collectionId}/items",
278+
"method": "POST",
279+
"payload": item,
280+
},
281+
{
282+
"path": "/collections/{collectionId}/items/{itemId}",
283+
"method": "PUT",
284+
"payload": item,
285+
},
286+
{
287+
"path": "/collections/{collectionId}/items/{itemId}",
288+
"method": "DELETE",
289+
"payload": "",
290+
},
291+
]
292+
api = self._build_api()
293+
api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)])
294+
self._assert_dependency_applied(api, test_routes)
295+
296+
def test_add_default_path_route_dependencies_after_building_api(
297+
self, collection, item
298+
):
299+
routes = [{"path": "*", "method": "POST"}]
300+
test_routes = [
301+
{
302+
"path": "/collections",
303+
"method": "POST",
304+
"payload": collection,
305+
},
306+
{
307+
"path": "/collections/{collectionId}/items",
308+
"method": "POST",
309+
"payload": item,
310+
},
311+
]
312+
test_not_routes = [
313+
{
314+
"path": "/collections/{collectionId}",
315+
"method": "PUT",
316+
"payload": collection,
317+
},
318+
{
319+
"path": "/collections/{collectionId}",
320+
"method": "DELETE",
321+
"payload": "",
322+
},
323+
{
324+
"path": "/collections/{collectionId}/items/{itemId}",
325+
"method": "PUT",
326+
"payload": item,
327+
},
328+
{
329+
"path": "/collections/{collectionId}/items/{itemId}",
330+
"method": "DELETE",
331+
"payload": "",
332+
},
333+
]
334+
api = self._build_api()
335+
api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)])
336+
self._assert_dependency_applied(api, test_routes)
337+
self._assert_dependency_not_applied(api, test_not_routes)
338+
339+
def test_add_default_method_route_dependencies_after_building_api(
340+
self, collection, item
341+
):
342+
routes = [
343+
{
344+
"path": "/collections/{collectionId}",
345+
"method": "*",
346+
},
347+
{
348+
"path": "/collections/{collectionId}/items/{itemId}",
349+
"method": "*",
350+
},
351+
]
352+
test_routes = [
353+
{
354+
"path": "/collections/{collectionId}",
355+
"method": "PUT",
356+
"payload": collection,
357+
},
358+
{
359+
"path": "/collections/{collectionId}",
360+
"method": "DELETE",
361+
"payload": "",
362+
},
363+
{
364+
"path": "/collections/{collectionId}/items/{itemId}",
365+
"method": "PUT",
366+
"payload": item,
367+
},
368+
{
369+
"path": "/collections/{collectionId}/items/{itemId}",
370+
"method": "DELETE",
371+
"payload": "",
372+
},
373+
]
374+
test_not_routes = [
375+
{
376+
"path": "/collections",
377+
"method": "POST",
378+
"payload": collection,
379+
},
380+
{
381+
"path": "/collections/{collectionId}/items",
382+
"method": "POST",
383+
"payload": item,
384+
},
385+
]
386+
api = self._build_api()
387+
api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)])
388+
self._assert_dependency_applied(api, test_routes)
389+
self._assert_dependency_not_applied(api, test_not_routes)
390+
119391

120392
class DummyCoreClient(core.BaseCoreClient):
121393
def all_collections(self, *args, **kwargs):

0 commit comments

Comments
 (0)