aws-lambda-powertools 3.0.1.dev2__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.dev2.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/METADATA +12 -2
  37. {aws_lambda_powertools-3.0.1.dev2.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/RECORD +39 -34
  38. {aws_lambda_powertools-3.0.1.dev2.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/LICENSE +0 -0
  39. {aws_lambda_powertools-3.0.1.dev2.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/WHEEL +0 -0
@@ -241,6 +241,18 @@ class UserMigrationTriggerEventResponse(DictWrapper):
241
241
  """
242
242
  self["response"]["forceAliasCreation"] = value
243
243
 
244
+ @property
245
+ def enable_sms_mfa(self) -> bool | None:
246
+ return self["response"].get("enableSMSMFA")
247
+
248
+ @enable_sms_mfa.setter
249
+ def enable_sms_mfa(self, value: bool):
250
+ """Set this parameter to "true" to require that your migrated user complete SMS text message multi-factor
251
+ authentication (MFA) to sign in. Your user pool must have MFA enabled. Your user's attributes
252
+ in the request parameters must include a phone number, or else the migration of that user will fail.
253
+ """
254
+ self["response"]["enableSMSMFA"] = value
255
+
244
256
 
245
257
  class UserMigrationTriggerEvent(BaseTriggerEvent):
246
258
  """Migrate User Lambda Trigger
@@ -272,6 +284,11 @@ class CustomMessageTriggerEventRequest(DictWrapper):
272
284
  """A string for you to use as the placeholder for the verification code in the custom message."""
273
285
  return self["request"]["codeParameter"]
274
286
 
287
+ @property
288
+ def link_parameter(self) -> str:
289
+ """A string for you to use as a placeholder for the verification link in the custom message."""
290
+ return self["request"]["linkParameter"]
291
+
275
292
  @property
276
293
  def username_parameter(self) -> str:
277
294
  """The username parameter. It is a required request parameter for the admin create user flow."""
@@ -459,7 +476,7 @@ class PreTokenGenerationTriggerEventRequest(DictWrapper):
459
476
  @property
460
477
  def user_attributes(self) -> dict[str, str]:
461
478
  """One or more name-value pairs representing user attributes."""
462
- return self["request"]["userAttributes"]
479
+ return self["request"].get("userAttributes") or {}
463
480
 
464
481
  @property
465
482
  def client_metadata(self) -> dict[str, str]:
@@ -468,6 +485,16 @@ class PreTokenGenerationTriggerEventRequest(DictWrapper):
468
485
  return self["request"].get("clientMetadata") or {}
469
486
 
470
487
 
488
+ class PreTokenGenerationTriggerV2EventRequest(PreTokenGenerationTriggerEventRequest):
489
+ @property
490
+ def scopes(self) -> list[str]:
491
+ """Your user's OAuth 2.0 scopes. The scopes that are present in an access token are
492
+ the user pool standard and custom scopes that your user requested,
493
+ and that you authorized your app client to issue.
494
+ """
495
+ return self["request"].get("scopes")
496
+
497
+
471
498
  class ClaimsOverrideDetails(DictWrapper):
472
499
  @property
473
500
  def claims_to_add_or_override(self) -> dict[str, str]:
@@ -522,6 +549,123 @@ class ClaimsOverrideDetails(DictWrapper):
522
549
  self["groupOverrideDetails"]["preferredRole"] = value
523
550
 
524
551
 
552
+ class TokenClaimsAndScopeOverrideDetails(DictWrapper):
553
+ @property
554
+ def claims_to_add_or_override(self) -> dict[str, str]:
555
+ return self.get("claimsToAddOrOverride") or {}
556
+
557
+ @claims_to_add_or_override.setter
558
+ def claims_to_add_or_override(self, value: dict[str, str]):
559
+ """A map of one or more key-value pairs of claims to add or override.
560
+ For group related claims, use groupOverrideDetails instead."""
561
+ self._data["claimsToAddOrOverride"] = value
562
+
563
+ @property
564
+ def claims_to_suppress(self) -> list[str]:
565
+ return self.get("claimsToSuppress") or []
566
+
567
+ @claims_to_suppress.setter
568
+ def claims_to_suppress(self, value: list[str]):
569
+ """A list that contains claims to be suppressed from the identity token."""
570
+ self._data["claimsToSuppress"] = value
571
+
572
+ @property
573
+ def scopes_to_add(self) -> list[str]:
574
+ return self.get("scopesToAdd") or []
575
+
576
+ @scopes_to_add.setter
577
+ def scopes_to_add(self, value: list[str]):
578
+ self._data["scopesToAdd"] = value
579
+
580
+ @property
581
+ def scopes_to_suppress(self) -> list[str]:
582
+ return self.get("scopesToSuppress") or []
583
+
584
+ @scopes_to_suppress.setter
585
+ def scopes_to_suppress(self, value: list[str]):
586
+ self._data["scopesToSuppress"] = value
587
+
588
+
589
+ class ClaimsAndScopeOverrideDetails(DictWrapper):
590
+
591
+ @property
592
+ def id_token_generation(self) -> TokenClaimsAndScopeOverrideDetails | None:
593
+ id_token_generation_details = self._data.get("idTokenGeneration")
594
+ return (
595
+ None
596
+ if id_token_generation_details is None
597
+ else TokenClaimsAndScopeOverrideDetails(id_token_generation_details)
598
+ )
599
+
600
+ @id_token_generation.setter
601
+ def id_token_generation(self, value: dict[str, Any]):
602
+ """The output object containing the current id token's claims and scope configuration.
603
+
604
+ It includes claimsToAddOrOverride, claimsToSuppress, scopesToAdd and scopesToSupprress.
605
+
606
+ The tokenClaimsAndScopeOverrideDetails object is replaced with the one you provide.
607
+ If you provide an empty or null object in the response, then the groups are suppressed.
608
+ To leave the existing group configuration as is, copy the value of the token's object
609
+ to the tokenClaimsAndScopeOverrideDetails object in the response, and pass it back to the service.
610
+ """
611
+ self._data["idTokenGeneration"] = value
612
+
613
+ @property
614
+ def access_token_generation(self) -> TokenClaimsAndScopeOverrideDetails | None:
615
+ access_token_generation_details = self._data.get("accessTokenGeneration")
616
+ return (
617
+ None
618
+ if access_token_generation_details is None
619
+ else TokenClaimsAndScopeOverrideDetails(access_token_generation_details)
620
+ )
621
+
622
+ @access_token_generation.setter
623
+ def access_token_generation(self, value: dict[str, Any]):
624
+ """The output object containing the current access token's claims and scope configuration.
625
+
626
+ It includes claimsToAddOrOverride, claimsToSuppress, scopesToAdd and scopesToSupprress.
627
+
628
+ The tokenClaimsAndScopeOverrideDetails object is replaced with the one you provide.
629
+ If you provide an empty or null object in the response, then the groups are suppressed.
630
+ To leave the existing group configuration as is, copy the value of the token's object to
631
+ the tokenClaimsAndScopeOverrideDetails object in the response, and pass it back to the service.
632
+ """
633
+ self._data["accessTokenGeneration"] = value
634
+
635
+ @property
636
+ def group_configuration(self) -> GroupOverrideDetails | None:
637
+ group_override_details = self.get("groupOverrideDetails")
638
+ return None if group_override_details is None else GroupOverrideDetails(group_override_details)
639
+
640
+ @group_configuration.setter
641
+ def group_configuration(self, value: dict[str, Any]):
642
+ """The output object containing the current group configuration.
643
+
644
+ It includes groupsToOverride, iamRolesToOverride, and preferredRole.
645
+
646
+ The groupOverrideDetails object is replaced with the one you provide. If you provide an empty or null
647
+ object in the response, then the groups are suppressed. To leave the existing group configuration
648
+ as is, copy the value of the request's groupConfiguration object to the groupOverrideDetails object
649
+ in the response, and pass it back to the service.
650
+ """
651
+ self._data["groupOverrideDetails"] = value
652
+
653
+ def set_group_configuration_groups_to_override(self, value: list[str]):
654
+ """A list of the group names that are associated with the user that the identity token is issued for."""
655
+ self._data.setdefault("groupOverrideDetails", {})
656
+ self["groupOverrideDetails"]["groupsToOverride"] = value
657
+
658
+ def set_group_configuration_iam_roles_to_override(self, value: list[str]):
659
+ """A list of the current IAM roles associated with these groups."""
660
+ self._data.setdefault("groupOverrideDetails", {})
661
+ self["groupOverrideDetails"]["iamRolesToOverride"] = value
662
+
663
+ def set_group_configuration_preferred_role(self, value: str):
664
+ """A string indicating the preferred IAM role."""
665
+ self._data.setdefault("groupOverrideDetails", {})
666
+ self["groupOverrideDetails"]["preferredRole"] = value
667
+
668
+
525
669
  class PreTokenGenerationTriggerEventResponse(DictWrapper):
526
670
  @property
527
671
  def claims_override_details(self) -> ClaimsOverrideDetails:
@@ -531,6 +675,15 @@ class PreTokenGenerationTriggerEventResponse(DictWrapper):
531
675
  return ClaimsOverrideDetails(self._data["response"]["claimsOverrideDetails"])
532
676
 
533
677
 
678
+ class PreTokenGenerationTriggerV2EventResponse(DictWrapper):
679
+ @property
680
+ def claims_scope_override_details(self) -> ClaimsAndScopeOverrideDetails:
681
+ # Ensure we have a `claimsAndScopeOverrideDetails` element and is not set to None
682
+ if self._data["response"].get("claimsAndScopeOverrideDetails") is None:
683
+ self._data["response"]["claimsAndScopeOverrideDetails"] = {}
684
+ return ClaimsAndScopeOverrideDetails(self._data["response"]["claimsAndScopeOverrideDetails"])
685
+
686
+
534
687
  class PreTokenGenerationTriggerEvent(BaseTriggerEvent):
535
688
  """Pre Token Generation Lambda Trigger
536
689
 
@@ -563,6 +716,38 @@ class PreTokenGenerationTriggerEvent(BaseTriggerEvent):
563
716
  return PreTokenGenerationTriggerEventResponse(self._data)
564
717
 
565
718
 
719
+ class PreTokenGenerationV2TriggerEvent(BaseTriggerEvent):
720
+ """Pre Token Generation Lambda Trigger for the V2 Event
721
+
722
+ Amazon Cognito invokes this trigger before token generation allowing you to customize identity token claims.
723
+
724
+ Notes:
725
+ ----
726
+ `triggerSource` can be one of the following:
727
+
728
+ - `TokenGeneration_HostedAuth` Called during authentication from the Amazon Cognito hosted UI sign-in page.
729
+ - `TokenGeneration_Authentication` Called after user authentication flows have completed.
730
+ - `TokenGeneration_NewPasswordChallenge` Called after the user is created by an admin. This flow is invoked
731
+ when the user has to change a temporary password.
732
+ - `TokenGeneration_AuthenticateDevice` Called at the end of the authentication of a user device.
733
+ - `TokenGeneration_RefreshTokens` Called when a user tries to refresh the identity and access tokens.
734
+
735
+ Documentation:
736
+ --------------
737
+ - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.html
738
+ """
739
+
740
+ @property
741
+ def request(self) -> PreTokenGenerationTriggerV2EventRequest:
742
+ """Pre Token Generation Request V2 Parameters"""
743
+ return PreTokenGenerationTriggerV2EventRequest(self._data)
744
+
745
+ @property
746
+ def response(self) -> PreTokenGenerationTriggerV2EventResponse:
747
+ """Pre Token Generation Response V2 Parameters"""
748
+ return PreTokenGenerationTriggerV2EventResponse(self._data)
749
+
750
+
566
751
  class ChallengeResult(DictWrapper):
567
752
  @property
568
753
  def challenge_name(self) -> str:
@@ -824,3 +1009,77 @@ class VerifyAuthChallengeResponseTriggerEvent(BaseTriggerEvent):
824
1009
  def response(self) -> VerifyAuthChallengeResponseTriggerEventResponse:
825
1010
  """Verify Auth Challenge Response Parameters"""
826
1011
  return VerifyAuthChallengeResponseTriggerEventResponse(self._data)
1012
+
1013
+
1014
+ class CustomEmailSenderTriggerEventRequest(DictWrapper):
1015
+ @property
1016
+ def type(self) -> str:
1017
+ """The request version. For a custom email sender event, the value of this string
1018
+ is always customEmailSenderRequestV1.
1019
+ """
1020
+ return self["request"]["type"]
1021
+
1022
+ @property
1023
+ def code(self) -> str:
1024
+ """The encrypted code that your function can decrypt and send to your user."""
1025
+ return self["request"]["code"]
1026
+
1027
+ @property
1028
+ def user_attributes(self) -> dict[str, str]:
1029
+ """One or more name-value pairs representing user attributes. The attribute names are the keys."""
1030
+ return self["request"]["userAttributes"]
1031
+
1032
+ @property
1033
+ def client_metadata(self) -> dict[str, str]:
1034
+ """One or more key-value pairs that you can provide as custom input to the
1035
+ custom email sender Lambda function trigger. To pass this data to your Lambda function,
1036
+ you can use the ClientMetadata parameter in the AdminRespondToAuthChallenge and
1037
+ RespondToAuthChallenge API actions. Amazon Cognito doesn't include data from the
1038
+ ClientMetadata parameter in AdminInitiateAuth and InitiateAuth API operations
1039
+ in the request that it passes to the post authentication function.
1040
+ """
1041
+ return self["request"].get("clientMetadata") or {}
1042
+
1043
+
1044
+ class CustomEmailSenderTriggerEvent(BaseTriggerEvent):
1045
+ @property
1046
+ def request(self) -> CustomEmailSenderTriggerEventRequest:
1047
+ """Custom Email Sender Request Parameters"""
1048
+ return CustomEmailSenderTriggerEventRequest(self._data)
1049
+
1050
+
1051
+ class CustomSMSSenderTriggerEventRequest(DictWrapper):
1052
+ @property
1053
+ def type(self) -> str:
1054
+ """The request version. For a custom SMS sender event, the value of this string is always
1055
+ customSMSSenderRequestV1.
1056
+ """
1057
+ return self["request"]["type"]
1058
+
1059
+ @property
1060
+ def code(self) -> str:
1061
+ """The encrypted code that your function can decrypt and send to your user."""
1062
+ return self["request"]["code"]
1063
+
1064
+ @property
1065
+ def user_attributes(self) -> dict[str, str]:
1066
+ """One or more name-value pairs representing user attributes. The attribute names are the keys."""
1067
+ return self["request"].get("userAttributes") or {}
1068
+
1069
+ @property
1070
+ def client_metadata(self) -> dict[str, str]:
1071
+ """One or more key-value pairs that you can provide as custom input to the
1072
+ custom SMS sender Lambda function trigger. To pass this data to your Lambda function,
1073
+ you can use the ClientMetadata parameter in the AdminRespondToAuthChallenge and
1074
+ RespondToAuthChallenge API actions. Amazon Cognito doesn't include data from the
1075
+ ClientMetadata parameter in AdminInitiateAuth and InitiateAuth API operations
1076
+ in the request that it passes to the post authentication function.
1077
+ """
1078
+ return self["request"].get("clientMetadata") or {}
1079
+
1080
+
1081
+ class CustomSMSSenderTriggerEvent(BaseTriggerEvent):
1082
+ @property
1083
+ def request(self) -> CustomSMSSenderTriggerEventRequest:
1084
+ """Custom SMS Sender Request Parameters"""
1085
+ return CustomSMSSenderTriggerEventRequest(self._data)
@@ -14,12 +14,12 @@ class KafkaEventRecord(DictWrapper):
14
14
  return self["topic"]
15
15
 
16
16
  @property
17
- def partition(self) -> str:
17
+ def partition(self) -> int:
18
18
  """The Kafka record parition."""
19
19
  return self["partition"]
20
20
 
21
21
  @property
22
- def offset(self) -> str:
22
+ def offset(self) -> int:
23
23
  """The Kafka record offset."""
24
24
  return self["offset"]
25
25
 
@@ -193,3 +193,10 @@ class VPCLatticeEventV2(VPCLatticeEventBase):
193
193
  """
194
194
  params = self.get("queryStringParameters") or {}
195
195
  return {k: ",".join(v) for k, v in params.items()}
196
+
197
+ @property
198
+ def resolved_headers_field(self) -> dict[str, str]:
199
+ if self.headers is not None:
200
+ return {key.lower(): value for key, value in self.headers.items()}
201
+
202
+ return {}
@@ -204,7 +204,7 @@ class DataMasking:
204
204
  self._call_action,
205
205
  action=action,
206
206
  provider_options=provider_options,
207
- **encryption_context,
207
+ **encryption_context, # type: ignore[arg-type]
208
208
  )
209
209
 
210
210
  # Iterate over each field to be parsed.
@@ -229,12 +229,12 @@ class DataMasking:
229
229
  self._call_action,
230
230
  action=action,
231
231
  provider_options=provider_options,
232
- **encryption_context,
232
+ **encryption_context, # type: ignore[arg-type]
233
233
  )
234
234
 
235
235
  json_parse.update(
236
236
  data_parsed,
237
- lambda field_value, fields, field_name: update_callback(field_value, fields, field_name), # noqa: B023
237
+ lambda field_value, fields, field_name: update_callback(field_value, fields, field_name), # type: ignore[misc] # noqa: B023
238
238
  )
239
239
 
240
240
  return data_parsed
@@ -245,7 +245,7 @@ class DataMasking:
245
245
  fields: dict[str, Any],
246
246
  field_name: str,
247
247
  action: Callable,
248
- provider_options: dict | None = None,
248
+ provider_options: dict[str, Any] | None = None,
249
249
  **encryption_context,
250
250
  ) -> None:
251
251
  """
@@ -4,6 +4,8 @@ import logging
4
4
  import traceback
5
5
  from typing import TYPE_CHECKING, Any, cast
6
6
 
7
+ from botocore.config import Config
8
+
7
9
  from aws_lambda_powertools.utilities import jmespath_utils
8
10
  from aws_lambda_powertools.utilities.feature_flags.base import StoreProvider
9
11
  from aws_lambda_powertools.utilities.feature_flags.exceptions import ConfigurationStoreError, StoreClientError
@@ -14,7 +16,9 @@ from aws_lambda_powertools.utilities.parameters import (
14
16
  )
15
17
 
16
18
  if TYPE_CHECKING:
19
+ import boto3
17
20
  from botocore.config import Config
21
+ from mypy_boto3_appconfigdata import AppConfigDataClient
18
22
 
19
23
  from aws_lambda_powertools.logging import Logger
20
24
 
@@ -30,6 +34,9 @@ class AppConfigStore(StoreProvider):
30
34
  envelope: str | None = "",
31
35
  jmespath_options: dict | None = None,
32
36
  logger: logging.Logger | Logger | None = None,
37
+ boto_config: Config | None = None,
38
+ boto3_session: boto3.session.Session | None = None,
39
+ boto3_client: AppConfigDataClient | None = None,
33
40
  ):
34
41
  """This class fetches JSON schemas from AWS AppConfig
35
42
 
@@ -51,6 +58,12 @@ class AppConfigStore(StoreProvider):
51
58
  Alternative JMESPath options to be included when filtering expr
52
59
  logger: A logging object
53
60
  Used to log messages. If None is supplied, one will be created.
61
+ boto_config: botocore.config.Config, optional
62
+ Botocore configuration to pass during client initialization
63
+ boto3_session : boto3.Session, optional
64
+ Boto3 session to use for AWS API communication
65
+ boto3_client : AppConfigDataClient, optional
66
+ Boto3 AppConfigDataClient Client to use, boto3_session and boto_config will be ignored if both are provided
54
67
  """
55
68
  super().__init__()
56
69
  self.logger = logger or logging.getLogger(__name__)
@@ -58,10 +71,16 @@ class AppConfigStore(StoreProvider):
58
71
  self.application = application
59
72
  self.name = name
60
73
  self.cache_seconds = max_age
61
- self.config = sdk_config
74
+ self.config = sdk_config or boto_config
62
75
  self.envelope = envelope
63
76
  self.jmespath_options = jmespath_options
64
- self._conf_store = AppConfigProvider(environment=environment, application=application, boto_config=sdk_config)
77
+ self._conf_store = AppConfigProvider(
78
+ environment=environment,
79
+ application=application,
80
+ config=sdk_config or boto_config,
81
+ boto3_client=boto3_client,
82
+ boto3_session=boto3_session,
83
+ )
65
84
 
66
85
  @property
67
86
  def get_raw_configuration(self) -> dict[str, Any]:
@@ -7,11 +7,13 @@ from __future__ import annotations
7
7
  import functools
8
8
  import logging
9
9
  import os
10
+ import warnings
10
11
  from inspect import isclass
11
12
  from typing import TYPE_CHECKING, Any, Callable, cast
12
13
 
13
14
  from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
14
15
  from aws_lambda_powertools.shared import constants
16
+ from aws_lambda_powertools.shared.functions import strtobool
15
17
  from aws_lambda_powertools.shared.types import AnyCallableT
16
18
  from aws_lambda_powertools.utilities.idempotency.base import IdempotencyHandler
17
19
  from aws_lambda_powertools.utilities.idempotency.config import IdempotencyConfig
@@ -26,6 +28,8 @@ if TYPE_CHECKING:
26
28
  )
27
29
  from aws_lambda_powertools.utilities.typing import LambdaContext
28
30
 
31
+ from aws_lambda_powertools.warnings import PowertoolsUserWarning
32
+
29
33
  logger = logging.getLogger(__name__)
30
34
 
31
35
 
@@ -70,7 +74,15 @@ def idempotent(
70
74
  >>> return {"StatusCode": 200}
71
75
  """
72
76
 
73
- if os.getenv(constants.IDEMPOTENCY_DISABLED_ENV):
77
+ # Skip idempotency controls when POWERTOOLS_IDEMPOTENCY_DISABLED has a truthy value
78
+ # Raises a warning if not running in development mode
79
+ if strtobool(os.getenv(constants.IDEMPOTENCY_DISABLED_ENV, "false")):
80
+ warnings.warn(
81
+ message="Disabling idempotency is intended for development environments only "
82
+ "and should not be used in production.",
83
+ category=PowertoolsUserWarning,
84
+ stacklevel=2,
85
+ )
74
86
  return handler(event, context, **kwargs)
75
87
 
76
88
  config = config or IdempotencyConfig()
@@ -154,7 +166,15 @@ def idempotent_function(
154
166
 
155
167
  @functools.wraps(function)
156
168
  def decorate(*args, **kwargs):
157
- if os.getenv(constants.IDEMPOTENCY_DISABLED_ENV):
169
+ # Skip idempotency controls when POWERTOOLS_IDEMPOTENCY_DISABLED has a truthy value
170
+ # Raises a warning if not running in development mode
171
+ if strtobool(os.getenv(constants.IDEMPOTENCY_DISABLED_ENV, "false")):
172
+ warnings.warn(
173
+ message="Disabling idempotency is intended for development environments only "
174
+ "and should not be used in production.",
175
+ category=PowertoolsUserWarning,
176
+ stacklevel=2,
177
+ )
158
178
  return function(*args, **kwargs)
159
179
 
160
180
  if data_keyword_argument not in kwargs:
@@ -305,7 +305,10 @@ class BasePersistenceLayer(ABC):
305
305
  payload_hash=self._get_hashed_payload(data=data),
306
306
  )
307
307
 
308
- if remaining_time_in_millis:
308
+ # When Lambda kills the container after timeout, the remaining_time_in_millis is 0, which is considered False.
309
+ # Therefore, we need to check if remaining_time_in_millis is not None (>=0) to handle this case.
310
+ # See: https://github.com/aws-powertools/powertools-lambda-python/issues/4759
311
+ if remaining_time_in_millis is not None:
309
312
  now = datetime.datetime.now()
310
313
  period = datetime.timedelta(milliseconds=remaining_time_in_millis)
311
314
  timestamp = (now + period).timestamp()
@@ -432,7 +432,7 @@ def set_secret(
432
432
  >>> parameters.set_secret(
433
433
  name="my-secret",
434
434
  value='{"password": "supers3cr3tllam@passw0rd"}',
435
- client_request_token="61f2af5f-5f75-44b1-a29f-0cc37af55b11"
435
+ client_request_token="YOUR_TOKEN_HERE"
436
436
  )
437
437
 
438
438
  URLs:
@@ -1,11 +1,14 @@
1
1
  from .alb import AlbModel, AlbRequestContext, AlbRequestContextData
2
2
  from .apigw import (
3
+ ApiGatewayAuthorizerRequest,
4
+ ApiGatewayAuthorizerToken,
3
5
  APIGatewayEventAuthorizer,
4
6
  APIGatewayEventIdentity,
5
7
  APIGatewayEventRequestContext,
6
8
  APIGatewayProxyEventModel,
7
9
  )
8
10
  from .apigwv2 import (
11
+ ApiGatewayAuthorizerRequestV2,
9
12
  APIGatewayProxyEventV2Model,
10
13
  RequestContextV2,
11
14
  RequestContextV2Authorizer,
@@ -101,6 +104,7 @@ from .vpc_latticev2 import VpcLatticeV2Model
101
104
 
102
105
  __all__ = [
103
106
  "APIGatewayProxyEventV2Model",
107
+ "ApiGatewayAuthorizerRequestV2",
104
108
  "RequestContextV2",
105
109
  "RequestContextV2Http",
106
110
  "RequestContextV2Authorizer",
@@ -161,6 +165,8 @@ __all__ = [
161
165
  "APIGatewayEventRequestContext",
162
166
  "APIGatewayEventAuthorizer",
163
167
  "APIGatewayEventIdentity",
168
+ "ApiGatewayAuthorizerRequest",
169
+ "ApiGatewayAuthorizerToken",
164
170
  "KafkaSelfManagedEventModel",
165
171
  "KafkaRecordModel",
166
172
  "KafkaMskEventModel",
@@ -88,5 +88,16 @@ class APIGatewayProxyEventModel(BaseModel):
88
88
  requestContext: APIGatewayEventRequestContext
89
89
  pathParameters: Optional[Dict[str, str]] = None
90
90
  stageVariables: Optional[Dict[str, str]] = None
91
- isBase64Encoded: bool
91
+ isBase64Encoded: Optional[bool] = None
92
92
  body: Optional[Union[str, Type[BaseModel]]] = None
93
+
94
+
95
+ class ApiGatewayAuthorizerToken(BaseModel):
96
+ type: Literal["TOKEN"]
97
+ methodArn: str
98
+ authorizationToken: str
99
+
100
+
101
+ class ApiGatewayAuthorizerRequest(APIGatewayProxyEventModel):
102
+ type: Literal["REQUEST"]
103
+ methodArn: str
@@ -66,4 +66,10 @@ class APIGatewayProxyEventV2Model(BaseModel):
66
66
  stageVariables: Optional[Dict[str, str]] = None
67
67
  requestContext: RequestContextV2
68
68
  body: Optional[Union[str, Type[BaseModel]]] = None
69
- isBase64Encoded: bool
69
+ isBase64Encoded: Optional[bool] = None
70
+
71
+
72
+ class ApiGatewayAuthorizerRequestV2(APIGatewayProxyEventV2Model):
73
+ type: Literal["REQUEST"]
74
+ routeArn: str
75
+ identitySource: List[str]
@@ -9,7 +9,13 @@ from aws_lambda_powertools.utilities.validation.exceptions import InvalidSchemaF
9
9
  logger = logging.getLogger(__name__)
10
10
 
11
11
 
12
- def validate_data_against_schema(data: dict | str, schema: dict, formats: dict | None = None):
12
+ def validate_data_against_schema(
13
+ data: dict | str,
14
+ schema: dict,
15
+ formats: dict | None = None,
16
+ handlers: dict | None = None,
17
+ provider_options: dict | None = None,
18
+ ) -> dict | str:
13
19
  """Validate dict data against given JSON Schema
14
20
 
15
21
  Parameters
@@ -20,6 +26,17 @@ def validate_data_against_schema(data: dict | str, schema: dict, formats: dict |
20
26
  JSON Schema to validate against
21
27
  formats: dict
22
28
  Custom formats containing a key (e.g. int64) and a value expressed as regex or callback returning bool
29
+ handlers: Dict
30
+ Custom methods to retrieve remote schemes, keyed off of URI scheme
31
+ provider_options: Dict
32
+ Arguments that will be passed directly to the underlying validation call, in this case fastjsonchema.validate.
33
+ For all supported arguments see: https://horejsek.github.io/python-fastjsonschema/#fastjsonschema.validate
34
+
35
+ Returns
36
+ -------
37
+ Dict
38
+ The validated event. If the schema specifies a `default` value for fields that are omitted,
39
+ those default values will be included in the response.
23
40
 
24
41
  Raises
25
42
  ------
@@ -30,7 +47,15 @@ def validate_data_against_schema(data: dict | str, schema: dict, formats: dict |
30
47
  """
31
48
  try:
32
49
  formats = formats or {}
33
- fastjsonschema.validate(definition=schema, data=data, formats=formats)
50
+ handlers = handlers or {}
51
+ provider_options = provider_options or {}
52
+ return fastjsonschema.validate(
53
+ definition=schema,
54
+ data=data,
55
+ formats=formats,
56
+ handlers=handlers,
57
+ **provider_options,
58
+ )
34
59
  except (TypeError, AttributeError, fastjsonschema.JsonSchemaDefinitionException) as e:
35
60
  raise InvalidSchemaFormatError(f"Schema received: {schema}, Formats: {formats}. Error: {e}")
36
61
  except fastjsonschema.JsonSchemaValueException as e: