Skip to content

Commit e002d5c

Browse files
feat(event_handler): add custom method for OpenAPI configuration (#6204)
* Adding specific method for OpenAPI configuration * Adding constants * Refactoring examples * Addressing Andrea's feedback
1 parent 7e56fe1 commit e002d5c

File tree

12 files changed

+353
-81
lines changed

12 files changed

+353
-81
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ complexity-baseline:
8484
$(info Maintenability index)
8585
poetry run radon mi aws_lambda_powertools
8686
$(info Cyclomatic complexity index)
87-
poetry run xenon --max-absolute C --max-modules A --max-average A aws_lambda_powertools --exclude aws_lambda_powertools/shared/json_encoder.py,aws_lambda_powertools/utilities/validation/base.py
87+
poetry run xenon --max-absolute C --max-modules A --max-average A aws_lambda_powertools --exclude aws_lambda_powertools/shared/json_encoder.py,aws_lambda_powertools/utilities/validation/base.py,aws_lambda_powertools/event_handler/api_gateway.py
8888

8989
#
9090
# Use `poetry version <major>/<minor></patch>` for version bump

aws_lambda_powertools/event_handler/api_gateway.py

+125-17
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@
1818

1919
from aws_lambda_powertools.event_handler import content_types
2020
from aws_lambda_powertools.event_handler.exceptions import NotFoundError, ServiceError
21-
from aws_lambda_powertools.event_handler.openapi.constants import DEFAULT_API_VERSION, DEFAULT_OPENAPI_VERSION
21+
from aws_lambda_powertools.event_handler.openapi.config import OpenAPIConfig
22+
from aws_lambda_powertools.event_handler.openapi.constants import (
23+
DEFAULT_API_VERSION,
24+
DEFAULT_OPENAPI_TITLE,
25+
DEFAULT_OPENAPI_VERSION,
26+
)
2227
from aws_lambda_powertools.event_handler.openapi.exceptions import (
2328
RequestValidationError,
2429
ResponseValidationError,
@@ -1537,6 +1542,7 @@ def __init__(
15371542
self.context: dict = {} # early init as customers might add context before event resolution
15381543
self.processed_stack_frames = []
15391544
self._response_builder_class = ResponseBuilder[BaseProxyEvent]
1545+
self.openapi_config = OpenAPIConfig() # starting an empty dataclass
15401546
self._has_response_validation_error = response_validation_error_http_code is not None
15411547
self._response_validation_error_http_code = self._validate_response_validation_error_http_code(
15421548
response_validation_error_http_code,
@@ -1580,16 +1586,12 @@ def _validate_response_validation_error_http_code(
15801586
msg = f"'{response_validation_error_http_code}' must be an integer representing an HTTP status code."
15811587
raise ValueError(msg) from None
15821588

1583-
return (
1584-
response_validation_error_http_code
1585-
if response_validation_error_http_code
1586-
else HTTPStatus.UNPROCESSABLE_ENTITY
1587-
)
1589+
return response_validation_error_http_code or HTTPStatus.UNPROCESSABLE_ENTITY
15881590

15891591
def get_openapi_schema(
15901592
self,
15911593
*,
1592-
title: str = "Powertools API",
1594+
title: str = DEFAULT_OPENAPI_TITLE,
15931595
version: str = DEFAULT_API_VERSION,
15941596
openapi_version: str = DEFAULT_OPENAPI_VERSION,
15951597
summary: str | None = None,
@@ -1641,6 +1643,29 @@ def get_openapi_schema(
16411643
The OpenAPI schema as a pydantic model.
16421644
"""
16431645

1646+
# DEPRECATION: Will be removed in v4.0.0. Use configure_api() instead.
1647+
# Maintained for backwards compatibility.
1648+
# See: https://github.com/aws-powertools/powertools-lambda-python/issues/6122
1649+
if title == DEFAULT_OPENAPI_TITLE and self.openapi_config.title:
1650+
title = self.openapi_config.title
1651+
1652+
if version == DEFAULT_API_VERSION and self.openapi_config.version:
1653+
version = self.openapi_config.version
1654+
1655+
if openapi_version == DEFAULT_OPENAPI_VERSION and self.openapi_config.openapi_version:
1656+
openapi_version = self.openapi_config.openapi_version
1657+
1658+
summary = summary or self.openapi_config.summary
1659+
description = description or self.openapi_config.description
1660+
tags = tags or self.openapi_config.tags
1661+
servers = servers or self.openapi_config.servers
1662+
terms_of_service = terms_of_service or self.openapi_config.terms_of_service
1663+
contact = contact or self.openapi_config.contact
1664+
license_info = license_info or self.openapi_config.license_info
1665+
security_schemes = security_schemes or self.openapi_config.security_schemes
1666+
security = security or self.openapi_config.security
1667+
openapi_extensions = openapi_extensions or self.openapi_config.openapi_extensions
1668+
16441669
from aws_lambda_powertools.event_handler.openapi.compat import (
16451670
GenerateJsonSchema,
16461671
get_compat_model_name_map,
@@ -1739,7 +1764,7 @@ def _get_openapi_servers(servers: list[Server] | None) -> list[Server]:
17391764

17401765
# If the 'servers' property is not provided or is an empty array,
17411766
# the default behavior is to return a Server Object with a URL value of "/".
1742-
return servers if servers else [Server(url="/")]
1767+
return servers or [Server(url="/")]
17431768

17441769
@staticmethod
17451770
def _get_openapi_security(
@@ -1771,7 +1796,7 @@ def _determine_openapi_version(openapi_version: str):
17711796
def get_openapi_json_schema(
17721797
self,
17731798
*,
1774-
title: str = "Powertools API",
1799+
title: str = DEFAULT_OPENAPI_TITLE,
17751800
version: str = DEFAULT_API_VERSION,
17761801
openapi_version: str = DEFAULT_OPENAPI_VERSION,
17771802
summary: str | None = None,
@@ -1822,6 +1847,7 @@ def get_openapi_json_schema(
18221847
str
18231848
The OpenAPI schema as a JSON serializable dict.
18241849
"""
1850+
18251851
from aws_lambda_powertools.event_handler.openapi.compat import model_json
18261852

18271853
return model_json(
@@ -1845,11 +1871,94 @@ def get_openapi_json_schema(
18451871
indent=2,
18461872
)
18471873

1874+
def configure_openapi(
1875+
self,
1876+
title: str = DEFAULT_OPENAPI_TITLE,
1877+
version: str = DEFAULT_API_VERSION,
1878+
openapi_version: str = DEFAULT_OPENAPI_VERSION,
1879+
summary: str | None = None,
1880+
description: str | None = None,
1881+
tags: list[Tag | str] | None = None,
1882+
servers: list[Server] | None = None,
1883+
terms_of_service: str | None = None,
1884+
contact: Contact | None = None,
1885+
license_info: License | None = None,
1886+
security_schemes: dict[str, SecurityScheme] | None = None,
1887+
security: list[dict[str, list[str]]] | None = None,
1888+
openapi_extensions: dict[str, Any] | None = None,
1889+
):
1890+
"""Configure OpenAPI specification settings for the API.
1891+
1892+
Sets up the OpenAPI documentation configuration that can be later used
1893+
when enabling Swagger UI or generating OpenAPI specifications.
1894+
1895+
Parameters
1896+
----------
1897+
title: str
1898+
The title of the application.
1899+
version: str
1900+
The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API
1901+
openapi_version: str, default = "3.0.0"
1902+
The version of the OpenAPI Specification (which the document uses).
1903+
summary: str, optional
1904+
A short summary of what the application does.
1905+
description: str, optional
1906+
A verbose explanation of the application behavior.
1907+
tags: list[Tag, str], optional
1908+
A list of tags used by the specification with additional metadata.
1909+
servers: list[Server], optional
1910+
An array of Server Objects, which provide connectivity information to a target server.
1911+
terms_of_service: str, optional
1912+
A URL to the Terms of Service for the API. MUST be in the format of a URL.
1913+
contact: Contact, optional
1914+
The contact information for the exposed API.
1915+
license_info: License, optional
1916+
The license information for the exposed API.
1917+
security_schemes: dict[str, SecurityScheme]], optional
1918+
A declaration of the security schemes available to be used in the specification.
1919+
security: list[dict[str, list[str]]], optional
1920+
A declaration of which security mechanisms are applied globally across the API.
1921+
openapi_extensions: Dict[str, Any], optional
1922+
Additional OpenAPI extensions as a dictionary.
1923+
1924+
Example
1925+
--------
1926+
>>> api.configure_openapi(
1927+
... title="My API",
1928+
... version="1.0.0",
1929+
... description="API for managing resources",
1930+
... contact=Contact(
1931+
... name="API Support",
1932+
... email="[email protected]"
1933+
... )
1934+
... )
1935+
1936+
See Also
1937+
--------
1938+
enable_swagger : Method to enable Swagger UI using these configurations
1939+
OpenAPIConfig : Data class containing all OpenAPI configuration options
1940+
"""
1941+
self.openapi_config = OpenAPIConfig(
1942+
title=title,
1943+
version=version,
1944+
openapi_version=openapi_version,
1945+
summary=summary,
1946+
description=description,
1947+
tags=tags,
1948+
servers=servers,
1949+
terms_of_service=terms_of_service,
1950+
contact=contact,
1951+
license_info=license_info,
1952+
security_schemes=security_schemes,
1953+
security=security,
1954+
openapi_extensions=openapi_extensions,
1955+
)
1956+
18481957
def enable_swagger(
18491958
self,
18501959
*,
18511960
path: str = "/swagger",
1852-
title: str = "Powertools for AWS Lambda (Python) API",
1961+
title: str = DEFAULT_OPENAPI_TITLE,
18531962
version: str = DEFAULT_API_VERSION,
18541963
openapi_version: str = DEFAULT_OPENAPI_VERSION,
18551964
summary: str | None = None,
@@ -1912,6 +2021,7 @@ def enable_swagger(
19122021
openapi_extensions: dict[str, Any], optional
19132022
Additional OpenAPI extensions as a dictionary.
19142023
"""
2024+
19152025
from aws_lambda_powertools.event_handler.openapi.compat import model_json
19162026
from aws_lambda_powertools.event_handler.openapi.models import Server
19172027
from aws_lambda_powertools.event_handler.openapi.swagger_ui import (
@@ -2156,10 +2266,7 @@ def _get_base_path(self) -> str:
21562266
@staticmethod
21572267
def _has_debug(debug: bool | None = None) -> bool:
21582268
# It might have been explicitly switched off (debug=False)
2159-
if debug is not None:
2160-
return debug
2161-
2162-
return powertools_dev_is_set()
2269+
return debug if debug is not None else powertools_dev_is_set()
21632270

21642271
@staticmethod
21652272
def _compile_regex(rule: str, base_regex: str = _ROUTE_REGEX):
@@ -2272,7 +2379,7 @@ def _path_starts_with(path: str, prefix: str):
22722379
if not isinstance(prefix, str) or prefix == "":
22732380
return False
22742381

2275-
return path.startswith(prefix + "/")
2382+
return path.startswith(f"{prefix}/")
22762383

22772384
def _handle_not_found(self, method: str, path: str) -> ResponseBuilder:
22782385
"""Called when no matching route was found and includes support for the cors preflight response"""
@@ -2543,8 +2650,9 @@ def _get_fields_from_routes(routes: Sequence[Route]) -> list[ModelField]:
25432650
if route.dependant.response_extra_models:
25442651
responses_from_routes.extend(route.dependant.response_extra_models)
25452652

2546-
flat_models = list(responses_from_routes + request_fields_from_routes + body_fields_from_routes)
2547-
return flat_models
2653+
return list(
2654+
responses_from_routes + request_fields_from_routes + body_fields_from_routes,
2655+
)
25482656

25492657

25502658
class Router(BaseRouter):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import TYPE_CHECKING, Any
5+
6+
from aws_lambda_powertools.event_handler.openapi.constants import (
7+
DEFAULT_API_VERSION,
8+
DEFAULT_OPENAPI_TITLE,
9+
DEFAULT_OPENAPI_VERSION,
10+
)
11+
12+
if TYPE_CHECKING:
13+
from aws_lambda_powertools.event_handler.openapi.models import (
14+
Contact,
15+
License,
16+
SecurityScheme,
17+
Server,
18+
Tag,
19+
)
20+
21+
22+
@dataclass
23+
class OpenAPIConfig:
24+
"""Configuration class for OpenAPI specification.
25+
26+
This class holds all the necessary configuration parameters to generate an OpenAPI specification.
27+
28+
Parameters
29+
----------
30+
title: str
31+
The title of the application.
32+
version: str
33+
The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API
34+
openapi_version: str, default = "3.0.0"
35+
The version of the OpenAPI Specification (which the document uses).
36+
summary: str, optional
37+
A short summary of what the application does.
38+
description: str, optional
39+
A verbose explanation of the application behavior.
40+
tags: list[Tag, str], optional
41+
A list of tags used by the specification with additional metadata.
42+
servers: list[Server], optional
43+
An array of Server Objects, which provide connectivity information to a target server.
44+
terms_of_service: str, optional
45+
A URL to the Terms of Service for the API. MUST be in the format of a URL.
46+
contact: Contact, optional
47+
The contact information for the exposed API.
48+
license_info: License, optional
49+
The license information for the exposed API.
50+
security_schemes: dict[str, SecurityScheme]], optional
51+
A declaration of the security schemes available to be used in the specification.
52+
security: list[dict[str, list[str]]], optional
53+
A declaration of which security mechanisms are applied globally across the API.
54+
openapi_extensions: Dict[str, Any], optional
55+
Additional OpenAPI extensions as a dictionary.
56+
57+
Example
58+
--------
59+
>>> config = OpenAPIConfig(
60+
... title="My API",
61+
... version="1.0.0",
62+
... description="This is my API description",
63+
... contact=Contact(name="API Support", email="[email protected]"),
64+
... servers=[Server(url="https://api.example.com/v1")]
65+
... )
66+
"""
67+
68+
title: str = DEFAULT_OPENAPI_TITLE
69+
version: str = DEFAULT_API_VERSION
70+
openapi_version: str = DEFAULT_OPENAPI_VERSION
71+
summary: str | None = None
72+
description: str | None = None
73+
tags: list[Tag | str] | None = None
74+
servers: list[Server] | None = None
75+
terms_of_service: str | None = None
76+
contact: Contact | None = None
77+
license_info: License | None = None
78+
security_schemes: dict[str, SecurityScheme] | None = None
79+
security: list[dict[str, list[str]]] | None = None
80+
openapi_extensions: dict[str, Any] | None = None
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
DEFAULT_API_VERSION = "1.0.0"
22
DEFAULT_OPENAPI_VERSION = "3.1.0"
3+
DEFAULT_OPENAPI_TITLE = "Powertools for AWS Lambda (Python) API"

docs/core/event_handler/_openapi_customization_metadata.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<!-- markdownlint-disable MD041 MD043 -->
22

3-
Defining and customizing OpenAPI metadata gives detailed, top-level information about your API. Here's the method to set and tailor this metadata:
3+
Defining and customizing OpenAPI metadata gives detailed, top-level information about your API. Use the method `app.configure_openapi` to set and tailor this metadata:
44

55
| Field Name | Type | Description |
66
| ------------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

docs/core/event_handler/api_gateway.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -1072,7 +1072,7 @@ Include extra parameters when exporting your OpenAPI specification to apply thes
10721072

10731073
=== "customizing_api_metadata.py"
10741074

1075-
```python hl_lines="25-31"
1075+
```python hl_lines="8-16"
10761076
--8<-- "examples/event_handler_rest/src/customizing_api_metadata.py"
10771077
```
10781078

@@ -1108,23 +1108,23 @@ Security schemes are declared at the top-level first. You can reference them glo
11081108

11091109
=== "Global OpenAPI security schemes"
11101110

1111-
```python title="security_schemes_global.py" hl_lines="32-42"
1111+
```python title="security_schemes_global.py" hl_lines="17-27"
11121112
--8<-- "examples/event_handler_rest/src/security_schemes_global.py"
11131113
```
11141114

11151115
1. Using the oauth security scheme defined earlier, scoped to the "admin" role.
11161116

11171117
=== "Per Operation security"
11181118

1119-
```python title="security_schemes_per_operation.py" hl_lines="17 32-41"
1119+
```python title="security_schemes_per_operation.py" hl_lines="17-26 30"
11201120
--8<-- "examples/event_handler_rest/src/security_schemes_per_operation.py"
11211121
```
11221122

11231123
1. Using the oauth security scheme defined bellow, scoped to the "admin" role.
11241124

11251125
=== "Global security schemes and optional security per route"
11261126

1127-
```python title="security_schemes_global_and_optional.py" hl_lines="22 37-46"
1127+
```python title="security_schemes_global_and_optional.py" hl_lines="17-26 35"
11281128
--8<-- "examples/event_handler_rest/src/security_schemes_global_and_optional.py"
11291129
```
11301130

0 commit comments

Comments
 (0)