From adf0a8e6887eacdda38732103f9c389015f5a828 Mon Sep 17 00:00:00 2001 From: Erik Seglem Date: Mon, 6 Feb 2023 10:52:01 -0500 Subject: [PATCH 1/5] Rework update_openapi function. --- stac_fastapi/api/stac_fastapi/api/openapi.py | 80 ++++++-------------- stac_fastapi/api/tests/test_api.py | 9 +++ 2 files changed, 33 insertions(+), 56 deletions(-) diff --git a/stac_fastapi/api/stac_fastapi/api/openapi.py b/stac_fastapi/api/stac_fastapi/api/openapi.py index 574176a46..fa3fd6199 100644 --- a/stac_fastapi/api/stac_fastapi/api/openapi.py +++ b/stac_fastapi/api/stac_fastapi/api/openapi.py @@ -1,68 +1,36 @@ """openapi.""" from fastapi import FastAPI -from fastapi.openapi.utils import get_openapi from starlette.requests import Request -from starlette.responses import JSONResponse - -from stac_fastapi.api.config import ApiExtensions -from stac_fastapi.types.config import ApiSettings - - -class VndOaiResponse(JSONResponse): - """JSON with custom, vendor content-type.""" - - media_type = "application/vnd.oai.openapi+json;version=3.0" +from starlette.responses import JSONResponse, Response +from starlette.routing import Route, request_response def update_openapi(app: FastAPI) -> FastAPI: """Update OpenAPI response content-type. This function modifies the openapi route to comply with the STAC API spec's - required content-type response header + required content-type response header. """ - urls = (server_data.get("url") for server_data in app.servers) - server_urls = {url for url in urls if url} - - async def openapi(req: Request) -> JSONResponse: - root_path = req.scope.get("root_path", "").rstrip("/") - if root_path not in server_urls: - if root_path and app.root_path_in_servers: - app.servers.insert(0, {"url": root_path}) - server_urls.add(root_path) - return VndOaiResponse(app.openapi()) - - # Remove the default openapi route - app.router.routes = list( - filter(lambda r: r.path != app.openapi_url, app.router.routes) + # Find the route for the openapi_url in the app. + openapi_route: Route = next( + route for route in app.router.routes if route.path == app.openapi_url ) - # Add the updated openapi route - app.add_route(app.openapi_url, openapi, include_in_schema=False) - return app - -# TODO: Remove or fix, this is currently unused -# and calls a missing method on ApiSettings -def config_openapi(app: FastAPI, settings: ApiSettings): - """Config openapi.""" - - def custom_openapi(): - """Config openapi.""" - if app.openapi_schema: - return app.openapi_schema - - openapi_schema = get_openapi( - title="Arturo STAC API", version="0.1", routes=app.routes - ) - - if settings.api_extension_is_enabled(ApiExtensions.fields): - openapi_schema["paths"]["/search"]["get"]["responses"]["200"]["content"][ - "application/json" - ]["schema"] = {"$ref": "#/components/schemas/ItemCollection"} - openapi_schema["paths"]["/search"]["post"]["responses"]["200"]["content"][ - "application/json" - ]["schema"] = {"$ref": "#/components/schemas/ItemCollection"} - - app.openapi_schema = openapi_schema - return app.openapi_schema - - app.openapi = custom_openapi + # Create a patched endpoint that modifies the content type of the response + async def patched_openapi_endpoint(req: Request) -> Response: + # Get the response from the original endpoint + response: JSONResponse = await openapi_route.endpoint(req) + # Update the content type header in place + response.headers[ + "content-type" + ] = "application/vnd.oai.openapi+json;version=3.0" + # Return the updated response + return response + + # When a route is accessed the `handle` function will call `self.app`. So we can + # leave the `endpoint` the original function for use in the patched function and + # just update the `app` to use the patched function. + openapi_route.app = request_response(patched_openapi_endpoint) + + # return the patched app + return app diff --git a/stac_fastapi/api/tests/test_api.py b/stac_fastapi/api/tests/test_api.py index ab5a304d4..15629e7b7 100644 --- a/stac_fastapi/api/tests/test_api.py +++ b/stac_fastapi/api/tests/test_api.py @@ -49,6 +49,15 @@ def _assert_dependency_applied(api, routes): ), "Authenticated requests should be accepted" assert response.json() == "dummy response" + def test_openapi_content_type(self): + api = self._build_api() + with TestClient(api.app) as client: + response = client.get(api.settings.openapi_url) + assert ( + response.headers["content-type"] + == "application/vnd.oai.openapi+json;version=3.0" + ) + def test_build_api_with_route_dependencies(self): routes = [ {"path": "/collections", "method": "POST"}, From f78584d93c5f33705d53e1df3b64c4639c0b9ba9 Mon Sep 17 00:00:00 2001 From: Erik Seglem Date: Mon, 6 Feb 2023 12:04:44 -0500 Subject: [PATCH 2/5] Update changelog. --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index d04917a10..8a663726d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ ### Changed * Updated CI to test against [pgstac v0.6.12](https://github.com/stac-utils/pgstac/releases/tag/v0.6.12) ([#511](https://github.com/stac-utils/stac-fastapi/pull/511)) +* Reworked `update_openapi` and added a test for it. ([#523](https://github.com/stac-utils/stac-fastapi/pull/523)) ### Removed From a318353c5366c17de785d7a579bef3a069fde069 Mon Sep 17 00:00:00 2001 From: Erik Seglem Date: Mon, 6 Feb 2023 21:26:48 -0500 Subject: [PATCH 3/5] Additional documentation and add removed code back as deprecated. --- CHANGES.md | 1 + stac_fastapi/api/stac_fastapi/api/openapi.py | 66 +++++++++++++++++--- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d11cbfb09..04d51b01a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ * Updated CI to test against [pgstac v0.6.12](https://github.com/stac-utils/pgstac/releases/tag/v0.6.12) ([#511](https://github.com/stac-utils/stac-fastapi/pull/511)) * Reworked `update_openapi` and added a test for it. ([#523](https://github.com/stac-utils/stac-fastapi/pull/523)) +* Deprecated `VndOaiResponse` and `config_openapi`. ([#523](https://github.com/stac-utils/stac-fastapi/pull/523)) ### Removed diff --git a/stac_fastapi/api/stac_fastapi/api/openapi.py b/stac_fastapi/api/stac_fastapi/api/openapi.py index fa3fd6199..0a2ee8004 100644 --- a/stac_fastapi/api/stac_fastapi/api/openapi.py +++ b/stac_fastapi/api/stac_fastapi/api/openapi.py @@ -1,9 +1,15 @@ """openapi.""" +import warnings + from fastapi import FastAPI +from fastapi.openapi.utils import get_openapi from starlette.requests import Request from starlette.responses import JSONResponse, Response from starlette.routing import Route, request_response +from stac_fastapi.api.config import ApiExtensions +from stac_fastapi.types.config import ApiSettings + def update_openapi(app: FastAPI) -> FastAPI: """Update OpenAPI response content-type. @@ -11,15 +17,17 @@ def update_openapi(app: FastAPI) -> FastAPI: This function modifies the openapi route to comply with the STAC API spec's required content-type response header. """ - # Find the route for the openapi_url in the app. + # Find the route for the openapi_url in the app openapi_route: Route = next( route for route in app.router.routes if route.path == app.openapi_url ) + # Store the old endpoint function so we can call it from the patched function + old_endpoint = openapi_route.endpoint - # Create a patched endpoint that modifies the content type of the response + # Create a patched endpoint function that modifies the content type of the response async def patched_openapi_endpoint(req: Request) -> Response: - # Get the response from the original endpoint - response: JSONResponse = await openapi_route.endpoint(req) + # Get the response from the old endpoint function + response: JSONResponse = await old_endpoint(req) # Update the content type header in place response.headers[ "content-type" @@ -27,10 +35,54 @@ async def patched_openapi_endpoint(req: Request) -> Response: # Return the updated response return response - # When a route is accessed the `handle` function will call `self.app`. So we can - # leave the `endpoint` the original function for use in the patched function and - # just update the `app` to use the patched function. + # When a Route is accessed the `handle` function calls `self.app`. Which is + # the endpoint function wrapped with `request_response`. So we need to wrap + # our patched function and replace the existing app with it. openapi_route.app = request_response(patched_openapi_endpoint) # return the patched app return app + + +class VndOaiResponse(JSONResponse): + """JSON with custom, vendor content-type.""" + + media_type = "application/vnd.oai.openapi+json;version=3.0" + + def __init__(self, *args, **kwargs): + """Init function with deprecation warning.""" + warnings.warn( + "VndOaiResponse is deprecated and will be removed in v3.0", + DeprecationWarning, + ) + super().__init__(*args, **kwargs) + + +def config_openapi(app: FastAPI, settings: ApiSettings): + """Config openapi.""" + warnings.warn( + "config_openapi is deprecated and will be removed in v3.0", + DeprecationWarning, + ) + + def custom_openapi(): + """Config openapi.""" + if app.openapi_schema: + return app.openapi_schema + + openapi_schema = get_openapi( + title="Arturo STAC API", version="0.1", routes=app.routes + ) + + if settings.api_extension_is_enabled(ApiExtensions.fields): + openapi_schema["paths"]["/search"]["get"]["responses"]["200"]["content"][ + "application/json" + ]["schema"] = {"$ref": "#/components/schemas/ItemCollection"} + openapi_schema["paths"]["/search"]["post"]["responses"]["200"]["content"][ + "application/json" + ]["schema"] = {"$ref": "#/components/schemas/ItemCollection"} + + app.openapi_schema = openapi_schema + return app.openapi_schema + + app.openapi = custom_openapi From 05671af1d94c0186792bee0c6e50cf27cfe612f0 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Tue, 7 Feb 2023 05:55:51 -0700 Subject: [PATCH 4/5] chore: fixup changelog --- CHANGES.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 04d51b01a..b6872a55b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,8 +10,7 @@ ### Changed * Updated CI to test against [pgstac v0.6.12](https://github.com/stac-utils/pgstac/releases/tag/v0.6.12) ([#511](https://github.com/stac-utils/stac-fastapi/pull/511)) -* Reworked `update_openapi` and added a test for it. ([#523](https://github.com/stac-utils/stac-fastapi/pull/523)) -* Deprecated `VndOaiResponse` and `config_openapi`. ([#523](https://github.com/stac-utils/stac-fastapi/pull/523)) +* Reworked `update_openapi` and added a test for it ([#523](https://github.com/stac-utils/stac-fastapi/pull/523)) ### Removed @@ -26,6 +25,10 @@ * Manually exclude non-truthy optional values from sqlalchemy serialization of Collections ([#508](https://github.com/stac-utils/stac-fastapi/pull/508)) * Deleting items that had repeated ids in other collections ([#520](https://github.com/stac-utils/stac-fastapi/pull/520)) +### Deprecated + +* Deprecated `VndOaiResponse` and `config_openapi`, will be removed in v3.0 ([#523](https://github.com/stac-utils/stac-fastapi/pull/523)) + ## [2.4.3] - 2022-11-25 ### Added From ffbb1bfc767fa77414e77615d40a99d2db4ba135 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Tue, 7 Feb 2023 05:57:15 -0700 Subject: [PATCH 5/5] refactor: move VndOaiResponse back to old location --- stac_fastapi/api/stac_fastapi/api/openapi.py | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/stac_fastapi/api/stac_fastapi/api/openapi.py b/stac_fastapi/api/stac_fastapi/api/openapi.py index 0a2ee8004..2ccd48282 100644 --- a/stac_fastapi/api/stac_fastapi/api/openapi.py +++ b/stac_fastapi/api/stac_fastapi/api/openapi.py @@ -11,6 +11,20 @@ from stac_fastapi.types.config import ApiSettings +class VndOaiResponse(JSONResponse): + """JSON with custom, vendor content-type.""" + + media_type = "application/vnd.oai.openapi+json;version=3.0" + + def __init__(self, *args, **kwargs): + """Init function with deprecation warning.""" + warnings.warn( + "VndOaiResponse is deprecated and will be removed in v3.0", + DeprecationWarning, + ) + super().__init__(*args, **kwargs) + + def update_openapi(app: FastAPI) -> FastAPI: """Update OpenAPI response content-type. @@ -44,20 +58,6 @@ async def patched_openapi_endpoint(req: Request) -> Response: return app -class VndOaiResponse(JSONResponse): - """JSON with custom, vendor content-type.""" - - media_type = "application/vnd.oai.openapi+json;version=3.0" - - def __init__(self, *args, **kwargs): - """Init function with deprecation warning.""" - warnings.warn( - "VndOaiResponse is deprecated and will be removed in v3.0", - DeprecationWarning, - ) - super().__init__(*args, **kwargs) - - def config_openapi(app: FastAPI, settings: ApiSettings): """Config openapi.""" warnings.warn(