From 25d410c1eb86ef3ef53aa2b59a64f176e68a95f8 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 29 Dec 2023 10:07:37 -0600 Subject: [PATCH 1/6] Add option to download OpenAPI spec file --- .../event_handler/api_gateway.py | 36 +++++++++++++------ docs/core/event_handler/api_gateway.md | 15 ++++++++ 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 79e194e3719..9a89b95c604 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -1559,6 +1559,7 @@ def enable_swagger( license_info: Optional["License"] = None, swagger_base_url: Optional[str] = None, middlewares: Optional[List[Callable[..., Response]]] = None, + enable_download_spec: Optional[bool] = False, ): """ Returns the OpenAPI schema as a JSON serializable dict @@ -1591,6 +1592,8 @@ def enable_swagger( The base url for the swagger UI. If not provided, we will serve a recent version of the Swagger UI. middlewares: List[Callable[..., Response]], optional List of middlewares to be used for the swagger route. + enable_download_spec: bool, optional + Enable download of the OpenAPI schema as a JSON file at the `.json` (ex. `/swagger.json`). """ from aws_lambda_powertools.event_handler.openapi.models import Server @@ -1614,6 +1617,17 @@ def swagger_css(): body=body, ) + if enable_download_spec: + + @self.get(f"{path}.json", include_in_schema=False) + def swagger_json(): + spec = get_swagger_spec() + return Response( + status_code=200, + content_type="application/json", + body=spec, + ) + @self.get(path, middlewares=middlewares, include_in_schema=False) def swagger_handler(): base_path = self._get_base_path() @@ -1625,9 +1639,19 @@ def swagger_handler(): swagger_js = f"{base_path}/swagger.js" swagger_css = f"{base_path}/swagger.css" - openapi_servers = servers or [Server(url=(base_path or "/"))] + spec = get_swagger_spec() + body = generate_swagger_html(spec, swagger_js, swagger_css) - spec = self.get_openapi_json_schema( + return Response( + status_code=200, + content_type="text/html", + body=body, + ) + + def get_swagger_spec(): + base_path = self._get_base_path() + openapi_servers = servers or [Server(url=(base_path or "/"))] + return self.get_openapi_json_schema( title=title, version=version, openapi_version=openapi_version, @@ -1640,14 +1664,6 @@ def swagger_handler(): license_info=license_info, ) - body = generate_swagger_html(spec, swagger_js, swagger_css) - - return Response( - status_code=200, - content_type="text/html", - body=body, - ) - def route( self, rule: str, diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 005ac3a4b7b..ced8f71a5e0 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -1019,6 +1019,21 @@ Include extra parameters when exporting your OpenAPI specification to apply thes --8<-- "examples/event_handler_rest/src/customizing_api_metadata.py" ``` +#### Exposing the OpenAPI specification + +If you would like to be able to download the OpenAPI specification file for generating +client libraries or hosting the documentation separately, you can set the +`enable_download_spec` input to `True` when calling `enable_swagger`: + +```python +app.enable_swagger(path="/_swagger", enable_download_spec=True) +``` + +With this setting enabled, you can access the specification at `.json`. So, for +the snippet above, the specification file would be retrievable at `/_swagger.json`. +To access this endpoint, you will have to add an additional endpoint in your template +file. + ### Custom serializer You can instruct event handler to use a custom serializer to best suit your needs, for example take into account Enums when serializing. From caa0f682063fdee71053bf4014b9f0bb8ecd93ad Mon Sep 17 00:00:00 2001 From: Thomas McKanna Date: Sat, 30 Dec 2023 13:22:31 -0600 Subject: [PATCH 2/6] Remove Optional from enable_download_spec Co-authored-by: Leandro Damascena Signed-off-by: Thomas McKanna --- aws_lambda_powertools/event_handler/api_gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 9a89b95c604..e81cb313fff 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -1559,7 +1559,7 @@ def enable_swagger( license_info: Optional["License"] = None, swagger_base_url: Optional[str] = None, middlewares: Optional[List[Callable[..., Response]]] = None, - enable_download_spec: Optional[bool] = False, + enable_download_spec: bool = False, ): """ Returns the OpenAPI schema as a JSON serializable dict From adea6fa0164cdd8db767978d20a2f21b42f50b0e Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 30 Dec 2023 13:51:44 -0600 Subject: [PATCH 3/6] Add test for enable_download_spec --- .../event_handler/test_openapi_swagger.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/functional/event_handler/test_openapi_swagger.py b/tests/functional/event_handler/test_openapi_swagger.py index da2bfe199f2..0582412a069 100644 --- a/tests/functional/event_handler/test_openapi_swagger.py +++ b/tests/functional/event_handler/test_openapi_swagger.py @@ -1,3 +1,6 @@ +import json +from typing import Dict + from aws_lambda_powertools.event_handler import APIGatewayRestResolver from tests.functional.utils import load_event @@ -68,3 +71,15 @@ def test_openapi_swagger_with_custom_base_url_no_embedded_assets(): LOAD_GW_EVENT["path"] = "/swagger.js" result = app(LOAD_GW_EVENT, {}) assert result["statusCode"] == 404 + + +def test_openapi_swagger_with_enabled_download_spec(): + app = APIGatewayRestResolver(enable_validation=True) + app.enable_swagger(enable_download_spec=True) + LOAD_GW_EVENT["path"] = "/swagger.json" + + result = app(LOAD_GW_EVENT, {}) + + assert result["statusCode"] == 200 + assert result["multiValueHeaders"]["Content-Type"] == ["application/json"] + assert isinstance(json.loads(result["body"]), Dict) From 5f97ecb59d9dde6ab5dccb86fb5732290dd07322 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 30 Dec 2023 13:55:47 -0600 Subject: [PATCH 4/6] Add test for custom path with enable_download_spec --- .../event_handler/test_openapi_swagger.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/functional/event_handler/test_openapi_swagger.py b/tests/functional/event_handler/test_openapi_swagger.py index 0582412a069..64e80af5220 100644 --- a/tests/functional/event_handler/test_openapi_swagger.py +++ b/tests/functional/event_handler/test_openapi_swagger.py @@ -73,7 +73,7 @@ def test_openapi_swagger_with_custom_base_url_no_embedded_assets(): assert result["statusCode"] == 404 -def test_openapi_swagger_with_enabled_download_spec(): +def test_openapi_swagger_with_enabled_download_spec_and_default_path(): app = APIGatewayRestResolver(enable_validation=True) app.enable_swagger(enable_download_spec=True) LOAD_GW_EVENT["path"] = "/swagger.json" @@ -83,3 +83,15 @@ def test_openapi_swagger_with_enabled_download_spec(): assert result["statusCode"] == 200 assert result["multiValueHeaders"]["Content-Type"] == ["application/json"] assert isinstance(json.loads(result["body"]), Dict) + + +def test_openapi_swagger_with_enabled_download_spec_and_custom_path(): + app = APIGatewayRestResolver(enable_validation=True) + app.enable_swagger(path="/fizzbuzz/foobar", enable_download_spec=True) + LOAD_GW_EVENT["path"] = "/fizzbuzz/foobar.json" + + result = app(LOAD_GW_EVENT, {}) + + assert result["statusCode"] == 200 + assert result["multiValueHeaders"]["Content-Type"] == ["application/json"] + assert isinstance(json.loads(result["body"]), Dict) From 4f37c15cc0008d7877a9afe4ef6dfa6c7d623a5d Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 5 Jan 2024 09:48:42 -0600 Subject: [PATCH 5/6] Show OpenAPI spec with conditional query string --- .../event_handler/api_gateway.py | 42 ++++++++----------- .../event_handler/openapi/swagger_ui/html.py | 5 ++- docs/core/event_handler/api_gateway.md | 15 ------- .../event_handler/test_openapi_swagger.py | 10 +++-- 4 files changed, 27 insertions(+), 45 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index e81cb313fff..6692ae37ee0 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -1559,7 +1559,6 @@ def enable_swagger( license_info: Optional["License"] = None, swagger_base_url: Optional[str] = None, middlewares: Optional[List[Callable[..., Response]]] = None, - enable_download_spec: bool = False, ): """ Returns the OpenAPI schema as a JSON serializable dict @@ -1592,8 +1591,6 @@ def enable_swagger( The base url for the swagger UI. If not provided, we will serve a recent version of the Swagger UI. middlewares: List[Callable[..., Response]], optional List of middlewares to be used for the swagger route. - enable_download_spec: bool, optional - Enable download of the OpenAPI schema as a JSON file at the `.json` (ex. `/swagger.json`). """ from aws_lambda_powertools.event_handler.openapi.models import Server @@ -1617,17 +1614,6 @@ def swagger_css(): body=body, ) - if enable_download_spec: - - @self.get(f"{path}.json", include_in_schema=False) - def swagger_json(): - spec = get_swagger_spec() - return Response( - status_code=200, - content_type="application/json", - body=spec, - ) - @self.get(path, middlewares=middlewares, include_in_schema=False) def swagger_handler(): base_path = self._get_base_path() @@ -1639,19 +1625,9 @@ def swagger_handler(): swagger_js = f"{base_path}/swagger.js" swagger_css = f"{base_path}/swagger.css" - spec = get_swagger_spec() - body = generate_swagger_html(spec, swagger_js, swagger_css) - - return Response( - status_code=200, - content_type="text/html", - body=body, - ) - - def get_swagger_spec(): base_path = self._get_base_path() openapi_servers = servers or [Server(url=(base_path or "/"))] - return self.get_openapi_json_schema( + spec = self.get_openapi_json_schema( title=title, version=version, openapi_version=openapi_version, @@ -1664,6 +1640,22 @@ def get_swagger_spec(): license_info=license_info, ) + query_params = self.current_event.query_string_parameters or {} + if query_params.get("format") == "json": + return Response( + status_code=200, + content_type="application/json", + body=spec, + ) + + body = generate_swagger_html(spec, path, swagger_js, swagger_css) + + return Response( + status_code=200, + content_type="text/html", + body=body, + ) + def route( self, rule: str, diff --git a/aws_lambda_powertools/event_handler/openapi/swagger_ui/html.py b/aws_lambda_powertools/event_handler/openapi/swagger_ui/html.py index d8ffb0efa19..97d98d93b88 100644 --- a/aws_lambda_powertools/event_handler/openapi/swagger_ui/html.py +++ b/aws_lambda_powertools/event_handler/openapi/swagger_ui/html.py @@ -1,4 +1,4 @@ -def generate_swagger_html(spec: str, js_url: str, css_url: str) -> str: +def generate_swagger_html(spec: str, path: str, js_url: str, css_url: str) -> str: """ Generate Swagger UI HTML page @@ -6,6 +6,8 @@ def generate_swagger_html(spec: str, js_url: str, css_url: str) -> str: ---------- spec: str The OpenAPI spec in the JSON format + path: str + The path to the Swagger documentation js_url: str The URL to the Swagger UI JavaScript file css_url: str @@ -54,6 +56,7 @@ def generate_swagger_html(spec: str, js_url: str, css_url: str) -> str: }} var ui = SwaggerUIBundle(swaggerUIOptions) + ui.specActions.updateUrl('{path}?format=json'); """.strip() diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index ced8f71a5e0..005ac3a4b7b 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -1019,21 +1019,6 @@ Include extra parameters when exporting your OpenAPI specification to apply thes --8<-- "examples/event_handler_rest/src/customizing_api_metadata.py" ``` -#### Exposing the OpenAPI specification - -If you would like to be able to download the OpenAPI specification file for generating -client libraries or hosting the documentation separately, you can set the -`enable_download_spec` input to `True` when calling `enable_swagger`: - -```python -app.enable_swagger(path="/_swagger", enable_download_spec=True) -``` - -With this setting enabled, you can access the specification at `.json`. So, for -the snippet above, the specification file would be retrievable at `/_swagger.json`. -To access this endpoint, you will have to add an additional endpoint in your template -file. - ### Custom serializer You can instruct event handler to use a custom serializer to best suit your needs, for example take into account Enums when serializing. diff --git a/tests/functional/event_handler/test_openapi_swagger.py b/tests/functional/event_handler/test_openapi_swagger.py index 64e80af5220..d2af99f7f6d 100644 --- a/tests/functional/event_handler/test_openapi_swagger.py +++ b/tests/functional/event_handler/test_openapi_swagger.py @@ -75,8 +75,9 @@ def test_openapi_swagger_with_custom_base_url_no_embedded_assets(): def test_openapi_swagger_with_enabled_download_spec_and_default_path(): app = APIGatewayRestResolver(enable_validation=True) - app.enable_swagger(enable_download_spec=True) - LOAD_GW_EVENT["path"] = "/swagger.json" + app.enable_swagger() + LOAD_GW_EVENT["path"] = "/swagger" + LOAD_GW_EVENT["queryStringParameters"] = {"format": "json"} result = app(LOAD_GW_EVENT, {}) @@ -87,8 +88,9 @@ def test_openapi_swagger_with_enabled_download_spec_and_default_path(): def test_openapi_swagger_with_enabled_download_spec_and_custom_path(): app = APIGatewayRestResolver(enable_validation=True) - app.enable_swagger(path="/fizzbuzz/foobar", enable_download_spec=True) - LOAD_GW_EVENT["path"] = "/fizzbuzz/foobar.json" + app.enable_swagger(path="/fizzbuzz/foobar") + LOAD_GW_EVENT["path"] = "/fizzbuzz/foobar" + LOAD_GW_EVENT["queryStringParameters"] = {"format": "json"} result = app(LOAD_GW_EVENT, {}) From cb4d0e84a120aabc917ca8442a433186d3ccaf78 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Tue, 9 Jan 2024 21:36:56 +0000 Subject: [PATCH 6/6] Resolving conflicts and minor changes --- .../event_handler/api_gateway.py | 19 ++++++++++++--- .../event_handler/openapi/swagger_ui/html.py | 24 +++---------------- .../event_handler/test_openapi_swagger.py | 10 ++++---- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 1dd3b7e1707..69e1c22c381 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -1592,6 +1592,7 @@ def enable_swagger( middlewares: List[Callable[..., Response]], optional List of middlewares to be used for the swagger route. """ + from aws_lambda_powertools.event_handler.openapi.compat import model_json from aws_lambda_powertools.event_handler.openapi.models import Server if not swagger_base_url: @@ -1625,7 +1626,6 @@ def swagger_handler(): swagger_js = f"{base_path}/swagger.js" swagger_css = f"{base_path}/swagger.css" - base_path = self._get_base_path() openapi_servers = servers or [Server(url=(base_path or "/"))] spec = self.get_openapi_schema( @@ -1641,15 +1641,28 @@ def swagger_handler(): license_info=license_info, ) + # The .replace(' or similar tags. Escaping the forward slash in str: +def generate_swagger_html(spec: str, path: str, js_url: str, css_url: str) -> str: """ Generate Swagger UI HTML page Parameters ---------- - spec: OpenAPI + spec: str The OpenAPI spec path: str The path to the Swagger documentation @@ -20,18 +14,6 @@ def generate_swagger_html(spec: "OpenAPI", path: str, js_url: str, css_url: str) The URL to the Swagger UI CSS file """ - from aws_lambda_powertools.event_handler.openapi.compat import model_json - - # The .replace(' or similar tags. Escaping the forward slash in @@ -62,7 +44,7 @@ def generate_swagger_html(spec: "OpenAPI", path: str, js_url: str, css_url: str) layout: "BaseLayout", showExtensions: true, showCommonExtensions: true, - spec: {escaped_spec}, + spec: {spec}, presets: [ SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset diff --git a/tests/functional/event_handler/test_openapi_swagger.py b/tests/functional/event_handler/test_openapi_swagger.py index d2af99f7f6d..18ed85ed676 100644 --- a/tests/functional/event_handler/test_openapi_swagger.py +++ b/tests/functional/event_handler/test_openapi_swagger.py @@ -73,9 +73,9 @@ def test_openapi_swagger_with_custom_base_url_no_embedded_assets(): assert result["statusCode"] == 404 -def test_openapi_swagger_with_enabled_download_spec_and_default_path(): +def test_openapi_swagger_json_view_with_default_path(): app = APIGatewayRestResolver(enable_validation=True) - app.enable_swagger() + app.enable_swagger(title="OpenAPI JSON View") LOAD_GW_EVENT["path"] = "/swagger" LOAD_GW_EVENT["queryStringParameters"] = {"format": "json"} @@ -84,11 +84,12 @@ def test_openapi_swagger_with_enabled_download_spec_and_default_path(): assert result["statusCode"] == 200 assert result["multiValueHeaders"]["Content-Type"] == ["application/json"] assert isinstance(json.loads(result["body"]), Dict) + assert "OpenAPI JSON View" in result["body"] -def test_openapi_swagger_with_enabled_download_spec_and_custom_path(): +def test_openapi_swagger_json_view_with_custom_path(): app = APIGatewayRestResolver(enable_validation=True) - app.enable_swagger(path="/fizzbuzz/foobar") + app.enable_swagger(path="/fizzbuzz/foobar", title="OpenAPI JSON View") LOAD_GW_EVENT["path"] = "/fizzbuzz/foobar" LOAD_GW_EVENT["queryStringParameters"] = {"format": "json"} @@ -97,3 +98,4 @@ def test_openapi_swagger_with_enabled_download_spec_and_custom_path(): assert result["statusCode"] == 200 assert result["multiValueHeaders"]["Content-Type"] == ["application/json"] assert isinstance(json.loads(result["body"]), Dict) + assert "OpenAPI JSON View" in result["body"]