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

OpenAPI construction #499

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@

## [2.5.0] - 2024-04-12

* Add support for setting OpenAPI metadata through environment variables.

### Added

* Add benchmark in CI ([#650](https://github.com/stac-utils/stac-fastapi/pull/650))
Expand Down
39 changes: 19 additions & 20 deletions stac_fastapi/api/stac_fastapi/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
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 api
from stac_pydantic.api.collections import Collections
Expand Down Expand Up @@ -74,12 +73,31 @@ class StacApi:
exceptions: Dict[Type[Exception], int] = attr.ib(
default=attr.Factory(lambda: DEFAULT_STATUS_CODES)
)
title: str = attr.ib(
default=attr.Factory(lambda self: self.settings.api_title, takes_self=True)
)
api_version: str = attr.ib(
default=attr.Factory(lambda self: self.settings.api_version, takes_self=True)
)
description: str = attr.ib(
default=attr.Factory(
lambda self: self.settings.api_description, takes_self=True
)
)
app: FastAPI = attr.ib(
default=attr.Factory(
lambda self: FastAPI(
openapi_url=self.settings.openapi_url,
docs_url=self.settings.docs_url,
redoc_url=None,
description=self.description,
title=self.title,
version=self.api_version,
servers=self.settings.api_servers,
terms_of_service=self.settings.api_terms_of_service,
contact=self.settings.api_contact,
license_info=self.settings.api_license_info,
openapi_tags=self.settings.api_tags,
),
takes_self=True,
),
Expand Down Expand Up @@ -395,22 +413,6 @@ def register_core(self):
self.register_get_collection()
self.register_get_item_collection()

def customize_openapi(self) -> Optional[Dict[str, Any]]:
"""Customize openapi schema."""
if self.app.openapi_schema:
return self.app.openapi_schema

openapi_schema = get_openapi(
title=self.title,
version=self.api_version,
description=self.description,
routes=self.app.routes,
servers=self.app.servers,
)

self.app.openapi_schema = openapi_schema
return self.app.openapi_schema

def add_health_check(self):
"""Add a health check."""
mgmt_router = APIRouter(prefix=self.app.state.router_prefix)
Expand Down Expand Up @@ -479,9 +481,6 @@ def __attrs_post_init__(self):
# register exception handlers
add_exception_handlers(self.app, status_codes=self.exceptions)

# customize openapi
self.app.openapi = self.customize_openapi

# add middlewares
for middleware in self.middlewares:
self.add_middleware(middleware)
Expand Down
88 changes: 88 additions & 0 deletions stac_fastapi/api/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from json import dumps, loads
from os import environ
from typing import Any, Dict

from fastapi import Depends, HTTPException, security, status
from pytest import MonkeyPatch, mark
from starlette.testclient import TestClient

from stac_fastapi.api.app import StacApi
Expand Down Expand Up @@ -445,3 +450,86 @@ def must_be_bob(
detail="You're not Bob",
headers={"WWW-Authenticate": "Basic"},
)


@mark.parametrize(
"env,",
(
{},
{
"api_description": "API Description for Testing",
"api_title": "API Title For Testing",
"api_version": "0.1-testing",
"api_servers": [
{"url": "http://api1", "description": "API 1"},
{"url": "http://api2"},
],
"api_terms_of_service": "http://terms-of-service",
"api_contact": {
"name": "Contact",
"url": "http://contact",
"email": "info@contact",
},
"api_license_info": {
"name": "License",
"url": "http://license",
},
"api_tags": [
{
"name": "Tag",
"description": "Test tag",
"externalDocs": {
"url": "http://tags/tag",
"description": "rtfm",
},
}
],
},
),
)
def test_openapi(monkeypatch: MonkeyPatch, env: Dict[str, Any]):
for key, value in env.items():
monkeypatch.setenv(
key.upper(),
value if isinstance(value, str) else dumps(value),
)
settings = config.ApiSettings()

api = StacApi(
**{
"settings": settings,
"client": DummyCoreClient(),
"extensions": [
TransactionExtension(
client=DummyTransactionsClient(), settings=settings
),
TokenPaginationExtension(),
],
}
)

with TestClient(api.app) as client:
response = client.get(api.app.openapi_url)

assert response.status_code == 200
assert (
response.headers["Content-Type"]
== "application/vnd.oai.openapi+json;version=3.0"
)

def expected_value(key: str, json=False) -> Any:
if key.upper() in environ:
value = environ[key.upper()]
return loads(value) if json else value
return getattr(settings, key)

data = response.json()
info = data["info"]
assert info["description"] == expected_value("api_description")
assert info["title"] == expected_value("api_title")
assert info["version"] == expected_value("api_version")
assert info.get("termsOfService", None) == expected_value("api_terms_of_service")
assert info.get("contact") == expected_value("api_contact", True)
assert info.get("license") == expected_value("api_license_info", True)
assert data.get("servers", []) == expected_value("api_servers", True)
assert data.get("tags", []) == expected_value("api_tags", True)
14 changes: 12 additions & 2 deletions stac_fastapi/types/stac_fastapi/types/config.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""stac_fastapi.types.config module."""

from typing import Optional
from typing import Any, Dict, List, Optional

from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import AnyHttpUrl
from pydantic_settings import AnyHttpUrl, BaseSettings, SettingsConfigDict


class ApiSettings(BaseSettings):
Expand Down Expand Up @@ -31,6 +32,15 @@ class ApiSettings(BaseSettings):

openapi_url: str = "/api"
docs_url: str = "/api.html"

api_title: str = "stac-fastapi"
api_description: str = "stac-fastapi"
api_version: str = "0.1"
api_servers: List[Dict[str, Any]] = []
api_terms_of_service: Optional[AnyHttpUrl] = None
api_contact: Optional[Dict[str, Any]] = None
api_license_info: Optional[Dict[str, Any]] = None
api_tags: List[Dict[str, Any]] = []

model_config = SettingsConfigDict(env_file=".env", extra="allow")

Expand Down
Loading