Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update to pydantic 2 #625

Merged
merged 44 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
013caee
update to pydantic 2
thomas-maschler Feb 10, 2024
669587e
update changelog
thomas-maschler Feb 10, 2024
a168498
typo
thomas-maschler Feb 10, 2024
cd9f75e
add CI for Python 3.12
thomas-maschler Feb 10, 2024
781c46e
drop support for python 3.8
thomas-maschler Feb 10, 2024
ce7f6e8
update python version for docs
thomas-maschler Feb 10, 2024
19cce99
update python for docs docker container
thomas-maschler Feb 10, 2024
36044d6
update python version in dockerfile
thomas-maschler Feb 10, 2024
9210501
handle post requests
thomas-maschler Feb 12, 2024
1fa87b7
test wrapper
thomas-maschler Feb 12, 2024
653f3a6
pass through StacBaseModel
thomas-maschler Feb 12, 2024
6727568
keep py38
thomas-maschler Feb 20, 2024
e01e95a
change install order
thomas-maschler Feb 20, 2024
db5cfb6
lint
thomas-maschler Feb 20, 2024
02f2702
revert back to >=3.8 in setup.py
thomas-maschler Feb 20, 2024
8118f10
add switch to use either TypeDict or StacPydantic Response
thomas-maschler Feb 23, 2024
b52f216
lint and format with ruff
thomas-maschler Feb 23, 2024
0f7d8c9
Merge branch 'main' into pydantic2
thomas-maschler Feb 23, 2024
f2f9374
remove comment
thomas-maschler Feb 23, 2024
273f819
update change log
thomas-maschler Feb 23, 2024
dc67a4d
use Optional not | None
thomas-maschler Feb 24, 2024
7208dd7
use Optional not | None
thomas-maschler Feb 24, 2024
e24351a
update dependencies
thomas-maschler Apr 4, 2024
5bdd615
hard code versions and address other comments
thomas-maschler Apr 4, 2024
eea9c82
remove response_model module, update openapi schema
thomas-maschler Apr 5, 2024
d0762eb
add responses to transactions
thomas-maschler Apr 5, 2024
9d9ab57
do not wrap response into response_class
vincentsarago Apr 5, 2024
68aff85
Merge pull request #2 from vincentsarago/patch/avoid-wrapping-response
thomas-maschler Apr 5, 2024
3d946f6
fix tests
thomas-maschler Apr 5, 2024
c9e6f0d
update changelog, remove redundant variable
thomas-maschler Apr 5, 2024
1652232
update from main
vincentsarago Apr 9, 2024
aa1ab5e
lint bench
vincentsarago Apr 9, 2024
8ad9f97
reorder installs
vincentsarago Apr 9, 2024
bf01ad9
do not push benchmark if not in stac-utils/stac-fastapi repo
vincentsarago Apr 9, 2024
6b0949a
Add text about response validation to readme.
thomas-maschler Apr 10, 2024
9b14b04
merge main
thomas-maschler Apr 10, 2024
6f1b478
fix warning
thomas-maschler Apr 24, 2024
0bb2019
merge main
thomas-maschler Apr 24, 2024
6d943aa
update from main
vincentsarago Apr 26, 2024
d46d287
remove versions
vincentsarago Apr 26, 2024
641614a
fix
vincentsarago Apr 26, 2024
3b50a6d
Update README.md
jonhealy1 Apr 26, 2024
37594ec
update changelog
vincentsarago Apr 26, 2024
83d19f8
Merge branch 'pydantic2' of https://github.com/thomas-maschler/stac-f…
vincentsarago Apr 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/cicd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ jobs:
run: python -m pytest stac_fastapi/api/tests/benchmarks.py --benchmark-only --benchmark-columns 'min, max, mean, median' --benchmark-json output.json

- name: Store and benchmark result
if: github.repository == 'stac-utils/stac-fastapi'
uses: benchmark-action/github-action-benchmark@v1
with:
name: STAC FastAPI Benchmarks
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/deploy_mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ jobs:
- name: Checkout main
uses: actions/checkout@v4

- name: Set up Python 3.8
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.8
python-version: 3.11

- name: Install dependencies
run: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ docs/api/*

# Virtualenv
venv
.venv/

# IDE
.vscode
9 changes: 3 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: "v0.0.267"
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.2.2"
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
- id: ruff-format
10 changes: 10 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## [Unreleased]

## Changes

* Update to pydantic v2 and stac_pydantic v3 ([#625](https://github.com/stac-utils/stac-fastapi/pull/625))
* Removed internal Search and Operator Types in favor of stac_pydantic Types ([#625](https://github.com/stac-utils/stac-fastapi/pull/625))
* Fix response model validation ([#625](https://github.com/stac-utils/stac-fastapi/pull/625))
* Add Response Model to OpenAPI, even if model validation is turned off ([#625](https://github.com/stac-utils/stac-fastapi/pull/625))
* Use status code 201 for Item/Collection creation ([#625](https://github.com/stac-utils/stac-fastapi/pull/625))
* Replace Black with Ruff Format ([#625](https://github.com/stac-utils/stac-fastapi/pull/625))

## [2.5.5.post1] - 2024-04-25

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

### Changed

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.8-slim as base
FROM python:3.11-slim as base

# Any python libraries that require system libraries to be installed will likely
# need the following packages in order to build
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.docs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.8-slim
FROM python:3.11-slim

# build-essential is required to build a wheel for ciso8601
RUN apt update && apt install -y build-essential
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ Backends are hosted in their own repositories:

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


## Response Model Validation

A common question when using this package is how request and response types are validated?

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.

jonhealy1 marked this conversation as resolved.
Show resolved Hide resolved
To turn on response validation, set `ENABLE_RESPONSE_MODELS` to `True`. Either as an environment variable or directly in the `ApiSettings`.

jonhealy1 marked this conversation as resolved.
Show resolved Hide resolved
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.


## Installation

```bash
Expand Down
11 changes: 7 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
[tool.ruff]
target-version = "py38" # minimum supported version
line-length = 90

[tool.ruff.lint]
select = [
"C9",
"D1",
Expand All @@ -9,13 +12,13 @@ select = [
"W",
]

[tool.ruff.per-file-ignores]
[tool.ruff.lint.per-file-ignores]
"**/tests/**/*.py" = ["D1"]

[tool.ruff.isort]
[tool.ruff.lint.isort]
known-first-party = ["stac_fastapi"]
known-third-party = ["stac_pydantic", "fastapi"]
section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]

[tool.black]
target-version = ["py38", "py39", "py310", "py311"]
[tool.ruff.format]
quote-style = "double"
3 changes: 0 additions & 3 deletions stac_fastapi/api/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
desc = f.read()

install_requires = [
"attrs",
"pydantic[dotenv]<2",
"stac_pydantic==2.0.*",
"brotli_asgi",
"stac-fastapi.types",
]
Expand Down
119 changes: 95 additions & 24 deletions stac_fastapi/api/stac_fastapi/api/app.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
"""Fastapi app creation."""


from typing import Any, Dict, List, Optional, Tuple, Type, Union

import attr
from brotli_asgi import BrotliMiddleware
from fastapi import APIRouter, FastAPI
from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends
from stac_pydantic import Collection, Item, ItemCollection
from stac_pydantic.api import ConformanceClasses, LandingPage
from stac_pydantic import api
from stac_pydantic.api.collections import Collections
from stac_pydantic.version import STAC_VERSION
from stac_pydantic.api.version import STAC_API_VERSION
from stac_pydantic.shared import MimeTypes
from starlette.responses import JSONResponse, Response

from stac_fastapi.api.errors import DEFAULT_STATUS_CODES, add_exception_handlers
Expand Down Expand Up @@ -94,7 +95,7 @@ class StacApi:
lambda self: self.settings.stac_fastapi_version, takes_self=True
)
)
stac_version: str = attr.ib(default=STAC_VERSION)
stac_version: str = attr.ib(default=STAC_API_VERSION)
description: str = attr.ib(
default=attr.Factory(
lambda self: self.settings.stac_fastapi_description, takes_self=True
Expand Down Expand Up @@ -138,9 +139,17 @@ def register_landing_page(self):
self.router.add_api_route(
name="Landing Page",
path="/",
response_model=LandingPage
if self.settings.enable_response_models
else None,
response_model=(
api.LandingPage if self.settings.enable_response_models else None
),
responses={
200: {
"content": {
MimeTypes.json.value: {},
},
"model": api.LandingPage,
},
},
response_class=self.response_class,
response_model_exclude_unset=False,
response_model_exclude_none=True,
Expand All @@ -157,9 +166,17 @@ def register_conformance_classes(self):
self.router.add_api_route(
name="Conformance Classes",
path="/conformance",
response_model=ConformanceClasses
if self.settings.enable_response_models
else None,
response_model=(
api.ConformanceClasses if self.settings.enable_response_models else None
),
responses={
200: {
"content": {
MimeTypes.json.value: {},
},
"model": api.ConformanceClasses,
},
},
response_class=self.response_class,
response_model_exclude_unset=True,
response_model_exclude_none=True,
Expand All @@ -176,7 +193,15 @@ def register_get_item(self):
self.router.add_api_route(
name="Get Item",
path="/collections/{collection_id}/items/{item_id}",
response_model=Item if self.settings.enable_response_models else None,
response_model=api.Item if self.settings.enable_response_models else None,
responses={
200: {
"content": {
MimeTypes.geojson.value: {},
},
"model": api.Item,
},
},
response_class=GeoJSONResponse,
response_model_exclude_unset=True,
response_model_exclude_none=True,
Expand All @@ -194,9 +219,19 @@ def register_post_search(self):
self.router.add_api_route(
name="Search",
path="/search",
response_model=(ItemCollection if not fields_ext else None)
if self.settings.enable_response_models
else None,
response_model=(
(api.ItemCollection if not fields_ext else None)
if self.settings.enable_response_models
else None
),
responses={
200: {
"content": {
MimeTypes.geojson.value: {},
},
"model": api.ItemCollection,
},
},
response_class=GeoJSONResponse,
response_model_exclude_unset=True,
response_model_exclude_none=True,
Expand All @@ -216,9 +251,19 @@ def register_get_search(self):
self.router.add_api_route(
name="Search",
path="/search",
response_model=(ItemCollection if not fields_ext else None)
if self.settings.enable_response_models
else None,
response_model=(
(api.ItemCollection if not fields_ext else None)
if self.settings.enable_response_models
else None
),
responses={
200: {
"content": {
MimeTypes.geojson.value: {},
},
"model": api.ItemCollection,
},
},
response_class=GeoJSONResponse,
response_model_exclude_unset=True,
response_model_exclude_none=True,
Expand All @@ -237,9 +282,17 @@ def register_get_collections(self):
self.router.add_api_route(
name="Get Collections",
path="/collections",
response_model=Collections
if self.settings.enable_response_models
else None,
response_model=(
Collections if self.settings.enable_response_models else None
),
responses={
200: {
"content": {
MimeTypes.json.value: {},
},
"model": Collections,
},
},
response_class=self.response_class,
response_model_exclude_unset=True,
response_model_exclude_none=True,
Expand All @@ -256,7 +309,17 @@ def register_get_collection(self):
self.router.add_api_route(
name="Get Collection",
path="/collections/{collection_id}",
response_model=Collection if self.settings.enable_response_models else None,
response_model=api.Collection
if self.settings.enable_response_models
else None,
responses={
200: {
"content": {
MimeTypes.json.value: {},
},
"model": api.Collection,
},
},
response_class=self.response_class,
response_model_exclude_unset=True,
response_model_exclude_none=True,
Expand All @@ -283,9 +346,17 @@ def register_get_item_collection(self):
self.router.add_api_route(
name="Get ItemCollection",
path="/collections/{collection_id}/items",
response_model=ItemCollection
if self.settings.enable_response_models
else None,
response_model=(
api.ItemCollection if self.settings.enable_response_models else None
),
responses={
200: {
"content": {
MimeTypes.geojson.value: {},
},
"model": api.ItemCollection,
},
},
response_class=GeoJSONResponse,
response_model_exclude_unset=True,
response_model_exclude_none=True,
Expand Down
1 change: 1 addition & 0 deletions stac_fastapi/api/stac_fastapi/api/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Application settings."""

import enum


Expand Down
3 changes: 2 additions & 1 deletion stac_fastapi/api/stac_fastapi/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Callable, Dict, Type, TypedDict

from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
from fastapi.exceptions import RequestValidationError, ResponseValidationError
from starlette import status
from starlette.requests import Request
from starlette.responses import JSONResponse
Expand All @@ -27,6 +27,7 @@
DatabaseError: status.HTTP_424_FAILED_DEPENDENCY,
Exception: status.HTTP_500_INTERNAL_SERVER_ERROR,
InvalidQueryParameter: status.HTTP_400_BAD_REQUEST,
ResponseValidationError: status.HTTP_500_INTERNAL_SERVER_ERROR,
}


Expand Down
1 change: 1 addition & 0 deletions stac_fastapi/api/stac_fastapi/api/middleware.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Api middleware."""

import re
import typing
from http.client import HTTP_PORT, HTTPS_PORT
Expand Down