Skip to content
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="support@example.com"
... )
... )

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="support@example.com"),
... 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