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.
Files changed (26) hide show
  1. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/PKG-INFO +1 -1
  2. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/app.py +4 -10
  3. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/config.py +1 -0
  4. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/routes.py +15 -1
  5. stac_fastapi_api-3.0.0a4/stac_fastapi/api/version.py +2 -0
  6. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/PKG-INFO +1 -1
  7. stac_fastapi_api-3.0.0a4/tests/test_api.py +447 -0
  8. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/tests/test_app.py +106 -2
  9. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/tests/test_middleware.py +30 -0
  10. stac_fastapi_api-3.0.0a2/stac_fastapi/api/version.py +0 -2
  11. stac_fastapi_api-3.0.0a2/tests/test_api.py +0 -172
  12. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/README.md +0 -0
  13. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/setup.cfg +0 -0
  14. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/setup.py +0 -0
  15. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/__init__.py +0 -0
  16. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/errors.py +0 -0
  17. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/middleware.py +0 -0
  18. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/models.py +0 -0
  19. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi/api/openapi.py +0 -0
  20. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/SOURCES.txt +0 -0
  21. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/dependency_links.txt +0 -0
  22. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/not-zip-safe +0 -0
  23. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/requires.txt +0 -0
  24. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/stac_fastapi.api.egg-info/top_level.txt +0 -0
  25. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/tests/test_app_prefix.py +0 -0
  26. {stac_fastapi_api-3.0.0a2 → stac_fastapi_api-3.0.0a4}/tests/test_models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stac-fastapi.api
3
- Version: 3.0.0a2
3
+ Version: 3.0.0a4
4
4
  Summary: An implementation of STAC API based on the FastAPI framework.
5
5
  Home-page: https://github.com/stac-utils/stac-fastapi
6
6
  Author: Arturo Engineering
@@ -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.add_middleware(middleware)
485
+ self.app.user_middleware.insert(0, middleware)
492
486
 
493
487
  # customize route dependencies
494
488
  for scopes, dependencies in self.route_dependencies:
@@ -18,6 +18,7 @@ class ApiExtensions(enum.Enum):
18
18
  query = "query"
19
19
  sort = "sort"
20
20
  transaction = "transaction"
21
+ aggregation = "aggregation"
21
22
 
22
23
 
23
24
  class AddOns(enum.Enum):
@@ -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
- 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})
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,2 @@
1
+ """Library version."""
2
+ __version__ = "3.0.0a4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stac-fastapi.api
3
- Version: 3.0.0a2
3
+ Version: 3.0.0a4
4
4
  Summary: An implementation of STAC API based on the FastAPI framework.
5
5
  Home-page: https://github.com/stac-utils/stac-fastapi
6
6
  Author: Arturo Engineering
@@ -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.filter.filter import FilterExtension
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,2 +0,0 @@
1
- """Library version."""
2
- __version__ = "3.0.0a2"
@@ -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
- )