Skip to content

Commit aaca9de

Browse files
Merge branch 'develop' into develop
2 parents 950583c + d0837c5 commit aaca9de

File tree

86 files changed

+4825
-782
lines changed

Some content is hidden

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

86 files changed

+4825
-782
lines changed

.github/workflows/publish_v2_layer.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ jobs:
124124

125125
- name: Set up Docker Buildx
126126
id: builder
127-
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
127+
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
128128
with:
129129
install: true
130130
driver: docker

.github/workflows/publish_v3_layer.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ jobs:
146146

147147
- name: Set up Docker Buildx
148148
id: builder
149-
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
149+
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
150150
with:
151151
install: true
152152
driver: docker

.github/workflows/quality_code_cdk_constructor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jobs:
5757
# NOTE: we need QEMU to build Layer against a different architecture (e.g., ARM)
5858
- name: Set up Docker Buildx
5959
id: builder
60-
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
60+
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
6161
with:
6262
install: true
6363
driver: docker

CHANGELOG.md

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,65 @@
44
<a name="unreleased"></a>
55
# Unreleased
66

7+
## Maintenance
8+
9+
* **deps:** bump redis from 5.3.0 to 6.2.0 ([#6827](https://github.com/aws-powertools/powertools-lambda-python/issues/6827))
10+
11+
12+
<a name="v3.15.1"></a>
13+
## [v3.15.1] - 2025-06-20
14+
## Features
15+
16+
* **kafka:** add logic to handle protobuf deserialization ([#6841](https://github.com/aws-powertools/powertools-lambda-python/issues/6841))
17+
18+
## Maintenance
19+
20+
* version bump
21+
* **ci:** new pre-release 3.15.1a0 ([#6839](https://github.com/aws-powertools/powertools-lambda-python/issues/6839))
22+
23+
24+
<a name="v3.15.0"></a>
25+
## [v3.15.0] - 2025-06-19
26+
## Bug Fixes
27+
28+
* **bedrock_agent:** fix querystring field resolution ([#6777](https://github.com/aws-powertools/powertools-lambda-python/issues/6777))
29+
730
## Documentation
831

32+
* **kafka:** add kafka documentation ([#6834](https://github.com/aws-powertools/powertools-lambda-python/issues/6834))
933
* **public_reference:** add Instil as a public reference ([#6763](https://github.com/aws-powertools/powertools-lambda-python/issues/6763))
1034

35+
## Features
36+
37+
* **kafka:** add support for Confluence Producers ([#6833](https://github.com/aws-powertools/powertools-lambda-python/issues/6833))
38+
* **kafka:** New Kafka utility ([#6821](https://github.com/aws-powertools/powertools-lambda-python/issues/6821))
39+
1140
## Maintenance
1241

42+
* version bump
43+
* **ci:** new pre-release 3.14.1a6 ([#6830](https://github.com/aws-powertools/powertools-lambda-python/issues/6830))
44+
* **ci:** new pre-release 3.14.1a5 ([#6820](https://github.com/aws-powertools/powertools-lambda-python/issues/6820))
1345
* **ci:** new pre-release 3.14.1a0 ([#6773](https://github.com/aws-powertools/powertools-lambda-python/issues/6773))
46+
* **ci:** new pre-release 3.14.1a4 ([#6812](https://github.com/aws-powertools/powertools-lambda-python/issues/6812))
47+
* **ci:** new pre-release 3.14.1a3 ([#6797](https://github.com/aws-powertools/powertools-lambda-python/issues/6797))
1448
* **ci:** new pre-release 3.14.1a1 ([#6778](https://github.com/aws-powertools/powertools-lambda-python/issues/6778))
15-
* **deps:** bump mkdocstrings-python from 1.16.11 to 1.16.12 ([#6765](https://github.com/aws-powertools/powertools-lambda-python/issues/6765))
49+
* **ci:** new pre-release 3.14.1a2 ([#6788](https://github.com/aws-powertools/powertools-lambda-python/issues/6788))
1650
* **deps:** bump mkdocstrings-python from 1.16.11 to 1.16.12 in /docs ([#6768](https://github.com/aws-powertools/powertools-lambda-python/issues/6768))
51+
* **deps:** bump mkdocstrings-python from 1.16.11 to 1.16.12 ([#6765](https://github.com/aws-powertools/powertools-lambda-python/issues/6765))
52+
* **deps:** bump protobuf from 6.31.0 to 6.31.1 ([#6815](https://github.com/aws-powertools/powertools-lambda-python/issues/6815))
1753
* **deps-dev:** bump boto3-stubs from 1.38.29 to 1.38.30 ([#6772](https://github.com/aws-powertools/powertools-lambda-python/issues/6772))
1854
* **deps-dev:** bump aws-cdk from 2.1017.1 to 2.1018.0 ([#6775](https://github.com/aws-powertools/powertools-lambda-python/issues/6775))
19-
* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.200.0a0 to 2.200.1a0 ([#6766](https://github.com/aws-powertools/powertools-lambda-python/issues/6766))
20-
* **deps-dev:** bump aws-cdk-lib from 2.200.0 to 2.200.1 ([#6767](https://github.com/aws-powertools/powertools-lambda-python/issues/6767))
55+
* **deps-dev:** bump boto3-stubs from 1.38.33 to 1.38.35 ([#6796](https://github.com/aws-powertools/powertools-lambda-python/issues/6796))
56+
* **deps-dev:** bump aws-cdk from 2.1018.0 to 2.1018.1 ([#6803](https://github.com/aws-powertools/powertools-lambda-python/issues/6803))
2157
* **deps-dev:** bump boto3-stubs from 1.38.30 to 1.38.31 ([#6776](https://github.com/aws-powertools/powertools-lambda-python/issues/6776))
58+
* **deps-dev:** bump requests from 2.32.3 to 2.32.4 ([#6789](https://github.com/aws-powertools/powertools-lambda-python/issues/6789))
2259
* **deps-dev:** bump boto3-stubs from 1.38.28 to 1.38.29 ([#6764](https://github.com/aws-powertools/powertools-lambda-python/issues/6764))
60+
* **deps-dev:** bump ruff from 0.11.12 to 0.11.13 ([#6780](https://github.com/aws-powertools/powertools-lambda-python/issues/6780))
61+
* **deps-dev:** bump boto3-stubs from 1.38.31 to 1.38.33 ([#6786](https://github.com/aws-powertools/powertools-lambda-python/issues/6786))
62+
* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.200.0a0 to 2.200.1a0 ([#6766](https://github.com/aws-powertools/powertools-lambda-python/issues/6766))
63+
* **deps-dev:** bump aws-cdk-lib from 2.200.0 to 2.200.1 ([#6767](https://github.com/aws-powertools/powertools-lambda-python/issues/6767))
64+
* **deps-dev:** bump pytest-cov from 6.1.1 to 6.2.1 ([#6800](https://github.com/aws-powertools/powertools-lambda-python/issues/6800))
65+
* **deps-dev:** bump requests from 2.32.3 to 2.32.4 ([#6787](https://github.com/aws-powertools/powertools-lambda-python/issues/6787))
2366

2467

2568
<a name="v3.14.0"></a>
@@ -6634,7 +6677,9 @@
66346677
* Merge pull request [#5](https://github.com/aws-powertools/powertools-lambda-python/issues/5) from jfuss/feat/python38
66356678

66366679

6637-
[Unreleased]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.14.0...HEAD
6680+
[Unreleased]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.15.1...HEAD
6681+
[v3.15.1]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.15.0...v3.15.1
6682+
[v3.15.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.14.0...v3.15.0
66386683
[v3.14.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.13.0...v3.14.0
66396684
[v3.13.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.12.0...v3.13.0
66406685
[v3.12.0]: https://github.com/aws-powertools/powertools-lambda-python/compare/v3.11.0...v3.12.0

aws_lambda_powertools/event_handler/api_gateway.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ def __init__(
407407

408408
# OpenAPI spec only understands paths with { }. So we'll have to convert Powertools' < >.
409409
# https://swagger.io/specification/#path-templating
410-
self.openapi_path = re.sub(r"<(.*?)>", lambda m: f"{{{''.join(m.group(1))}}}", self.path)
410+
self.openapi_path = re.sub(r"<(.*?)>", lambda m: f"{{{''.join(m.group(1))}}}", self.path) # type: ignore[arg-type]
411411

412412
self.rule = rule
413413
self.func = func
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.14.1a2"
3+
VERSION = "3.15.1"

aws_lambda_powertools/utilities/data_classes/kafka_event.py

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,19 @@
1010
from collections.abc import Iterator
1111

1212

13-
class KafkaEventRecord(DictWrapper):
13+
class KafkaEventRecordSchemaMetadata(DictWrapper):
14+
@property
15+
def data_format(self) -> str | None:
16+
"""The data format of the Kafka record."""
17+
return self.get("dataFormat", None)
18+
19+
@property
20+
def schema_id(self) -> str | None:
21+
"""The schema id of the Kafka record."""
22+
return self.get("schemaId", None)
23+
24+
25+
class KafkaEventRecordBase(DictWrapper):
1426
@property
1527
def topic(self) -> str:
1628
"""The Kafka topic."""
@@ -36,6 +48,24 @@ def timestamp_type(self) -> str:
3648
"""The Kafka record timestamp type."""
3749
return self["timestampType"]
3850

51+
@property
52+
def key_schema_metadata(self) -> KafkaEventRecordSchemaMetadata | None:
53+
"""The metadata of the Key Kafka record."""
54+
return (
55+
None if self.get("keySchemaMetadata") is None else KafkaEventRecordSchemaMetadata(self["keySchemaMetadata"])
56+
)
57+
58+
@property
59+
def value_schema_metadata(self) -> KafkaEventRecordSchemaMetadata | None:
60+
"""The metadata of the Value Kafka record."""
61+
return (
62+
None
63+
if self.get("valueSchemaMetadata") is None
64+
else KafkaEventRecordSchemaMetadata(self["valueSchemaMetadata"])
65+
)
66+
67+
68+
class KafkaEventRecord(KafkaEventRecordBase):
3969
@property
4070
def key(self) -> str | None:
4171
"""
@@ -83,18 +113,7 @@ def decoded_headers(self) -> dict[str, bytes]:
83113
return CaseInsensitiveDict((k, bytes(v)) for chunk in self.headers for k, v in chunk.items())
84114

85115

86-
class KafkaEvent(DictWrapper):
87-
"""Self-managed or MSK Apache Kafka event trigger
88-
Documentation:
89-
--------------
90-
- https://docs.aws.amazon.com/lambda/latest/dg/with-kafka.html
91-
- https://docs.aws.amazon.com/lambda/latest/dg/with-msk.html
92-
"""
93-
94-
def __init__(self, data: dict[str, Any]):
95-
super().__init__(data)
96-
self._records: Iterator[KafkaEventRecord] | None = None
97-
116+
class KafkaEventBase(DictWrapper):
98117
@property
99118
def event_source(self) -> str:
100119
"""The AWS service from which the Kafka event record originated."""
@@ -115,6 +134,19 @@ def decoded_bootstrap_servers(self) -> list[str]:
115134
"""The decoded Kafka bootstrap URL."""
116135
return self.bootstrap_servers.split(",")
117136

137+
138+
class KafkaEvent(KafkaEventBase):
139+
"""Self-managed or MSK Apache Kafka event trigger
140+
Documentation:
141+
--------------
142+
- https://docs.aws.amazon.com/lambda/latest/dg/with-kafka.html
143+
- https://docs.aws.amazon.com/lambda/latest/dg/with-msk.html
144+
"""
145+
146+
def __init__(self, data: dict[str, Any]):
147+
super().__init__(data)
148+
self._records: Iterator[KafkaEventRecord] | None = None
149+
118150
@property
119151
def records(self) -> Iterator[KafkaEventRecord]:
120152
"""The Kafka records."""
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from aws_lambda_powertools.utilities.kafka.consumer_records import ConsumerRecords
2+
from aws_lambda_powertools.utilities.kafka.kafka_consumer import kafka_consumer
3+
from aws_lambda_powertools.utilities.kafka.schema_config import SchemaConfig
4+
5+
__all__ = [
6+
"kafka_consumer",
7+
"ConsumerRecords",
8+
"SchemaConfig",
9+
]
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from functools import cached_property
5+
from typing import TYPE_CHECKING, Any
6+
7+
from aws_lambda_powertools.utilities.data_classes.common import CaseInsensitiveDict
8+
from aws_lambda_powertools.utilities.data_classes.kafka_event import KafkaEventBase, KafkaEventRecordBase
9+
from aws_lambda_powertools.utilities.kafka.deserializer.deserializer import get_deserializer
10+
from aws_lambda_powertools.utilities.kafka.serialization.serialization import serialize_to_output_type
11+
12+
if TYPE_CHECKING:
13+
from collections.abc import Iterator
14+
15+
from aws_lambda_powertools.utilities.kafka.schema_config import SchemaConfig
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
class ConsumerRecordRecords(KafkaEventRecordBase):
21+
"""
22+
A Kafka Consumer Record
23+
"""
24+
25+
def __init__(self, data: dict[str, Any], schema_config: SchemaConfig | None = None):
26+
super().__init__(data)
27+
self.schema_config = schema_config
28+
29+
@cached_property
30+
def key(self) -> Any:
31+
key = self.get("key")
32+
33+
# Return None if key doesn't exist
34+
if not key:
35+
return None
36+
37+
logger.debug("Deserializing key field")
38+
39+
# Determine schema type and schema string
40+
schema_type = None
41+
schema_value = None
42+
output_serializer = None
43+
44+
if self.schema_config and self.schema_config.key_schema_type:
45+
schema_type = self.schema_config.key_schema_type
46+
schema_value = self.schema_config.key_schema
47+
output_serializer = self.schema_config.key_output_serializer
48+
49+
# Always use get_deserializer if None it will default to DEFAULT
50+
deserializer = get_deserializer(
51+
schema_type=schema_type,
52+
schema_value=schema_value,
53+
field_metadata=self.key_schema_metadata,
54+
)
55+
deserialized_value = deserializer.deserialize(key)
56+
57+
# Apply output serializer if specified
58+
if output_serializer:
59+
return serialize_to_output_type(deserialized_value, output_serializer)
60+
61+
return deserialized_value
62+
63+
@cached_property
64+
def value(self) -> Any:
65+
value = self["value"]
66+
67+
# Determine schema type and schema string
68+
schema_type = None
69+
schema_value = None
70+
output_serializer = None
71+
72+
logger.debug("Deserializing value field")
73+
74+
if self.schema_config and self.schema_config.value_schema_type:
75+
schema_type = self.schema_config.value_schema_type
76+
schema_value = self.schema_config.value_schema
77+
output_serializer = self.schema_config.value_output_serializer
78+
79+
# Always use get_deserializer if None it will default to DEFAULT
80+
deserializer = get_deserializer(
81+
schema_type=schema_type,
82+
schema_value=schema_value,
83+
field_metadata=self.value_schema_metadata,
84+
)
85+
deserialized_value = deserializer.deserialize(value)
86+
87+
# Apply output serializer if specified
88+
if output_serializer:
89+
return serialize_to_output_type(deserialized_value, output_serializer)
90+
91+
return deserialized_value
92+
93+
@property
94+
def original_value(self) -> str:
95+
"""The original (base64 encoded) Kafka record value."""
96+
return self["value"]
97+
98+
@property
99+
def original_key(self) -> str | None:
100+
"""
101+
The original (base64 encoded) Kafka record key.
102+
103+
This key is optional; if not provided,
104+
a round-robin algorithm will be used to determine
105+
the partition for the message.
106+
"""
107+
108+
return self.get("key")
109+
110+
@property
111+
def original_headers(self) -> list[dict[str, list[int]]]:
112+
"""The raw Kafka record headers."""
113+
return self["headers"]
114+
115+
@cached_property
116+
def headers(self) -> dict[str, bytes]:
117+
"""Decodes the headers as a single dictionary."""
118+
return CaseInsensitiveDict((k, bytes(v)) for chunk in self.original_headers for k, v in chunk.items())
119+
120+
121+
class ConsumerRecords(KafkaEventBase):
122+
"""Self-managed or MSK Apache Kafka event trigger
123+
Documentation:
124+
--------------
125+
- https://docs.aws.amazon.com/lambda/latest/dg/with-kafka.html
126+
- https://docs.aws.amazon.com/lambda/latest/dg/with-msk.html
127+
"""
128+
129+
def __init__(self, data: dict[str, Any], schema_config: SchemaConfig | None = None):
130+
super().__init__(data)
131+
self._records: Iterator[ConsumerRecordRecords] | None = None
132+
self.schema_config = schema_config
133+
134+
@property
135+
def records(self) -> Iterator[ConsumerRecordRecords]:
136+
"""The Kafka records."""
137+
for chunk in self["records"].values():
138+
for record in chunk:
139+
yield ConsumerRecordRecords(data=record, schema_config=self.schema_config)
140+
141+
@property
142+
def record(self) -> ConsumerRecordRecords:
143+
"""
144+
Returns the next Kafka record using an iterator.
145+
146+
Returns
147+
-------
148+
ConsumerRecordRecords
149+
The next Kafka record.
150+
151+
Raises
152+
------
153+
StopIteration
154+
If there are no more records available.
155+
156+
"""
157+
if self._records is None:
158+
self._records = self.records
159+
return next(self._records)

aws_lambda_powertools/utilities/kafka/deserializer/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)