stac-fastapi.api 3.0.2__tar.gz → 3.0.4__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 (24) hide show
  1. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/PKG-INFO +2 -1
  2. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/setup.py +1 -0
  3. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/stac_fastapi/api/app.py +3 -2
  4. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/stac_fastapi/api/errors.py +2 -1
  5. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/stac_fastapi/api/version.py +1 -1
  6. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/stac_fastapi.api.egg-info/PKG-INFO +2 -1
  7. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/tests/test_app.py +70 -5
  8. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/tests/test_app_prefix.py +64 -0
  9. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/tests/test_models.py +12 -9
  10. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/README.md +0 -0
  11. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/setup.cfg +0 -0
  12. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/stac_fastapi/api/__init__.py +0 -0
  13. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/stac_fastapi/api/config.py +0 -0
  14. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/stac_fastapi/api/middleware.py +0 -0
  15. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/stac_fastapi/api/models.py +0 -0
  16. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/stac_fastapi/api/openapi.py +0 -0
  17. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/stac_fastapi/api/routes.py +0 -0
  18. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/stac_fastapi.api.egg-info/SOURCES.txt +0 -0
  19. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/stac_fastapi.api.egg-info/dependency_links.txt +0 -0
  20. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/stac_fastapi.api.egg-info/not-zip-safe +0 -0
  21. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/stac_fastapi.api.egg-info/requires.txt +0 -0
  22. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/stac_fastapi.api.egg-info/top_level.txt +0 -0
  23. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/tests/test_api.py +0 -0
  24. {stac_fastapi_api-3.0.2 → stac_fastapi_api-3.0.4}/tests/test_middleware.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stac-fastapi.api
3
- Version: 3.0.2
3
+ Version: 3.0.4
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
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.9
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
18
19
  Classifier: License :: OSI Approved :: MIT License
19
20
  Requires-Python: >=3.8
20
21
  Description-Content-Type: text/markdown
@@ -41,6 +41,7 @@ setup(
41
41
  "Programming Language :: Python :: 3.10",
42
42
  "Programming Language :: Python :: 3.11",
43
43
  "Programming Language :: Python :: 3.12",
44
+ "Programming Language :: Python :: 3.13",
44
45
  "License :: OSI Approved :: MIT License",
45
46
  ],
46
47
  keywords="STAC FastAPI COG",
@@ -10,8 +10,8 @@ from fastapi.openapi.utils import get_openapi
10
10
  from fastapi.params import Depends
11
11
  from stac_pydantic import api
12
12
  from stac_pydantic.api.collections import Collections
13
- from stac_pydantic.api.version import STAC_API_VERSION
14
13
  from stac_pydantic.shared import MimeTypes
14
+ from stac_pydantic.version import STAC_VERSION
15
15
  from starlette.middleware import Middleware
16
16
  from starlette.responses import JSONResponse, Response
17
17
 
@@ -77,6 +77,7 @@ class StacApi:
77
77
  openapi_url=self.settings.openapi_url,
78
78
  docs_url=self.settings.docs_url,
79
79
  redoc_url=None,
80
+ root_path=self.settings.root_path,
80
81
  ),
81
82
  takes_self=True,
82
83
  ),
@@ -93,7 +94,7 @@ class StacApi:
93
94
  lambda self: self.settings.stac_fastapi_version, takes_self=True
94
95
  )
95
96
  )
96
- stac_version: str = attr.ib(default=STAC_API_VERSION)
97
+ stac_version: str = attr.ib(default=STAC_VERSION)
97
98
  description: str = attr.ib(
98
99
  default=attr.Factory(
99
100
  lambda self: self.settings.stac_fastapi_description, takes_self=True
@@ -4,6 +4,7 @@ import logging
4
4
  from typing import Callable, Dict, Type, TypedDict
5
5
 
6
6
  from fastapi import FastAPI
7
+ from fastapi.encoders import jsonable_encoder
7
8
  from fastapi.exceptions import RequestValidationError, ResponseValidationError
8
9
  from starlette import status
9
10
  from starlette.requests import Request
@@ -88,7 +89,7 @@ def add_exception_handlers(
88
89
  request: Request, exc: RequestValidationError
89
90
  ) -> JSONResponse:
90
91
  return JSONResponse(
91
- content=ErrorResponse(code=exc.__class__.__name__, description=str(exc)),
92
+ content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
92
93
  status_code=status.HTTP_400_BAD_REQUEST,
93
94
  )
94
95
 
@@ -1,3 +1,3 @@
1
1
  """Library version."""
2
2
 
3
- __version__ = "3.0.2"
3
+ __version__ = "3.0.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stac-fastapi.api
3
- Version: 3.0.2
3
+ Version: 3.0.4
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
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.9
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
18
19
  Classifier: License :: OSI Approved :: MIT License
19
20
  Requires-Python: >=3.8
20
21
  Description-Content-Type: text/markdown
@@ -165,13 +165,11 @@ def test_filter_extension(validate, TestCoreClient, item_dict):
165
165
  type="FeatureCollection", features=[stac.Item(**item_dict)]
166
166
  )
167
167
 
168
- post_request_model = create_post_request_model([FilterExtension()])
169
-
170
168
  test_app = app.StacApi(
171
169
  settings=ApiSettings(enable_response_models=validate),
172
- client=FilterClient(post_request_model=post_request_model),
170
+ client=FilterClient(),
173
171
  search_get_request_model=create_get_request_model([FilterExtension()]),
174
- search_post_request_model=post_request_model,
172
+ search_post_request_model=create_post_request_model([FilterExtension()]),
175
173
  extensions=[FilterExtension()],
176
174
  )
177
175
 
@@ -191,7 +189,7 @@ def test_filter_extension(validate, TestCoreClient, item_dict):
191
189
  "collections": ["test"],
192
190
  "filter": {},
193
191
  "filter-crs": "EPSG:4326",
194
- "filter-lang": "cql2-text",
192
+ "filter-lang": "cql2-json",
195
193
  },
196
194
  )
197
195
 
@@ -387,3 +385,70 @@ def test_request_model(AsyncTestCoreClient):
387
385
  "/collections/test_collection/items/test_item", params={"user": "Chewbacca"}
388
386
  )
389
387
  assert resp.status_code == 200
388
+
389
+
390
+ def test_client_datetime_input_params():
391
+ """Test all endpoints. Verify input models."""
392
+
393
+ class FakeClient(BaseCoreClient):
394
+ def post_search(self, search_request: BaseSearchPostRequest, **kwargs):
395
+ return search_request.datetime
396
+
397
+ def get_search(
398
+ self,
399
+ collections: Optional[List[str]] = None,
400
+ ids: Optional[List[str]] = None,
401
+ bbox: Optional[List[NumType]] = None,
402
+ intersects: Optional[str] = None,
403
+ datetime: Optional[Union[str, datetime]] = None,
404
+ limit: Optional[int] = 10,
405
+ **kwargs,
406
+ ):
407
+ return datetime
408
+
409
+ def get_item(self, item_id: str, collection_id: str, **kwargs) -> stac.Item:
410
+ raise NotImplementedError
411
+
412
+ def all_collections(self, **kwargs) -> stac.Collections:
413
+ raise NotImplementedError
414
+
415
+ def get_collection(self, collection_id: str, **kwargs) -> stac.Collection:
416
+ raise NotImplementedError
417
+
418
+ def item_collection(
419
+ self,
420
+ collection_id: str,
421
+ bbox: Optional[List[Union[float, int]]] = None,
422
+ datetime: Optional[Union[str, datetime]] = None,
423
+ limit: int = 10,
424
+ token: str = None,
425
+ **kwargs,
426
+ ) -> stac.ItemCollection:
427
+ raise NotImplementedError
428
+
429
+ test_app = app.StacApi(
430
+ settings=ApiSettings(enable_response_models=False),
431
+ client=FakeClient(),
432
+ )
433
+
434
+ with TestClient(test_app.app) as client:
435
+ get_search = client.get(
436
+ "/search",
437
+ params={
438
+ "collections": ["test"],
439
+ "datetime": "2020-01-01T00:00:00.00001Z",
440
+ },
441
+ )
442
+ post_search = client.post(
443
+ "/search",
444
+ json={
445
+ "collections": ["test"],
446
+ "datetime": "2020-01-01T00:00:00.00001Z",
447
+ },
448
+ )
449
+
450
+ assert get_search.status_code == 200, get_search.text
451
+ assert get_search.json() == "2020-01-01T00:00:00.000010+00:00"
452
+
453
+ assert post_search.status_code == 200, post_search.text
454
+ assert post_search.json() == "2020-01-01T00:00:00.00001Z"
@@ -144,3 +144,67 @@ def test_async_api_prefix(AsyncTestCoreClient, prefix):
144
144
 
145
145
  resp = client.get(prefix + expected_path)
146
146
  assert resp.status_code == 200
147
+
148
+
149
+ @pytest.mark.parametrize("prefix", ["", "/a_prefix"])
150
+ def test_api_prefix_with_root_path(TestCoreClient, prefix):
151
+ api_settings = ApiSettings(
152
+ openapi_url=f"{prefix}/api", docs_url=f"{prefix}/api.html", root_path="/api/v1"
153
+ )
154
+
155
+ api = StacApi(
156
+ settings=api_settings,
157
+ client=TestCoreClient(),
158
+ router=APIRouter(prefix=prefix),
159
+ )
160
+
161
+ prefix = "/api/v1" + prefix
162
+ with TestClient(api.app, base_url="http://stac.io", root_path="/api/v1") as client:
163
+ landing = client.get(f"{prefix}/")
164
+ assert landing.status_code == 200, landing.json()
165
+ print(landing.text)
166
+ service_doc = client.get(f"{prefix}/api.html")
167
+ assert service_doc.status_code == 200, service_doc.text
168
+
169
+ service_desc = client.get(f"{prefix}/api")
170
+ assert service_desc.status_code == 200, service_desc.json()
171
+
172
+ conformance = client.get(f"{prefix}/conformance")
173
+ assert conformance.status_code == 200, conformance.json()
174
+
175
+ # NOTE: The collections/collection/items/item links do not have the prefix
176
+ # because they are created in the fixtures
177
+ collections = client.get(f"{prefix}/collections")
178
+ assert collections.status_code == 200, collections.json()
179
+ collection_id = collections.json()["collections"][0]["id"]
180
+
181
+ collection = client.get(f"{prefix}/collections/{collection_id}")
182
+ assert collection.status_code == 200, collection.json()
183
+
184
+ items = client.get(f"{prefix}/collections/{collection_id}/items")
185
+ assert items.status_code == 200, items.json()
186
+
187
+ item_id = items.json()["features"][0]["id"]
188
+ item = client.get(f"{prefix}/collections/{collection_id}/items/{item_id}")
189
+ assert item.status_code == 200, item.json()
190
+
191
+ link_tests = [
192
+ ("root", "application/json", "/"),
193
+ ("conformance", "application/json", "/conformance"),
194
+ ("data", "application/json", "/collections"),
195
+ ("search", "application/geo+json", "/search"),
196
+ ("service-doc", "text/html", "/api.html"),
197
+ ("service-desc", "application/vnd.oai.openapi+json;version=3.0", "/api"),
198
+ ]
199
+
200
+ for rel_type, expected_media_type, expected_path in link_tests:
201
+ link = get_link(landing.json(), rel_type)
202
+
203
+ assert link is not None, f"Missing {rel_type} link in landing page"
204
+ assert link.get("type") == expected_media_type
205
+
206
+ link_path = urllib.parse.urlsplit(link.get("href")).path
207
+ assert link_path == prefix + expected_path
208
+
209
+ resp = client.get(prefix + expected_path)
210
+ assert resp.status_code == 200
@@ -26,7 +26,7 @@ def test_create_get_request_model():
26
26
  "coordinates": [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]],
27
27
  }
28
28
  ),
29
- datetime="2020-01-01T00:00:00Z",
29
+ datetime="2020-01-01T00:00:00.00001Z",
30
30
  limit=10,
31
31
  filter="test==test",
32
32
  filter_crs="epsg:4326",
@@ -35,6 +35,8 @@ def test_create_get_request_model():
35
35
 
36
36
  assert model.collections == ["test1", "test2"]
37
37
  assert model.filter_crs == "epsg:4326"
38
+ d = model.datetime
39
+ assert d.microsecond == 10
38
40
 
39
41
  with pytest.raises(HTTPException):
40
42
  request_model(datetime="yo")
@@ -62,10 +64,10 @@ def test_create_get_request_model():
62
64
 
63
65
 
64
66
  @pytest.mark.parametrize(
65
- "filter,passes",
66
- [(None, True), ({"test": "test"}, True), ("test==test", False), ([], False)],
67
+ "filter_val,passes",
68
+ [(None, True), ({"test": "test"}, True), ([], False)],
67
69
  )
68
- def test_create_post_request_model(filter, passes):
70
+ def test_create_post_request_model(filter_val, passes):
69
71
  request_model = create_post_request_model(
70
72
  extensions=[FilterExtension(), FieldsExtension()],
71
73
  base_model=BaseSearchPostRequest,
@@ -73,21 +75,22 @@ def test_create_post_request_model(filter, passes):
73
75
 
74
76
  if not passes:
75
77
  with pytest.raises(ValidationError):
76
- model = request_model(filter=filter)
78
+ model = request_model(filter=filter_val)
77
79
  else:
78
80
  model = request_model(
79
81
  collections=["test1", "test2"],
80
82
  ids=["test1", "test2"],
81
83
  bbox=[0, 0, 1, 1],
82
- datetime="2020-01-01T00:00:00Z",
84
+ datetime="2020-01-01T00:00:00.00001Z",
83
85
  limit=10,
84
- filter=filter,
85
- **{"filter-crs": "epsg:4326", "filter-lang": "cql2-text"},
86
+ filter=filter_val,
87
+ **{"filter-crs": "epsg:4326", "filter-lang": "cql2-json"},
86
88
  )
87
89
 
88
90
  assert model.collections == ["test1", "test2"]
89
91
  assert model.filter_crs == "epsg:4326"
90
- assert model.filter == filter
92
+ assert model.filter == filter_val
93
+ assert model.datetime == "2020-01-01T00:00:00.00001Z"
91
94
 
92
95
 
93
96
  @pytest.mark.parametrize(