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

add benchmark #650

Merged
merged 8 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/cicd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,45 @@ jobs:
- uses: actions/checkout@v4
- name: Test generating docs
run: make docs

benchmark:
needs: [test]
runs-on: ubuntu-20.04
steps:
- name: Check out repository code
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install types
run: |
python -m pip install ./stac_fastapi/types[dev]

- name: Install extensions
run: |
python -m pip install ./stac_fastapi/extensions

- name: Install core api
run: |
python -m pip install ./stac_fastapi/api[dev,benchmark]
vincentsarago marked this conversation as resolved.
Show resolved Hide resolved

- name: Run Benchmark
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
uses: benchmark-action/github-action-benchmark@v1
with:
name: STAC FastAPI Benchmarks
tool: 'pytest'
output-file-path: output.json
alert-threshold: '130%'
comment-on-alert: true
fail-on-alert: false
# GitHub API token to make a commit comment
github-token: ${{ secrets.GITHUB_TOKEN }}
gh-pages-branch: 'gh-benchmarks'
# Make a commit only if main
auto-push: ${{ github.ref == 'refs/heads/main' }}
12 changes: 12 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

## [Unreleased]

### Changed

* 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))

### Removed

* Deprecate `response_class` option in `stac_fastapi.api.routes.create_async_endpoint` method ([#650](https://github.com/stac-utils/stac-fastapi/pull/650))

### Added

* Add benchmark in CI ([#650](https://github.com/stac-utils/stac-fastapi/pull/650))

## [2.4.9] - 2023-11-17

### Added
Expand Down
3 changes: 3 additions & 0 deletions stac_fastapi/api/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"requests",
"pystac[validation]==1.*",
],
"benchmark": [
"pytest-benchmark",
],
"docs": ["mkdocs", "mkdocs-material", "pdocs"],
}

Expand Down
28 changes: 8 additions & 20 deletions stac_fastapi/api/stac_fastapi/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,7 @@ def register_landing_page(self):
response_model_exclude_unset=False,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(
self.client.landing_page, EmptyRequest, self.response_class
),
endpoint=create_async_endpoint(self.client.landing_page, EmptyRequest),
vincentsarago marked this conversation as resolved.
Show resolved Hide resolved
)

def register_conformance_classes(self):
Expand All @@ -153,9 +151,7 @@ def register_conformance_classes(self):
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(
self.client.conformance, EmptyRequest, self.response_class
),
endpoint=create_async_endpoint(self.client.conformance, EmptyRequest),
)

def register_get_item(self):
Expand All @@ -172,9 +168,7 @@ def register_get_item(self):
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(
self.client.get_item, ItemUri, GeoJSONResponse
),
endpoint=create_async_endpoint(self.client.get_item, ItemUri),
)

def register_post_search(self):
Expand All @@ -195,7 +189,7 @@ def register_post_search(self):
response_model_exclude_none=True,
methods=["POST"],
endpoint=create_async_endpoint(
self.client.post_search, self.search_post_request_model, GeoJSONResponse
self.client.post_search, self.search_post_request_model
),
)

Expand All @@ -217,7 +211,7 @@ def register_get_search(self):
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(
self.client.get_search, self.search_get_request_model, GeoJSONResponse
self.client.get_search, self.search_get_request_model
),
)

Expand All @@ -237,9 +231,7 @@ def register_get_collections(self):
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(
self.client.all_collections, EmptyRequest, self.response_class
),
endpoint=create_async_endpoint(self.client.all_collections, EmptyRequest),
)

def register_get_collection(self):
Expand All @@ -256,9 +248,7 @@ def register_get_collection(self):
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(
self.client.get_collection, CollectionUri, self.response_class
),
endpoint=create_async_endpoint(self.client.get_collection, CollectionUri),
)

def register_get_item_collection(self):
Expand Down Expand Up @@ -287,9 +277,7 @@ def register_get_item_collection(self):
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(
self.client.item_collection, request_model, GeoJSONResponse
),
endpoint=create_async_endpoint(self.client.item_collection, request_model),
)

def register_core(self):
Expand Down
31 changes: 16 additions & 15 deletions stac_fastapi/api/stac_fastapi/api/routes.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
"""Route factories."""

import functools
import inspect
import warnings
from typing import Any, Callable, Dict, List, Optional, Type, TypedDict, Union

from fastapi import Depends, params
from fastapi.dependencies.utils import get_parameterless_sub_dependant
from pydantic import BaseModel
from starlette.concurrency import run_in_threadpool
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.responses import Response
from starlette.routing import BaseRoute, Match
from starlette.status import HTTP_204_NO_CONTENT

from stac_fastapi.api.models import APIRequest


def _wrap_response(resp: Any, response_class: Type[Response]) -> Response:
if isinstance(resp, Response):
def _wrap_response(resp: Any) -> Any:
vincentsarago marked this conversation as resolved.
Show resolved Hide resolved
if resp is not None:
return resp
elif resp is not None:
return response_class(resp)
else: # None is returned as 204 No Content
return Response(status_code=HTTP_204_NO_CONTENT)
vincentsarago marked this conversation as resolved.
Show resolved Hide resolved

Expand All @@ -37,12 +37,19 @@ async def run(*args, **kwargs):
def create_async_endpoint(
func: Callable,
request_model: Union[Type[APIRequest], Type[BaseModel], Dict],
response_class: Type[Response] = JSONResponse,
vincentsarago marked this conversation as resolved.
Show resolved Hide resolved
response_class: Optional[Type[Response]] = None,
):
"""Wrap a function in a coroutine which may be used to create a FastAPI endpoint.

Synchronous functions are executed asynchronously using a background thread.
"""

if response_class:
warnings.warns(
"`response_class` option is deprecated, please set the Response class directly in the endpoint.", # noqa: E501
DeprecationWarning,
)

if not inspect.iscoroutinefunction(func):
func = sync_to_async(func)

Expand All @@ -53,9 +60,7 @@ async def _endpoint(
request_data: request_model = Depends(), # type:ignore
):
"""Endpoint."""
return _wrap_response(
await func(request=request, **request_data.kwargs()), response_class
)
return _wrap_response(await func(request=request, **request_data.kwargs()))

elif issubclass(request_model, BaseModel):

Expand All @@ -64,9 +69,7 @@ async def _endpoint(
request_data: request_model, # type:ignore
):
"""Endpoint."""
return _wrap_response(
await func(request_data, request=request), response_class
)
return _wrap_response(await func(request_data, request=request))

else:

Expand All @@ -75,9 +78,7 @@ async def _endpoint(
request_data: Dict[str, Any], # type:ignore
):
"""Endpoint."""
return _wrap_response(
await func(request_data, request=request), response_class
)
return _wrap_response(await func(request_data, request=request))

return _endpoint

Expand Down
Loading
Loading