stac-fastapi.api 2.5.5__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.
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/PKG-INFO +2 -6
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/setup.py +1 -5
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/stac_fastapi/api/app.py +115 -48
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/stac_fastapi/api/config.py +4 -1
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/stac_fastapi/api/errors.py +2 -1
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/stac_fastapi/api/middleware.py +1 -0
- stac_fastapi_api-3.0.0/stac_fastapi/api/models.py +138 -0
- stac_fastapi_api-3.0.0/stac_fastapi/api/openapi.py +37 -0
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/stac_fastapi/api/routes.py +15 -9
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/stac_fastapi/api/version.py +2 -1
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/stac_fastapi.api.egg-info/PKG-INFO +2 -6
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/stac_fastapi.api.egg-info/SOURCES.txt +3 -1
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/stac_fastapi.api.egg-info/requires.txt +1 -5
- stac_fastapi_api-3.0.0/tests/test_api.py +456 -0
- stac_fastapi_api-3.0.0/tests/test_app.py +389 -0
- stac_fastapi_api-3.0.0/tests/test_app_prefix.py +146 -0
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/tests/test_middleware.py +30 -0
- stac_fastapi_api-3.0.0/tests/test_models.py +131 -0
- stac_fastapi_api-2.5.5/stac_fastapi/api/models.py +0 -185
- stac_fastapi_api-2.5.5/stac_fastapi/api/openapi.py +0 -88
- stac_fastapi_api-2.5.5/tests/test_api.py +0 -140
- stac_fastapi_api-2.5.5/tests/test_app_prefix.py +0 -66
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/README.md +0 -0
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/setup.cfg +0 -0
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/stac_fastapi/api/__init__.py +0 -0
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/stac_fastapi.api.egg-info/dependency_links.txt +0 -0
- {stac_fastapi_api-2.5.5 → stac_fastapi_api-3.0.0}/stac_fastapi.api.egg-info/not-zip-safe +0 -0
- {stac_fastapi_api-2.5.5 → 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:
|
|
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
|
|
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
|
|
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=
|
|
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
|
-
|
|
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: [
|
|
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=
|
|
142
|
-
|
|
143
|
-
|
|
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=
|
|
161
|
-
|
|
162
|
-
|
|
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(
|
|
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=
|
|
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=
|
|
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=
|
|
241
|
-
|
|
242
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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=
|
|
287
|
-
|
|
288
|
-
|
|
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(
|
|
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.
|
|
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
|
|
|
@@ -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
|
-
|
|
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]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: stac-fastapi.api
|
|
3
|
-
Version:
|
|
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
|