Skip to content

feat(event_handler): add custom method for OpenAPI configuration #6204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Mar 17, 2025
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ complexity-baseline:
$(info Maintenability index)
poetry run radon mi aws_lambda_powertools
$(info Cyclomatic complexity index)
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
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

#
# Use `poetry version <major>/<minor></patch>` for version bump
Expand Down
142 changes: 125 additions & 17 deletions aws_lambda_powertools/event_handler/api_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@

from aws_lambda_powertools.event_handler import content_types
from aws_lambda_powertools.event_handler.exceptions import NotFoundError, ServiceError
from aws_lambda_powertools.event_handler.openapi.constants import DEFAULT_API_VERSION, DEFAULT_OPENAPI_VERSION
from aws_lambda_powertools.event_handler.openapi.config import OpenAPIConfig
from aws_lambda_powertools.event_handler.openapi.constants import (
DEFAULT_API_VERSION,
DEFAULT_OPENAPI_TITLE,
DEFAULT_OPENAPI_VERSION,
)
from aws_lambda_powertools.event_handler.openapi.exceptions import (
RequestValidationError,
ResponseValidationError,
Expand Down Expand Up @@ -1537,6 +1542,7 @@ def __init__(
self.context: dict = {} # early init as customers might add context before event resolution
self.processed_stack_frames = []
self._response_builder_class = ResponseBuilder[BaseProxyEvent]
self.openapi_config = OpenAPIConfig() # starting an empty dataclass
self._has_response_validation_error = response_validation_error_http_code is not None
self._response_validation_error_http_code = self._validate_response_validation_error_http_code(
response_validation_error_http_code,
Expand Down Expand Up @@ -1580,16 +1586,12 @@ def _validate_response_validation_error_http_code(
msg = f"'{response_validation_error_http_code}' must be an integer representing an HTTP status code."
raise ValueError(msg) from None

return (
response_validation_error_http_code
if response_validation_error_http_code
else HTTPStatus.UNPROCESSABLE_ENTITY
)
return response_validation_error_http_code or HTTPStatus.UNPROCESSABLE_ENTITY

def get_openapi_schema(
self,
*,
title: str = "Powertools API",
title: str = DEFAULT_OPENAPI_TITLE,
version: str = DEFAULT_API_VERSION,
openapi_version: str = DEFAULT_OPENAPI_VERSION,
summary: str | None = None,
Expand Down Expand Up @@ -1641,6 +1643,29 @@ def get_openapi_schema(
The OpenAPI schema as a pydantic model.
"""

# DEPRECATION: Will be removed in v4.0.0. Use configure_api() instead.
# Maintained for backwards compatibility.
# See: https://github.com/aws-powertools/powertools-lambda-python/issues/6122
if title == DEFAULT_OPENAPI_TITLE and self.openapi_config.title:
title = self.openapi_config.title

if version == DEFAULT_API_VERSION and self.openapi_config.version:
version = self.openapi_config.version

if openapi_version == DEFAULT_OPENAPI_VERSION and self.openapi_config.openapi_version:
openapi_version = self.openapi_config.openapi_version

summary = summary or self.openapi_config.summary
description = description or self.openapi_config.description
tags = tags or self.openapi_config.tags
servers = servers or self.openapi_config.servers
terms_of_service = terms_of_service or self.openapi_config.terms_of_service
contact = contact or self.openapi_config.contact
license_info = license_info or self.openapi_config.license_info
security_schemes = security_schemes or self.openapi_config.security_schemes
security = security or self.openapi_config.security
openapi_extensions = openapi_extensions or self.openapi_config.openapi_extensions

from aws_lambda_powertools.event_handler.openapi.compat import (
GenerateJsonSchema,
get_compat_model_name_map,
Expand Down Expand Up @@ -1739,7 +1764,7 @@ def _get_openapi_servers(servers: list[Server] | None) -> list[Server]:

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

@staticmethod
def _get_openapi_security(
Expand Down Expand Up @@ -1771,7 +1796,7 @@ def _determine_openapi_version(openapi_version: str):
def get_openapi_json_schema(
self,
*,
title: str = "Powertools API",
title: str = DEFAULT_OPENAPI_TITLE,
version: str = DEFAULT_API_VERSION,
openapi_version: str = DEFAULT_OPENAPI_VERSION,
summary: str | None = None,
Expand Down Expand Up @@ -1822,6 +1847,7 @@ def get_openapi_json_schema(
str
The OpenAPI schema as a JSON serializable dict.
"""

from aws_lambda_powertools.event_handler.openapi.compat import model_json

return model_json(
Expand All @@ -1845,11 +1871,94 @@ def get_openapi_json_schema(
indent=2,
)

def configure_openapi(
self,
title: str = DEFAULT_OPENAPI_TITLE,
version: str = DEFAULT_API_VERSION,
openapi_version: str = DEFAULT_OPENAPI_VERSION,
summary: str | None = None,
description: str | None = None,
tags: list[Tag | str] | None = None,
servers: list[Server] | None = None,
terms_of_service: str | None = None,
contact: Contact | None = None,
license_info: License | None = None,
security_schemes: dict[str, SecurityScheme] | None = None,
security: list[dict[str, list[str]]] | None = None,
openapi_extensions: dict[str, Any] | None = None,
):
"""Configure OpenAPI specification settings for the API.

Sets up the OpenAPI documentation configuration that can be later used
when enabling Swagger UI or generating OpenAPI specifications.

Parameters
----------
title: str
The title of the application.
version: str
The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API
openapi_version: str, default = "3.0.0"
The version of the OpenAPI Specification (which the document uses).
summary: str, optional
A short summary of what the application does.
description: str, optional
A verbose explanation of the application behavior.
tags: list[Tag, str], optional
A list of tags used by the specification with additional metadata.
servers: list[Server], optional
An array of Server Objects, which provide connectivity information to a target server.
terms_of_service: str, optional
A URL to the Terms of Service for the API. MUST be in the format of a URL.
contact: Contact, optional
The contact information for the exposed API.
license_info: License, optional
The license information for the exposed API.
security_schemes: dict[str, SecurityScheme]], optional
A declaration of the security schemes available to be used in the specification.
security: list[dict[str, list[str]]], optional
A declaration of which security mechanisms are applied globally across the API.
openapi_extensions: Dict[str, Any], optional
Additional OpenAPI extensions as a dictionary.

Example
--------
>>> api.configure_openapi(
... title="My API",
... version="1.0.0",
... description="API for managing resources",
... contact=Contact(
... name="API Support",
... email="[email protected]"
... )
... )

See Also
--------
enable_swagger : Method to enable Swagger UI using these configurations
OpenAPIConfig : Data class containing all OpenAPI configuration options
"""
self.openapi_config = OpenAPIConfig(
title=title,
version=version,
openapi_version=openapi_version,
summary=summary,
description=description,
tags=tags,
servers=servers,
terms_of_service=terms_of_service,
contact=contact,
license_info=license_info,
security_schemes=security_schemes,
security=security,
openapi_extensions=openapi_extensions,
)

def enable_swagger(
self,
*,
path: str = "/swagger",
title: str = "Powertools for AWS Lambda (Python) API",
title: str = DEFAULT_OPENAPI_TITLE,
version: str = DEFAULT_API_VERSION,
openapi_version: str = DEFAULT_OPENAPI_VERSION,
summary: str | None = None,
Expand Down Expand Up @@ -1912,6 +2021,7 @@ def enable_swagger(
openapi_extensions: dict[str, Any], optional
Additional OpenAPI extensions as a dictionary.
"""

from aws_lambda_powertools.event_handler.openapi.compat import model_json
from aws_lambda_powertools.event_handler.openapi.models import Server
from aws_lambda_powertools.event_handler.openapi.swagger_ui import (
Expand Down Expand Up @@ -2156,10 +2266,7 @@ def _get_base_path(self) -> str:
@staticmethod
def _has_debug(debug: bool | None = None) -> bool:
# It might have been explicitly switched off (debug=False)
if debug is not None:
return debug

return powertools_dev_is_set()
return debug if debug is not None else powertools_dev_is_set()

@staticmethod
def _compile_regex(rule: str, base_regex: str = _ROUTE_REGEX):
Expand Down Expand Up @@ -2272,7 +2379,7 @@ def _path_starts_with(path: str, prefix: str):
if not isinstance(prefix, str) or prefix == "":
return False

return path.startswith(prefix + "/")
return path.startswith(f"{prefix}/")

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

flat_models = list(responses_from_routes + request_fields_from_routes + body_fields_from_routes)
return flat_models
return list(
responses_from_routes + request_fields_from_routes + body_fields_from_routes,
)


class Router(BaseRouter):
Expand Down
80 changes: 80 additions & 0 deletions aws_lambda_powertools/event_handler/openapi/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from aws_lambda_powertools.event_handler.openapi.constants import (
DEFAULT_API_VERSION,
DEFAULT_OPENAPI_TITLE,
DEFAULT_OPENAPI_VERSION,
)

if TYPE_CHECKING:
from aws_lambda_powertools.event_handler.openapi.models import (
Contact,
License,
SecurityScheme,
Server,
Tag,
)


@dataclass
class OpenAPIConfig:
"""Configuration class for OpenAPI specification.

This class holds all the necessary configuration parameters to generate an OpenAPI specification.

Parameters
----------
title: str
The title of the application.
version: str
The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API
openapi_version: str, default = "3.0.0"
The version of the OpenAPI Specification (which the document uses).
summary: str, optional
A short summary of what the application does.
description: str, optional
A verbose explanation of the application behavior.
tags: list[Tag, str], optional
A list of tags used by the specification with additional metadata.
servers: list[Server], optional
An array of Server Objects, which provide connectivity information to a target server.
terms_of_service: str, optional
A URL to the Terms of Service for the API. MUST be in the format of a URL.
contact: Contact, optional
The contact information for the exposed API.
license_info: License, optional
The license information for the exposed API.
security_schemes: dict[str, SecurityScheme]], optional
A declaration of the security schemes available to be used in the specification.
security: list[dict[str, list[str]]], optional
A declaration of which security mechanisms are applied globally across the API.
openapi_extensions: Dict[str, Any], optional
Additional OpenAPI extensions as a dictionary.

Example
--------
>>> config = OpenAPIConfig(
... title="My API",
... version="1.0.0",
... description="This is my API description",
... contact=Contact(name="API Support", email="[email protected]"),
... servers=[Server(url="https://api.example.com/v1")]
... )
"""

title: str = DEFAULT_OPENAPI_TITLE
version: str = DEFAULT_API_VERSION
openapi_version: str = DEFAULT_OPENAPI_VERSION
summary: str | None = None
description: str | None = None
tags: list[Tag | str] | None = None
servers: list[Server] | None = None
terms_of_service: str | None = None
contact: Contact | None = None
license_info: License | None = None
security_schemes: dict[str, SecurityScheme] | None = None
security: list[dict[str, list[str]]] | None = None
openapi_extensions: dict[str, Any] | None = None
1 change: 1 addition & 0 deletions aws_lambda_powertools/event_handler/openapi/constants.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
DEFAULT_API_VERSION = "1.0.0"
DEFAULT_OPENAPI_VERSION = "3.1.0"
DEFAULT_OPENAPI_TITLE = "Powertools for AWS Lambda (Python) API"
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!-- markdownlint-disable MD041 MD043 -->

Defining and customizing OpenAPI metadata gives detailed, top-level information about your API. Here's the method to set and tailor this metadata:
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:

| Field Name | Type | Description |
| ------------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
Expand Down
8 changes: 4 additions & 4 deletions docs/core/event_handler/api_gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@ Include extra parameters when exporting your OpenAPI specification to apply thes

=== "customizing_api_metadata.py"

```python hl_lines="25-31"
```python hl_lines="8-16"
--8<-- "examples/event_handler_rest/src/customizing_api_metadata.py"
```

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

=== "Global OpenAPI security schemes"

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

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

=== "Per Operation security"

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

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

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

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

Expand Down
Loading
Loading