Skip to content

Commit 63cac39

Browse files
thomas-maschlervincentsaragojonhealy1
authored
update to pydantic 2 (#625)
* update to pydantic 2 * update changelog * typo * add CI for Python 3.12 * drop support for python 3.8 * update python version for docs * update python for docs docker container * update python version in dockerfile * handle post requests * test wrapper * pass through StacBaseModel * keep py38 * change install order * lint * revert back to >=3.8 in setup.py * add switch to use either TypeDict or StacPydantic Response * lint and format with ruff * remove comment * update change log * use Optional not | None * use Optional not | None * update dependencies * hard code versions and address other comments * remove response_model module, update openapi schema * add responses to transactions * do not wrap response into response_class * fix tests * update changelog, remove redundant variable * lint bench * reorder installs * do not push benchmark if not in stac-utils/stac-fastapi repo * Add text about response validation to readme. * fix warning * remove versions * fix * Update README.md * update changelog --------- Co-authored-by: vincentsarago <[email protected]> Co-authored-by: Jonathan Healy <[email protected]>
1 parent 1299bea commit 63cac39

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+688
-417
lines changed

Diff for: .github/workflows/cicd.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ jobs:
8181
run: python -m pytest stac_fastapi/api/tests/benchmarks.py --benchmark-only --benchmark-columns 'min, max, mean, median' --benchmark-json output.json
8282

8383
- name: Store and benchmark result
84+
if: github.repository == 'stac-utils/stac-fastapi'
8485
uses: benchmark-action/github-action-benchmark@v1
8586
with:
8687
name: STAC FastAPI Benchmarks

Diff for: .github/workflows/deploy_mkdocs.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ jobs:
2020
- name: Checkout main
2121
uses: actions/checkout@v4
2222

23-
- name: Set up Python 3.8
23+
- name: Set up Python 3.11
2424
uses: actions/setup-python@v5
2525
with:
26-
python-version: 3.8
26+
python-version: 3.11
2727

2828
- name: Install dependencies
2929
run: |

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ docs/api/*
129129

130130
# Virtualenv
131131
venv
132+
.venv/
132133

133134
# IDE
134135
.vscode

Diff for: .pre-commit-config.yaml

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
repos:
2-
- repo: https://github.com/charliermarsh/ruff-pre-commit
3-
rev: "v0.0.267"
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: "v0.2.2"
44
hooks:
55
- id: ruff
66
args: [--fix, --exit-non-zero-on-fix]
7-
- repo: https://github.com/psf/black
8-
rev: 23.3.0
9-
hooks:
10-
- id: black
7+
- id: ruff-format

Diff for: CHANGES.md

+10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
## [Unreleased]
44

5+
## Changes
6+
7+
* Update to pydantic v2 and stac_pydantic v3 ([#625](https://github.com/stac-utils/stac-fastapi/pull/625))
8+
* Removed internal Search and Operator Types in favor of stac_pydantic Types ([#625](https://github.com/stac-utils/stac-fastapi/pull/625))
9+
* Fix response model validation ([#625](https://github.com/stac-utils/stac-fastapi/pull/625))
10+
* Add Response Model to OpenAPI, even if model validation is turned off ([#625](https://github.com/stac-utils/stac-fastapi/pull/625))
11+
* Use status code 201 for Item/Collection creation ([#625](https://github.com/stac-utils/stac-fastapi/pull/625))
12+
* Replace Black with Ruff Format ([#625](https://github.com/stac-utils/stac-fastapi/pull/625))
13+
514
## [2.5.5.post1] - 2024-04-25
615

716
### Fixed
@@ -48,6 +57,7 @@
4857
* Add `/queryables` link to the landing page ([#587](https://github.com/stac-utils/stac-fastapi/pull/587))
4958
- `id`, `title`, `description` and `api_version` fields can be customized via env variables
5059
* Add `DeprecationWarning` for the `ContextExtension`
60+
* Add support for Python 3.12
5161

5262
### Changed
5363

Diff for: Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.8-slim as base
1+
FROM python:3.11-slim as base
22

33
# Any python libraries that require system libraries to be installed will likely
44
# need the following packages in order to build

Diff for: Dockerfile.docs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.8-slim
1+
FROM python:3.11-slim
22

33
# build-essential is required to build a wheel for ciso8601
44
RUN apt update && apt install -y build-essential

Diff for: README.md

+12
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ Backends are hosted in their own repositories:
4141

4242
`stac-fastapi` was initially developed by [arturo-ai](https://github.com/arturo-ai).
4343

44+
45+
## Response Model Validation
46+
47+
A common question when using this package is how request and response types are validated?
48+
49+
This package uses [`stac-pydantic`](https://github.com/stac-utils/stac-pydantic) to validate and document STAC objects. However, by default, validation of response types is turned off and the API will simply forward responses without validating them against the Pydantic model first. This decision was made with the assumption that responses usually come from a (typed) database and can be considered safe. Extra validation would only increase latency, in particular for large payloads.
50+
51+
To turn on response validation, set `ENABLE_RESPONSE_MODELS` to `True`. Either as an environment variable or directly in the `ApiSettings`.
52+
53+
With the introduction of Pydantic 2, the extra [time it takes to validate models became negatable](https://github.com/stac-utils/stac-fastapi/pull/625#issuecomment-2045824578). While `ENABLE_RESPONSE_MODELS` still defaults to `False` there should be no penalty for users to turn on this feature but users discretion is advised.
54+
55+
4456
## Installation
4557

4658
```bash

Diff for: pyproject.toml

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
[tool.ruff]
2+
target-version = "py38" # minimum supported version
23
line-length = 90
4+
5+
[tool.ruff.lint]
36
select = [
47
"C9",
58
"D1",
@@ -9,13 +12,13 @@ select = [
912
"W",
1013
]
1114

12-
[tool.ruff.per-file-ignores]
15+
[tool.ruff.lint.per-file-ignores]
1316
"**/tests/**/*.py" = ["D1"]
1417

15-
[tool.ruff.isort]
18+
[tool.ruff.lint.isort]
1619
known-first-party = ["stac_fastapi"]
1720
known-third-party = ["stac_pydantic", "fastapi"]
1821
section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
1922

20-
[tool.black]
21-
target-version = ["py38", "py39", "py310", "py311"]
23+
[tool.ruff.format]
24+
quote-style = "double"

Diff for: stac_fastapi/api/setup.py

-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@
66
desc = f.read()
77

88
install_requires = [
9-
"attrs",
10-
"pydantic[dotenv]<2",
11-
"stac_pydantic==2.0.*",
129
"brotli_asgi",
1310
"stac-fastapi.types",
1411
]

Diff for: stac_fastapi/api/stac_fastapi/api/app.py

+95-24
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
"""Fastapi app creation."""
22

3+
34
from typing import Any, Dict, List, Optional, Tuple, Type, Union
45

56
import attr
67
from brotli_asgi import BrotliMiddleware
78
from fastapi import APIRouter, FastAPI
89
from fastapi.openapi.utils import get_openapi
910
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
1212
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
1415
from starlette.responses import JSONResponse, Response
1516

1617
from stac_fastapi.api.errors import DEFAULT_STATUS_CODES, add_exception_handlers
@@ -94,7 +95,7 @@ class StacApi:
9495
lambda self: self.settings.stac_fastapi_version, takes_self=True
9596
)
9697
)
97-
stac_version: str = attr.ib(default=STAC_VERSION)
98+
stac_version: str = attr.ib(default=STAC_API_VERSION)
9899
description: str = attr.ib(
99100
default=attr.Factory(
100101
lambda self: self.settings.stac_fastapi_description, takes_self=True
@@ -138,9 +139,17 @@ def register_landing_page(self):
138139
self.router.add_api_route(
139140
name="Landing Page",
140141
path="/",
141-
response_model=LandingPage
142-
if self.settings.enable_response_models
143-
else None,
142+
response_model=(
143+
api.LandingPage if self.settings.enable_response_models else None
144+
),
145+
responses={
146+
200: {
147+
"content": {
148+
MimeTypes.json.value: {},
149+
},
150+
"model": api.LandingPage,
151+
},
152+
},
144153
response_class=self.response_class,
145154
response_model_exclude_unset=False,
146155
response_model_exclude_none=True,
@@ -157,9 +166,17 @@ def register_conformance_classes(self):
157166
self.router.add_api_route(
158167
name="Conformance Classes",
159168
path="/conformance",
160-
response_model=ConformanceClasses
161-
if self.settings.enable_response_models
162-
else None,
169+
response_model=(
170+
api.ConformanceClasses if self.settings.enable_response_models else None
171+
),
172+
responses={
173+
200: {
174+
"content": {
175+
MimeTypes.json.value: {},
176+
},
177+
"model": api.ConformanceClasses,
178+
},
179+
},
163180
response_class=self.response_class,
164181
response_model_exclude_unset=True,
165182
response_model_exclude_none=True,
@@ -176,7 +193,15 @@ def register_get_item(self):
176193
self.router.add_api_route(
177194
name="Get Item",
178195
path="/collections/{collection_id}/items/{item_id}",
179-
response_model=Item if self.settings.enable_response_models else None,
196+
response_model=api.Item if self.settings.enable_response_models else None,
197+
responses={
198+
200: {
199+
"content": {
200+
MimeTypes.geojson.value: {},
201+
},
202+
"model": api.Item,
203+
},
204+
},
180205
response_class=GeoJSONResponse,
181206
response_model_exclude_unset=True,
182207
response_model_exclude_none=True,
@@ -194,9 +219,19 @@ def register_post_search(self):
194219
self.router.add_api_route(
195220
name="Search",
196221
path="/search",
197-
response_model=(ItemCollection if not fields_ext else None)
198-
if self.settings.enable_response_models
199-
else None,
222+
response_model=(
223+
(api.ItemCollection if not fields_ext else None)
224+
if self.settings.enable_response_models
225+
else None
226+
),
227+
responses={
228+
200: {
229+
"content": {
230+
MimeTypes.geojson.value: {},
231+
},
232+
"model": api.ItemCollection,
233+
},
234+
},
200235
response_class=GeoJSONResponse,
201236
response_model_exclude_unset=True,
202237
response_model_exclude_none=True,
@@ -216,9 +251,19 @@ def register_get_search(self):
216251
self.router.add_api_route(
217252
name="Search",
218253
path="/search",
219-
response_model=(ItemCollection if not fields_ext else None)
220-
if self.settings.enable_response_models
221-
else None,
254+
response_model=(
255+
(api.ItemCollection if not fields_ext else None)
256+
if self.settings.enable_response_models
257+
else None
258+
),
259+
responses={
260+
200: {
261+
"content": {
262+
MimeTypes.geojson.value: {},
263+
},
264+
"model": api.ItemCollection,
265+
},
266+
},
222267
response_class=GeoJSONResponse,
223268
response_model_exclude_unset=True,
224269
response_model_exclude_none=True,
@@ -237,9 +282,17 @@ def register_get_collections(self):
237282
self.router.add_api_route(
238283
name="Get Collections",
239284
path="/collections",
240-
response_model=Collections
241-
if self.settings.enable_response_models
242-
else None,
285+
response_model=(
286+
Collections if self.settings.enable_response_models else None
287+
),
288+
responses={
289+
200: {
290+
"content": {
291+
MimeTypes.json.value: {},
292+
},
293+
"model": Collections,
294+
},
295+
},
243296
response_class=self.response_class,
244297
response_model_exclude_unset=True,
245298
response_model_exclude_none=True,
@@ -256,7 +309,17 @@ def register_get_collection(self):
256309
self.router.add_api_route(
257310
name="Get Collection",
258311
path="/collections/{collection_id}",
259-
response_model=Collection if self.settings.enable_response_models else None,
312+
response_model=api.Collection
313+
if self.settings.enable_response_models
314+
else None,
315+
responses={
316+
200: {
317+
"content": {
318+
MimeTypes.json.value: {},
319+
},
320+
"model": api.Collection,
321+
},
322+
},
260323
response_class=self.response_class,
261324
response_model_exclude_unset=True,
262325
response_model_exclude_none=True,
@@ -283,9 +346,17 @@ def register_get_item_collection(self):
283346
self.router.add_api_route(
284347
name="Get ItemCollection",
285348
path="/collections/{collection_id}/items",
286-
response_model=ItemCollection
287-
if self.settings.enable_response_models
288-
else None,
349+
response_model=(
350+
api.ItemCollection if self.settings.enable_response_models else None
351+
),
352+
responses={
353+
200: {
354+
"content": {
355+
MimeTypes.geojson.value: {},
356+
},
357+
"model": api.ItemCollection,
358+
},
359+
},
289360
response_class=GeoJSONResponse,
290361
response_model_exclude_unset=True,
291362
response_model_exclude_none=True,

Diff for: stac_fastapi/api/stac_fastapi/api/config.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Application settings."""
2+
23
import enum
34

45

Diff for: stac_fastapi/api/stac_fastapi/api/errors.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Callable, Dict, Type, TypedDict
55

66
from fastapi import FastAPI
7-
from fastapi.exceptions import RequestValidationError
7+
from fastapi.exceptions import RequestValidationError, ResponseValidationError
88
from starlette import status
99
from starlette.requests import Request
1010
from starlette.responses import JSONResponse
@@ -27,6 +27,7 @@
2727
DatabaseError: status.HTTP_424_FAILED_DEPENDENCY,
2828
Exception: status.HTTP_500_INTERNAL_SERVER_ERROR,
2929
InvalidQueryParameter: status.HTTP_400_BAD_REQUEST,
30+
ResponseValidationError: status.HTTP_500_INTERNAL_SERVER_ERROR,
3031
}
3132

3233

Diff for: stac_fastapi/api/stac_fastapi/api/middleware.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Api middleware."""
2+
23
import re
34
import typing
45
from http.client import HTTP_PORT, HTTPS_PORT

0 commit comments

Comments
 (0)