stac-fastapi.api 2.5.5.post1__tar.gz → 3.0.0__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 (28) hide show
  1. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/PKG-INFO +2 -6
  2. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/setup.py +1 -5
  3. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/stac_fastapi/api/app.py +115 -48
  4. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/stac_fastapi/api/config.py +4 -1
  5. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/stac_fastapi/api/errors.py +2 -1
  6. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/stac_fastapi/api/middleware.py +1 -0
  7. stac_fastapi_api-3.0.0/stac_fastapi/api/models.py +138 -0
  8. stac_fastapi_api-3.0.0/stac_fastapi/api/openapi.py +37 -0
  9. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/stac_fastapi/api/routes.py +15 -9
  10. stac_fastapi_api-3.0.0/stac_fastapi/api/version.py +3 -0
  11. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/stac_fastapi.api.egg-info/PKG-INFO +2 -6
  12. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/stac_fastapi.api.egg-info/SOURCES.txt +3 -1
  13. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/stac_fastapi.api.egg-info/requires.txt +1 -5
  14. stac_fastapi_api-3.0.0/tests/test_api.py +456 -0
  15. stac_fastapi_api-3.0.0/tests/test_app.py +389 -0
  16. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/tests/test_app_prefix.py +1 -1
  17. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/tests/test_middleware.py +30 -0
  18. stac_fastapi_api-3.0.0/tests/test_models.py +131 -0
  19. stac_fastapi_api-2.5.5.post1/stac_fastapi/api/models.py +0 -185
  20. stac_fastapi_api-2.5.5.post1/stac_fastapi/api/openapi.py +0 -88
  21. stac_fastapi_api-2.5.5.post1/stac_fastapi/api/version.py +0 -2
  22. stac_fastapi_api-2.5.5.post1/tests/test_api.py +0 -140
  23. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/README.md +0 -0
  24. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/setup.cfg +0 -0
  25. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/stac_fastapi/api/__init__.py +0 -0
  26. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/stac_fastapi.api.egg-info/dependency_links.txt +0 -0
  27. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/stac_fastapi.api.egg-info/not-zip-safe +0 -0
  28. {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0}/stac_fastapi.api.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stac-fastapi.api
3
- Version: 2.5.5.post1
3
+ Version: 3.0.0
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
@@ -18,11 +18,8 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: License :: OSI Approved :: MIT License
19
19
  Requires-Python: >=3.8
20
20
  Description-Content-Type: text/markdown
21
- Requires-Dist: attrs
22
- Requires-Dist: pydantic[dotenv]<2
23
- Requires-Dist: stac_pydantic==2.0.*
24
21
  Requires-Dist: brotli_asgi
25
- Requires-Dist: stac-fastapi.types
22
+ Requires-Dist: stac-fastapi.types~=3.0
26
23
  Provides-Extra: dev
27
24
  Requires-Dist: httpx; extra == "dev"
28
25
  Requires-Dist: pytest; extra == "dev"
@@ -30,7 +27,6 @@ Requires-Dist: pytest-cov; extra == "dev"
30
27
  Requires-Dist: pytest-asyncio; extra == "dev"
31
28
  Requires-Dist: pre-commit; extra == "dev"
32
29
  Requires-Dist: requests; extra == "dev"
33
- Requires-Dist: pystac[validation]==1.*; extra == "dev"
34
30
  Provides-Extra: benchmark
35
31
  Requires-Dist: pytest-benchmark; extra == "benchmark"
36
32
  Provides-Extra: docs
@@ -6,11 +6,8 @@ with open("README.md") as f:
6
6
  desc = f.read()
7
7
 
8
8
  install_requires = [
9
- "attrs",
10
- "pydantic[dotenv]<2",
11
- "stac_pydantic==2.0.*",
12
9
  "brotli_asgi",
13
- "stac-fastapi.types",
10
+ "stac-fastapi.types~=3.0",
14
11
  ]
15
12
 
16
13
  extra_reqs = {
@@ -21,7 +18,6 @@ extra_reqs = {
21
18
  "pytest-asyncio",
22
19
  "pre-commit",
23
20
  "requests",
24
- "pystac[validation]==1.*",
25
21
  ],
26
22
  "benchmark": [
27
23
  "pytest-benchmark",
@@ -1,5 +1,6 @@
1
1
  """Fastapi app creation."""
2
2
 
3
+
3
4
  from typing import Any, Dict, List, Optional, Tuple, Type, Union
4
5
 
5
6
  import attr
@@ -7,27 +8,25 @@ from brotli_asgi import BrotliMiddleware
7
8
  from fastapi import APIRouter, FastAPI
8
9
  from fastapi.openapi.utils import get_openapi
9
10
  from fastapi.params import Depends
10
- from stac_pydantic import Collection, Item, ItemCollection
11
- from stac_pydantic.api import ConformanceClasses, LandingPage
11
+ from stac_pydantic import api
12
12
  from stac_pydantic.api.collections import Collections
13
- from stac_pydantic.version import STAC_VERSION
13
+ from stac_pydantic.api.version import STAC_API_VERSION
14
+ from stac_pydantic.shared import MimeTypes
15
+ from starlette.middleware import Middleware
14
16
  from starlette.responses import JSONResponse, Response
15
17
 
16
18
  from stac_fastapi.api.errors import DEFAULT_STATUS_CODES, add_exception_handlers
17
19
  from stac_fastapi.api.middleware import CORSMiddleware, ProxyHeaderMiddleware
18
20
  from stac_fastapi.api.models import (
21
+ APIRequest,
19
22
  CollectionUri,
20
23
  EmptyRequest,
21
24
  GeoJSONResponse,
22
25
  ItemCollectionUri,
23
26
  ItemUri,
24
- create_request_model,
25
27
  )
26
28
  from stac_fastapi.api.openapi import update_openapi
27
29
  from stac_fastapi.api.routes import Scope, add_route_dependencies, create_async_endpoint
28
-
29
- # TODO: make this module not depend on `stac_fastapi.extensions`
30
- from stac_fastapi.extensions.core import FieldsExtension, TokenPaginationExtension
31
30
  from stac_fastapi.types.config import ApiSettings, Settings
32
31
  from stac_fastapi.types.core import AsyncBaseCoreClient, BaseCoreClient
33
32
  from stac_fastapi.types.extension import ApiExtension
@@ -94,7 +93,7 @@ class StacApi:
94
93
  lambda self: self.settings.stac_fastapi_version, takes_self=True
95
94
  )
96
95
  )
97
- stac_version: str = attr.ib(default=STAC_VERSION)
96
+ stac_version: str = attr.ib(default=STAC_API_VERSION)
98
97
  description: str = attr.ib(
99
98
  default=attr.Factory(
100
99
  lambda self: self.settings.stac_fastapi_description, takes_self=True
@@ -106,11 +105,18 @@ class StacApi:
106
105
  search_post_request_model: Type[BaseSearchPostRequest] = attr.ib(
107
106
  default=BaseSearchPostRequest
108
107
  )
109
- pagination_extension = attr.ib(default=TokenPaginationExtension)
108
+ collections_get_request_model: Type[APIRequest] = attr.ib(default=EmptyRequest)
109
+ collection_get_request_model: Type[APIRequest] = attr.ib(default=CollectionUri)
110
+ items_get_request_model: Type[APIRequest] = attr.ib(default=ItemCollectionUri)
111
+ item_get_request_model: Type[APIRequest] = attr.ib(default=ItemUri)
110
112
  response_class: Type[Response] = attr.ib(default=JSONResponse)
111
- middlewares: List = attr.ib(
113
+ middlewares: List[Middleware] = attr.ib(
112
114
  default=attr.Factory(
113
- lambda: [BrotliMiddleware, CORSMiddleware, ProxyHeaderMiddleware]
115
+ lambda: [
116
+ Middleware(BrotliMiddleware),
117
+ Middleware(CORSMiddleware),
118
+ Middleware(ProxyHeaderMiddleware),
119
+ ]
114
120
  )
115
121
  )
116
122
  route_dependencies: List[Tuple[List[Scope], List[Depends]]] = attr.ib(default=[])
@@ -138,9 +144,17 @@ class StacApi:
138
144
  self.router.add_api_route(
139
145
  name="Landing Page",
140
146
  path="/",
141
- response_model=LandingPage
142
- if self.settings.enable_response_models
143
- else None,
147
+ response_model=(
148
+ api.LandingPage if self.settings.enable_response_models else None
149
+ ),
150
+ responses={
151
+ 200: {
152
+ "content": {
153
+ MimeTypes.json.value: {},
154
+ },
155
+ "model": api.LandingPage,
156
+ },
157
+ },
144
158
  response_class=self.response_class,
145
159
  response_model_exclude_unset=False,
146
160
  response_model_exclude_none=True,
@@ -157,9 +171,17 @@ class StacApi:
157
171
  self.router.add_api_route(
158
172
  name="Conformance Classes",
159
173
  path="/conformance",
160
- response_model=ConformanceClasses
161
- if self.settings.enable_response_models
162
- else None,
174
+ response_model=(
175
+ api.Conformance if self.settings.enable_response_models else None
176
+ ),
177
+ responses={
178
+ 200: {
179
+ "content": {
180
+ MimeTypes.json.value: {},
181
+ },
182
+ "model": api.Conformance,
183
+ },
184
+ },
163
185
  response_class=self.response_class,
164
186
  response_model_exclude_unset=True,
165
187
  response_model_exclude_none=True,
@@ -176,12 +198,22 @@ class StacApi:
176
198
  self.router.add_api_route(
177
199
  name="Get Item",
178
200
  path="/collections/{collection_id}/items/{item_id}",
179
- response_model=Item if self.settings.enable_response_models else None,
201
+ response_model=api.Item if self.settings.enable_response_models else None,
202
+ responses={
203
+ 200: {
204
+ "content": {
205
+ MimeTypes.geojson.value: {},
206
+ },
207
+ "model": api.Item,
208
+ },
209
+ },
180
210
  response_class=GeoJSONResponse,
181
211
  response_model_exclude_unset=True,
182
212
  response_model_exclude_none=True,
183
213
  methods=["GET"],
184
- endpoint=create_async_endpoint(self.client.get_item, ItemUri),
214
+ endpoint=create_async_endpoint(
215
+ self.client.get_item, self.item_get_request_model
216
+ ),
185
217
  )
186
218
 
187
219
  def register_post_search(self):
@@ -190,13 +222,20 @@ class StacApi:
190
222
  Returns:
191
223
  None
192
224
  """
193
- fields_ext = self.get_extension(FieldsExtension)
194
225
  self.router.add_api_route(
195
226
  name="Search",
196
227
  path="/search",
197
- response_model=(ItemCollection if not fields_ext else None)
228
+ response_model=api.ItemCollection
198
229
  if self.settings.enable_response_models
199
230
  else None,
231
+ responses={
232
+ 200: {
233
+ "content": {
234
+ MimeTypes.geojson.value: {},
235
+ },
236
+ "model": api.ItemCollection,
237
+ },
238
+ },
200
239
  response_class=GeoJSONResponse,
201
240
  response_model_exclude_unset=True,
202
241
  response_model_exclude_none=True,
@@ -212,13 +251,20 @@ class StacApi:
212
251
  Returns:
213
252
  None
214
253
  """
215
- fields_ext = self.get_extension(FieldsExtension)
216
254
  self.router.add_api_route(
217
255
  name="Search",
218
256
  path="/search",
219
- response_model=(ItemCollection if not fields_ext else None)
257
+ response_model=api.ItemCollection
220
258
  if self.settings.enable_response_models
221
259
  else None,
260
+ responses={
261
+ 200: {
262
+ "content": {
263
+ MimeTypes.geojson.value: {},
264
+ },
265
+ "model": api.ItemCollection,
266
+ },
267
+ },
222
268
  response_class=GeoJSONResponse,
223
269
  response_model_exclude_unset=True,
224
270
  response_model_exclude_none=True,
@@ -237,14 +283,24 @@ class StacApi:
237
283
  self.router.add_api_route(
238
284
  name="Get Collections",
239
285
  path="/collections",
240
- response_model=Collections
241
- if self.settings.enable_response_models
242
- else None,
286
+ response_model=(
287
+ Collections if self.settings.enable_response_models else None
288
+ ),
289
+ responses={
290
+ 200: {
291
+ "content": {
292
+ MimeTypes.json.value: {},
293
+ },
294
+ "model": Collections,
295
+ },
296
+ },
243
297
  response_class=self.response_class,
244
298
  response_model_exclude_unset=True,
245
299
  response_model_exclude_none=True,
246
300
  methods=["GET"],
247
- endpoint=create_async_endpoint(self.client.all_collections, EmptyRequest),
301
+ endpoint=create_async_endpoint(
302
+ self.client.all_collections, self.collections_get_request_model
303
+ ),
248
304
  )
249
305
 
250
306
  def register_get_collection(self):
@@ -256,12 +312,24 @@ class StacApi:
256
312
  self.router.add_api_route(
257
313
  name="Get Collection",
258
314
  path="/collections/{collection_id}",
259
- response_model=Collection if self.settings.enable_response_models else None,
315
+ response_model=api.Collection
316
+ if self.settings.enable_response_models
317
+ else None,
318
+ responses={
319
+ 200: {
320
+ "content": {
321
+ MimeTypes.json.value: {},
322
+ },
323
+ "model": api.Collection,
324
+ },
325
+ },
260
326
  response_class=self.response_class,
261
327
  response_model_exclude_unset=True,
262
328
  response_model_exclude_none=True,
263
329
  methods=["GET"],
264
- endpoint=create_async_endpoint(self.client.get_collection, CollectionUri),
330
+ endpoint=create_async_endpoint(
331
+ self.client.get_collection, self.collection_get_request_model
332
+ ),
265
333
  )
266
334
 
267
335
  def register_get_item_collection(self):
@@ -270,27 +338,27 @@ class StacApi:
270
338
  Returns:
271
339
  None
272
340
  """
273
- pagination_extension = self.get_extension(self.pagination_extension)
274
- if pagination_extension is not None:
275
- mixins = [pagination_extension.GET]
276
- else:
277
- mixins = None
278
- request_model = create_request_model(
279
- "ItemCollectionURI",
280
- base_model=ItemCollectionUri,
281
- mixins=mixins,
282
- )
283
341
  self.router.add_api_route(
284
342
  name="Get ItemCollection",
285
343
  path="/collections/{collection_id}/items",
286
- response_model=ItemCollection
287
- if self.settings.enable_response_models
288
- else None,
344
+ response_model=(
345
+ api.ItemCollection if self.settings.enable_response_models else None
346
+ ),
347
+ responses={
348
+ 200: {
349
+ "content": {
350
+ MimeTypes.geojson.value: {},
351
+ },
352
+ "model": api.ItemCollection,
353
+ },
354
+ },
289
355
  response_class=GeoJSONResponse,
290
356
  response_model_exclude_unset=True,
291
357
  response_model_exclude_none=True,
292
358
  methods=["GET"],
293
- endpoint=create_async_endpoint(self.client.item_collection, request_model),
359
+ endpoint=create_async_endpoint(
360
+ self.client.item_collection, self.items_get_request_model
361
+ ),
294
362
  )
295
363
 
296
364
  def register_core(self):
@@ -377,10 +445,6 @@ class StacApi:
377
445
  self.client.title = self.title
378
446
  self.client.description = self.description
379
447
 
380
- fields_ext = self.get_extension(FieldsExtension)
381
- if fields_ext:
382
- self.settings.default_includes = fields_ext.default_includes
383
-
384
448
  Settings.set(self.settings)
385
449
  self.app.state.settings = self.settings
386
450
 
@@ -406,8 +470,11 @@ class StacApi:
406
470
  self.app.openapi = self.customize_openapi
407
471
 
408
472
  # add middlewares
473
+ if self.middlewares and self.app.middleware_stack is not None:
474
+ raise RuntimeError("Cannot add middleware after an application has started")
475
+
409
476
  for middleware in self.middlewares:
410
- self.app.add_middleware(middleware)
477
+ self.app.user_middleware.insert(0, middleware)
411
478
 
412
479
  # customize route dependencies
413
480
  for scopes, dependencies in self.route_dependencies:
@@ -1,4 +1,5 @@
1
1
  """Application settings."""
2
+
2
3
  import enum
3
4
 
4
5
 
@@ -11,12 +12,14 @@ class ApiExtensions(enum.Enum):
11
12
  Ref: https://github.com/stac-api-extensions
12
13
  """
13
14
 
14
- context = "context"
15
15
  fields = "fields"
16
16
  filter = "filter"
17
17
  query = "query"
18
18
  sort = "sort"
19
19
  transaction = "transaction"
20
+ aggregation = "aggregation"
21
+ collection_search = "collection-search"
22
+ free_text = "free-text"
20
23
 
21
24
 
22
25
  class AddOns(enum.Enum):
@@ -4,7 +4,7 @@ import logging
4
4
  from typing import Callable, Dict, Type, TypedDict
5
5
 
6
6
  from fastapi import FastAPI
7
- from fastapi.exceptions import RequestValidationError
7
+ from fastapi.exceptions import RequestValidationError, ResponseValidationError
8
8
  from starlette import status
9
9
  from starlette.requests import Request
10
10
  from starlette.responses import JSONResponse
@@ -27,6 +27,7 @@ DEFAULT_STATUS_CODES = {
27
27
  DatabaseError: status.HTTP_424_FAILED_DEPENDENCY,
28
28
  Exception: status.HTTP_500_INTERNAL_SERVER_ERROR,
29
29
  InvalidQueryParameter: status.HTTP_400_BAD_REQUEST,
30
+ ResponseValidationError: status.HTTP_500_INTERNAL_SERVER_ERROR,
30
31
  }
31
32
 
32
33
 
@@ -1,4 +1,5 @@
1
1
  """Api middleware."""
2
+
2
3
  import re
3
4
  import typing
4
5
  from http.client import HTTP_PORT, HTTPS_PORT
@@ -0,0 +1,138 @@
1
+ """Api request/response models."""
2
+
3
+ from typing import List, Optional, Type, Union
4
+
5
+ import attr
6
+ from fastapi import Path, Query
7
+ from pydantic import BaseModel, create_model
8
+ from stac_pydantic.shared import BBox
9
+ from typing_extensions import Annotated
10
+
11
+ from stac_fastapi.types.extension import ApiExtension
12
+ from stac_fastapi.types.rfc3339 import DateTimeType
13
+ from stac_fastapi.types.search import (
14
+ APIRequest,
15
+ BaseSearchGetRequest,
16
+ BaseSearchPostRequest,
17
+ Limit,
18
+ _bbox_converter,
19
+ _datetime_converter,
20
+ )
21
+
22
+ try:
23
+ import orjson # noqa
24
+ from fastapi.responses import ORJSONResponse as JSONResponse
25
+ except ImportError: # pragma: nocover
26
+ from starlette.responses import JSONResponse
27
+
28
+
29
+ def create_request_model(
30
+ model_name="SearchGetRequest",
31
+ base_model: Union[Type[BaseModel], APIRequest] = BaseSearchGetRequest,
32
+ extensions: Optional[List[ApiExtension]] = None,
33
+ mixins: Optional[Union[List[BaseModel], List[APIRequest]]] = None,
34
+ request_type: Optional[str] = "GET",
35
+ ) -> Union[Type[BaseModel], APIRequest]:
36
+ """Create a pydantic model for validating request bodies."""
37
+ fields = {}
38
+ extension_models = []
39
+
40
+ # Check extensions for additional parameters to search
41
+ for extension in extensions or []:
42
+ if extension_model := extension.get_request_model(request_type):
43
+ extension_models.append(extension_model)
44
+
45
+ mixins = mixins or []
46
+
47
+ models = [base_model] + extension_models + mixins
48
+
49
+ # Handle GET requests
50
+ if all([issubclass(m, APIRequest) for m in models]):
51
+ return attr.make_class(model_name, attrs={}, bases=tuple(models))
52
+
53
+ # Handle POST requests
54
+ elif all([issubclass(m, BaseModel) for m in models]):
55
+ for model in models:
56
+ for k, field_info in model.model_fields.items():
57
+ fields[k] = (field_info.annotation, field_info)
58
+ return create_model(model_name, **fields, __base__=base_model)
59
+
60
+ raise TypeError("Mixed Request Model types. Check extension request types.")
61
+
62
+
63
+ def create_get_request_model(
64
+ extensions: Optional[List[ApiExtension]],
65
+ base_model: BaseSearchGetRequest = BaseSearchGetRequest,
66
+ ) -> APIRequest:
67
+ """Wrap create_request_model to create the GET request model."""
68
+
69
+ return create_request_model(
70
+ "SearchGetRequest",
71
+ base_model=base_model,
72
+ extensions=extensions,
73
+ request_type="GET",
74
+ )
75
+
76
+
77
+ def create_post_request_model(
78
+ extensions: Optional[List[ApiExtension]],
79
+ base_model: BaseSearchPostRequest = BaseSearchPostRequest,
80
+ ) -> Type[BaseModel]:
81
+ """Wrap create_request_model to create the POST request model."""
82
+ return create_request_model(
83
+ "SearchPostRequest",
84
+ base_model=base_model,
85
+ extensions=extensions,
86
+ request_type="POST",
87
+ )
88
+
89
+
90
+ @attr.s
91
+ class CollectionUri(APIRequest):
92
+ """Get or delete collection."""
93
+
94
+ collection_id: Annotated[str, Path(description="Collection ID")] = attr.ib()
95
+
96
+
97
+ @attr.s
98
+ class ItemUri(APIRequest):
99
+ """Get or delete item."""
100
+
101
+ collection_id: Annotated[str, Path(description="Collection ID")] = attr.ib()
102
+ item_id: Annotated[str, Path(description="Item ID")] = attr.ib()
103
+
104
+
105
+ @attr.s
106
+ class EmptyRequest(APIRequest):
107
+ """Empty request."""
108
+
109
+ ...
110
+
111
+
112
+ @attr.s
113
+ class ItemCollectionUri(APIRequest):
114
+ """Get item collection."""
115
+
116
+ collection_id: Annotated[str, Path(description="Collection ID")] = attr.ib()
117
+ limit: Annotated[
118
+ Optional[Limit],
119
+ Query(
120
+ description="Limits the number of results that are included in each page of the response (capped to 10_000)." # noqa: E501
121
+ ),
122
+ ] = attr.ib(default=10)
123
+ bbox: Optional[BBox] = attr.ib(default=None, converter=_bbox_converter)
124
+ datetime: Optional[DateTimeType] = attr.ib(
125
+ default=None, converter=_datetime_converter
126
+ )
127
+
128
+
129
+ class GeoJSONResponse(JSONResponse):
130
+ """JSON with custom, vendor content-type."""
131
+
132
+ media_type = "application/geo+json"
133
+
134
+
135
+ class JSONSchemaResponse(JSONResponse):
136
+ """JSON with custom, vendor content-type."""
137
+
138
+ media_type = "application/schema+json"
@@ -0,0 +1,37 @@
1
+ """openapi."""
2
+
3
+ from fastapi import FastAPI
4
+ from starlette.requests import Request
5
+ from starlette.responses import JSONResponse, Response
6
+ from starlette.routing import Route, request_response
7
+
8
+
9
+ def update_openapi(app: FastAPI) -> FastAPI:
10
+ """Update OpenAPI response content-type.
11
+
12
+ This function modifies the openapi route to comply with the STAC API spec's required
13
+ content-type response header.
14
+ """
15
+ # Find the route for the openapi_url in the app
16
+ openapi_route: Route = next(
17
+ route for route in app.router.routes if route.path == app.openapi_url
18
+ )
19
+ # Store the old endpoint function so we can call it from the patched function
20
+ old_endpoint = openapi_route.endpoint
21
+
22
+ # Create a patched endpoint function that modifies the content type of the response
23
+ async def patched_openapi_endpoint(req: Request) -> Response:
24
+ # Get the response from the old endpoint function
25
+ response: JSONResponse = await old_endpoint(req)
26
+ # Update the content type header in place
27
+ response.headers["content-type"] = "application/vnd.oai.openapi+json;version=3.0"
28
+ # Return the updated response
29
+ return response
30
+
31
+ # When a Route is accessed the `handle` function calls `self.app`. Which is
32
+ # the endpoint function wrapped with `request_response`. So we need to wrap
33
+ # our patched function and replace the existing app with it.
34
+ openapi_route.app = request_response(patched_openapi_endpoint)
35
+
36
+ # return the patched app
37
+ return app
@@ -1,8 +1,8 @@
1
1
  """Route factories."""
2
2
 
3
+ import copy
3
4
  import functools
4
5
  import inspect
5
- import warnings
6
6
  from typing import Any, Callable, Dict, List, Optional, Type, TypedDict, Union
7
7
 
8
8
  from fastapi import Depends, params
@@ -37,19 +37,12 @@ def sync_to_async(func):
37
37
  def create_async_endpoint(
38
38
  func: Callable,
39
39
  request_model: Union[Type[APIRequest], Type[BaseModel], Dict],
40
- response_class: Optional[Type[Response]] = None,
41
40
  ):
42
41
  """Wrap a function in a coroutine which may be used to create a FastAPI endpoint.
43
42
 
44
43
  Synchronous functions are executed asynchronously using a background thread.
45
44
  """
46
45
 
47
- if response_class:
48
- warnings.warn(
49
- "`response_class` option is deprecated, please set the Response class directly in the endpoint.", # noqa: E501
50
- DeprecationWarning,
51
- )
52
-
53
46
  if not inspect.iscoroutinefunction(func):
54
47
  func = sync_to_async(func)
55
48
 
@@ -100,15 +93,28 @@ def add_route_dependencies(
100
93
  Allows a developer to add dependencies to a route after the route has been
101
94
  defined.
102
95
 
96
+ "*" can be used for path or method to match all allowed routes.
97
+
103
98
  Returns:
104
99
  None
105
100
  """
106
101
  for scope in scopes:
102
+ _scope = copy.deepcopy(scope)
107
103
  for route in routes:
108
- match, _ = route.matches({"type": "http", **scope})
104
+ if scope["path"] == "*":
105
+ _scope["path"] = route.path
106
+
107
+ if scope["method"] == "*":
108
+ _scope["method"] = list(route.methods)[0]
109
+
110
+ match, _ = route.matches({"type": "http", **_scope})
109
111
  if match != Match.FULL:
110
112
  continue
111
113
 
114
+ # Ignore paths without dependants, e.g. /api, /api.html, /docs/oauth2-redirect
115
+ if not hasattr(route, "dependant"):
116
+ continue
117
+
112
118
  # Mimicking how APIRoute handles dependencies:
113
119
  # https://github.com/tiangolo/fastapi/blob/1760da0efa55585c19835d81afa8ca386036c325/fastapi/routing.py#L408-L412
114
120
  for depends in dependencies[::-1]:
@@ -0,0 +1,3 @@
1
+ """Library version."""
2
+
3
+ __version__ = "3.0.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stac-fastapi.api
3
- Version: 2.5.5.post1
3
+ Version: 3.0.0
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
@@ -18,11 +18,8 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: License :: OSI Approved :: MIT License
19
19
  Requires-Python: >=3.8
20
20
  Description-Content-Type: text/markdown
21
- Requires-Dist: attrs
22
- Requires-Dist: pydantic[dotenv]<2
23
- Requires-Dist: stac_pydantic==2.0.*
24
21
  Requires-Dist: brotli_asgi
25
- Requires-Dist: stac-fastapi.types
22
+ Requires-Dist: stac-fastapi.types~=3.0
26
23
  Provides-Extra: dev
27
24
  Requires-Dist: httpx; extra == "dev"
28
25
  Requires-Dist: pytest; extra == "dev"
@@ -30,7 +27,6 @@ Requires-Dist: pytest-cov; extra == "dev"
30
27
  Requires-Dist: pytest-asyncio; extra == "dev"
31
28
  Requires-Dist: pre-commit; extra == "dev"
32
29
  Requires-Dist: requests; extra == "dev"
33
- Requires-Dist: pystac[validation]==1.*; extra == "dev"
34
30
  Provides-Extra: benchmark
35
31
  Requires-Dist: pytest-benchmark; extra == "benchmark"
36
32
  Provides-Extra: docs
@@ -17,5 +17,7 @@ stac_fastapi/api/openapi.py
17
17
  stac_fastapi/api/routes.py
18
18
  stac_fastapi/api/version.py
19
19
  tests/test_api.py
20
+ tests/test_app.py
20
21
  tests/test_app_prefix.py
21
- tests/test_middleware.py
22
+ tests/test_middleware.py
23
+ tests/test_models.py