Skip to content

Commit 58ffa89

Browse files
authored
Merge branch 'aws-powertools:develop' into nested_event_sources
2 parents 630bac9 + a15a358 commit 58ffa89

File tree

97 files changed

+3717
-995
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+3717
-995
lines changed

.github/workflows/quality_check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171
- name: Complexity baseline
7272
run: make complexity-baseline
7373
- name: Upload coverage to Codecov
74-
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # 3.1.4
74+
uses: codecov/codecov-action@ab904c41d6ece82784817410c45d8b8c02684457 # 3.1.6
7575
with:
7676
file: ./coverage.xml
7777
env_vars: PYTHON

CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,32 @@
44
<a name="unreleased"></a>
55
# Unreleased
66

7+
## Bug Fixes
8+
9+
* **event-handler:** strip whitespace from Content-Type headers during OpenAPI schema validation ([#3677](https://github.com/aws-powertools/powertools-lambda-python/issues/3677))
10+
711
## Documentation
812

913
* **metrics:** fix empty metric warning filter ([#3660](https://github.com/aws-powertools/powertools-lambda-python/issues/3660))
14+
* **proccess:** add versioning and maintenance policy ([#3682](https://github.com/aws-powertools/powertools-lambda-python/issues/3682))
1015

1116
## Features
1217

1318
* **event_handler:** add support for multiValueQueryStringParameters in OpenAPI schema ([#3667](https://github.com/aws-powertools/powertools-lambda-python/issues/3667))
1419

1520
## Maintenance
1621

17-
* **deps:** bump pydantic from 1.10.13 to 1.10.14 ([#3655](https://github.com/aws-powertools/powertools-lambda-python/issues/3655))
22+
* **deps:** bump codecov/codecov-action from 3.1.4 to 3.1.5 ([#3674](https://github.com/aws-powertools/powertools-lambda-python/issues/3674))
23+
* **deps:** bump squidfunk/mkdocs-material from `58eef6c` to `9aad7af` in /docs ([#3670](https://github.com/aws-powertools/powertools-lambda-python/issues/3670))
24+
* **deps:** bump squidfunk/mkdocs-material from `9aad7af` to `a4a2029` in /docs ([#3679](https://github.com/aws-powertools/powertools-lambda-python/issues/3679))
1825
* **deps:** bump the layer-balancer group in /layer/scripts/layer-balancer with 1 update ([#3665](https://github.com/aws-powertools/powertools-lambda-python/issues/3665))
26+
* **deps:** bump codecov/codecov-action from 3.1.5 to 3.1.6 ([#3683](https://github.com/aws-powertools/powertools-lambda-python/issues/3683))
27+
* **deps:** bump pydantic from 1.10.13 to 1.10.14 ([#3655](https://github.com/aws-powertools/powertools-lambda-python/issues/3655))
28+
* **deps-dev:** bump aws-cdk from 2.123.0 to 2.124.0 ([#3678](https://github.com/aws-powertools/powertools-lambda-python/issues/3678))
29+
* **deps-dev:** bump sentry-sdk from 1.39.2 to 1.40.0 ([#3684](https://github.com/aws-powertools/powertools-lambda-python/issues/3684))
1930
* **deps-dev:** bump ruff from 0.1.13 to 0.1.14 ([#3656](https://github.com/aws-powertools/powertools-lambda-python/issues/3656))
31+
* **deps-dev:** bump ruff from 0.1.14 to 0.1.15 ([#3685](https://github.com/aws-powertools/powertools-lambda-python/issues/3685))
32+
* **deps-dev:** bump aws-cdk from 2.122.0 to 2.123.0 ([#3673](https://github.com/aws-powertools/powertools-lambda-python/issues/3673))
2033

2134

2235
<a name="v2.32.0"></a>

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ dev:
88
pip install --upgrade pip pre-commit poetry
99
poetry config --local virtualenvs.in-project true
1010
@$(MAKE) dev-version-plugin
11-
poetry install --extras "all datamasking-aws-sdk redis"
11+
poetry install --extras "all redis"
1212
pre-commit install
1313

1414
dev-gitpod:
1515
pip install --upgrade pip poetry
1616
@$(MAKE) dev-version-plugin
17-
poetry install --extras "all datamasking-aws-sdk redis"
17+
poetry install --extras "all redis"
1818
pre-commit install
1919

2020
format:

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverles
3030
* **[Event source data classes](https://docs.powertools.aws.dev/lambda/python/latest/utilities/data_classes/)** - Data classes describing the schema of common Lambda event triggers
3131
* **[Parser](https://docs.powertools.aws.dev/lambda/python/latest/utilities/parser/)** - Data parsing and deep validation using Pydantic
3232
* **[Idempotency](https://docs.powertools.aws.dev/lambda/python/latest/utilities/idempotency/)** - Convert your Lambda functions into idempotent operations which are safe to retry
33+
* **[Data Masking](https://docs.powertools.aws.dev/lambda/python/latest/utilities/data_masking/)** - Protect confidential data with easy removal or encryption
3334
* **[Feature Flags](https://docs.powertools.aws.dev/lambda/python/latest/utilities/feature_flags/)** - A simple rule engine to evaluate when one or multiple features should be enabled depending on the input
3435
* **[Streaming](https://docs.powertools.aws.dev/lambda/python/latest/utilities/streaming/)** - Streams datasets larger than the available memory as streaming data.
3536

aws_lambda_powertools/event_handler/middlewares/openapi_validation.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,22 @@ def handler(self, app: EventHandlerInstance, next_middleware: NextMiddleware) ->
8181
query_string,
8282
)
8383

84+
# Normalize header values before validate this
85+
headers = _normalize_multi_header_values_with_param(
86+
app.current_event.resolved_headers_field,
87+
route.dependant.header_params,
88+
)
89+
90+
# Process header values
91+
header_values, header_errors = _request_params_to_args(
92+
route.dependant.header_params,
93+
headers,
94+
)
95+
8496
values.update(path_values)
8597
values.update(query_values)
86-
errors += path_errors + query_errors
98+
values.update(header_values)
99+
errors += path_errors + query_errors + header_errors
87100

88101
# Process the request body, if it exists
89102
if route.dependant.body_params:
@@ -212,7 +225,7 @@ def _get_body(self, app: EventHandlerInstance) -> Dict[str, Any]:
212225
"""
213226

214227
content_type_value = app.current_event.get_header_value("content-type")
215-
if not content_type_value or content_type_value.startswith("application/json"):
228+
if not content_type_value or content_type_value.strip().startswith("application/json"):
216229
try:
217230
return app.current_event.json_body
218231
except json.JSONDecodeError as e:
@@ -243,12 +256,14 @@ def _request_params_to_args(
243256
errors = []
244257

245258
for field in required_params:
246-
value = received_params.get(field.alias)
247-
248259
field_info = field.field_info
260+
261+
# To ensure early failure, we check if it's not an instance of Param.
249262
if not isinstance(field_info, Param):
250263
raise AssertionError(f"Expected Param field_info, got {field_info}")
251264

265+
value = received_params.get(field.alias)
266+
252267
loc = (field_info.in_.value, field.alias)
253268

254269
# If we don't have a value, see if it's required or has a default
@@ -377,3 +392,30 @@ def _normalize_multi_query_string_with_param(query_string: Optional[Dict[str, st
377392
except KeyError:
378393
pass
379394
return query_string
395+
396+
397+
def _normalize_multi_header_values_with_param(headers: Optional[Dict[str, str]], params: Sequence[ModelField]):
398+
"""
399+
Extract and normalize resolved_headers_field
400+
401+
Parameters
402+
----------
403+
headers: Dict
404+
A dictionary containing the initial header parameters.
405+
params: Sequence[ModelField]
406+
A sequence of ModelField objects representing parameters.
407+
408+
Returns
409+
-------
410+
A dictionary containing the processed headers.
411+
"""
412+
if headers:
413+
for param in filter(is_scalar_field, params):
414+
try:
415+
if len(headers[param.alias]) == 1:
416+
# if the target parameter is a scalar and the list contains only 1 element
417+
# we keep the first value of the headers regardless if there are more in the payload
418+
headers[param.alias] = headers[param.alias][0]
419+
except KeyError:
420+
pass
421+
return headers

aws_lambda_powertools/event_handler/openapi/dependant.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
from aws_lambda_powertools.event_handler.openapi.params import (
1515
Body,
1616
Dependant,
17+
Header,
1718
Param,
1819
ParamTypes,
1920
Query,
2021
_File,
2122
_Form,
22-
_Header,
2323
analyze_param,
2424
create_response_field,
2525
get_flat_dependant,
@@ -59,16 +59,21 @@ def add_param_to_fields(
5959
6060
"""
6161
field_info = cast(Param, field.field_info)
62-
if field_info.in_ == ParamTypes.path:
63-
dependant.path_params.append(field)
64-
elif field_info.in_ == ParamTypes.query:
65-
dependant.query_params.append(field)
66-
elif field_info.in_ == ParamTypes.header:
67-
dependant.header_params.append(field)
62+
63+
# Dictionary to map ParamTypes to their corresponding lists in dependant
64+
param_type_map = {
65+
ParamTypes.path: dependant.path_params,
66+
ParamTypes.query: dependant.query_params,
67+
ParamTypes.header: dependant.header_params,
68+
ParamTypes.cookie: dependant.cookie_params,
69+
}
70+
71+
# Check if field_info.in_ is a valid key in param_type_map and append the field to the corresponding list
72+
# or raise an exception if it's not a valid key.
73+
if field_info.in_ in param_type_map:
74+
param_type_map[field_info.in_].append(field)
6875
else:
69-
if field_info.in_ != ParamTypes.cookie:
70-
raise AssertionError(f"Unsupported param type: {field_info.in_}")
71-
dependant.cookie_params.append(field)
76+
raise AssertionError(f"Unsupported param type: {field_info.in_}")
7277

7378

7479
def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any:
@@ -265,7 +270,7 @@ def is_body_param(*, param_field: ModelField, is_path_param: bool) -> bool:
265270
return False
266271
elif is_scalar_field(field=param_field):
267272
return False
268-
elif isinstance(param_field.field_info, (Query, _Header)) and is_scalar_sequence_field(param_field):
273+
elif isinstance(param_field.field_info, (Query, Header)) and is_scalar_sequence_field(param_field):
269274
return False
270275
else:
271276
if not isinstance(param_field.field_info, Body):

aws_lambda_powertools/event_handler/openapi/params.py

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ def __init__(
486486
)
487487

488488

489-
class _Header(Param):
489+
class Header(Param):
490490
"""
491491
A class used internally to represent a header parameter in a path operation.
492492
"""
@@ -527,12 +527,75 @@ def __init__(
527527
json_schema_extra: Union[Dict[str, Any], None] = None,
528528
**extra: Any,
529529
):
530+
"""
531+
Constructs a new Query param.
532+
533+
Parameters
534+
----------
535+
default: Any
536+
The default value of the parameter
537+
default_factory: Callable[[], Any], optional
538+
Callable that will be called when a default value is needed for this field
539+
annotation: Any, optional
540+
The type annotation of the parameter
541+
alias: str, optional
542+
The public name of the field
543+
alias_priority: int, optional
544+
Priority of the alias. This affects whether an alias generator is used
545+
validation_alias: str | AliasPath | AliasChoices | None, optional
546+
Alias to be used for validation only
547+
serialization_alias: str | AliasPath | AliasChoices | None, optional
548+
Alias to be used for serialization only
549+
convert_underscores: bool
550+
If true convert "_" to "-"
551+
See RFC: https://www.rfc-editor.org/rfc/rfc9110.html#name-field-name-registry
552+
title: str, optional
553+
The title of the parameter
554+
description: str, optional
555+
The description of the parameter
556+
gt: float, optional
557+
Only applies to numbers, required the field to be "greater than"
558+
ge: float, optional
559+
Only applies to numbers, required the field to be "greater than or equal"
560+
lt: float, optional
561+
Only applies to numbers, required the field to be "less than"
562+
le: float, optional
563+
Only applies to numbers, required the field to be "less than or equal"
564+
min_length: int, optional
565+
Only applies to strings, required the field to have a minimum length
566+
max_length: int, optional
567+
Only applies to strings, required the field to have a maximum length
568+
pattern: str, optional
569+
Only applies to strings, requires the field match against a regular expression pattern string
570+
discriminator: str, optional
571+
Parameter field name for discriminating the type in a tagged union
572+
strict: bool, optional
573+
Enables Pydantic's strict mode for the field
574+
multiple_of: float, optional
575+
Only applies to numbers, requires the field to be a multiple of the given value
576+
allow_inf_nan: bool, optional
577+
Only applies to numbers, requires the field to allow infinity and NaN values
578+
max_digits: int, optional
579+
Only applies to Decimals, requires the field to have a maxmium number of digits within the decimal.
580+
decimal_places: int, optional
581+
Only applies to Decimals, requires the field to have at most a number of decimal places
582+
examples: List[Any], optional
583+
A list of examples for the parameter
584+
deprecated: bool, optional
585+
If `True`, the parameter will be marked as deprecated
586+
include_in_schema: bool, optional
587+
If `False`, the parameter will be excluded from the generated OpenAPI schema
588+
json_schema_extra: Dict[str, Any], optional
589+
Extra values to include in the generated OpenAPI schema
590+
"""
530591
self.convert_underscores = convert_underscores
592+
self._alias = alias
593+
531594
super().__init__(
532595
default=default,
533596
default_factory=default_factory,
534597
annotation=annotation,
535-
alias=alias,
598+
alias=self._alias,
536599
alias_priority=alias_priority,
537600
validation_alias=validation_alias,
538601
serialization_alias=serialization_alias,
@@ -558,6 +621,18 @@ def __init__(
558621
**extra,
559622
)
560623

624+
@property
625+
def alias(self):
626+
return self._alias
627+
628+
@alias.setter
629+
def alias(self, value: Optional[str] = None):
630+
if value is not None:
631+
# Headers are case-insensitive according to RFC 7540 (HTTP/2), so we lower the parameter name
632+
# This ensures that customers can access headers with any casing, as per the RFC guidelines.
633+
# Reference: https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2
634+
self._alias = value.lower()
635+
561636

562637
class Body(FieldInfo):
563638
"""

aws_lambda_powertools/shared/functions.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,18 @@ def resolve_env_var_choice(
9696

9797
def base64_decode(value: str) -> bytes:
9898
try:
99-
logger.debug("Decoding base64 record item before parsing")
99+
logger.debug("Decoding base64 item to bytes")
100100
return base64.b64decode(value)
101101
except (BinAsciiError, TypeError):
102-
raise ValueError("base64 decode failed")
102+
raise ValueError("base64 decode failed - is this base64 encoded string?")
103+
104+
105+
def bytes_to_base64_string(value: bytes) -> str:
106+
try:
107+
logger.debug("Encoding bytes to base64 string")
108+
return base64.b64encode(value).decode()
109+
except TypeError:
110+
raise ValueError(f"base64 encoding failed - is this bytes data? type: {type(value)}")
103111

104112

105113
def bytes_to_string(value: bytes) -> str:

0 commit comments

Comments
 (0)