Skip to content

Commit f715467

Browse files
authored
Merge branch 'main' into patch/remove-defaults-in-openapi-schema
2 parents e6560bc + 752b41e commit f715467

File tree

12 files changed

+173
-36
lines changed

12 files changed

+173
-36
lines changed

CHANGES.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
- Remove defaults in OpenAPI schemas
88

9+
### Added
10+
11+
- add `enable_direct_response` settings to by-pass Pydantic validation and FastAPI serialization for responses
12+
913
## [5.1.1] - 2025-03-17
1014

1115
### Fixed
@@ -373,7 +377,7 @@ Full changelog: https://stac-utils.github.io/stac-fastapi/migrations/v3.0.0/#cha
373377
### Added
374378

375379
* Nginx service as second docker-compose stack to demonstrate proxy ([#503](https://github.com/stac-utils/stac-fastapi/pull/503))
376-
* Validation checks in CI using [stac-api-validator](github.com/stac-utils/stac-api-validator) ([#508](https://github.com/stac-utils/stac-fastapi/pull/508))
380+
* Validation checks in CI using [stac-api-validator](https://github.com/stac-utils/stac-api-validator) ([#508](https://github.com/stac-utils/stac-fastapi/pull/508))
377381
* Required links to the sqlalchemy ItemCollection endpoint ([#508](https://github.com/stac-utils/stac-fastapi/pull/508))
378382
* Publication of docker images to GHCR ([#525](https://github.com/stac-utils/stac-fastapi/pull/525))
379383

Dockerfile

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.11-slim as base
1+
FROM python:3.12-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
@@ -10,12 +10,13 @@ RUN apt-get update && \
1010

1111
ENV CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
1212

13-
FROM base as builder
13+
FROM base AS builder
1414

1515
WORKDIR /app
1616

1717
COPY . /app
1818

19-
RUN python -m pip install -e ./stac_fastapi/types[dev] && \
20-
python -m pip install -e ./stac_fastapi/api[dev] && \
21-
python -m pip install -e ./stac_fastapi/extensions[dev]
19+
RUN python -m pip install \
20+
-e ./stac_fastapi/types[dev] \
21+
-e ./stac_fastapi/api[dev] \
22+
-e ./stac_fastapi/extensions[dev]

Dockerfile.docs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
FROM python:3.11-slim
1+
FROM python:3.12-slim
22

33
# build-essential is required to build a wheel for ciso8601
4-
RUN apt update && apt install -y build-essential
4+
RUN apt update && apt install -y build-essential && \
5+
apt-get clean && \
6+
rm -rf /var/lib/apt/lists/*
57

68
RUN python -m pip install --upgrade pip
79

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ install:
1111

1212
.PHONY: docs-image
1313
docs-image:
14-
docker compose -f docker-compose.docs.yml \
14+
docker compose -f compose.docs.yml \
1515
build
1616

1717
.PHONY: docs
1818
docs: docs-image
19-
docker compose -f docker-compose.docs.yml \
19+
docker compose -f compose.docs.yml \
2020
run docs
2121

2222
.PHONY: test

README.md

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<!-- markdownlint-disable MD033 MD041 -->
22

33
<p align="center">
4-
<img src="https://github.com/radiantearth/stac-site/raw/master/images/logo/stac-030-long.png" width=400>
4+
<img src="https://github.com/radiantearth/stac-site/raw/master/images/logo/stac-030-long.png" width=400 alt="SpatioTemporal Asset Catalog (STAC) logo">
55
<p align="center">FastAPI implemention of the STAC API spec.</p>
66
</p>
77
<p align="center">
@@ -21,25 +21,35 @@
2121

2222
---
2323

24-
Python library for building a STAC compliant FastAPI application. The project is split up into several namespace
25-
packages:
24+
Python library for building a STAC-compliant FastAPI application.
2625

27-
| Package | Description | Version
28-
| ------- |------------- | -------
29-
[**stac_fastapi.api**](https://github.com/stac-utils/stac-fastapi/tree/main/stac_fastapi/api) | An API layer which enforces the [stac-api-spec](https://github.com/radiantearth/stac-api-spec). | [![stac-fastapi.api](https://img.shields.io/pypi/v/stac-fastapi.api?color=%2334D058&label=pypi)](https://pypi.org/project/stac-fastapi.api)
30-
[**stac_fastapi.extensions**](https://github.com/stac-utils/stac-fastapi/tree/main/stac_fastapi/extensions) | Abstract base classes for [STAC API extensions](https://github.com/radiantearth/stac-api-spec/blob/master/extensions.md) and third-party extensions. | [![stac-fastapi.extensions](https://img.shields.io/pypi/v/stac-fastapi.extensions?color=%2334D058&label=pypi)](https://pypi.org/project/stac-fastapi.extensions)
31-
[**stac_fastapi.types**](https://github.com/stac-utils/stac-fastapi/tree/main/stac_fastapi/types) | Shared types and abstract base classes used by the library. | [![stac-fastapi.types](https://img.shields.io/pypi/v/stac-fastapi.types?color=%2334D058&label=pypi)](https://pypi.org/project/stac-fastapi.types)
26+
`stac-fastapi` was initially developed by [arturo-ai](https://github.com/arturo-ai).
27+
28+
The project contains several namespace packages:
29+
30+
| Package | Description | Version |
31+
| ------- |------------- | ------- |
32+
| [**stac_fastapi.api**](https://github.com/stac-utils/stac-fastapi/tree/main/stac_fastapi/api) | An API layer which enforces the [stac-api-spec](https://github.com/radiantearth/stac-api-spec). | [![stac-fastapi.api](https://img.shields.io/pypi/v/stac-fastapi.api?color=%2334D058&label=pypi)](https://pypi.org/project/stac-fastapi.api) |
33+
| [**stac_fastapi.extensions**](https://github.com/stac-utils/stac-fastapi/tree/main/stac_fastapi/extensions) | Abstract base classes for [STAC API extensions](https://github.com/radiantearth/stac-api-spec/blob/master/extensions.md) and third-party extensions. | [![stac-fastapi.extensions](https://img.shields.io/pypi/v/stac-fastapi.extensions?color=%2334D058&label=pypi)](https://pypi.org/project/stac-fastapi.extensions) |
34+
| [**stac_fastapi.types**](https://github.com/stac-utils/stac-fastapi/tree/main/stac_fastapi/types) | Shared types and abstract base classes used by the library. | [![stac-fastapi.types](https://img.shields.io/pypi/v/stac-fastapi.types?color=%2334D058&label=pypi)](https://pypi.org/project/stac-fastapi.types) |
3235

3336
#### Backends
3437

35-
Backends are hosted in their own repositories:
38+
In addition to the packages in this repository, a server implemention will also require the selection of a backend to
39+
connect with a database for STAC metadata storage. There are several different backend options, and each has their own
40+
repository.
3641

37-
- [stac-fastapi-pgstac](https://github.com/stac-utils/stac-fastapi-pgstac): Postgres backend implementation with [PgSTAC](https://github.com/stac-utils/pgstac).
38-
- [stac-fastapi-elasticsearch](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch): Backend implementation with [Elasticsearch](https://github.com/elastic/elasticsearch).
39-
- [stac-fastapi-sqlalchemy](https://github.com/stac-utils/stac-fastapi-sqlalchemy): Postgres backend implementation with [sqlalchemy](https://www.sqlalchemy.org/).
42+
The two most widely-used and supported backends are:
4043

41-
`stac-fastapi` was initially developed by [arturo-ai](https://github.com/arturo-ai).
44+
- [stac-fastapi-pgstac](https://github.com/stac-utils/stac-fastapi-pgstac): [PostgreSQL](https://github.com/postgres/postgres) + [PostGIS](https://github.com/postgis/postgis) via [PgSTAC](https://github.com/stac-utils/pgstac).
45+
- [stac-fastapi-elasticsearch-opensearch](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch): [Elasticsearch](https://github.com/elastic/elasticsearch) or [OpenSearch](https://github.com/opensearch-project/OpenSearch)
4246

47+
Other implementations include:
48+
49+
- [stac-fastapi-mongo](https://github.com/Healy-Hyperspatial/stac-fastapi-mongo): [MongoDB](https://github.com/mongodb/mongo)
50+
- [stac-fastapi-geoparquet)](https://github.com/stac-utils/stac-fastapi-geoparquet): [GeoParquet](https://geoparquet.org) via [stacrs](https://github.com/stac-utils/stacrs) (experimental)
51+
- [stac-fastapi-duckdb](https://github.com/Healy-Hyperspatial/stac-fastapi-duckdb): [DuckDB](https://github.com/duckdb/duckdb) (experimental)
52+
- [stac-fastapi-sqlalchemy](https://github.com/stac-utils/stac-fastapi-sqlalchemy): [PostgreSQL](https://github.com/postgres/postgres) + [PostGIS](https://github.com/postgis/postgis) via [SQLAlchemy](https://www.sqlalchemy.org/) (abandoned in favor of stac-fastapi-pgstac)
4353

4454
## Response Model Validation
4555

@@ -51,16 +61,13 @@ To turn on response validation, set `ENABLE_RESPONSE_MODELS` to `True`. Either a
5161

5262
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.
5363

54-
5564
## Installation
5665

5766
```bash
5867
# Install from PyPI
5968
python -m pip install stac-fastapi.types stac-fastapi.api stac-fastapi.extensions
6069

6170
# Install a backend of your choice
62-
python -m pip install stac-fastapi.sqlalchemy
63-
# or
6471
python -m pip install stac-fastapi.pgstac
6572
```
6673

@@ -71,14 +78,18 @@ Other backends may be available from other sources, search [PyPI](https://pypi.o
7178
Install the packages in editable mode:
7279

7380
```shell
74-
python -m pip install -e \
75-
'stac_fastapi/types[dev]' \
76-
'stac_fastapi/api[dev]' \
77-
'stac_fastapi/extensions[dev]'
81+
python -m pip install \
82+
-e 'stac_fastapi/types[dev]' \
83+
-e 'stac_fastapi/api[dev]' \
84+
-e 'stac_fastapi/extensions[dev]'
7885
```
7986

8087
To run the tests:
8188

8289
```shell
8390
python -m pytest
8491
```
92+
93+
## Releasing
94+
95+
See [RELEASING.md](./RELEASING.md).
File renamed without changes.

docs/src/tips-and-tricks.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
This page contains a few 'tips and tricks' for getting **stac-fastapi** working in various situations.
44

5+
## Avoid FastAPI (slow) serialization
6+
7+
When not using Pydantic validation for responses, FastAPI will still use a complex (slow) [serialization process](https://github.com/fastapi/fastapi/discussions/8165).
8+
9+
Starting with stac-fastapi `5.2.0`, we've added `ENABLE_DIRECT_RESPONSE` option to by-pass the default FastAPI serialization by wrapping the endpoint responses into `starlette.Response` classes.
10+
11+
Ref: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/issues/347
12+
513
## Application Middlewares
614

715
By default the `StacApi` class will enable 3 Middlewares (`BrotliMiddleware`, `CORSMiddleware` and `ProxyHeaderMiddleware`). You may want to overwrite the defaults configuration by editing your backend's `app.py`:

stac_fastapi/api/stac_fastapi/api/app.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@
2525
ItemUri,
2626
)
2727
from stac_fastapi.api.openapi import update_openapi
28-
from stac_fastapi.api.routes import Scope, add_route_dependencies, create_async_endpoint
28+
from stac_fastapi.api.routes import (
29+
Scope,
30+
add_direct_response,
31+
add_route_dependencies,
32+
create_async_endpoint,
33+
)
2934
from stac_fastapi.types.config import ApiSettings, Settings
3035
from stac_fastapi.types.core import AsyncBaseCoreClient, BaseCoreClient
3136
from stac_fastapi.types.extension import ApiExtension
@@ -368,7 +373,7 @@ async def ping():
368373
self.app.include_router(mgmt_router, tags=["Liveliness/Readiness"])
369374

370375
def add_route_dependencies(
371-
self, scopes: List[Scope], dependencies=List[Depends]
376+
self, scopes: List[Scope], dependencies: List[Depends]
372377
) -> None:
373378
"""Add custom dependencies to routes.
374379
@@ -425,3 +430,6 @@ def __attrs_post_init__(self) -> None:
425430
# customize route dependencies
426431
for scopes, dependencies in self.route_dependencies:
427432
self.add_route_dependencies(scopes=scopes, dependencies=dependencies)
433+
434+
if self.app.state.settings.enable_direct_response:
435+
add_direct_response(self.app)

stac_fastapi/api/stac_fastapi/api/routes.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
import inspect
66
from typing import Any, Callable, Dict, List, Optional, Type, TypedDict, Union
77

8-
from fastapi import Depends, params
9-
from fastapi.dependencies.utils import get_parameterless_sub_dependant
8+
from fastapi import Depends, FastAPI, params
9+
from fastapi.datastructures import DefaultPlaceholder
10+
from fastapi.dependencies.utils import get_dependant, get_parameterless_sub_dependant
11+
from fastapi.routing import APIRoute
1012
from pydantic import BaseModel
1113
from starlette.concurrency import run_in_threadpool
1214
from starlette.requests import Request
1315
from starlette.responses import Response
14-
from starlette.routing import BaseRoute, Match
16+
from starlette.routing import BaseRoute, Match, request_response
1517
from starlette.status import HTTP_204_NO_CONTENT
1618

1719
from stac_fastapi.api.models import APIRequest
@@ -86,7 +88,7 @@ class Scope(TypedDict, total=False):
8688

8789

8890
def add_route_dependencies(
89-
routes: List[BaseRoute], scopes: List[Scope], dependencies=List[params.Depends]
91+
routes: List[BaseRoute], scopes: List[Scope], dependencies: List[params.Depends]
9092
) -> None:
9193
"""Add dependencies to routes.
9294
@@ -131,3 +133,33 @@ def add_route_dependencies(
131133
# https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/applications.py#L337-L360
132134
# https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/routing.py#L677-L678
133135
route.dependencies.extend(dependencies)
136+
137+
138+
def add_direct_response(app: FastAPI) -> None:
139+
"""
140+
Setup FastAPI application's endpoints to return Response Object directly, avoiding
141+
Pydantic validation and FastAPI (slow) serialization.
142+
143+
ref: https://gist.github.com/Zaczero/00f3a2679ebc0a25eb938ed82bc63553
144+
"""
145+
146+
def wrap_endpoint(endpoint: Callable, cls: Type[Response]):
147+
@functools.wraps(endpoint)
148+
async def wrapper(*args, **kwargs):
149+
content = await endpoint(*args, **kwargs)
150+
return content if isinstance(content, Response) else cls(content)
151+
152+
return wrapper
153+
154+
for route in app.routes:
155+
if not isinstance(route, APIRoute):
156+
continue
157+
158+
response_class = route.response_class
159+
if isinstance(response_class, DefaultPlaceholder):
160+
response_class = response_class.value
161+
162+
if issubclass(response_class, Response):
163+
route.endpoint = wrap_endpoint(route.endpoint, response_class)
164+
route.dependant = get_dependant(path=route.path_format, call=route.endpoint)
165+
route.app = request_response(route.get_route_handler())

stac_fastapi/api/tests/test_app.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,31 @@ def get_item(self, item_id: str, collection_id: str, **kwargs) -> stac.Item:
108108
assert item.status_code == 200, item.text
109109

110110

111+
def test_client_response_by_pass(TestCoreClient, item_dict):
112+
"""Check with `enable_direct_response` option."""
113+
114+
class InValidResponseClient(TestCoreClient):
115+
def get_item(self, item_id: str, collection_id: str, **kwargs) -> stac.Item:
116+
item_dict.pop("bbox", None)
117+
item_dict.pop("geometry", None)
118+
return item_dict
119+
120+
test_app = app.StacApi(
121+
settings=ApiSettings(
122+
enable_response_models=False,
123+
enable_direct_response=True,
124+
),
125+
client=InValidResponseClient(),
126+
)
127+
128+
with TestClient(test_app.app) as client:
129+
item = client.get("/collections/test/items/test")
130+
131+
assert item.json()
132+
assert item.status_code == 200
133+
assert item.headers["content-type"] == "application/geo+json"
134+
135+
111136
def test_client_openapi(TestCoreClient):
112137
"""Test if response models are all documented with OpenAPI."""
113138

0 commit comments

Comments
 (0)