Skip to content

Commit de68078

Browse files
add benchmark (#650)
* add benchmark * fix workflow * fix * do not add Response output to endpoints * update changelog and add warning * Update CHANGES.md Co-authored-by: Pete Gadomski <[email protected]> * store benchmark results --------- Co-authored-by: Pete Gadomski <[email protected]>
1 parent 4fb10ec commit de68078

File tree

6 files changed

+253
-35
lines changed

6 files changed

+253
-35
lines changed

.github/workflows/cicd.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,45 @@ jobs:
7676
- uses: actions/checkout@v4
7777
- name: Test generating docs
7878
run: make docs
79+
80+
benchmark:
81+
needs: [test]
82+
runs-on: ubuntu-20.04
83+
steps:
84+
- name: Check out repository code
85+
uses: actions/checkout@v4
86+
87+
- name: Setup Python
88+
uses: actions/setup-python@v5
89+
with:
90+
python-version: "3.11"
91+
92+
- name: Install types
93+
run: |
94+
python -m pip install ./stac_fastapi/types[dev]
95+
96+
- name: Install extensions
97+
run: |
98+
python -m pip install ./stac_fastapi/extensions
99+
100+
- name: Install core api
101+
run: |
102+
python -m pip install ./stac_fastapi/api[dev,benchmark]
103+
104+
- name: Run Benchmark
105+
run: python -m pytest stac_fastapi/api/tests/benchmarks.py --benchmark-only --benchmark-columns 'min, max, mean, median' --benchmark-json output.json
106+
107+
- name: Store and benchmark result
108+
uses: benchmark-action/github-action-benchmark@v1
109+
with:
110+
name: STAC FastAPI Benchmarks
111+
tool: 'pytest'
112+
output-file-path: output.json
113+
alert-threshold: '130%'
114+
comment-on-alert: true
115+
fail-on-alert: false
116+
# GitHub API token to make a commit comment
117+
github-token: ${{ secrets.GITHUB_TOKEN }}
118+
gh-pages-branch: 'gh-benchmarks'
119+
# Make a commit only if main
120+
auto-push: ${{ github.ref == 'refs/heads/main' }}

CHANGES.md

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

33
## [Unreleased]
44

5+
### Changed
6+
7+
* Make sure FastAPI uses Pydantic validation and serialization by not wrapping endpoint output with a Response object ([#650](https://github.com/stac-utils/stac-fastapi/pull/650))
8+
9+
### Removed
10+
11+
* Deprecate `response_class` option in `stac_fastapi.api.routes.create_async_endpoint` method ([#650](https://github.com/stac-utils/stac-fastapi/pull/650))
12+
13+
### Added
14+
15+
* Add benchmark in CI ([#650](https://github.com/stac-utils/stac-fastapi/pull/650))
16+
517
## [2.4.9] - 2023-11-17
618

719
### Added

stac_fastapi/api/setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
"requests",
2424
"pystac[validation]==1.*",
2525
],
26+
"benchmark": [
27+
"pytest-benchmark",
28+
],
2629
"docs": ["mkdocs", "mkdocs-material", "pdocs"],
2730
}
2831

stac_fastapi/api/stac_fastapi/api/app.py

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,7 @@ def register_landing_page(self):
132132
response_model_exclude_unset=False,
133133
response_model_exclude_none=True,
134134
methods=["GET"],
135-
endpoint=create_async_endpoint(
136-
self.client.landing_page, EmptyRequest, self.response_class
137-
),
135+
endpoint=create_async_endpoint(self.client.landing_page, EmptyRequest),
138136
)
139137

140138
def register_conformance_classes(self):
@@ -153,9 +151,7 @@ def register_conformance_classes(self):
153151
response_model_exclude_unset=True,
154152
response_model_exclude_none=True,
155153
methods=["GET"],
156-
endpoint=create_async_endpoint(
157-
self.client.conformance, EmptyRequest, self.response_class
158-
),
154+
endpoint=create_async_endpoint(self.client.conformance, EmptyRequest),
159155
)
160156

161157
def register_get_item(self):
@@ -172,9 +168,7 @@ def register_get_item(self):
172168
response_model_exclude_unset=True,
173169
response_model_exclude_none=True,
174170
methods=["GET"],
175-
endpoint=create_async_endpoint(
176-
self.client.get_item, ItemUri, GeoJSONResponse
177-
),
171+
endpoint=create_async_endpoint(self.client.get_item, ItemUri),
178172
)
179173

180174
def register_post_search(self):
@@ -195,7 +189,7 @@ def register_post_search(self):
195189
response_model_exclude_none=True,
196190
methods=["POST"],
197191
endpoint=create_async_endpoint(
198-
self.client.post_search, self.search_post_request_model, GeoJSONResponse
192+
self.client.post_search, self.search_post_request_model
199193
),
200194
)
201195

@@ -217,7 +211,7 @@ def register_get_search(self):
217211
response_model_exclude_none=True,
218212
methods=["GET"],
219213
endpoint=create_async_endpoint(
220-
self.client.get_search, self.search_get_request_model, GeoJSONResponse
214+
self.client.get_search, self.search_get_request_model
221215
),
222216
)
223217

@@ -237,9 +231,7 @@ def register_get_collections(self):
237231
response_model_exclude_unset=True,
238232
response_model_exclude_none=True,
239233
methods=["GET"],
240-
endpoint=create_async_endpoint(
241-
self.client.all_collections, EmptyRequest, self.response_class
242-
),
234+
endpoint=create_async_endpoint(self.client.all_collections, EmptyRequest),
243235
)
244236

245237
def register_get_collection(self):
@@ -256,9 +248,7 @@ def register_get_collection(self):
256248
response_model_exclude_unset=True,
257249
response_model_exclude_none=True,
258250
methods=["GET"],
259-
endpoint=create_async_endpoint(
260-
self.client.get_collection, CollectionUri, self.response_class
261-
),
251+
endpoint=create_async_endpoint(self.client.get_collection, CollectionUri),
262252
)
263253

264254
def register_get_item_collection(self):
@@ -287,9 +277,7 @@ def register_get_item_collection(self):
287277
response_model_exclude_unset=True,
288278
response_model_exclude_none=True,
289279
methods=["GET"],
290-
endpoint=create_async_endpoint(
291-
self.client.item_collection, request_model, GeoJSONResponse
292-
),
280+
endpoint=create_async_endpoint(self.client.item_collection, request_model),
293281
)
294282

295283
def register_core(self):

stac_fastapi/api/stac_fastapi/api/routes.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
"""Route factories."""
2+
23
import functools
34
import inspect
5+
import warnings
46
from typing import Any, Callable, Dict, List, Optional, Type, TypedDict, Union
57

68
from fastapi import Depends, params
79
from fastapi.dependencies.utils import get_parameterless_sub_dependant
810
from pydantic import BaseModel
911
from starlette.concurrency import run_in_threadpool
1012
from starlette.requests import Request
11-
from starlette.responses import JSONResponse, Response
13+
from starlette.responses import Response
1214
from starlette.routing import BaseRoute, Match
1315
from starlette.status import HTTP_204_NO_CONTENT
1416

1517
from stac_fastapi.api.models import APIRequest
1618

1719

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:
2022
return resp
21-
elif resp is not None:
22-
return response_class(resp)
2323
else: # None is returned as 204 No Content
2424
return Response(status_code=HTTP_204_NO_CONTENT)
2525

@@ -37,12 +37,19 @@ async def run(*args, **kwargs):
3737
def create_async_endpoint(
3838
func: Callable,
3939
request_model: Union[Type[APIRequest], Type[BaseModel], Dict],
40-
response_class: Type[Response] = JSONResponse,
40+
response_class: Optional[Type[Response]] = None,
4141
):
4242
"""Wrap a function in a coroutine which may be used to create a FastAPI endpoint.
4343
4444
Synchronous functions are executed asynchronously using a background thread.
4545
"""
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+
4653
if not inspect.iscoroutinefunction(func):
4754
func = sync_to_async(func)
4855

@@ -53,9 +60,7 @@ async def _endpoint(
5360
request_data: request_model = Depends(), # type:ignore
5461
):
5562
"""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()))
5964

6065
elif issubclass(request_model, BaseModel):
6166

@@ -64,9 +69,7 @@ async def _endpoint(
6469
request_data: request_model, # type:ignore
6570
):
6671
"""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))
7073

7174
else:
7275

@@ -75,9 +78,7 @@ async def _endpoint(
7578
request_data: Dict[str, Any], # type:ignore
7679
):
7780
"""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))
8182

8283
return _endpoint
8384

0 commit comments

Comments
 (0)