Skip to content

Commit c62eab2

Browse files
Merging from develop
2 parents 863d6f9 + 7e56fe1 commit c62eab2

File tree

23 files changed

+583
-86
lines changed

23 files changed

+583
-86
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,28 @@
1414

1515
## Maintenance
1616

17+
* **ci:** new pre-release 3.8.1a3 ([#6259](https://github.com/aws-powertools/powertools-lambda-python/issues/6259))
18+
* **ci:** new pre-release 3.8.1a0 ([#6244](https://github.com/aws-powertools/powertools-lambda-python/issues/6244))
19+
* **ci:** new pre-release 3.8.1a4 ([#6268](https://github.com/aws-powertools/powertools-lambda-python/issues/6268))
20+
* **ci:** new pre-release 3.8.1a1 ([#6250](https://github.com/aws-powertools/powertools-lambda-python/issues/6250))
21+
* **ci:** new pre-release 3.8.1a2 ([#6253](https://github.com/aws-powertools/powertools-lambda-python/issues/6253))
22+
* **ci:** new pre-release 3.8.1a5 ([#6276](https://github.com/aws-powertools/powertools-lambda-python/issues/6276))
23+
* **deps:** bump squidfunk/mkdocs-material from `047452c` to `479a06a` in /docs ([#6261](https://github.com/aws-powertools/powertools-lambda-python/issues/6261))
24+
* **deps-dev:** bump mkdocs-material from 9.6.7 to 9.6.8 ([#6264](https://github.com/aws-powertools/powertools-lambda-python/issues/6264))
25+
* **deps-dev:** bump aws-cdk from 2.1003.0 to 2.1004.0 ([#6262](https://github.com/aws-powertools/powertools-lambda-python/issues/6262))
26+
* **deps-dev:** bump boto3-stubs from 1.37.10 to 1.37.11 ([#6252](https://github.com/aws-powertools/powertools-lambda-python/issues/6252))
27+
* **deps-dev:** bump cfn-lint from 1.29.1 to 1.30.0 ([#6263](https://github.com/aws-powertools/powertools-lambda-python/issues/6263))
28+
* **deps-dev:** bump cfn-lint from 1.28.0 to 1.29.1 ([#6249](https://github.com/aws-powertools/powertools-lambda-python/issues/6249))
29+
* **deps-dev:** bump boto3-stubs from 1.37.8 to 1.37.10 ([#6248](https://github.com/aws-powertools/powertools-lambda-python/issues/6248))
30+
* **deps-dev:** bump aws-cdk-lib from 2.182.0 to 2.183.0 ([#6257](https://github.com/aws-powertools/powertools-lambda-python/issues/6257))
31+
* **deps-dev:** bump boto3-stubs from 1.37.11 to 1.37.12 ([#6266](https://github.com/aws-powertools/powertools-lambda-python/issues/6266))
32+
* **deps-dev:** bump filelock from 3.17.0 to 3.18.0 ([#6270](https://github.com/aws-powertools/powertools-lambda-python/issues/6270))
1733
* **deps-dev:** bump ruff from 0.9.9 to 0.9.10 ([#6241](https://github.com/aws-powertools/powertools-lambda-python/issues/6241))
1834
* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.295 to 0.1.296 ([#6240](https://github.com/aws-powertools/powertools-lambda-python/issues/6240))
1935
* **deps-dev:** bump boto3-stubs from 1.37.7 to 1.37.8 ([#6239](https://github.com/aws-powertools/powertools-lambda-python/issues/6239))
36+
* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.183.0a0 to 2.184.1a0 ([#6271](https://github.com/aws-powertools/powertools-lambda-python/issues/6271))
37+
* **deps-dev:** bump aws-cdk-lib from 2.183.0 to 2.184.1 ([#6272](https://github.com/aws-powertools/powertools-lambda-python/issues/6272))
38+
* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.182.0a0 to 2.183.0a0 ([#6258](https://github.com/aws-powertools/powertools-lambda-python/issues/6258))
2039

2140

2241
<a name="v3.8.0"></a>

aws_lambda_powertools/event_handler/api_gateway.py

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@
2424
DEFAULT_OPENAPI_TITLE,
2525
DEFAULT_OPENAPI_VERSION,
2626
)
27-
from aws_lambda_powertools.event_handler.openapi.exceptions import RequestValidationError, SchemaValidationError
27+
from aws_lambda_powertools.event_handler.openapi.exceptions import (
28+
RequestValidationError,
29+
ResponseValidationError,
30+
SchemaValidationError,
31+
)
2832
from aws_lambda_powertools.event_handler.openapi.types import (
2933
COMPONENT_REF_PREFIX,
3034
METHODS_WITH_BODY,
@@ -1501,6 +1505,7 @@ def __init__(
15011505
serializer: Callable[[dict], str] | None = None,
15021506
strip_prefixes: list[str | Pattern] | None = None,
15031507
enable_validation: bool = False,
1508+
response_validation_error_http_code: HTTPStatus | int | None = None,
15041509
):
15051510
"""
15061511
Parameters
@@ -1520,6 +1525,8 @@ def __init__(
15201525
Each prefix can be a static string or a compiled regex pattern
15211526
enable_validation: bool | None
15221527
Enables validation of the request body against the route schema, by default False.
1528+
response_validation_error_http_code
1529+
Sets the returned status code if response is not validated. enable_validation must be True.
15231530
"""
15241531
self._proxy_type = proxy_type
15251532
self._dynamic_routes: list[Route] = []
@@ -1536,6 +1543,11 @@ def __init__(
15361543
self.processed_stack_frames = []
15371544
self._response_builder_class = ResponseBuilder[BaseProxyEvent]
15381545
self.openapi_config = OpenAPIConfig() # starting an empty dataclass
1546+
self._has_response_validation_error = response_validation_error_http_code is not None
1547+
self._response_validation_error_http_code = self._validate_response_validation_error_http_code(
1548+
response_validation_error_http_code,
1549+
enable_validation,
1550+
)
15391551

15401552
# Allow for a custom serializer or a concise json serialization
15411553
self._serializer = serializer or partial(json.dumps, separators=(",", ":"), cls=Encoder)
@@ -1545,7 +1557,36 @@ def __init__(
15451557

15461558
# Note the serializer argument: only use custom serializer if provided by the caller
15471559
# Otherwise, fully rely on the internal Pydantic based mechanism to serialize responses for validation.
1548-
self.use([OpenAPIValidationMiddleware(validation_serializer=serializer)])
1560+
self.use(
1561+
[
1562+
OpenAPIValidationMiddleware(
1563+
validation_serializer=serializer,
1564+
has_response_validation_error=self._has_response_validation_error,
1565+
),
1566+
],
1567+
)
1568+
1569+
def _validate_response_validation_error_http_code(
1570+
self,
1571+
response_validation_error_http_code: HTTPStatus | int | None,
1572+
enable_validation: bool,
1573+
) -> HTTPStatus:
1574+
if response_validation_error_http_code and not enable_validation:
1575+
msg = "'response_validation_error_http_code' cannot be set when enable_validation is False."
1576+
raise ValueError(msg)
1577+
1578+
if (
1579+
not isinstance(response_validation_error_http_code, HTTPStatus)
1580+
and response_validation_error_http_code is not None
1581+
):
1582+
1583+
try:
1584+
response_validation_error_http_code = HTTPStatus(response_validation_error_http_code)
1585+
except ValueError:
1586+
msg = f"'{response_validation_error_http_code}' must be an integer representing an HTTP status code."
1587+
raise ValueError(msg) from None
1588+
1589+
return response_validation_error_http_code or HTTPStatus.UNPROCESSABLE_ENTITY
15491590

15501591
def get_openapi_schema(
15511592
self,
@@ -1723,7 +1764,7 @@ def _get_openapi_servers(servers: list[Server] | None) -> list[Server]:
17231764

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

17281769
@staticmethod
17291770
def _get_openapi_security(
@@ -2225,10 +2266,7 @@ def _get_base_path(self) -> str:
22252266
@staticmethod
22262267
def _has_debug(debug: bool | None = None) -> bool:
22272268
# It might have been explicitly switched off (debug=False)
2228-
if debug is not None:
2229-
return debug
2230-
2231-
return powertools_dev_is_set()
2269+
return debug if debug is not None else powertools_dev_is_set()
22322270

22332271
@staticmethod
22342272
def _compile_regex(rule: str, base_regex: str = _ROUTE_REGEX):
@@ -2341,7 +2379,7 @@ def _path_starts_with(path: str, prefix: str):
23412379
if not isinstance(prefix, str) or prefix == "":
23422380
return False
23432381

2344-
return path.startswith(prefix + "/")
2382+
return path.startswith(f"{prefix}/")
23452383

23462384
def _handle_not_found(self, method: str, path: str) -> ResponseBuilder:
23472385
"""Called when no matching route was found and includes support for the cors preflight response"""
@@ -2484,6 +2522,21 @@ def _call_exception_handler(self, exp: Exception, route: Route) -> ResponseBuild
24842522
route=route,
24852523
)
24862524

2525+
# OpenAPIValidationMiddleware will only raise ResponseValidationError when
2526+
# 'self._response_validation_error_http_code' is not None
2527+
if isinstance(exp, ResponseValidationError):
2528+
http_code = self._response_validation_error_http_code
2529+
errors = [{"loc": e["loc"], "type": e["type"]} for e in exp.errors()]
2530+
return self._response_builder_class(
2531+
response=Response(
2532+
status_code=http_code.value,
2533+
content_type=content_types.APPLICATION_JSON,
2534+
body={"statusCode": self._response_validation_error_http_code, "detail": errors},
2535+
),
2536+
serializer=self._serializer,
2537+
route=route,
2538+
)
2539+
24872540
if isinstance(exp, ServiceError):
24882541
return self._response_builder_class(
24892542
response=Response(
@@ -2597,8 +2650,9 @@ def _get_fields_from_routes(routes: Sequence[Route]) -> list[ModelField]:
25972650
if route.dependant.response_extra_models:
25982651
responses_from_routes.extend(route.dependant.response_extra_models)
25992652

2600-
flat_models = list(responses_from_routes + request_fields_from_routes + body_fields_from_routes)
2601-
return flat_models
2653+
return list(
2654+
responses_from_routes + request_fields_from_routes + body_fields_from_routes,
2655+
)
26022656

26032657

26042658
class Router(BaseRouter):
@@ -2696,6 +2750,7 @@ def __init__(
26962750
serializer: Callable[[dict], str] | None = None,
26972751
strip_prefixes: list[str | Pattern] | None = None,
26982752
enable_validation: bool = False,
2753+
response_validation_error_http_code: HTTPStatus | int | None = None,
26992754
):
27002755
"""Amazon API Gateway REST and HTTP API v1 payload resolver"""
27012756
super().__init__(
@@ -2705,6 +2760,7 @@ def __init__(
27052760
serializer,
27062761
strip_prefixes,
27072762
enable_validation,
2763+
response_validation_error_http_code,
27082764
)
27092765

27102766
def _get_base_path(self) -> str:
@@ -2778,6 +2834,7 @@ def __init__(
27782834
serializer: Callable[[dict], str] | None = None,
27792835
strip_prefixes: list[str | Pattern] | None = None,
27802836
enable_validation: bool = False,
2837+
response_validation_error_http_code: HTTPStatus | int | None = None,
27812838
):
27822839
"""Amazon API Gateway HTTP API v2 payload resolver"""
27832840
super().__init__(
@@ -2787,6 +2844,7 @@ def __init__(
27872844
serializer,
27882845
strip_prefixes,
27892846
enable_validation,
2847+
response_validation_error_http_code,
27902848
)
27912849

27922850
def _get_base_path(self) -> str:
@@ -2815,9 +2873,18 @@ def __init__(
28152873
serializer: Callable[[dict], str] | None = None,
28162874
strip_prefixes: list[str | Pattern] | None = None,
28172875
enable_validation: bool = False,
2876+
response_validation_error_http_code: HTTPStatus | int | None = None,
28182877
):
28192878
"""Amazon Application Load Balancer (ALB) resolver"""
2820-
super().__init__(ProxyEventType.ALBEvent, cors, debug, serializer, strip_prefixes, enable_validation)
2879+
super().__init__(
2880+
ProxyEventType.ALBEvent,
2881+
cors,
2882+
debug,
2883+
serializer,
2884+
strip_prefixes,
2885+
enable_validation,
2886+
response_validation_error_http_code,
2887+
)
28212888

28222889
def _get_base_path(self) -> str:
28232890
# ALB doesn't have a stage variable, so we just return an empty string

aws_lambda_powertools/event_handler/lambda_function_url.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
)
99

1010
if TYPE_CHECKING:
11+
from http import HTTPStatus
12+
1113
from aws_lambda_powertools.event_handler import CORSConfig
1214
from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent
1315

@@ -57,6 +59,7 @@ def __init__(
5759
serializer: Callable[[dict], str] | None = None,
5860
strip_prefixes: list[str | Pattern] | None = None,
5961
enable_validation: bool = False,
62+
response_validation_error_http_code: HTTPStatus | int | None = None,
6063
):
6164
super().__init__(
6265
ProxyEventType.LambdaFunctionUrlEvent,
@@ -65,6 +68,7 @@ def __init__(
6568
serializer,
6669
strip_prefixes,
6770
enable_validation,
71+
response_validation_error_http_code,
6872
)
6973

7074
def _get_base_path(self) -> str:

aws_lambda_powertools/event_handler/middlewares/openapi_validation.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
)
1818
from aws_lambda_powertools.event_handler.openapi.dependant import is_scalar_field
1919
from aws_lambda_powertools.event_handler.openapi.encoders import jsonable_encoder
20-
from aws_lambda_powertools.event_handler.openapi.exceptions import RequestValidationError
20+
from aws_lambda_powertools.event_handler.openapi.exceptions import RequestValidationError, ResponseValidationError
2121
from aws_lambda_powertools.event_handler.openapi.params import Param
2222

2323
if TYPE_CHECKING:
@@ -58,7 +58,11 @@ def get_todos(): list[Todo]:
5858
```
5959
"""
6060

61-
def __init__(self, validation_serializer: Callable[[Any], str] | None = None):
61+
def __init__(
62+
self,
63+
validation_serializer: Callable[[Any], str] | None = None,
64+
has_response_validation_error: bool = False,
65+
):
6266
"""
6367
Initialize the OpenAPIValidationMiddleware.
6468
@@ -67,8 +71,13 @@ def __init__(self, validation_serializer: Callable[[Any], str] | None = None):
6771
validation_serializer : Callable, optional
6872
Optional serializer to use when serializing the response for validation.
6973
Use it when you have a custom type that cannot be serialized by the default jsonable_encoder.
74+
75+
has_response_validation_error: bool, optional
76+
Optional flag used to distinguish between payload and validation errors.
77+
By setting this flag to True, ResponseValidationError will be raised if response could not be validated.
7078
"""
7179
self._validation_serializer = validation_serializer
80+
self._has_response_validation_error = has_response_validation_error
7281

7382
def handler(self, app: EventHandlerInstance, next_middleware: NextMiddleware) -> Response:
7483
logger.debug("OpenAPIValidationMiddleware handler")
@@ -164,6 +173,8 @@ def _serialize_response(
164173
errors: list[dict[str, Any]] = []
165174
value = _validate_field(field=field, value=response_content, loc=("response",), existing_errors=errors)
166175
if errors:
176+
if self._has_response_validation_error:
177+
raise ResponseValidationError(errors=_normalize_errors(errors), body=response_content)
167178
raise RequestValidationError(errors=_normalize_errors(errors), body=response_content)
168179

169180
if hasattr(field, "serialize"):

aws_lambda_powertools/event_handler/openapi/exceptions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
2323
self.body = body
2424

2525

26+
class ResponseValidationError(ValidationException):
27+
"""
28+
Raised when the response body does not match the OpenAPI schema
29+
"""
30+
31+
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
32+
super().__init__(errors)
33+
self.body = body
34+
35+
2636
class SerializationError(Exception):
2737
"""
2838
Base exception for all encoding errors

aws_lambda_powertools/event_handler/vpc_lattice.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
)
99

1010
if TYPE_CHECKING:
11+
from http import HTTPStatus
12+
1113
from aws_lambda_powertools.event_handler import CORSConfig
1214
from aws_lambda_powertools.utilities.data_classes import VPCLatticeEvent, VPCLatticeEventV2
1315

@@ -53,9 +55,18 @@ def __init__(
5355
serializer: Callable[[dict], str] | None = None,
5456
strip_prefixes: list[str | Pattern] | None = None,
5557
enable_validation: bool = False,
58+
response_validation_error_http_code: HTTPStatus | int | None = None,
5659
):
5760
"""Amazon VPC Lattice resolver"""
58-
super().__init__(ProxyEventType.VPCLatticeEvent, cors, debug, serializer, strip_prefixes, enable_validation)
61+
super().__init__(
62+
ProxyEventType.VPCLatticeEvent,
63+
cors,
64+
debug,
65+
serializer,
66+
strip_prefixes,
67+
enable_validation,
68+
response_validation_error_http_code,
69+
)
5970

6071
def _get_base_path(self) -> str:
6172
return ""
@@ -102,9 +113,18 @@ def __init__(
102113
serializer: Callable[[dict], str] | None = None,
103114
strip_prefixes: list[str | Pattern] | None = None,
104115
enable_validation: bool = False,
116+
response_validation_error_http_code: HTTPStatus | int | None = None,
105117
):
106118
"""Amazon VPC Lattice resolver"""
107-
super().__init__(ProxyEventType.VPCLatticeEventV2, cors, debug, serializer, strip_prefixes, enable_validation)
119+
super().__init__(
120+
ProxyEventType.VPCLatticeEventV2,
121+
cors,
122+
debug,
123+
serializer,
124+
strip_prefixes,
125+
enable_validation,
126+
response_validation_error_http_code,
127+
)
108128

109129
def _get_base_path(self) -> str:
110130
return ""
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Exposes version constant to avoid circular dependencies."""
22

3-
VERSION = "3.8.1a0"
3+
VERSION = "3.8.1a5"

aws_lambda_powertools/utilities/parser/models/s3.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class S3Message(BaseModel):
5353
s3SchemaVersion: str
5454
configurationId: str
5555
bucket: S3Bucket
56-
object: S3Object # noqa: A003,VNE003
56+
object: S3Object # noqa: A003
5757

5858

5959
class S3EventNotificationObjectModel(BaseModel):
@@ -71,7 +71,7 @@ class S3EventNotificationEventBridgeBucketModel(BaseModel):
7171
class S3EventNotificationEventBridgeDetailModel(BaseModel):
7272
version: str
7373
bucket: S3EventNotificationEventBridgeBucketModel
74-
object: S3EventNotificationObjectModel # noqa: A003,VNE003
74+
object: S3EventNotificationObjectModel # noqa: A003
7575
request_id: str = Field(..., alias="request-id")
7676
requester: str
7777
source_ip_address: Optional[str] = Field(None, alias="source-ip-address")

docs/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# v9.1.18
2-
FROM squidfunk/mkdocs-material@sha256:047452c6641137c9caa3647d050ddb7fa67b59ed48cc67ec3a4995f3d360ab32
2+
FROM squidfunk/mkdocs-material@sha256:479a06a8f5a320a9b2b17e72cb7012388d66ea71a8568235cfa072eb152bc30c
33
# pip-compile --generate-hashes --output-file=requirements.txt requirements.in
44
COPY requirements.txt /tmp/
55
RUN pip install --require-hashes -r /tmp/requirements.txt

0 commit comments

Comments
 (0)