aws-lambda-powertools 3.0.1.dev3__py3-none-any.whl → 3.0.2.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. aws_lambda_powertools/event_handler/api_gateway.py +227 -54
  2. aws_lambda_powertools/event_handler/appsync.py +343 -97
  3. aws_lambda_powertools/event_handler/bedrock_agent.py +121 -0
  4. aws_lambda_powertools/event_handler/exceptions.py +1 -1
  5. aws_lambda_powertools/event_handler/graphql_appsync/__init__.py +0 -0
  6. aws_lambda_powertools/event_handler/graphql_appsync/_registry.py +78 -0
  7. aws_lambda_powertools/event_handler/graphql_appsync/base.py +160 -0
  8. aws_lambda_powertools/event_handler/graphql_appsync/exceptions.py +10 -0
  9. aws_lambda_powertools/event_handler/graphql_appsync/router.py +62 -0
  10. aws_lambda_powertools/event_handler/middlewares/openapi_validation.py +1 -1
  11. aws_lambda_powertools/event_handler/openapi/encoders.py +94 -70
  12. aws_lambda_powertools/event_handler/openapi/exceptions.py +12 -0
  13. aws_lambda_powertools/event_handler/openapi/models.py +38 -5
  14. aws_lambda_powertools/event_handler/openapi/swagger_ui/html.py +1 -4
  15. aws_lambda_powertools/event_handler/util.py +55 -2
  16. aws_lambda_powertools/logging/utils.py +5 -5
  17. aws_lambda_powertools/metrics/provider/cloudwatch_emf/metric_properties.py +1 -0
  18. aws_lambda_powertools/shared/cookies.py +2 -2
  19. aws_lambda_powertools/shared/dynamodb_deserializer.py +13 -0
  20. aws_lambda_powertools/shared/version.py +1 -1
  21. aws_lambda_powertools/tracing/tracer.py +4 -2
  22. aws_lambda_powertools/utilities/batch/base.py +13 -4
  23. aws_lambda_powertools/utilities/data_classes/cognito_user_pool_event.py +260 -1
  24. aws_lambda_powertools/utilities/data_classes/kafka_event.py +2 -2
  25. aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +7 -0
  26. aws_lambda_powertools/utilities/data_masking/base.py +4 -4
  27. aws_lambda_powertools/utilities/feature_flags/appconfig.py +21 -2
  28. aws_lambda_powertools/utilities/idempotency/idempotency.py +22 -2
  29. aws_lambda_powertools/utilities/idempotency/persistence/base.py +4 -1
  30. aws_lambda_powertools/utilities/parameters/secrets.py +1 -1
  31. aws_lambda_powertools/utilities/parser/models/__init__.py +6 -0
  32. aws_lambda_powertools/utilities/parser/models/apigw.py +12 -1
  33. aws_lambda_powertools/utilities/parser/models/apigwv2.py +7 -1
  34. aws_lambda_powertools/utilities/validation/base.py +27 -2
  35. aws_lambda_powertools/utilities/validation/validator.py +49 -4
  36. {aws_lambda_powertools-3.0.1.dev3.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/METADATA +12 -2
  37. {aws_lambda_powertools-3.0.1.dev3.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/RECORD +39 -34
  38. {aws_lambda_powertools-3.0.1.dev3.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/LICENSE +0 -0
  39. {aws_lambda_powertools-3.0.1.dev3.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/WHEEL +0 -0
@@ -19,6 +19,8 @@ from aws_lambda_powertools.event_handler.openapi.compat import _model_dump
19
19
  if TYPE_CHECKING:
20
20
  from aws_lambda_powertools.event_handler.openapi.types import IncEx
21
21
 
22
+ from aws_lambda_powertools.event_handler.openapi.exceptions import SerializationError
23
+
22
24
  """
23
25
  This module contains the encoders used by jsonable_encoder to convert Python objects to JSON serializable data types.
24
26
  """
@@ -72,88 +74,98 @@ def jsonable_encoder( # noqa: PLR0911
72
74
  if exclude is not None and not isinstance(exclude, (set, dict)):
73
75
  exclude = set(exclude)
74
76
 
75
- # Pydantic models
76
- if isinstance(obj, BaseModel):
77
- return _dump_base_model(
78
- obj=obj,
79
- include=include,
80
- exclude=exclude,
81
- by_alias=by_alias,
82
- exclude_unset=exclude_unset,
83
- exclude_none=exclude_none,
84
- exclude_defaults=exclude_defaults,
85
- )
77
+ try:
78
+ # Pydantic models
79
+ if isinstance(obj, BaseModel):
80
+ return _dump_base_model(
81
+ obj=obj,
82
+ include=include,
83
+ exclude=exclude,
84
+ by_alias=by_alias,
85
+ exclude_unset=exclude_unset,
86
+ exclude_none=exclude_none,
87
+ exclude_defaults=exclude_defaults,
88
+ )
86
89
 
87
- # Dataclasses
88
- if dataclasses.is_dataclass(obj):
89
- obj_dict = dataclasses.asdict(obj)
90
- return jsonable_encoder(
91
- obj_dict,
92
- include=include,
93
- exclude=exclude,
94
- by_alias=by_alias,
95
- exclude_unset=exclude_unset,
96
- exclude_defaults=exclude_defaults,
97
- exclude_none=exclude_none,
98
- )
90
+ # Dataclasses
91
+ if dataclasses.is_dataclass(obj):
92
+ obj_dict = dataclasses.asdict(obj) # type: ignore[call-overload]
93
+ return jsonable_encoder(
94
+ obj_dict,
95
+ include=include,
96
+ exclude=exclude,
97
+ by_alias=by_alias,
98
+ exclude_unset=exclude_unset,
99
+ exclude_defaults=exclude_defaults,
100
+ exclude_none=exclude_none,
101
+ custom_serializer=custom_serializer,
102
+ )
99
103
 
100
- # Enums
101
- if isinstance(obj, Enum):
102
- return obj.value
104
+ # Enums
105
+ if isinstance(obj, Enum):
106
+ return obj.value
103
107
 
104
- # Paths
105
- if isinstance(obj, PurePath):
106
- return str(obj)
108
+ # Paths
109
+ if isinstance(obj, PurePath):
110
+ return str(obj)
107
111
 
108
- # Scalars
109
- if isinstance(obj, (str, int, float, type(None))):
110
- return obj
112
+ # Scalars
113
+ if isinstance(obj, (str, int, float, type(None))):
114
+ return obj
111
115
 
112
- # Dictionaries
113
- if isinstance(obj, dict):
114
- return _dump_dict(
115
- obj=obj,
116
- include=include,
117
- exclude=exclude,
118
- by_alias=by_alias,
119
- exclude_none=exclude_none,
120
- exclude_unset=exclude_unset,
121
- )
116
+ # Dictionaries
117
+ if isinstance(obj, dict):
118
+ return _dump_dict(
119
+ obj=obj,
120
+ include=include,
121
+ exclude=exclude,
122
+ by_alias=by_alias,
123
+ exclude_unset=exclude_unset,
124
+ exclude_none=exclude_none,
125
+ custom_serializer=custom_serializer,
126
+ )
122
127
 
123
- # Sequences
124
- if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)):
125
- return _dump_sequence(
128
+ # Sequences
129
+ if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)):
130
+ return _dump_sequence(
131
+ obj=obj,
132
+ include=include,
133
+ exclude=exclude,
134
+ by_alias=by_alias,
135
+ exclude_none=exclude_none,
136
+ exclude_defaults=exclude_defaults,
137
+ exclude_unset=exclude_unset,
138
+ custom_serializer=custom_serializer,
139
+ )
140
+
141
+ # Other types
142
+ if type(obj) in ENCODERS_BY_TYPE:
143
+ return ENCODERS_BY_TYPE[type(obj)](obj)
144
+
145
+ for encoder, classes_tuple in encoders_by_class_tuples.items():
146
+ if isinstance(obj, classes_tuple):
147
+ return encoder(obj)
148
+
149
+ # Use custom serializer if present
150
+ if custom_serializer:
151
+ return custom_serializer(obj)
152
+
153
+ # Default
154
+ return _dump_other(
126
155
  obj=obj,
127
156
  include=include,
128
157
  exclude=exclude,
129
158
  by_alias=by_alias,
130
159
  exclude_none=exclude_none,
131
- exclude_defaults=exclude_defaults,
132
160
  exclude_unset=exclude_unset,
161
+ exclude_defaults=exclude_defaults,
162
+ custom_serializer=custom_serializer,
133
163
  )
134
-
135
- # Other types
136
- if type(obj) in ENCODERS_BY_TYPE:
137
- return ENCODERS_BY_TYPE[type(obj)](obj)
138
-
139
- for encoder, classes_tuple in encoders_by_class_tuples.items():
140
- if isinstance(obj, classes_tuple):
141
- return encoder(obj)
142
-
143
- # Use custom serializer if present
144
- if custom_serializer:
145
- return custom_serializer(obj)
146
-
147
- # Default
148
- return _dump_other(
149
- obj=obj,
150
- include=include,
151
- exclude=exclude,
152
- by_alias=by_alias,
153
- exclude_none=exclude_none,
154
- exclude_unset=exclude_unset,
155
- exclude_defaults=exclude_defaults,
156
- )
164
+ except ValueError as exc:
165
+ raise SerializationError(
166
+ f"Unable to serialize the object {obj} as it is not a supported type. Error details: {exc}",
167
+ "See: https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#serializing-objects",
168
+ ) from exc
157
169
 
158
170
 
159
171
  def _dump_base_model(
@@ -197,9 +209,15 @@ def _dump_dict(
197
209
  by_alias: bool = True,
198
210
  exclude_unset: bool = False,
199
211
  exclude_none: bool = False,
212
+ custom_serializer: Callable[[Any], str] | None = None,
200
213
  ) -> dict[str, Any]:
201
214
  """
202
215
  Dump a dict to a dict, using the same parameters as jsonable_encoder
216
+
217
+ Parameters
218
+ ----------
219
+ custom_serializer : Callable, optional
220
+ A custom serializer to use for encoding the object, when everything else fails.
203
221
  """
204
222
  encoded_dict = {}
205
223
  allowed_keys = set(obj.keys())
@@ -218,12 +236,14 @@ def _dump_dict(
218
236
  by_alias=by_alias,
219
237
  exclude_unset=exclude_unset,
220
238
  exclude_none=exclude_none,
239
+ custom_serializer=custom_serializer,
221
240
  )
222
241
  encoded_value = jsonable_encoder(
223
242
  value,
224
243
  by_alias=by_alias,
225
244
  exclude_unset=exclude_unset,
226
245
  exclude_none=exclude_none,
246
+ custom_serializer=custom_serializer,
227
247
  )
228
248
  encoded_dict[encoded_key] = encoded_value
229
249
  return encoded_dict
@@ -238,9 +258,10 @@ def _dump_sequence(
238
258
  exclude_unset: bool = False,
239
259
  exclude_none: bool = False,
240
260
  exclude_defaults: bool = False,
261
+ custom_serializer: Callable[[Any], str] | None = None,
241
262
  ) -> list[Any]:
242
263
  """
243
- Dump a sequence to a list, using the same parameters as jsonable_encoder
264
+ Dump a sequence to a list, using the same parameters as jsonable_encoder.
244
265
  """
245
266
  encoded_list = []
246
267
  for item in obj:
@@ -253,6 +274,7 @@ def _dump_sequence(
253
274
  exclude_unset=exclude_unset,
254
275
  exclude_defaults=exclude_defaults,
255
276
  exclude_none=exclude_none,
277
+ custom_serializer=custom_serializer,
256
278
  ),
257
279
  )
258
280
  return encoded_list
@@ -267,6 +289,7 @@ def _dump_other(
267
289
  exclude_unset: bool = False,
268
290
  exclude_none: bool = False,
269
291
  exclude_defaults: bool = False,
292
+ custom_serializer: Callable[[Any], str] | None = None,
270
293
  ) -> Any:
271
294
  """
272
295
  Dump an object to a hashable object, using the same parameters as jsonable_encoder
@@ -288,6 +311,7 @@ def _dump_other(
288
311
  exclude_unset=exclude_unset,
289
312
  exclude_defaults=exclude_defaults,
290
313
  exclude_none=exclude_none,
314
+ custom_serializer=custom_serializer,
291
315
  )
292
316
 
293
317
 
@@ -21,3 +21,15 @@ class RequestValidationError(ValidationException):
21
21
  def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
22
22
  super().__init__(errors)
23
23
  self.body = body
24
+
25
+
26
+ class SerializationError(Exception):
27
+ """
28
+ Base exception for all encoding errors
29
+ """
30
+
31
+
32
+ class SchemaValidationError(ValidationException):
33
+ """
34
+ Raised when the OpenAPI schema validation fails
35
+ """
@@ -2,10 +2,11 @@
2
2
  from enum import Enum
3
3
  from typing import Any, Dict, List, Literal, Optional, Set, Union
4
4
 
5
- from pydantic import AnyUrl, BaseModel, ConfigDict, Field
5
+ from pydantic import AnyUrl, BaseModel, ConfigDict, Field, model_validator
6
6
  from typing_extensions import Annotated
7
7
 
8
8
  from aws_lambda_powertools.event_handler.openapi.compat import model_rebuild
9
+ from aws_lambda_powertools.event_handler.openapi.exceptions import SchemaValidationError
9
10
 
10
11
  MODEL_CONFIG_ALLOW = ConfigDict(extra="allow")
11
12
  MODEL_CONFIG_IGNORE = ConfigDict(extra="ignore")
@@ -16,6 +17,38 @@ These models can be used to parse OpenAPI JSON/YAML files into Python objects, o
16
17
  """
17
18
 
18
19
 
20
+ class OpenAPIExtensions(BaseModel):
21
+ """
22
+ This class serves as a Pydantic proxy model to add OpenAPI extensions.
23
+
24
+ OpenAPI extensions are arbitrary fields, so we remove openapi_extensions when dumping
25
+ and add only the provided value in the schema.
26
+ """
27
+
28
+ openapi_extensions: Optional[Dict[str, Any]] = None
29
+
30
+ # If the 'openapi_extensions' field is present in the 'values' dictionary,
31
+ # And if the extension starts with x- (must respect the RFC)
32
+ # update the 'values' dictionary with the contents of 'openapi_extensions',
33
+ # and then remove the 'openapi_extensions' field from the 'values' dictionary
34
+ model_config = {"extra": "allow"}
35
+
36
+ @model_validator(mode="before")
37
+ def serialize_openapi_extension_v2(self):
38
+ if isinstance(self, dict) and self.get("openapi_extensions"):
39
+
40
+ openapi_extension_value = self.get("openapi_extensions")
41
+
42
+ for extension_key in openapi_extension_value:
43
+ if not str(extension_key).startswith("x-"):
44
+ raise SchemaValidationError("An OpenAPI extension key must start with x-")
45
+
46
+ self.update(openapi_extension_value)
47
+ self.pop("openapi_extensions", None)
48
+
49
+ return self
50
+
51
+
19
52
  # https://swagger.io/specification/#contact-object
20
53
  class Contact(BaseModel):
21
54
  name: Optional[str] = None
@@ -57,7 +90,7 @@ class ServerVariable(BaseModel):
57
90
 
58
91
 
59
92
  # https://swagger.io/specification/#server-object
60
- class Server(BaseModel):
93
+ class Server(OpenAPIExtensions):
61
94
  url: Union[AnyUrl, str]
62
95
  description: Optional[str] = None
63
96
  variables: Optional[Dict[str, ServerVariable]] = None
@@ -287,7 +320,7 @@ class Tag(BaseModel):
287
320
 
288
321
 
289
322
  # https://swagger.io/specification/#operation-object
290
- class Operation(BaseModel):
323
+ class Operation(OpenAPIExtensions):
291
324
  tags: Optional[List[str]] = None
292
325
  summary: Optional[str] = None
293
326
  description: Optional[str] = None
@@ -332,7 +365,7 @@ class SecuritySchemeType(Enum):
332
365
  openIdConnect = "openIdConnect"
333
366
 
334
367
 
335
- class SecurityBase(BaseModel):
368
+ class SecurityBase(OpenAPIExtensions):
336
369
  type_: SecuritySchemeType = Field(alias="type")
337
370
  description: Optional[str] = None
338
371
 
@@ -428,7 +461,7 @@ class Components(BaseModel):
428
461
 
429
462
 
430
463
  # https://swagger.io/specification/#openapi-object
431
- class OpenAPI(BaseModel):
464
+ class OpenAPI(OpenAPIExtensions):
432
465
  openapi: str
433
466
  info: Info
434
467
  jsonSchemaDialect: Optional[str] = None
@@ -8,7 +8,6 @@ if TYPE_CHECKING:
8
8
 
9
9
  def generate_swagger_html(
10
10
  spec: str,
11
- path: str,
12
11
  swagger_js: str,
13
12
  swagger_css: str,
14
13
  swagger_base_url: str,
@@ -22,8 +21,6 @@ def generate_swagger_html(
22
21
  ----------
23
22
  spec: str
24
23
  The OpenAPI spec
25
- path: str
26
- The path to the Swagger documentation
27
24
  swagger_js: str
28
25
  Swagger UI JavaScript source code or URL
29
26
  swagger_css: str
@@ -97,7 +94,7 @@ def generate_swagger_html(
97
94
  }}
98
95
 
99
96
  var ui = SwaggerUIBundle(swaggerUIOptions)
100
- ui.specActions.updateUrl('{path}?format=json');
97
+ ui.specActions.updateUrl(currentUrl.pathname + "?format=json");
101
98
  {oauth2_content}
102
99
  </script>
103
100
  </html>
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Mapping
3
+ from typing import Any, Dict, List, Mapping
4
4
 
5
5
 
6
6
  class _FrozenDict(dict):
@@ -18,7 +18,31 @@ class _FrozenDict(dict):
18
18
  return hash(frozenset(self.keys()))
19
19
 
20
20
 
21
- def extract_origin_header(resolved_headers: Mapping[str, Any]) -> str | None:
21
+ class _FrozenListDict(List[Dict[str, List[str]]]):
22
+ """
23
+ Freezes a list of dictionaries containing lists of strings.
24
+
25
+ This function takes a list of dictionaries where the values are lists of strings and converts it into
26
+ a frozen set of frozen sets of frozen dictionaries. This is done by iterating over the input list,
27
+ converting each dictionary's values (lists of strings) into frozen sets of strings, and then
28
+ converting the resulting dictionary into a frozen dictionary. Finally, all these frozen dictionaries
29
+ are collected into a frozen set of frozen sets.
30
+
31
+ This operation is useful when you want to ensure the immutability of the data structure and make it
32
+ hashable, which is required for certain operations like using it as a key in a dictionary or as an
33
+ element in a set.
34
+
35
+ Example: [{"TestAuth": ["test", "test1"]}]
36
+ """
37
+
38
+ def __hash__(self):
39
+ hashable_items = []
40
+ for item in self:
41
+ hashable_items.extend((key, frozenset(value)) for key, value in item.items())
42
+ return hash(frozenset(hashable_items))
43
+
44
+
45
+ def extract_origin_header(resolved_headers: Mapping[str, Any]):
22
46
  """
23
47
  Extracts the 'origin' or 'Origin' header from the provided resolver headers.
24
48
 
@@ -34,3 +58,32 @@ def extract_origin_header(resolved_headers: Mapping[str, Any]) -> str | None:
34
58
  if isinstance(resolved_header, list):
35
59
  return resolved_header[0]
36
60
  return resolved_header
61
+
62
+
63
+ def _validate_openapi_security_parameters(
64
+ security: list[dict[str, list[str]]],
65
+ security_schemes: dict[str, Any] | None = None,
66
+ ) -> bool:
67
+ """
68
+ This function checks if all security requirements listed in the 'security'
69
+ parameter are defined in the 'security_schemes' dictionary, as specified
70
+ in the OpenAPI schema.
71
+
72
+ Parameters
73
+ ----------
74
+ security: List[Dict[str, List[str]]]
75
+ A list of security requirements
76
+ security_schemes: Optional[Dict[str, Any]]
77
+ A dictionary mapping security scheme names to their corresponding security scheme objects.
78
+
79
+ Returns
80
+ -------
81
+ bool
82
+ Whether list of security schemes match allowed security_schemes.
83
+ """
84
+
85
+ security_schemes = security_schemes or {}
86
+
87
+ security_schema_match = all(key in security_schemes for sec in security for key in sec)
88
+
89
+ return bool(security_schema_match and security_schemes)
@@ -7,6 +7,7 @@ if TYPE_CHECKING:
7
7
  from aws_lambda_powertools.logging.logger import Logger
8
8
 
9
9
  PACKAGE_LOGGER = "aws_lambda_powertools"
10
+ LOGGER = logging.getLogger(__name__)
10
11
 
11
12
 
12
13
  def copy_config_to_registered_loggers(
@@ -59,7 +60,7 @@ def copy_config_to_registered_loggers(
59
60
  loggers = exclude
60
61
  filter_func = _exclude_registered_loggers_filter
61
62
 
62
- registered_loggers = _find_registered_loggers(source_logger, loggers, filter_func)
63
+ registered_loggers = _find_registered_loggers(loggers=loggers, filter_func=filter_func)
63
64
  for logger in registered_loggers:
64
65
  _configure_logger(source_logger=source_logger, logger=logger, level=level, ignore_log_level=ignore_log_level)
65
66
 
@@ -75,13 +76,12 @@ def _exclude_registered_loggers_filter(loggers: set[str]) -> list[logging.Logger
75
76
 
76
77
 
77
78
  def _find_registered_loggers(
78
- source_logger: Logger,
79
79
  loggers: set[str],
80
80
  filter_func: Callable[[set[str]], list[logging.Logger]],
81
81
  ) -> list[logging.Logger]:
82
82
  """Filter root loggers based on provided parameters."""
83
83
  root_loggers = filter_func(loggers)
84
- source_logger.debug(f"Filtered root loggers: {root_loggers}")
84
+ LOGGER.debug(f"Filtered root loggers: {root_loggers}")
85
85
  return root_loggers
86
86
 
87
87
 
@@ -94,7 +94,7 @@ def _configure_logger(
94
94
  # customers may not want to copy the same log level from Logger to discovered loggers
95
95
  if not ignore_log_level:
96
96
  logger.setLevel(level)
97
- source_logger.debug(f"Logger {logger} reconfigured to use logging level {level}")
97
+ LOGGER.debug(f"Logger {logger} reconfigured to use logging level {level}")
98
98
 
99
99
  logger.handlers = []
100
100
  logger.propagate = False # ensure we don't propagate logs to existing loggers, #1073
@@ -102,4 +102,4 @@ def _configure_logger(
102
102
 
103
103
  for source_handler in source_logger.handlers:
104
104
  logger.addHandler(source_handler)
105
- source_logger.debug(f"Logger {logger} reconfigured to use {source_handler}")
105
+ LOGGER.debug(f"Logger {logger} reconfigured to use {source_handler}")
@@ -30,6 +30,7 @@ class MetricUnit(Enum):
30
30
  GigabitsPerSecond = "Gigabits/Second"
31
31
  TerabitsPerSecond = "Terabits/Second"
32
32
  CountPerSecond = "Count/Second"
33
+ NoUnit = "None"
33
34
 
34
35
 
35
36
  class MetricResolution(Enum):
@@ -101,10 +101,10 @@ class Cookie:
101
101
 
102
102
  if self.max_age:
103
103
  if self.max_age > 0:
104
- payload.write(f"; MaxAge={self.max_age}")
104
+ payload.write(f"; Max-Age={self.max_age}")
105
105
  else:
106
106
  # negative or zero max-age should be set to 0
107
- payload.write("; MaxAge=0")
107
+ payload.write("; Max-Age=0")
108
108
 
109
109
  if self.http_only:
110
110
  payload.write("; HttpOnly")
@@ -72,6 +72,19 @@ class TypeDeserializer:
72
72
  return value
73
73
 
74
74
  def _deserialize_n(self, value: str) -> Decimal:
75
+ # value is None or "."? It's zero
76
+ # then return early
77
+ value = value.lstrip("0")
78
+ if not value or value == ".":
79
+ return DYNAMODB_CONTEXT.create_decimal(0)
80
+
81
+ if len(value) > 38:
82
+ # See: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Number
83
+ # Calculate the number of trailing zeros after the 38th character
84
+ tail = len(value[38:]) - len(value[38:].rstrip("0"))
85
+ # Trim the value: remove trailing zeros if any, or just take the first 38 characters
86
+ value = value[:-tail] if tail > 0 else value[:38]
87
+
75
88
  return DYNAMODB_CONTEXT.create_decimal(value)
76
89
 
77
90
  def _deserialize_s(self, value: str) -> str:
@@ -1,3 +1,3 @@
1
1
  """Exposes version constant to avoid circular dependencies."""
2
2
 
3
- VERSION = "3.0.1.dev3"
3
+ VERSION = "3.0.2.dev1"
@@ -6,7 +6,7 @@ import functools
6
6
  import inspect
7
7
  import logging
8
8
  import os
9
- from typing import TYPE_CHECKING, Any, Callable, Sequence, cast, overload
9
+ from typing import TYPE_CHECKING, Any, Callable, Sequence, TypeVar, cast, overload
10
10
 
11
11
  from aws_lambda_powertools.shared import constants
12
12
  from aws_lambda_powertools.shared.functions import (
@@ -27,6 +27,8 @@ logger = logging.getLogger(__name__)
27
27
 
28
28
  aws_xray_sdk = LazyLoader(constants.XRAY_SDK_MODULE, globals(), constants.XRAY_SDK_MODULE)
29
29
 
30
+ T = TypeVar("T")
31
+
30
32
 
31
33
  class Tracer:
32
34
  """Tracer using AWS-XRay to provide decorators with known defaults for Lambda functions
@@ -255,7 +257,7 @@ class Tracer:
255
257
 
256
258
  def capture_lambda_handler(
257
259
  self,
258
- lambda_handler: Callable[[dict, Any], Any] | Callable[[dict, Any, dict | None], Any] | None = None,
260
+ lambda_handler: Callable[[T, Any], Any] | Callable[[T, Any, Any], Any] | None = None,
259
261
  capture_response: bool | None = None,
260
262
  capture_error: bool | None = None,
261
263
  ):
@@ -229,7 +229,12 @@ class BasePartialProcessor(ABC):
229
229
  class BasePartialBatchProcessor(BasePartialProcessor): # noqa
230
230
  DEFAULT_RESPONSE: PartialItemFailureResponse = {"batchItemFailures": []}
231
231
 
232
- def __init__(self, event_type: EventType, model: BatchTypeModels | None = None):
232
+ def __init__(
233
+ self,
234
+ event_type: EventType,
235
+ model: BatchTypeModels | None = None,
236
+ raise_on_entire_batch_failure: bool = True,
237
+ ):
233
238
  """Process batch and partially report failed items
234
239
 
235
240
  Parameters
@@ -238,6 +243,9 @@ class BasePartialBatchProcessor(BasePartialProcessor): # noqa
238
243
  Whether this is a SQS, DynamoDB Streams, or Kinesis Data Stream event
239
244
  model: BatchTypeModels | None
240
245
  Parser's data model using either SqsRecordModel, DynamoDBStreamRecordModel, KinesisDataStreamRecord
246
+ raise_on_entire_batch_failure: bool
247
+ Raise an exception when the entire batch has failed processing.
248
+ When set to False, partial failures are reported in the response
241
249
 
242
250
  Exceptions
243
251
  ----------
@@ -246,6 +254,7 @@ class BasePartialBatchProcessor(BasePartialProcessor): # noqa
246
254
  """
247
255
  self.event_type = event_type
248
256
  self.model = model
257
+ self.raise_on_entire_batch_failure = raise_on_entire_batch_failure
249
258
  self.batch_response: PartialItemFailureResponse = copy.deepcopy(self.DEFAULT_RESPONSE)
250
259
  self._COLLECTOR_MAPPING = {
251
260
  EventType.SQS: self._collect_sqs_failures,
@@ -281,7 +290,7 @@ class BasePartialBatchProcessor(BasePartialProcessor): # noqa
281
290
  if not self._has_messages_to_report():
282
291
  return
283
292
 
284
- if self._entire_batch_failed():
293
+ if self._entire_batch_failed() and self.raise_on_entire_batch_failure:
285
294
  raise BatchProcessingError(
286
295
  msg=f"All records failed processing. {len(self.exceptions)} individual errors logged "
287
296
  f"separately below.",
@@ -478,7 +487,7 @@ class BatchProcessor(BasePartialBatchProcessor): # Keep old name for compatibil
478
487
  Raises
479
488
  ------
480
489
  BatchProcessingError
481
- When all batch records fail processing
490
+ When all batch records fail processing and raise_on_entire_batch_failure is True
482
491
 
483
492
  Limitations
484
493
  -----------
@@ -627,7 +636,7 @@ class AsyncBatchProcessor(BasePartialBatchProcessor):
627
636
  Raises
628
637
  ------
629
638
  BatchProcessingError
630
- When all batch records fail processing
639
+ When all batch records fail processing and raise_on_entire_batch_failure is True
631
640
 
632
641
  Limitations
633
642
  -----------