Skip to content

Commit a1d2b0f

Browse files
committed
Merge remote-tracking branch 'origin/develop' into tonnico/feat/support-pydantic
2 parents 343cbcd + 61c9f6b commit a1d2b0f

File tree

23 files changed

+492
-352
lines changed

23 files changed

+492
-352
lines changed

.github/workflows/dependency-review.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ jobs:
2222
- name: 'Checkout Repository'
2323
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
2424
- name: 'Dependency Review'
25-
uses: actions/dependency-review-action@595b5aeba73380359d98a5e087f648dbb0edce1b # v4.7.3
25+
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0

CHANGELOG.md

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414

1515
## Documentation
1616

17+
* **event_handler:** remove the wrong CORS warning ([#7385](https://github.com/aws-powertools/powertools-lambda-python/issues/7385))
1718
* **event_handler:** update test section ([#7374](https://github.com/aws-powertools/powertools-lambda-python/issues/7374))
1819
* **event_handler:** add info section about types ([#7368](https://github.com/aws-powertools/powertools-lambda-python/issues/7368))
20+
* **logger:** clarify Advanced Logging Controls interaction with sampling ([#7429](https://github.com/aws-powertools/powertools-lambda-python/issues/7429))
1921

2022
## Features
2123

@@ -25,19 +27,49 @@
2527

2628
## Maintenance
2729

28-
* **ci:** new pre-release 3.20.1a1 ([#7372](https://github.com/aws-powertools/powertools-lambda-python/issues/7372))
30+
* **ci:** new pre-release 3.20.1a2 ([#7383](https://github.com/aws-powertools/powertools-lambda-python/issues/7383))
2931
* **ci:** new pre-release 3.20.1a0 ([#7362](https://github.com/aws-powertools/powertools-lambda-python/issues/7362))
32+
* **ci:** new pre-release 3.20.1a6 ([#7442](https://github.com/aws-powertools/powertools-lambda-python/issues/7442))
33+
* **ci:** new pre-release 3.20.1a1 ([#7372](https://github.com/aws-powertools/powertools-lambda-python/issues/7372))
34+
* **ci:** new pre-release 3.20.1a3 ([#7393](https://github.com/aws-powertools/powertools-lambda-python/issues/7393))
35+
* **ci:** new pre-release 3.20.1a4 ([#7404](https://github.com/aws-powertools/powertools-lambda-python/issues/7404))
36+
* **ci:** new pre-release 3.20.1a5 ([#7418](https://github.com/aws-powertools/powertools-lambda-python/issues/7418))
37+
* **deps:** bump mkdocs-material from 9.6.19 to 9.6.20 in /docs ([#7387](https://github.com/aws-powertools/powertools-lambda-python/issues/7387))
38+
* **deps:** bump pydantic-settings from 2.10.1 to 2.11.0 ([#7434](https://github.com/aws-powertools/powertools-lambda-python/issues/7434))
3039
* **deps:** bump mkdocs-material from 9.6.18 to 9.6.19 ([#7359](https://github.com/aws-powertools/powertools-lambda-python/issues/7359))
40+
* **deps:** bump valkey-glide from 2.0.1 to 2.1.0 ([#7403](https://github.com/aws-powertools/powertools-lambda-python/issues/7403))
41+
* **deps:** bump mkdocs-llmstxt from 0.3.1 to 0.3.2 ([#7436](https://github.com/aws-powertools/powertools-lambda-python/issues/7436))
42+
* **deps:** bump actions/dependency-review-action from 4.7.3 to 4.8.0 ([#7432](https://github.com/aws-powertools/powertools-lambda-python/issues/7432))
3143
* **deps:** bump protobuf from 6.32.0 to 6.32.1 ([#7376](https://github.com/aws-powertools/powertools-lambda-python/issues/7376))
32-
* **deps-dev:** bump boto3-stubs from 1.40.27 to 1.40.29 ([#7371](https://github.com/aws-powertools/powertools-lambda-python/issues/7371))
33-
* **deps-dev:** bump mypy from 1.17.1 to 1.18.1 ([#7375](https://github.com/aws-powertools/powertools-lambda-python/issues/7375))
44+
* **deps:** bump pydantic from 2.11.7 to 2.11.9 ([#7390](https://github.com/aws-powertools/powertools-lambda-python/issues/7390))
45+
* **deps:** bump mkdocs-llmstxt from 0.3.1 to 0.3.2 in /docs ([#7409](https://github.com/aws-powertools/powertools-lambda-python/issues/7409))
46+
* **deps:** bump squidfunk/mkdocs-material from `209b62d` to `86d21da` in /docs ([#7386](https://github.com/aws-powertools/powertools-lambda-python/issues/7386))
47+
* **deps:** bump mkdocs-material from 9.6.19 to 9.6.20 ([#7389](https://github.com/aws-powertools/powertools-lambda-python/issues/7389))
48+
* **deps-dev:** bump boto3-stubs from 1.40.30 to 1.40.31 ([#7388](https://github.com/aws-powertools/powertools-lambda-python/issues/7388))
49+
* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.214.0a0 to 2.215.0a0 ([#7391](https://github.com/aws-powertools/powertools-lambda-python/issues/7391))
50+
* **deps-dev:** bump sentry-sdk from 2.38.0 to 2.39.0 ([#7426](https://github.com/aws-powertools/powertools-lambda-python/issues/7426))
51+
* **deps-dev:** bump aws-cdk-lib from 2.215.0 to 2.217.0 ([#7423](https://github.com/aws-powertools/powertools-lambda-python/issues/7423))
52+
* **deps-dev:** bump types-protobuf from 6.30.2.20250822 to 6.32.1.20250918 ([#7406](https://github.com/aws-powertools/powertools-lambda-python/issues/7406))
53+
* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.215.0a0 to 2.217.0a0 ([#7424](https://github.com/aws-powertools/powertools-lambda-python/issues/7424))
54+
* **deps-dev:** bump boto3-stubs from 1.40.32 to 1.40.39 ([#7425](https://github.com/aws-powertools/powertools-lambda-python/issues/7425))
55+
* **deps-dev:** bump coverage from 7.10.6 to 7.10.7 ([#7437](https://github.com/aws-powertools/powertools-lambda-python/issues/7437))
3456
* **deps-dev:** bump pytest-asyncio from 1.1.0 to 1.2.0 ([#7377](https://github.com/aws-powertools/powertools-lambda-python/issues/7377))
35-
* **deps-dev:** bump pytest-cov from 6.3.0 to 7.0.0 ([#7357](https://github.com/aws-powertools/powertools-lambda-python/issues/7357))
57+
* **deps-dev:** bump boto3-stubs from 1.40.31 to 1.40.32 ([#7395](https://github.com/aws-powertools/powertools-lambda-python/issues/7395))
58+
* **deps-dev:** bump mypy from 1.17.1 to 1.18.1 ([#7375](https://github.com/aws-powertools/powertools-lambda-python/issues/7375))
59+
* **deps-dev:** bump ruff from 0.12.12 to 0.13.2 ([#7427](https://github.com/aws-powertools/powertools-lambda-python/issues/7427))
60+
* **deps-dev:** bump sentry-sdk from 2.37.1 to 2.38.0 ([#7402](https://github.com/aws-powertools/powertools-lambda-python/issues/7402))
61+
* **deps-dev:** bump boto3-stubs from 1.40.27 to 1.40.29 ([#7371](https://github.com/aws-powertools/powertools-lambda-python/issues/7371))
3662
* **deps-dev:** bump aws-cdk from 2.1029.0 to 2.1029.1 ([#7370](https://github.com/aws-powertools/powertools-lambda-python/issues/7370))
63+
* **deps-dev:** bump aws-cdk from 2.1029.1 to 2.1029.2 ([#7400](https://github.com/aws-powertools/powertools-lambda-python/issues/7400))
64+
* **deps-dev:** bump mypy from 1.18.1 to 1.18.2 ([#7435](https://github.com/aws-powertools/powertools-lambda-python/issues/7435))
65+
* **deps-dev:** bump boto3-stubs from 1.40.39 to 1.40.40 ([#7433](https://github.com/aws-powertools/powertools-lambda-python/issues/7433))
66+
* **deps-dev:** bump boto3-stubs from 1.40.26 to 1.40.27 ([#7358](https://github.com/aws-powertools/powertools-lambda-python/issues/7358))
67+
* **deps-dev:** bump pytest-cov from 6.3.0 to 7.0.0 ([#7357](https://github.com/aws-powertools/powertools-lambda-python/issues/7357))
68+
* **deps-dev:** bump aws-cdk from 2.1029.2 to 2.1029.3 ([#7420](https://github.com/aws-powertools/powertools-lambda-python/issues/7420))
3769
* **deps-dev:** bump testcontainers from 4.12.0 to 4.13.0 ([#7360](https://github.com/aws-powertools/powertools-lambda-python/issues/7360))
3870
* **deps-dev:** bump sentry-sdk from 2.37.0 to 2.37.1 ([#7356](https://github.com/aws-powertools/powertools-lambda-python/issues/7356))
71+
* **deps-dev:** bump pytest-mock from 3.15.0 to 3.15.1 ([#7396](https://github.com/aws-powertools/powertools-lambda-python/issues/7396))
3972
* **deps-dev:** bump boto3-stubs from 1.40.29 to 1.40.30 ([#7378](https://github.com/aws-powertools/powertools-lambda-python/issues/7378))
40-
* **deps-dev:** bump boto3-stubs from 1.40.26 to 1.40.27 ([#7358](https://github.com/aws-powertools/powertools-lambda-python/issues/7358))
4173

4274

4375
<a name="v3.20.0"></a>

aws_lambda_powertools/event_handler/middlewares/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def __name__(self) -> str: # noqa A003
1919
...
2020

2121

22-
class BaseMiddlewareHandler(Generic[EventHandlerInstance], ABC):
22+
class BaseMiddlewareHandler(ABC, Generic[EventHandlerInstance]):
2323
"""Base implementation for Middlewares to run code before and after in a chain.
2424
2525

aws_lambda_powertools/event_handler/middlewares/openapi_validation.py

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import dataclasses
44
import json
55
import logging
6-
from copy import deepcopy
76
from typing import TYPE_CHECKING, Any, Callable, Mapping, MutableMapping, Sequence, cast, get_origin
87
from urllib.parse import parse_qs
98

@@ -15,6 +14,7 @@
1514
_normalize_errors,
1615
_regenerate_error_with_loc,
1716
get_missing_field_error,
17+
is_sequence_field,
1818
lenient_issubclass,
1919
)
2020
from aws_lambda_powertools.event_handler.openapi.dependant import is_scalar_field
@@ -153,11 +153,10 @@ def _parse_form_data(self, app: EventHandlerInstance) -> dict[str, Any]:
153153
"""Parse URL-encoded form data from the request body."""
154154
try:
155155
body = app.current_event.decoded_body or ""
156-
# parse_qs returns dict[str, list[str]], but we want dict[str, str] for single values
156+
# NOTE: Keep values as lists; we'll normalize per-field later based on the expected type.
157+
# This avoids breaking List[...] fields when only a single value is provided.
157158
parsed = parse_qs(body, keep_blank_values=True)
158-
159-
result: dict[str, Any] = {key: values[0] if len(values) == 1 else values for key, values in parsed.items()}
160-
return result
159+
return parsed
161160

162161
except Exception as e: # pragma: no cover
163162
raise RequestValidationError( # pragma: no cover
@@ -317,12 +316,12 @@ def _prepare_response_content(
317316
def _request_params_to_args(
318317
required_params: Sequence[ModelField],
319318
received_params: Mapping[str, Any],
320-
) -> tuple[dict[str, Any], list[Any]]:
319+
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
321320
"""
322321
Convert the request params to a dictionary of values using validation, and returns a list of errors.
323322
"""
324-
values = {}
325-
errors = []
323+
values: dict[str, Any] = {}
324+
errors: list[dict[str, Any]] = []
326325

327326
for field in required_params:
328327
field_info = field.field_info
@@ -331,16 +330,12 @@ def _request_params_to_args(
331330
if not isinstance(field_info, Param):
332331
raise AssertionError(f"Expected Param field_info, got {field_info}")
333332

334-
value = received_params.get(field.alias)
335-
336333
loc = (field_info.in_.value, field.alias)
334+
value = received_params.get(field.alias)
337335

338336
# If we don't have a value, see if it's required or has a default
339337
if value is None:
340-
if field.required:
341-
errors.append(get_missing_field_error(loc=loc))
342-
else:
343-
values[field.name] = deepcopy(field.default)
338+
_handle_missing_field_value(field, values, errors, loc)
344339
continue
345340

346341
# Finally, validate the value
@@ -366,39 +361,64 @@ def _request_body_to_args(
366361
)
367362

368363
for field in required_params:
369-
# This sets the location to:
370-
# { "user": { object } } if field.alias == user
371-
# { { object } if field_alias is omitted
372-
loc: tuple[str, ...] = ("body", field.alias)
373-
if field_alias_omitted:
374-
loc = ("body",)
375-
376-
value: Any | None = None
377-
378-
# Now that we know what to look for, try to get the value from the received body
379-
if received_body is not None:
380-
try:
381-
value = received_body.get(field.alias)
382-
except AttributeError:
383-
errors.append(get_missing_field_error(loc))
384-
continue
385-
386-
# Determine if the field is required
364+
loc = _get_body_field_location(field, field_alias_omitted)
365+
value = _extract_field_value_from_body(field, received_body, loc, errors)
366+
367+
# If we don't have a value, see if it's required or has a default
387368
if value is None:
388-
if field.required:
389-
errors.append(get_missing_field_error(loc))
390-
else:
391-
values[field.name] = deepcopy(field.default)
369+
_handle_missing_field_value(field, values, errors, loc)
392370
continue
393371

394-
# MAINTENANCE: Handle byte and file fields
395-
396-
# Finally, validate the value
372+
value = _normalize_field_value(field, value)
397373
values[field.name] = _validate_field(field=field, value=value, loc=loc, existing_errors=errors)
398374

399375
return values, errors
400376

401377

378+
def _get_body_field_location(field: ModelField, field_alias_omitted: bool) -> tuple[str, ...]:
379+
"""Get the location tuple for a body field based on whether the field alias is omitted."""
380+
if field_alias_omitted:
381+
return ("body",)
382+
return ("body", field.alias)
383+
384+
385+
def _extract_field_value_from_body(
386+
field: ModelField,
387+
received_body: dict[str, Any] | None,
388+
loc: tuple[str, ...],
389+
errors: list[dict[str, Any]],
390+
) -> Any | None:
391+
"""Extract field value from the received body, handling potential AttributeError."""
392+
if received_body is None:
393+
return None
394+
395+
try:
396+
return received_body.get(field.alias)
397+
except AttributeError:
398+
errors.append(get_missing_field_error(loc))
399+
return None
400+
401+
402+
def _handle_missing_field_value(
403+
field: ModelField,
404+
values: dict[str, Any],
405+
errors: list[dict[str, Any]],
406+
loc: tuple[str, ...],
407+
) -> None:
408+
"""Handle the case when a field value is missing."""
409+
if field.required:
410+
errors.append(get_missing_field_error(loc))
411+
else:
412+
values[field.name] = field.get_default()
413+
414+
415+
def _normalize_field_value(field: ModelField, value: Any) -> Any:
416+
"""Normalize field value, converting lists to single values for non-sequence fields."""
417+
if isinstance(value, list) and not is_sequence_field(field):
418+
return value[0]
419+
return value
420+
421+
402422
def _validate_field(
403423
*,
404424
field: ModelField,
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.20.1a1"
3+
VERSION = "3.20.1a6"

aws_lambda_powertools/utilities/kafka/schema_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def __init__(
6060
value_schema_type: Literal["AVRO", "PROTOBUF", "JSON"] | None = None,
6161
value_schema: str | None = None,
6262
value_output_serializer: Any | None = None,
63-
key_schema_type: Literal["AVRO", "PROTOBUF", "JSON", None] | None = None,
63+
key_schema_type: Literal["AVRO", "PROTOBUF", "JSON"] | None = None,
6464
key_schema: str | None = None,
6565
key_output_serializer: Any | None = None,
6666
):
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from typing import Literal
22

3-
TransformOptions = Literal["json", "binary", "auto", None]
3+
TransformOptions = Literal["json", "binary", "auto", None] # noqa PYI061

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:209b62dd9530163cc5cf9a49853b5bb8570ffb3f3b5fe4eadc1d319bbda5ce2f
2+
FROM squidfunk/mkdocs-material@sha256:86d21da4f45f16e30774bf911e5b4795da13ce0cd197dbf8d3d059f256b2cc37
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

docs/core/event_handler/api_gateway.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1144,7 +1144,7 @@ You can enable debug mode via `debug` param, or via `POWERTOOLS_DEV` [environmen
11441144
This will enable full tracebacks errors in the response, print request and responses, and set CORS in development mode.
11451145

11461146
???+ danger
1147-
This might reveal sensitive information in your logs and relax CORS restrictions, use it sparingly.
1147+
This might reveal sensitive information in your logs, use it sparingly.
11481148

11491149
It's best to use for local development only!
11501150

docs/core/logger.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ If you want to access the numeric value of the current log level, you can use th
375375
When you want to set a logging policy to drop informational or verbose logs for one or all AWS Lambda functions, regardless of runtime and logger used.
376376

377377
<!-- markdownlint-disable MD013 -->
378-
With [AWS Lambda Advanced Logging Controls (ALC)](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced){target="_blank"}, you can enforce a minimum log level that Lambda will accept from your application code.
378+
With [AWS Lambda Advanced Logging Controls (ALC)](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs-log-level.html#monitoring-cloudwatchlogs-log-level-setting){target="_blank"}, you can enforce a minimum log level that Lambda will accept from your application code.
379379

380380
When enabled, you should keep `Logger` and ALC log level in sync to avoid data loss.
381381

@@ -409,6 +409,11 @@ We prioritise log level settings in this order:
409409
2. Explicit log level in `Logger` constructor, or by calling the `logger.setLevel()` method
410410
3. `POWERTOOLS_LOG_LEVEL` environment variable
411411

412+
!!! info "AWS CDK and Advanced Logging Controls"
413+
When using AWS CDK's `applicationLogLevelV2` parameter or setting log levels through the Lambda console, AWS Lambda automatically sets the `AWS_LAMBDA_LOG_LEVEL` environment variable. This means Lambda's log level takes precedence over Powertools for AWS configuration, potentially overriding both `POWERTOOLS_LOG_LEVEL` and sampling settings.
414+
415+
**Example**: If you set `applicationLogLevelV2=DEBUG` in CDK while having `POWERTOOLS_LOG_LEVEL=INFO`, the DEBUG level will be used because Lambda automatically sets the environment variable `AWS_LAMBDA_LOG_LEVEL` to the debug level.
416+
412417
If you set `Logger` level lower than ALC, we will emit a warning informing you that your messages will be discarded by Lambda.
413418

414419
> **NOTE**
@@ -825,6 +830,8 @@ Use sampling when you want to dynamically change your log level to **DEBUG** bas
825830

826831
You can use values ranging from `0.0` to `1` (100%) when setting `POWERTOOLS_LOGGER_SAMPLE_RATE` env var, or `sampling_rate` parameter in Logger.
827832

833+
**AWS Lambda Advanced Logging Controls (ALC)** settings can affect Sampling behavior. See [how it works](#aws-lambda-advanced-logging-controls-alc).
834+
828835
???+ tip "Tip: When is this useful?"
829836
Log sampling allows you to capture debug information for a fraction of your requests, helping you diagnose rare or intermittent issues without increasing the overall verbosity of your logs.
830837

0 commit comments

Comments
 (0)