Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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 @@ -2,6 +2,8 @@

## Unreleased

* Add support for setting OpenAPI metadata through environment variables.

### Added

### Changed
Expand Down
44 changes: 20 additions & 24 deletions stac_fastapi/api/stac_fastapi/api/app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"""fastapi app creation."""
from typing import Any, Dict, List, Optional, Tuple, Type, Union
from typing import 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
Expand Down Expand Up @@ -61,22 +60,38 @@ 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,
),
converter=update_openapi,
)
router: APIRouter = attr.ib(default=attr.Factory(APIRouter))
title: str = attr.ib(default="stac-fastapi")
api_version: str = attr.ib(default="0.1")
stac_version: str = attr.ib(default=STAC_VERSION)
description: str = attr.ib(default="stac-fastapi")
search_get_request_model: Type[BaseSearchGetRequest] = attr.ib(
default=BaseSearchGetRequest
)
Expand Down Expand Up @@ -308,22 +323,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 @@ -388,9 +387,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.app.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 @@ -129,3 +134,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)
13 changes: 11 additions & 2 deletions stac_fastapi/types/stac_fastapi/types/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""stac_fastapi.types.config module."""
from typing import Optional, Set
from typing import Any, Dict, List, Optional, Set

from pydantic import BaseSettings
from pydantic import AnyHttpUrl, BaseSettings


class ApiSettings(BaseSettings):
Expand Down Expand Up @@ -29,6 +29,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]] = []

class Config:
"""model config (https://pydantic-docs.helpmanual.io/usage/model_config/)."""

Expand Down