stac-fastapi.api 3.0.0a2__tar.gz → 3.0.0a4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/PKG-INFO +1 -1
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/app.py +4 -10
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/config.py +1 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/routes.py +15 -1
- stac_fastapi_api-3.0.0a4/stac_fastapi/api/version.py +2 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/PKG-INFO +1 -1
- stac_fastapi_api-3.0.0a4/tests/test_api.py +447 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/tests/test_app.py +106 -2
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/tests/test_middleware.py +30 -0
- stac_fastapi_api-3.0.0a2/stac_fastapi/api/version.py +0 -2
- stac_fastapi_api-3.0.0a2/tests/test_api.py +0 -172
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/README.md +0 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/setup.cfg +0 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/setup.py +0 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/__init__.py +0 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/errors.py +0 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/middleware.py +0 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/models.py +0 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/openapi.py +0 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/SOURCES.txt +0 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/dependency_links.txt +0 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/not-zip-safe +0 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/requires.txt +0 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/top_level.txt +0 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/tests/test_app_prefix.py +0 -0
- {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/tests/test_models.py +0 -0
|
@@ -439,11 +439,6 @@ class StacApi:
|
|
|
439
439
|
"""
|
|
440
440
|
return add_route_dependencies(self.app.router.routes, scopes, dependencies)
|
|
441
441
|
|
|
442
|
-
def add_middleware(self, middleware: Middleware):
|
|
443
|
-
"""Add a middleware class to the application."""
|
|
444
|
-
self.app.user_middleware.insert(0, middleware)
|
|
445
|
-
self.app.middleware_stack = self.app.build_middleware_stack()
|
|
446
|
-
|
|
447
442
|
def __attrs_post_init__(self):
|
|
448
443
|
"""Post-init hook.
|
|
449
444
|
|
|
@@ -458,10 +453,6 @@ class StacApi:
|
|
|
458
453
|
self.client.title = self.title
|
|
459
454
|
self.client.description = self.description
|
|
460
455
|
|
|
461
|
-
fields_ext = self.get_extension(FieldsExtension)
|
|
462
|
-
if fields_ext:
|
|
463
|
-
self.settings.default_includes = fields_ext.default_includes
|
|
464
|
-
|
|
465
456
|
Settings.set(self.settings)
|
|
466
457
|
self.app.state.settings = self.settings
|
|
467
458
|
|
|
@@ -487,8 +478,11 @@ class StacApi:
|
|
|
487
478
|
self.app.openapi = self.customize_openapi
|
|
488
479
|
|
|
489
480
|
# add middlewares
|
|
481
|
+
if self.middlewares and self.app.middleware_stack is not None:
|
|
482
|
+
raise RuntimeError("Cannot add middleware after an application has started")
|
|
483
|
+
|
|
490
484
|
for middleware in self.middlewares:
|
|
491
|
-
self.
|
|
485
|
+
self.app.user_middleware.insert(0, middleware)
|
|
492
486
|
|
|
493
487
|
# customize route dependencies
|
|
494
488
|
for scopes, dependencies in self.route_dependencies:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Route factories."""
|
|
2
2
|
|
|
3
|
+
import copy
|
|
3
4
|
import functools
|
|
4
5
|
import inspect
|
|
5
6
|
import warnings
|
|
@@ -100,15 +101,28 @@ def add_route_dependencies(
|
|
|
100
101
|
Allows a developer to add dependencies to a route after the route has been
|
|
101
102
|
defined.
|
|
102
103
|
|
|
104
|
+
"*" can be used for path or method to match all allowed routes.
|
|
105
|
+
|
|
103
106
|
Returns:
|
|
104
107
|
None
|
|
105
108
|
"""
|
|
106
109
|
for scope in scopes:
|
|
110
|
+
_scope = copy.deepcopy(scope)
|
|
107
111
|
for route in routes:
|
|
108
|
-
|
|
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})
|
|
109
119
|
if match != Match.FULL:
|
|
110
120
|
continue
|
|
111
121
|
|
|
122
|
+
# Ignore paths without dependants, e.g. /api, /api.html, /docs/oauth2-redirect
|
|
123
|
+
if not hasattr(route, "dependant"):
|
|
124
|
+
continue
|
|
125
|
+
|
|
112
126
|
# Mimicking how APIRoute handles dependencies:
|
|
113
127
|
# https://github.com/tiangolo/fastapi/blob/1760da0efa55585c19835d81afa8ca386036c325/fastapi/routing.py#L408-L412
|
|
114
128
|
for depends in dependencies[::-1]:
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
from fastapi import Depends, HTTPException, security, status
|
|
2
|
+
from starlette.testclient import TestClient
|
|
3
|
+
|
|
4
|
+
from stac_fastapi.api.app import StacApi
|
|
5
|
+
from stac_fastapi.extensions.core import (
|
|
6
|
+
TokenPaginationExtension,
|
|
7
|
+
TransactionExtension,
|
|
8
|
+
)
|
|
9
|
+
from stac_fastapi.types import config, core
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestRouteDependencies:
|
|
13
|
+
@staticmethod
|
|
14
|
+
def _build_api(**overrides):
|
|
15
|
+
settings = config.ApiSettings()
|
|
16
|
+
return StacApi(
|
|
17
|
+
**{
|
|
18
|
+
"settings": settings,
|
|
19
|
+
"client": DummyCoreClient(),
|
|
20
|
+
"extensions": [
|
|
21
|
+
TransactionExtension(
|
|
22
|
+
client=DummyTransactionsClient(), settings=settings
|
|
23
|
+
),
|
|
24
|
+
TokenPaginationExtension(),
|
|
25
|
+
],
|
|
26
|
+
**overrides,
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def _assert_dependency_applied(api, routes):
|
|
32
|
+
with TestClient(api.app) as client:
|
|
33
|
+
for route in routes:
|
|
34
|
+
response = getattr(client, route["method"].lower())(route["path"])
|
|
35
|
+
assert (
|
|
36
|
+
response.status_code == 401
|
|
37
|
+
), "Unauthenticated requests should be rejected"
|
|
38
|
+
assert response.json() == {"detail": "Not authenticated"}
|
|
39
|
+
|
|
40
|
+
path = route["path"].format(
|
|
41
|
+
collectionId="test_collection", itemId="test_item"
|
|
42
|
+
)
|
|
43
|
+
response = client.request(
|
|
44
|
+
method=route["method"].lower(),
|
|
45
|
+
url=path,
|
|
46
|
+
auth=("bob", "dobbs"),
|
|
47
|
+
content=route["payload"],
|
|
48
|
+
headers={"content-type": "application/json"},
|
|
49
|
+
)
|
|
50
|
+
assert (
|
|
51
|
+
200 <= response.status_code < 300
|
|
52
|
+
), "Authenticated requests should be accepted"
|
|
53
|
+
assert response.json() == "dummy response"
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _assert_dependency_not_applied(api, routes):
|
|
57
|
+
with TestClient(api.app) as client:
|
|
58
|
+
for route in routes:
|
|
59
|
+
path = route["path"].format(
|
|
60
|
+
collectionId="test_collection", itemId="test_item"
|
|
61
|
+
)
|
|
62
|
+
response = client.request(
|
|
63
|
+
method=route["method"].lower(),
|
|
64
|
+
url=path,
|
|
65
|
+
content=route["payload"],
|
|
66
|
+
headers={"content-type": "application/json"},
|
|
67
|
+
)
|
|
68
|
+
assert (
|
|
69
|
+
200 <= response.status_code < 300
|
|
70
|
+
), "Authenticated requests should be accepted"
|
|
71
|
+
assert response.json() == "dummy response"
|
|
72
|
+
|
|
73
|
+
def test_openapi_content_type(self):
|
|
74
|
+
api = self._build_api()
|
|
75
|
+
with TestClient(api.app) as client:
|
|
76
|
+
response = client.get(api.settings.openapi_url)
|
|
77
|
+
assert (
|
|
78
|
+
response.headers["content-type"]
|
|
79
|
+
== "application/vnd.oai.openapi+json;version=3.0"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def test_build_api_with_route_dependencies(self, collection, item):
|
|
83
|
+
routes = [
|
|
84
|
+
{"path": "/collections", "method": "POST", "payload": collection},
|
|
85
|
+
{
|
|
86
|
+
"path": "/collections/{collectionId}",
|
|
87
|
+
"method": "PUT",
|
|
88
|
+
"payload": collection,
|
|
89
|
+
},
|
|
90
|
+
{"path": "/collections/{collectionId}", "method": "DELETE", "payload": ""},
|
|
91
|
+
{
|
|
92
|
+
"path": "/collections/{collectionId}/items",
|
|
93
|
+
"method": "POST",
|
|
94
|
+
"payload": item,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
98
|
+
"method": "PUT",
|
|
99
|
+
"payload": item,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
103
|
+
"method": "DELETE",
|
|
104
|
+
"payload": "",
|
|
105
|
+
},
|
|
106
|
+
]
|
|
107
|
+
dependencies = [Depends(must_be_bob)]
|
|
108
|
+
api = self._build_api(route_dependencies=[(routes, dependencies)])
|
|
109
|
+
self._assert_dependency_applied(api, routes)
|
|
110
|
+
|
|
111
|
+
def test_add_route_dependencies_after_building_api(self, collection, item):
|
|
112
|
+
routes = [
|
|
113
|
+
{"path": "/collections", "method": "POST", "payload": collection},
|
|
114
|
+
{
|
|
115
|
+
"path": "/collections/{collectionId}",
|
|
116
|
+
"method": "PUT",
|
|
117
|
+
"payload": collection,
|
|
118
|
+
},
|
|
119
|
+
{"path": "/collections/{collectionId}", "method": "DELETE", "payload": ""},
|
|
120
|
+
{
|
|
121
|
+
"path": "/collections/{collectionId}/items",
|
|
122
|
+
"method": "POST",
|
|
123
|
+
"payload": item,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
127
|
+
"method": "PUT",
|
|
128
|
+
"payload": item,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
132
|
+
"method": "DELETE",
|
|
133
|
+
"payload": "",
|
|
134
|
+
},
|
|
135
|
+
]
|
|
136
|
+
api = self._build_api()
|
|
137
|
+
api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)])
|
|
138
|
+
self._assert_dependency_applied(api, routes)
|
|
139
|
+
|
|
140
|
+
def test_build_api_with_default_route_dependencies(self, collection, item):
|
|
141
|
+
routes = [{"path": "*", "method": "*"}]
|
|
142
|
+
test_routes = [
|
|
143
|
+
{"path": "/collections", "method": "POST", "payload": collection},
|
|
144
|
+
{
|
|
145
|
+
"path": "/collections/{collectionId}",
|
|
146
|
+
"method": "PUT",
|
|
147
|
+
"payload": collection,
|
|
148
|
+
},
|
|
149
|
+
{"path": "/collections/{collectionId}", "method": "DELETE", "payload": ""},
|
|
150
|
+
{
|
|
151
|
+
"path": "/collections/{collectionId}/items",
|
|
152
|
+
"method": "POST",
|
|
153
|
+
"payload": item,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
157
|
+
"method": "PUT",
|
|
158
|
+
"payload": item,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
162
|
+
"method": "DELETE",
|
|
163
|
+
"payload": "",
|
|
164
|
+
},
|
|
165
|
+
]
|
|
166
|
+
dependencies = [Depends(must_be_bob)]
|
|
167
|
+
api = self._build_api(route_dependencies=[(routes, dependencies)])
|
|
168
|
+
self._assert_dependency_applied(api, test_routes)
|
|
169
|
+
|
|
170
|
+
def test_build_api_with_default_path_route_dependencies(self, collection, item):
|
|
171
|
+
routes = [{"path": "*", "method": "POST"}]
|
|
172
|
+
test_routes = [
|
|
173
|
+
{
|
|
174
|
+
"path": "/collections",
|
|
175
|
+
"method": "POST",
|
|
176
|
+
"payload": collection,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"path": "/collections/{collectionId}/items",
|
|
180
|
+
"method": "POST",
|
|
181
|
+
"payload": item,
|
|
182
|
+
},
|
|
183
|
+
]
|
|
184
|
+
test_not_routes = [
|
|
185
|
+
{
|
|
186
|
+
"path": "/collections/{collectionId}",
|
|
187
|
+
"method": "PUT",
|
|
188
|
+
"payload": collection,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"path": "/collections/{collectionId}",
|
|
192
|
+
"method": "DELETE",
|
|
193
|
+
"payload": "",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
197
|
+
"method": "PUT",
|
|
198
|
+
"payload": item,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
202
|
+
"method": "DELETE",
|
|
203
|
+
"payload": "",
|
|
204
|
+
},
|
|
205
|
+
]
|
|
206
|
+
dependencies = [Depends(must_be_bob)]
|
|
207
|
+
api = self._build_api(route_dependencies=[(routes, dependencies)])
|
|
208
|
+
self._assert_dependency_applied(api, test_routes)
|
|
209
|
+
self._assert_dependency_not_applied(api, test_not_routes)
|
|
210
|
+
|
|
211
|
+
def test_build_api_with_default_method_route_dependencies(self, collection, item):
|
|
212
|
+
routes = [
|
|
213
|
+
{
|
|
214
|
+
"path": "/collections/{collectionId}",
|
|
215
|
+
"method": "*",
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
219
|
+
"method": "*",
|
|
220
|
+
},
|
|
221
|
+
]
|
|
222
|
+
test_routes = [
|
|
223
|
+
{
|
|
224
|
+
"path": "/collections/{collectionId}",
|
|
225
|
+
"method": "PUT",
|
|
226
|
+
"payload": collection,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"path": "/collections/{collectionId}",
|
|
230
|
+
"method": "DELETE",
|
|
231
|
+
"payload": "",
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
235
|
+
"method": "PUT",
|
|
236
|
+
"payload": item,
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
240
|
+
"method": "DELETE",
|
|
241
|
+
"payload": "",
|
|
242
|
+
},
|
|
243
|
+
]
|
|
244
|
+
test_not_routes = [
|
|
245
|
+
{
|
|
246
|
+
"path": "/collections",
|
|
247
|
+
"method": "POST",
|
|
248
|
+
"payload": collection,
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"path": "/collections/{collectionId}/items",
|
|
252
|
+
"method": "POST",
|
|
253
|
+
"payload": item,
|
|
254
|
+
},
|
|
255
|
+
]
|
|
256
|
+
dependencies = [Depends(must_be_bob)]
|
|
257
|
+
api = self._build_api(route_dependencies=[(routes, dependencies)])
|
|
258
|
+
self._assert_dependency_applied(api, test_routes)
|
|
259
|
+
self._assert_dependency_not_applied(api, test_not_routes)
|
|
260
|
+
|
|
261
|
+
def test_add_default_route_dependencies_after_building_api(self, collection, item):
|
|
262
|
+
routes = [{"path": "*", "method": "*"}]
|
|
263
|
+
test_routes = [
|
|
264
|
+
{
|
|
265
|
+
"path": "/collections",
|
|
266
|
+
"method": "POST",
|
|
267
|
+
"payload": collection,
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"path": "/collections/{collectionId}",
|
|
271
|
+
"method": "PUT",
|
|
272
|
+
"payload": collection,
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"path": "/collections/{collectionId}",
|
|
276
|
+
"method": "DELETE",
|
|
277
|
+
"payload": "",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"path": "/collections/{collectionId}/items",
|
|
281
|
+
"method": "POST",
|
|
282
|
+
"payload": item,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
286
|
+
"method": "PUT",
|
|
287
|
+
"payload": item,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
291
|
+
"method": "DELETE",
|
|
292
|
+
"payload": "",
|
|
293
|
+
},
|
|
294
|
+
]
|
|
295
|
+
api = self._build_api()
|
|
296
|
+
api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)])
|
|
297
|
+
self._assert_dependency_applied(api, test_routes)
|
|
298
|
+
|
|
299
|
+
def test_add_default_path_route_dependencies_after_building_api(
|
|
300
|
+
self, collection, item
|
|
301
|
+
):
|
|
302
|
+
routes = [{"path": "*", "method": "POST"}]
|
|
303
|
+
test_routes = [
|
|
304
|
+
{
|
|
305
|
+
"path": "/collections",
|
|
306
|
+
"method": "POST",
|
|
307
|
+
"payload": collection,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"path": "/collections/{collectionId}/items",
|
|
311
|
+
"method": "POST",
|
|
312
|
+
"payload": item,
|
|
313
|
+
},
|
|
314
|
+
]
|
|
315
|
+
test_not_routes = [
|
|
316
|
+
{
|
|
317
|
+
"path": "/collections/{collectionId}",
|
|
318
|
+
"method": "PUT",
|
|
319
|
+
"payload": collection,
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"path": "/collections/{collectionId}",
|
|
323
|
+
"method": "DELETE",
|
|
324
|
+
"payload": "",
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
328
|
+
"method": "PUT",
|
|
329
|
+
"payload": item,
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
333
|
+
"method": "DELETE",
|
|
334
|
+
"payload": "",
|
|
335
|
+
},
|
|
336
|
+
]
|
|
337
|
+
api = self._build_api()
|
|
338
|
+
api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)])
|
|
339
|
+
self._assert_dependency_applied(api, test_routes)
|
|
340
|
+
self._assert_dependency_not_applied(api, test_not_routes)
|
|
341
|
+
|
|
342
|
+
def test_add_default_method_route_dependencies_after_building_api(
|
|
343
|
+
self, collection, item
|
|
344
|
+
):
|
|
345
|
+
routes = [
|
|
346
|
+
{
|
|
347
|
+
"path": "/collections/{collectionId}",
|
|
348
|
+
"method": "*",
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
352
|
+
"method": "*",
|
|
353
|
+
},
|
|
354
|
+
]
|
|
355
|
+
test_routes = [
|
|
356
|
+
{
|
|
357
|
+
"path": "/collections/{collectionId}",
|
|
358
|
+
"method": "PUT",
|
|
359
|
+
"payload": collection,
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"path": "/collections/{collectionId}",
|
|
363
|
+
"method": "DELETE",
|
|
364
|
+
"payload": "",
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
368
|
+
"method": "PUT",
|
|
369
|
+
"payload": item,
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
373
|
+
"method": "DELETE",
|
|
374
|
+
"payload": "",
|
|
375
|
+
},
|
|
376
|
+
]
|
|
377
|
+
test_not_routes = [
|
|
378
|
+
{
|
|
379
|
+
"path": "/collections",
|
|
380
|
+
"method": "POST",
|
|
381
|
+
"payload": collection,
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
"path": "/collections/{collectionId}/items",
|
|
385
|
+
"method": "POST",
|
|
386
|
+
"payload": item,
|
|
387
|
+
},
|
|
388
|
+
]
|
|
389
|
+
api = self._build_api()
|
|
390
|
+
api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)])
|
|
391
|
+
self._assert_dependency_applied(api, test_routes)
|
|
392
|
+
self._assert_dependency_not_applied(api, test_not_routes)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class DummyCoreClient(core.BaseCoreClient):
|
|
396
|
+
def all_collections(self, *args, **kwargs):
|
|
397
|
+
...
|
|
398
|
+
|
|
399
|
+
def get_collection(self, *args, **kwargs):
|
|
400
|
+
...
|
|
401
|
+
|
|
402
|
+
def get_item(self, *args, **kwargs):
|
|
403
|
+
...
|
|
404
|
+
|
|
405
|
+
def get_search(self, *args, **kwargs):
|
|
406
|
+
...
|
|
407
|
+
|
|
408
|
+
def post_search(self, *args, **kwargs):
|
|
409
|
+
...
|
|
410
|
+
|
|
411
|
+
def item_collection(self, *args, **kwargs):
|
|
412
|
+
...
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class DummyTransactionsClient(core.BaseTransactionsClient):
|
|
416
|
+
"""Defines a pattern for implementing the STAC transaction extension."""
|
|
417
|
+
|
|
418
|
+
def create_item(self, *args, **kwargs):
|
|
419
|
+
return "dummy response"
|
|
420
|
+
|
|
421
|
+
def update_item(self, *args, **kwargs):
|
|
422
|
+
return "dummy response"
|
|
423
|
+
|
|
424
|
+
def delete_item(self, *args, **kwargs):
|
|
425
|
+
return "dummy response"
|
|
426
|
+
|
|
427
|
+
def create_collection(self, *args, **kwargs):
|
|
428
|
+
return "dummy response"
|
|
429
|
+
|
|
430
|
+
def update_collection(self, *args, **kwargs):
|
|
431
|
+
return "dummy response"
|
|
432
|
+
|
|
433
|
+
def delete_collection(self, *args, **kwargs):
|
|
434
|
+
return "dummy response"
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def must_be_bob(
|
|
438
|
+
credentials: security.HTTPBasicCredentials = Depends(security.HTTPBasic()),
|
|
439
|
+
):
|
|
440
|
+
if credentials.username == "bob":
|
|
441
|
+
return True
|
|
442
|
+
|
|
443
|
+
raise HTTPException(
|
|
444
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
445
|
+
detail="You're not Bob",
|
|
446
|
+
headers={"WWW-Authenticate": "Basic"},
|
|
447
|
+
)
|
|
@@ -8,10 +8,10 @@ from stac_pydantic import api
|
|
|
8
8
|
|
|
9
9
|
from stac_fastapi.api import app
|
|
10
10
|
from stac_fastapi.api.models import create_get_request_model, create_post_request_model
|
|
11
|
-
from stac_fastapi.extensions.core
|
|
11
|
+
from stac_fastapi.extensions.core import FieldsExtension, FilterExtension
|
|
12
12
|
from stac_fastapi.types import stac
|
|
13
13
|
from stac_fastapi.types.config import ApiSettings
|
|
14
|
-
from stac_fastapi.types.core import NumType
|
|
14
|
+
from stac_fastapi.types.core import BaseCoreClient, NumType
|
|
15
15
|
from stac_fastapi.types.search import BaseSearchPostRequest
|
|
16
16
|
|
|
17
17
|
|
|
@@ -190,3 +190,107 @@ def test_filter_extension(validate, TestCoreClient, item_dict):
|
|
|
190
190
|
assert landing.status_code == 200, landing.text
|
|
191
191
|
assert get_search.status_code == 200, get_search.text
|
|
192
192
|
assert post_search.status_code == 200, post_search.text
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@pytest.mark.parametrize("validate", [True, False])
|
|
196
|
+
def test_fields_extension(validate, TestCoreClient, item_dict):
|
|
197
|
+
"""Test if fields Parameters are passed correctly."""
|
|
198
|
+
|
|
199
|
+
class BadCoreClient(BaseCoreClient):
|
|
200
|
+
def post_search(
|
|
201
|
+
self, search_request: BaseSearchPostRequest, **kwargs
|
|
202
|
+
) -> stac.ItemCollection:
|
|
203
|
+
return {"not": "a proper stac item"}
|
|
204
|
+
|
|
205
|
+
def get_search(
|
|
206
|
+
self,
|
|
207
|
+
collections: Optional[List[str]] = None,
|
|
208
|
+
ids: Optional[List[str]] = None,
|
|
209
|
+
bbox: Optional[List[NumType]] = None,
|
|
210
|
+
intersects: Optional[str] = None,
|
|
211
|
+
datetime: Optional[Union[str, datetime]] = None,
|
|
212
|
+
limit: Optional[int] = 10,
|
|
213
|
+
**kwargs,
|
|
214
|
+
) -> stac.ItemCollection:
|
|
215
|
+
return {"not": "a proper stac item"}
|
|
216
|
+
|
|
217
|
+
def get_item(self, item_id: str, collection_id: str, **kwargs) -> stac.Item:
|
|
218
|
+
raise NotImplementedError
|
|
219
|
+
|
|
220
|
+
def all_collections(self, **kwargs) -> stac.Collections:
|
|
221
|
+
raise NotImplementedError
|
|
222
|
+
|
|
223
|
+
def get_collection(self, collection_id: str, **kwargs) -> stac.Collection:
|
|
224
|
+
raise NotImplementedError
|
|
225
|
+
|
|
226
|
+
def item_collection(
|
|
227
|
+
self,
|
|
228
|
+
collection_id: str,
|
|
229
|
+
bbox: Optional[List[Union[float, int]]] = None,
|
|
230
|
+
datetime: Optional[Union[str, datetime]] = None,
|
|
231
|
+
limit: int = 10,
|
|
232
|
+
token: str = None,
|
|
233
|
+
**kwargs,
|
|
234
|
+
) -> stac.ItemCollection:
|
|
235
|
+
raise NotImplementedError
|
|
236
|
+
|
|
237
|
+
test_app = app.StacApi(
|
|
238
|
+
settings=ApiSettings(enable_response_models=validate),
|
|
239
|
+
client=BadCoreClient(),
|
|
240
|
+
search_get_request_model=create_get_request_model([FieldsExtension()]),
|
|
241
|
+
search_post_request_model=create_post_request_model([FieldsExtension()]),
|
|
242
|
+
extensions=[FieldsExtension()],
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
with TestClient(test_app.app) as client:
|
|
246
|
+
get_search = client.get(
|
|
247
|
+
"/search",
|
|
248
|
+
params={"fields": "properties.datetime"},
|
|
249
|
+
)
|
|
250
|
+
post_search = client.post(
|
|
251
|
+
"/search",
|
|
252
|
+
json={
|
|
253
|
+
"collections": ["test"],
|
|
254
|
+
"fields": {
|
|
255
|
+
"include": ["properties.datetime"],
|
|
256
|
+
"exclude": [],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
assert get_search.status_code == 200, get_search.text
|
|
262
|
+
assert post_search.status_code == 200, post_search.text
|
|
263
|
+
|
|
264
|
+
test_app = app.StacApi(
|
|
265
|
+
settings=ApiSettings(enable_response_models=validate),
|
|
266
|
+
client=BadCoreClient(),
|
|
267
|
+
search_get_request_model=create_get_request_model([FieldsExtension()]),
|
|
268
|
+
search_post_request_model=create_post_request_model([FieldsExtension()]),
|
|
269
|
+
extensions=[],
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
with TestClient(test_app.app) as client:
|
|
273
|
+
get_search = client.get(
|
|
274
|
+
"/search",
|
|
275
|
+
params={"fields": "properties.datetime"},
|
|
276
|
+
)
|
|
277
|
+
post_search = client.post(
|
|
278
|
+
"/search",
|
|
279
|
+
json={
|
|
280
|
+
"collections": ["test"],
|
|
281
|
+
"fields": {
|
|
282
|
+
"include": ["properties.datetime"],
|
|
283
|
+
"exclude": [],
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
)
|
|
287
|
+
if validate:
|
|
288
|
+
assert get_search.status_code == 500, (
|
|
289
|
+
get_search.json()["code"] == "ResponseValidationError"
|
|
290
|
+
)
|
|
291
|
+
assert post_search.status_code == 500, (
|
|
292
|
+
post_search.json()["code"] == "ResponseValidationError"
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
assert get_search.status_code == 200, get_search.text
|
|
296
|
+
assert post_search.status_code == 200, post_search.text
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from unittest import mock
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
|
+
from fastapi import Request
|
|
5
|
+
from fastapi.responses import JSONResponse
|
|
4
6
|
from starlette.applications import Starlette
|
|
5
7
|
from starlette.testclient import TestClient
|
|
6
8
|
|
|
@@ -166,3 +168,31 @@ def test_cors_middleware(test_client):
|
|
|
166
168
|
resp = test_client.get("/_mgmt/ping", headers={"Origin": "http://netloc"})
|
|
167
169
|
assert resp.status_code == 200
|
|
168
170
|
assert resp.headers["access-control-allow-origin"] == "*"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_middleware_stack():
|
|
174
|
+
stac_api = StacApi(
|
|
175
|
+
settings=ApiSettings(), client=mock.create_autospec(BaseCoreClient)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
179
|
+
return JSONResponse(
|
|
180
|
+
status_code=400,
|
|
181
|
+
content={"customerrordetail": "yoo", "body": "yo"},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
class CustomException(Exception):
|
|
185
|
+
"Custom Exception"
|
|
186
|
+
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
stac_api.app.add_exception_handler(CustomException, exception_handler)
|
|
190
|
+
|
|
191
|
+
@stac_api.app.get("/error")
|
|
192
|
+
def error_endpoint():
|
|
193
|
+
raise CustomException("got you!")
|
|
194
|
+
|
|
195
|
+
with TestClient(stac_api.app) as client:
|
|
196
|
+
resp = client.get("/error")
|
|
197
|
+
assert resp.status_code == 400
|
|
198
|
+
assert resp.json()["customerrordetail"] == "yoo"
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
from fastapi import Depends, HTTPException, security, status
|
|
2
|
-
from starlette.testclient import TestClient
|
|
3
|
-
|
|
4
|
-
from stac_fastapi.api.app import StacApi
|
|
5
|
-
from stac_fastapi.extensions.core import TokenPaginationExtension, TransactionExtension
|
|
6
|
-
from stac_fastapi.types import config, core
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class TestRouteDependencies:
|
|
10
|
-
@staticmethod
|
|
11
|
-
def _build_api(**overrides):
|
|
12
|
-
settings = config.ApiSettings()
|
|
13
|
-
return StacApi(
|
|
14
|
-
**{
|
|
15
|
-
"settings": settings,
|
|
16
|
-
"client": DummyCoreClient(),
|
|
17
|
-
"extensions": [
|
|
18
|
-
TransactionExtension(
|
|
19
|
-
client=DummyTransactionsClient(), settings=settings
|
|
20
|
-
),
|
|
21
|
-
TokenPaginationExtension(),
|
|
22
|
-
],
|
|
23
|
-
**overrides,
|
|
24
|
-
}
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
@staticmethod
|
|
28
|
-
def _assert_dependency_applied(api, routes):
|
|
29
|
-
with TestClient(api.app) as client:
|
|
30
|
-
for route in routes:
|
|
31
|
-
response = getattr(client, route["method"].lower())(route["path"])
|
|
32
|
-
assert (
|
|
33
|
-
response.status_code == 401
|
|
34
|
-
), "Unauthenticated requests should be rejected"
|
|
35
|
-
assert response.json() == {"detail": "Not authenticated"}
|
|
36
|
-
|
|
37
|
-
path = route["path"].format(
|
|
38
|
-
collectionId="test_collection", itemId="test_item"
|
|
39
|
-
)
|
|
40
|
-
response = client.request(
|
|
41
|
-
method=route["method"].lower(),
|
|
42
|
-
url=path,
|
|
43
|
-
auth=("bob", "dobbs"),
|
|
44
|
-
content=route["payload"],
|
|
45
|
-
headers={"content-type": "application/json"},
|
|
46
|
-
)
|
|
47
|
-
assert (
|
|
48
|
-
200 <= response.status_code < 300
|
|
49
|
-
), "Authenticated requests should be accepted"
|
|
50
|
-
assert response.json() == "dummy response"
|
|
51
|
-
|
|
52
|
-
def test_openapi_content_type(self):
|
|
53
|
-
api = self._build_api()
|
|
54
|
-
with TestClient(api.app) as client:
|
|
55
|
-
response = client.get(api.settings.openapi_url)
|
|
56
|
-
assert (
|
|
57
|
-
response.headers["content-type"]
|
|
58
|
-
== "application/vnd.oai.openapi+json;version=3.0"
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
def test_build_api_with_route_dependencies(self, collection, item):
|
|
62
|
-
routes = [
|
|
63
|
-
{"path": "/collections", "method": "POST", "payload": collection},
|
|
64
|
-
{
|
|
65
|
-
"path": "/collections/{collectionId}",
|
|
66
|
-
"method": "PUT",
|
|
67
|
-
"payload": collection,
|
|
68
|
-
},
|
|
69
|
-
{"path": "/collections/{collectionId}", "method": "DELETE", "payload": ""},
|
|
70
|
-
{
|
|
71
|
-
"path": "/collections/{collectionId}/items",
|
|
72
|
-
"method": "POST",
|
|
73
|
-
"payload": item,
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
"path": "/collections/{collectionId}/items/{itemId}",
|
|
77
|
-
"method": "PUT",
|
|
78
|
-
"payload": item,
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
"path": "/collections/{collectionId}/items/{itemId}",
|
|
82
|
-
"method": "DELETE",
|
|
83
|
-
"payload": "",
|
|
84
|
-
},
|
|
85
|
-
]
|
|
86
|
-
dependencies = [Depends(must_be_bob)]
|
|
87
|
-
api = self._build_api(route_dependencies=[(routes, dependencies)])
|
|
88
|
-
self._assert_dependency_applied(api, routes)
|
|
89
|
-
|
|
90
|
-
def test_add_route_dependencies_after_building_api(self, collection, item):
|
|
91
|
-
routes = [
|
|
92
|
-
{"path": "/collections", "method": "POST", "payload": collection},
|
|
93
|
-
{
|
|
94
|
-
"path": "/collections/{collectionId}",
|
|
95
|
-
"method": "PUT",
|
|
96
|
-
"payload": collection,
|
|
97
|
-
},
|
|
98
|
-
{"path": "/collections/{collectionId}", "method": "DELETE", "payload": ""},
|
|
99
|
-
{
|
|
100
|
-
"path": "/collections/{collectionId}/items",
|
|
101
|
-
"method": "POST",
|
|
102
|
-
"payload": item,
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
"path": "/collections/{collectionId}/items/{itemId}",
|
|
106
|
-
"method": "PUT",
|
|
107
|
-
"payload": item,
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
"path": "/collections/{collectionId}/items/{itemId}",
|
|
111
|
-
"method": "DELETE",
|
|
112
|
-
"payload": "",
|
|
113
|
-
},
|
|
114
|
-
]
|
|
115
|
-
api = self._build_api()
|
|
116
|
-
api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)])
|
|
117
|
-
self._assert_dependency_applied(api, routes)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
class DummyCoreClient(core.BaseCoreClient):
|
|
121
|
-
def all_collections(self, *args, **kwargs):
|
|
122
|
-
...
|
|
123
|
-
|
|
124
|
-
def get_collection(self, *args, **kwargs):
|
|
125
|
-
...
|
|
126
|
-
|
|
127
|
-
def get_item(self, *args, **kwargs):
|
|
128
|
-
...
|
|
129
|
-
|
|
130
|
-
def get_search(self, *args, **kwargs):
|
|
131
|
-
...
|
|
132
|
-
|
|
133
|
-
def post_search(self, *args, **kwargs):
|
|
134
|
-
...
|
|
135
|
-
|
|
136
|
-
def item_collection(self, *args, **kwargs):
|
|
137
|
-
...
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
class DummyTransactionsClient(core.BaseTransactionsClient):
|
|
141
|
-
"""Defines a pattern for implementing the STAC transaction extension."""
|
|
142
|
-
|
|
143
|
-
def create_item(self, *args, **kwargs):
|
|
144
|
-
return "dummy response"
|
|
145
|
-
|
|
146
|
-
def update_item(self, *args, **kwargs):
|
|
147
|
-
return "dummy response"
|
|
148
|
-
|
|
149
|
-
def delete_item(self, *args, **kwargs):
|
|
150
|
-
return "dummy response"
|
|
151
|
-
|
|
152
|
-
def create_collection(self, *args, **kwargs):
|
|
153
|
-
return "dummy response"
|
|
154
|
-
|
|
155
|
-
def update_collection(self, *args, **kwargs):
|
|
156
|
-
return "dummy response"
|
|
157
|
-
|
|
158
|
-
def delete_collection(self, *args, **kwargs):
|
|
159
|
-
return "dummy response"
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def must_be_bob(
|
|
163
|
-
credentials: security.HTTPBasicCredentials = Depends(security.HTTPBasic()),
|
|
164
|
-
):
|
|
165
|
-
if credentials.username == "bob":
|
|
166
|
-
return True
|
|
167
|
-
|
|
168
|
-
raise HTTPException(
|
|
169
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
170
|
-
detail="You're not Bob",
|
|
171
|
-
headers={"WWW-Authenticate": "Basic"},
|
|
172
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/requires.txt
RENAMED
|
File without changes
|
{stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|