diff --git a/CHANGES.md b/CHANGES.md index 3d63fea65..a9f3d6a4e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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)) diff --git a/stac_fastapi/api/stac_fastapi/api/app.py b/stac_fastapi/api/stac_fastapi/api/app.py index 5fe7f9d08..b9f41fded 100644 --- a/stac_fastapi/api/stac_fastapi/api/app.py +++ b/stac_fastapi/api/stac_fastapi/api/app.py @@ -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 @@ -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, ), @@ -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) @@ -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) diff --git a/stac_fastapi/api/tests/test_api.py b/stac_fastapi/api/tests/test_api.py index d559a377a..4b485142a 100644 --- a/stac_fastapi/api/tests/test_api.py +++ b/stac_fastapi/api/tests/test_api.py @@ -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 @@ -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) diff --git a/stac_fastapi/types/stac_fastapi/types/config.py b/stac_fastapi/types/stac_fastapi/types/config.py index 75d0bd399..ca03e4142 100644 --- a/stac_fastapi/types/stac_fastapi/types/config.py +++ b/stac_fastapi/types/stac_fastapi/types/config.py @@ -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): @@ -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")