stac-fastapi.api 2.4.8__tar.gz → 2.5.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 (23) hide show
  1. stac_fastapi_api-2.5.0/PKG-INFO +39 -0
  2. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/setup.py +8 -1
  3. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/stac_fastapi/api/app.py +24 -23
  4. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/stac_fastapi/api/middleware.py +2 -1
  5. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/stac_fastapi/api/models.py +5 -3
  6. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/stac_fastapi/api/routes.py +16 -15
  7. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/stac_fastapi/api/version.py +1 -1
  8. stac_fastapi_api-2.5.0/stac_fastapi.api.egg-info/PKG-INFO +39 -0
  9. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/stac_fastapi.api.egg-info/SOURCES.txt +3 -1
  10. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/stac_fastapi.api.egg-info/requires.txt +4 -1
  11. stac_fastapi_api-2.5.0/tests/test_api.py +140 -0
  12. stac_fastapi_api-2.5.0/tests/test_middleware.py +168 -0
  13. stac-fastapi.api-2.4.8/PKG-INFO +0 -18
  14. stac-fastapi.api-2.4.8/stac_fastapi.api.egg-info/PKG-INFO +0 -18
  15. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/README.md +0 -0
  16. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/setup.cfg +0 -0
  17. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/stac_fastapi/api/__init__.py +0 -0
  18. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/stac_fastapi/api/config.py +0 -0
  19. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/stac_fastapi/api/errors.py +0 -0
  20. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/stac_fastapi/api/openapi.py +0 -0
  21. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/stac_fastapi.api.egg-info/dependency_links.txt +0 -0
  22. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/stac_fastapi.api.egg-info/not-zip-safe +0 -0
  23. {stac-fastapi.api-2.4.8 → stac_fastapi_api-2.5.0}/stac_fastapi.api.egg-info/top_level.txt +0 -0
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.1
2
+ Name: stac-fastapi.api
3
+ Version: 2.5.0
4
+ Summary: An implementation of STAC API based on the FastAPI framework.
5
+ Home-page: https://github.com/stac-utils/stac-fastapi
6
+ Author: Arturo Engineering
7
+ Author-email: engineering@arturo.ai
8
+ License: MIT
9
+ Keywords: STAC FastAPI COG
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Information Technology
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: attrs
22
+ Requires-Dist: pydantic[dotenv]<2
23
+ Requires-Dist: stac_pydantic==2.0.*
24
+ Requires-Dist: brotli_asgi
25
+ Requires-Dist: stac-fastapi.types
26
+ Provides-Extra: dev
27
+ Requires-Dist: httpx; extra == "dev"
28
+ Requires-Dist: pytest; extra == "dev"
29
+ Requires-Dist: pytest-cov; extra == "dev"
30
+ Requires-Dist: pytest-asyncio; extra == "dev"
31
+ Requires-Dist: pre-commit; extra == "dev"
32
+ Requires-Dist: requests; extra == "dev"
33
+ Requires-Dist: pystac[validation]==1.*; extra == "dev"
34
+ Provides-Extra: benchmark
35
+ Requires-Dist: pytest-benchmark; extra == "benchmark"
36
+ Provides-Extra: docs
37
+ Requires-Dist: mkdocs; extra == "docs"
38
+ Requires-Dist: mkdocs-material; extra == "docs"
39
+ Requires-Dist: pdocs; extra == "docs"
@@ -7,7 +7,7 @@ with open("README.md") as f:
7
7
 
8
8
  install_requires = [
9
9
  "attrs",
10
- "pydantic[dotenv]",
10
+ "pydantic[dotenv]<2",
11
11
  "stac_pydantic==2.0.*",
12
12
  "brotli_asgi",
13
13
  "stac-fastapi.types",
@@ -23,6 +23,9 @@ extra_reqs = {
23
23
  "requests",
24
24
  "pystac[validation]==1.*",
25
25
  ],
26
+ "benchmark": [
27
+ "pytest-benchmark",
28
+ ],
26
29
  "docs": ["mkdocs", "mkdocs-material", "pdocs"],
27
30
  }
28
31
 
@@ -38,6 +41,10 @@ setup(
38
41
  "Intended Audience :: Information Technology",
39
42
  "Intended Audience :: Science/Research",
40
43
  "Programming Language :: Python :: 3.8",
44
+ "Programming Language :: Python :: 3.9",
45
+ "Programming Language :: Python :: 3.10",
46
+ "Programming Language :: Python :: 3.11",
47
+ "Programming Language :: Python :: 3.12",
41
48
  "License :: OSI Approved :: MIT License",
42
49
  ],
43
50
  keywords="STAC FastAPI COG",
@@ -1,4 +1,5 @@
1
1
  """Fastapi app creation."""
2
+
2
3
  from typing import Any, Dict, List, Optional, Tuple, Type, Union
3
4
 
4
5
  import attr
@@ -83,10 +84,22 @@ class StacApi:
83
84
  converter=update_openapi,
84
85
  )
85
86
  router: APIRouter = attr.ib(default=attr.Factory(APIRouter))
86
- title: str = attr.ib(default="stac-fastapi")
87
- api_version: str = attr.ib(default="0.1")
87
+ title: str = attr.ib(
88
+ default=attr.Factory(
89
+ lambda self: self.settings.stac_fastapi_title, takes_self=True
90
+ )
91
+ )
92
+ api_version: str = attr.ib(
93
+ default=attr.Factory(
94
+ lambda self: self.settings.stac_fastapi_version, takes_self=True
95
+ )
96
+ )
88
97
  stac_version: str = attr.ib(default=STAC_VERSION)
89
- description: str = attr.ib(default="stac-fastapi")
98
+ description: str = attr.ib(
99
+ default=attr.Factory(
100
+ lambda self: self.settings.stac_fastapi_description, takes_self=True
101
+ )
102
+ )
90
103
  search_get_request_model: Type[BaseSearchGetRequest] = attr.ib(
91
104
  default=BaseSearchGetRequest
92
105
  )
@@ -132,9 +145,7 @@ class StacApi:
132
145
  response_model_exclude_unset=False,
133
146
  response_model_exclude_none=True,
134
147
  methods=["GET"],
135
- endpoint=create_async_endpoint(
136
- self.client.landing_page, EmptyRequest, self.response_class
137
- ),
148
+ endpoint=create_async_endpoint(self.client.landing_page, EmptyRequest),
138
149
  )
139
150
 
140
151
  def register_conformance_classes(self):
@@ -153,9 +164,7 @@ class StacApi:
153
164
  response_model_exclude_unset=True,
154
165
  response_model_exclude_none=True,
155
166
  methods=["GET"],
156
- endpoint=create_async_endpoint(
157
- self.client.conformance, EmptyRequest, self.response_class
158
- ),
167
+ endpoint=create_async_endpoint(self.client.conformance, EmptyRequest),
159
168
  )
160
169
 
161
170
  def register_get_item(self):
@@ -172,9 +181,7 @@ class StacApi:
172
181
  response_model_exclude_unset=True,
173
182
  response_model_exclude_none=True,
174
183
  methods=["GET"],
175
- endpoint=create_async_endpoint(
176
- self.client.get_item, ItemUri, GeoJSONResponse
177
- ),
184
+ endpoint=create_async_endpoint(self.client.get_item, ItemUri),
178
185
  )
179
186
 
180
187
  def register_post_search(self):
@@ -195,7 +202,7 @@ class StacApi:
195
202
  response_model_exclude_none=True,
196
203
  methods=["POST"],
197
204
  endpoint=create_async_endpoint(
198
- self.client.post_search, self.search_post_request_model, GeoJSONResponse
205
+ self.client.post_search, self.search_post_request_model
199
206
  ),
200
207
  )
201
208
 
@@ -217,7 +224,7 @@ class StacApi:
217
224
  response_model_exclude_none=True,
218
225
  methods=["GET"],
219
226
  endpoint=create_async_endpoint(
220
- self.client.get_search, self.search_get_request_model, GeoJSONResponse
227
+ self.client.get_search, self.search_get_request_model
221
228
  ),
222
229
  )
223
230
 
@@ -237,9 +244,7 @@ class StacApi:
237
244
  response_model_exclude_unset=True,
238
245
  response_model_exclude_none=True,
239
246
  methods=["GET"],
240
- endpoint=create_async_endpoint(
241
- self.client.all_collections, EmptyRequest, self.response_class
242
- ),
247
+ endpoint=create_async_endpoint(self.client.all_collections, EmptyRequest),
243
248
  )
244
249
 
245
250
  def register_get_collection(self):
@@ -256,9 +261,7 @@ class StacApi:
256
261
  response_model_exclude_unset=True,
257
262
  response_model_exclude_none=True,
258
263
  methods=["GET"],
259
- endpoint=create_async_endpoint(
260
- self.client.get_collection, CollectionUri, self.response_class
261
- ),
264
+ endpoint=create_async_endpoint(self.client.get_collection, CollectionUri),
262
265
  )
263
266
 
264
267
  def register_get_item_collection(self):
@@ -287,9 +290,7 @@ class StacApi:
287
290
  response_model_exclude_unset=True,
288
291
  response_model_exclude_none=True,
289
292
  methods=["GET"],
290
- endpoint=create_async_endpoint(
291
- self.client.item_collection, request_model, GeoJSONResponse
292
- ),
293
+ endpoint=create_async_endpoint(self.client.item_collection, request_model),
293
294
  )
294
295
 
295
296
  def register_core(self):
@@ -10,7 +10,7 @@ from starlette.types import ASGIApp, Receive, Scope, Send
10
10
 
11
11
  class CORSMiddleware(_CORSMiddleware):
12
12
  """Subclass of Starlette's standard CORS middleware with default values set to those
13
- reccomended by the STAC API spec.
13
+ recommended by the STAC API spec.
14
14
 
15
15
  https://github.com/radiantearth/stac-api-spec/blob/914cf8108302e2ec734340080a45aaae4859bb63/implementation.md#cors
16
16
  """
@@ -103,6 +103,7 @@ class ProxyHeaderMiddleware:
103
103
  # ignore ports that are not valid integers
104
104
  pass
105
105
  else:
106
+ domain = self._get_header_value_by_name(scope, "x-forwarded-host", domain)
106
107
  proto = self._get_header_value_by_name(scope, "x-forwarded-proto", proto)
107
108
  port_str = self._get_header_value_by_name(scope, "x-forwarded-port", port)
108
109
  try:
@@ -7,13 +7,15 @@ import attr
7
7
  from fastapi import Body, Path
8
8
  from pydantic import BaseModel, create_model
9
9
  from pydantic.fields import UndefinedType
10
+ from stac_pydantic.shared import BBox
10
11
 
11
12
  from stac_fastapi.types.extension import ApiExtension
13
+ from stac_fastapi.types.rfc3339 import DateTimeType
12
14
  from stac_fastapi.types.search import (
13
15
  APIRequest,
14
16
  BaseSearchGetRequest,
15
17
  BaseSearchPostRequest,
16
- str2list,
18
+ str2bbox,
17
19
  )
18
20
 
19
21
 
@@ -124,8 +126,8 @@ class ItemCollectionUri(CollectionUri):
124
126
  """Get item collection."""
125
127
 
126
128
  limit: int = attr.ib(default=10)
127
- bbox: Optional[str] = attr.ib(default=None, converter=str2list)
128
- datetime: Optional[str] = attr.ib(default=None)
129
+ bbox: Optional[BBox] = attr.ib(default=None, converter=str2bbox)
130
+ datetime: Optional[DateTimeType] = attr.ib(default=None)
129
131
 
130
132
 
131
133
  class POSTTokenPagination(BaseModel):
@@ -1,6 +1,8 @@
1
1
  """Route factories."""
2
+
2
3
  import functools
3
4
  import inspect
5
+ import warnings
4
6
  from typing import Any, Callable, Dict, List, Optional, Type, TypedDict, Union
5
7
 
6
8
  from fastapi import Depends, params
@@ -8,18 +10,16 @@ from fastapi.dependencies.utils import get_parameterless_sub_dependant
8
10
  from pydantic import BaseModel
9
11
  from starlette.concurrency import run_in_threadpool
10
12
  from starlette.requests import Request
11
- from starlette.responses import JSONResponse, Response
13
+ from starlette.responses import Response
12
14
  from starlette.routing import BaseRoute, Match
13
15
  from starlette.status import HTTP_204_NO_CONTENT
14
16
 
15
17
  from stac_fastapi.api.models import APIRequest
16
18
 
17
19
 
18
- def _wrap_response(resp: Any, response_class: Type[Response]) -> Response:
19
- if isinstance(resp, Response):
20
+ def _wrap_response(resp: Any) -> Any:
21
+ if resp is not None:
20
22
  return resp
21
- elif resp is not None:
22
- return response_class(resp)
23
23
  else: # None is returned as 204 No Content
24
24
  return Response(status_code=HTTP_204_NO_CONTENT)
25
25
 
@@ -37,12 +37,19 @@ 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: Type[Response] = JSONResponse,
40
+ response_class: Optional[Type[Response]] = None,
41
41
  ):
42
42
  """Wrap a function in a coroutine which may be used to create a FastAPI endpoint.
43
43
 
44
44
  Synchronous functions are executed asynchronously using a background thread.
45
45
  """
46
+
47
+ if response_class:
48
+ warnings.warns(
49
+ "`response_class` option is deprecated, please set the Response class directly in the endpoint.", # noqa: E501
50
+ DeprecationWarning,
51
+ )
52
+
46
53
  if not inspect.iscoroutinefunction(func):
47
54
  func = sync_to_async(func)
48
55
 
@@ -53,9 +60,7 @@ def create_async_endpoint(
53
60
  request_data: request_model = Depends(), # type:ignore
54
61
  ):
55
62
  """Endpoint."""
56
- return _wrap_response(
57
- await func(request=request, **request_data.kwargs()), response_class
58
- )
63
+ return _wrap_response(await func(request=request, **request_data.kwargs()))
59
64
 
60
65
  elif issubclass(request_model, BaseModel):
61
66
 
@@ -64,9 +69,7 @@ def create_async_endpoint(
64
69
  request_data: request_model, # type:ignore
65
70
  ):
66
71
  """Endpoint."""
67
- return _wrap_response(
68
- await func(request_data, request=request), response_class
69
- )
72
+ return _wrap_response(await func(request_data, request=request))
70
73
 
71
74
  else:
72
75
 
@@ -75,9 +78,7 @@ def create_async_endpoint(
75
78
  request_data: Dict[str, Any], # type:ignore
76
79
  ):
77
80
  """Endpoint."""
78
- return _wrap_response(
79
- await func(request_data, request=request), response_class
80
- )
81
+ return _wrap_response(await func(request_data, request=request))
81
82
 
82
83
  return _endpoint
83
84
 
@@ -1,2 +1,2 @@
1
1
  """Library version."""
2
- __version__ = "2.4.8"
2
+ __version__ = "2.5.0"
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.1
2
+ Name: stac-fastapi.api
3
+ Version: 2.5.0
4
+ Summary: An implementation of STAC API based on the FastAPI framework.
5
+ Home-page: https://github.com/stac-utils/stac-fastapi
6
+ Author: Arturo Engineering
7
+ Author-email: engineering@arturo.ai
8
+ License: MIT
9
+ Keywords: STAC FastAPI COG
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Information Technology
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: attrs
22
+ Requires-Dist: pydantic[dotenv]<2
23
+ Requires-Dist: stac_pydantic==2.0.*
24
+ Requires-Dist: brotli_asgi
25
+ Requires-Dist: stac-fastapi.types
26
+ Provides-Extra: dev
27
+ Requires-Dist: httpx; extra == "dev"
28
+ Requires-Dist: pytest; extra == "dev"
29
+ Requires-Dist: pytest-cov; extra == "dev"
30
+ Requires-Dist: pytest-asyncio; extra == "dev"
31
+ Requires-Dist: pre-commit; extra == "dev"
32
+ Requires-Dist: requests; extra == "dev"
33
+ Requires-Dist: pystac[validation]==1.*; extra == "dev"
34
+ Provides-Extra: benchmark
35
+ Requires-Dist: pytest-benchmark; extra == "benchmark"
36
+ Provides-Extra: docs
37
+ Requires-Dist: mkdocs; extra == "docs"
38
+ Requires-Dist: mkdocs-material; extra == "docs"
39
+ Requires-Dist: pdocs; extra == "docs"
@@ -15,4 +15,6 @@ stac_fastapi/api/middleware.py
15
15
  stac_fastapi/api/models.py
16
16
  stac_fastapi/api/openapi.py
17
17
  stac_fastapi/api/routes.py
18
- stac_fastapi/api/version.py
18
+ stac_fastapi/api/version.py
19
+ tests/test_api.py
20
+ tests/test_middleware.py
@@ -1,9 +1,12 @@
1
1
  attrs
2
- pydantic[dotenv]
2
+ pydantic[dotenv]<2
3
3
  stac_pydantic==2.0.*
4
4
  brotli_asgi
5
5
  stac-fastapi.types
6
6
 
7
+ [benchmark]
8
+ pytest-benchmark
9
+
7
10
  [dev]
8
11
  httpx
9
12
  pytest
@@ -0,0 +1,140 @@
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='{"dummy": "payload"}',
45
+ headers={"content-type": "application/json"},
46
+ )
47
+ assert (
48
+ response.status_code == 200
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):
62
+ routes = [
63
+ {"path": "/collections", "method": "POST"},
64
+ {"path": "/collections/{collectionId}", "method": "PUT"},
65
+ {"path": "/collections/{collectionId}", "method": "DELETE"},
66
+ {"path": "/collections/{collectionId}/items", "method": "POST"},
67
+ {"path": "/collections/{collectionId}/items/{itemId}", "method": "PUT"},
68
+ {"path": "/collections/{collectionId}/items/{itemId}", "method": "DELETE"},
69
+ ]
70
+ dependencies = [Depends(must_be_bob)]
71
+ api = self._build_api(route_dependencies=[(routes, dependencies)])
72
+ self._assert_dependency_applied(api, routes)
73
+
74
+ def test_add_route_dependencies_after_building_api(self):
75
+ routes = [
76
+ {"path": "/collections", "method": "POST"},
77
+ {"path": "/collections/{collectionId}", "method": "PUT"},
78
+ {"path": "/collections/{collectionId}", "method": "DELETE"},
79
+ {"path": "/collections/{collectionId}/items", "method": "POST"},
80
+ {"path": "/collections/{collectionId}/items/{itemId}", "method": "PUT"},
81
+ {"path": "/collections/{collectionId}/items/{itemId}", "method": "DELETE"},
82
+ ]
83
+ api = self._build_api()
84
+ api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)])
85
+ self._assert_dependency_applied(api, routes)
86
+
87
+
88
+ class DummyCoreClient(core.BaseCoreClient):
89
+ def all_collections(self, *args, **kwargs):
90
+ ...
91
+
92
+ def get_collection(self, *args, **kwargs):
93
+ ...
94
+
95
+ def get_item(self, *args, **kwargs):
96
+ ...
97
+
98
+ def get_search(self, *args, **kwargs):
99
+ ...
100
+
101
+ def post_search(self, *args, **kwargs):
102
+ ...
103
+
104
+ def item_collection(self, *args, **kwargs):
105
+ ...
106
+
107
+
108
+ class DummyTransactionsClient(core.BaseTransactionsClient):
109
+ """Defines a pattern for implementing the STAC transaction extension."""
110
+
111
+ def create_item(self, *args, **kwargs):
112
+ return "dummy response"
113
+
114
+ def update_item(self, *args, **kwargs):
115
+ return "dummy response"
116
+
117
+ def delete_item(self, *args, **kwargs):
118
+ return "dummy response"
119
+
120
+ def create_collection(self, *args, **kwargs):
121
+ return "dummy response"
122
+
123
+ def update_collection(self, *args, **kwargs):
124
+ return "dummy response"
125
+
126
+ def delete_collection(self, *args, **kwargs):
127
+ return "dummy response"
128
+
129
+
130
+ def must_be_bob(
131
+ credentials: security.HTTPBasicCredentials = Depends(security.HTTPBasic()),
132
+ ):
133
+ if credentials.username == "bob":
134
+ return True
135
+
136
+ raise HTTPException(
137
+ status_code=status.HTTP_401_UNAUTHORIZED,
138
+ detail="You're not Bob",
139
+ headers={"WWW-Authenticate": "Basic"},
140
+ )
@@ -0,0 +1,168 @@
1
+ from unittest import mock
2
+
3
+ import pytest
4
+ from starlette.applications import Starlette
5
+ from starlette.testclient import TestClient
6
+
7
+ from stac_fastapi.api.app import StacApi
8
+ from stac_fastapi.api.middleware import ProxyHeaderMiddleware
9
+ from stac_fastapi.types.config import ApiSettings
10
+ from stac_fastapi.types.core import BaseCoreClient
11
+
12
+
13
+ @pytest.fixture
14
+ def proxy_header_middleware() -> ProxyHeaderMiddleware:
15
+ app = Starlette()
16
+ return ProxyHeaderMiddleware(app)
17
+
18
+
19
+ @pytest.fixture
20
+ def test_client() -> TestClient:
21
+ app = StacApi(settings=ApiSettings(), client=mock.create_autospec(BaseCoreClient))
22
+ with TestClient(app.app) as client:
23
+ yield client
24
+
25
+
26
+ @pytest.mark.parametrize(
27
+ "headers,key,expected",
28
+ [
29
+ ([(b"host", b"testserver")], "host", "testserver"),
30
+ ([(b"host", b"testserver")], "user-agent", None),
31
+ (
32
+ [(b"host", b"testserver"), (b"accept-encoding", b"gzip, deflate, br")],
33
+ "accept-encoding",
34
+ "gzip, deflate, br",
35
+ ),
36
+ ],
37
+ )
38
+ def test_get_header_value_by_name(
39
+ proxy_header_middleware: ProxyHeaderMiddleware, headers, key, expected
40
+ ):
41
+ scope = {"headers": headers}
42
+ actual = proxy_header_middleware._get_header_value_by_name(scope, key)
43
+ assert actual == expected
44
+
45
+
46
+ @pytest.mark.parametrize(
47
+ "headers,key,value",
48
+ [
49
+ ([(b"host", b"testserver")], "host", "another-server"),
50
+ ([(b"host", b"testserver")], "user-agent", "agent"),
51
+ (
52
+ [(b"host", b"testserver"), (b"accept-encoding", b"gzip, deflate, br")],
53
+ "accept-encoding",
54
+ "deflate",
55
+ ),
56
+ ],
57
+ )
58
+ def test_replace_header_value_by_name(
59
+ proxy_header_middleware: ProxyHeaderMiddleware, headers, key, value
60
+ ):
61
+ scope = {"headers": headers}
62
+ updated_headers = proxy_header_middleware._replace_header_value_by_name(
63
+ scope, key, value
64
+ )
65
+
66
+ header_value = proxy_header_middleware._get_header_value_by_name(
67
+ {"headers": updated_headers}, key
68
+ )
69
+ assert header_value == value
70
+
71
+
72
+ @pytest.mark.parametrize(
73
+ "scope,expected",
74
+ [
75
+ (
76
+ {"scheme": "https", "server": ["testserver", 80], "headers": []},
77
+ ("https", "testserver", 80),
78
+ ),
79
+ (
80
+ {
81
+ "scheme": "http",
82
+ "server": ["testserver", 80],
83
+ "headers": [(b"host", b"testserver:81")],
84
+ },
85
+ ("http", "testserver", 81),
86
+ ),
87
+ (
88
+ {
89
+ "scheme": "http",
90
+ "server": ["testserver", 80],
91
+ "headers": [(b"host", b"testserver")],
92
+ },
93
+ ("http", "testserver", None),
94
+ ),
95
+ (
96
+ {
97
+ "scheme": "http",
98
+ "server": ["testserver", 80],
99
+ "headers": [(b"forwarded", b"proto=https;host=test:1234")],
100
+ },
101
+ ("https", "test", 1234),
102
+ ),
103
+ (
104
+ {
105
+ "scheme": "http",
106
+ "server": ["testserver", 80],
107
+ "headers": [(b"forwarded", b"proto=https;host=test:not-an-integer")],
108
+ },
109
+ ("https", "test", 80),
110
+ ),
111
+ (
112
+ {
113
+ "scheme": "http",
114
+ "server": ["testserver", 80],
115
+ "headers": [(b"x-forwarded-host", b"test")],
116
+ },
117
+ ("http", "test", 80),
118
+ ),
119
+ (
120
+ {
121
+ "scheme": "http",
122
+ "server": ["testserver", 80],
123
+ "headers": [(b"x-forwarded-proto", b"https")],
124
+ },
125
+ ("https", "testserver", 80),
126
+ ),
127
+ (
128
+ {
129
+ "scheme": "http",
130
+ "server": ["testserver", 80],
131
+ "headers": [(b"x-forwarded-port", b"1111")],
132
+ },
133
+ ("http", "testserver", 1111),
134
+ ),
135
+ (
136
+ {
137
+ "scheme": "http",
138
+ "server": ["testserver", 80],
139
+ "headers": [(b"x-forwarded-port", b"not-an-integer")],
140
+ },
141
+ ("http", "testserver", 80),
142
+ ),
143
+ (
144
+ {
145
+ "scheme": "http",
146
+ "server": ["testserver", 80],
147
+ "headers": [
148
+ (b"forwarded", b"proto=https;host=test:1234"),
149
+ (b"x-forwarded-host", b"another-test"),
150
+ (b"x-forwarded-port", b"1111"),
151
+ (b"x-forwarded-proto", b"https"),
152
+ ],
153
+ },
154
+ ("https", "test", 1234),
155
+ ),
156
+ ],
157
+ )
158
+ def test_get_forwarded_url_parts(
159
+ proxy_header_middleware: ProxyHeaderMiddleware, scope, expected
160
+ ):
161
+ actual = proxy_header_middleware._get_forwarded_url_parts(scope)
162
+ assert actual == expected
163
+
164
+
165
+ def test_cors_middleware(test_client):
166
+ resp = test_client.get("/_mgmt/ping", headers={"Origin": "http://netloc"})
167
+ assert resp.status_code == 200
168
+ assert resp.headers["access-control-allow-origin"] == "*"
@@ -1,18 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: stac-fastapi.api
3
- Version: 2.4.8
4
- Summary: An implementation of STAC API based on the FastAPI framework.
5
- Home-page: https://github.com/stac-utils/stac-fastapi
6
- Author: Arturo Engineering
7
- Author-email: engineering@arturo.ai
8
- License: MIT
9
- Keywords: STAC FastAPI COG
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Intended Audience :: Information Technology
12
- Classifier: Intended Audience :: Science/Research
13
- Classifier: Programming Language :: Python :: 3.8
14
- Classifier: License :: OSI Approved :: MIT License
15
- Requires-Python: >=3.8
16
- Description-Content-Type: text/markdown
17
- Provides-Extra: dev
18
- Provides-Extra: docs
@@ -1,18 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: stac-fastapi.api
3
- Version: 2.4.8
4
- Summary: An implementation of STAC API based on the FastAPI framework.
5
- Home-page: https://github.com/stac-utils/stac-fastapi
6
- Author: Arturo Engineering
7
- Author-email: engineering@arturo.ai
8
- License: MIT
9
- Keywords: STAC FastAPI COG
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Intended Audience :: Information Technology
12
- Classifier: Intended Audience :: Science/Research
13
- Classifier: Programming Language :: Python :: 3.8
14
- Classifier: License :: OSI Approved :: MIT License
15
- Requires-Python: >=3.8
16
- Description-Content-Type: text/markdown
17
- Provides-Extra: dev
18
- Provides-Extra: docs