Skip to content

Commit 85f4b9d

Browse files
committed
Merge branch 'develop' into docs/dynamic-feature-toggles
* develop: feat(params): expose high level max_age, raise_on_transform_error (#567) fix(parser): apigw wss validation check_message_id; housekeeping (#553) chore(deps-dev): bump isort from 5.9.2 to 5.9.3 (#574) feat(data-classes): decode json_body if based64 encoded (#560) chore(deps-dev): bump mkdocs-material from 7.2.0 to 7.2.1 (#566)
2 parents 9890170 + dfe42b1 commit 85f4b9d

File tree

15 files changed

+132
-45
lines changed

15 files changed

+132
-45
lines changed

aws_lambda_powertools/tracing/tracer.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ def decorate(event, context, **kwargs):
335335
# see #465
336336
@overload
337337
def capture_method(self, method: "AnyCallableT") -> "AnyCallableT":
338-
...
338+
... # pragma: no cover
339339

340340
@overload
341341
def capture_method(
@@ -344,7 +344,7 @@ def capture_method(
344344
capture_response: Optional[bool] = None,
345345
capture_error: Optional[bool] = None,
346346
) -> Callable[["AnyCallableT"], "AnyCallableT"]:
347-
...
347+
... # pragma: no cover
348348

349349
def capture_method(
350350
self,

aws_lambda_powertools/utilities/data_classes/common.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def body(self) -> Optional[str]:
6565
@property
6666
def json_body(self) -> Any:
6767
"""Parses the submitted body as json"""
68-
return json.loads(self["body"])
68+
return json.loads(self.decoded_body)
6969

7070
@property
7171
def decoded_body(self) -> str:

aws_lambda_powertools/utilities/feature_toggles/appconfig_fetcher.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from typing import Any, Dict, Optional
2+
from typing import Any, Dict, Optional, cast
33

44
from botocore.config import Config
55

@@ -56,11 +56,15 @@ def get_json_configuration(self) -> Dict[str, Any]:
5656
parsed JSON dictionary
5757
"""
5858
try:
59-
return self._conf_store.get(
60-
name=self.configuration_name,
61-
transform=TRANSFORM_TYPE,
62-
max_age=self._cache_seconds,
63-
) # parse result conf as JSON, keep in cache for self.max_age seconds
59+
# parse result conf as JSON, keep in cache for self.max_age seconds
60+
return cast(
61+
dict,
62+
self._conf_store.get(
63+
name=self.configuration_name,
64+
transform=TRANSFORM_TYPE,
65+
max_age=self._cache_seconds,
66+
),
67+
)
6468
except (GetParameterError, TransformParameterError) as exc:
6569
error_str = f"unable to get AWS AppConfig configuration file, exception={str(exc)}"
6670
self._logger.error(error_str)

aws_lambda_powertools/utilities/idempotency/idempotency.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,7 @@ def idempotent(
7878
try:
7979
return idempotency_handler.handle()
8080
except IdempotencyInconsistentStateError:
81-
if i < max_handler_retries:
82-
continue
83-
else:
81+
if i == max_handler_retries:
8482
# Allow the exception to bubble up after max retries exceeded
8583
raise
8684

@@ -117,7 +115,6 @@ def __init__(
117115
self.context = context
118116
self.event = event
119117
self.lambda_handler = lambda_handler
120-
self.max_handler_retries = 2
121118

122119
def handle(self) -> Any:
123120
"""

aws_lambda_powertools/utilities/idempotency/persistence/dynamodb.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def _update_record(self, data_record: DataRecord):
154154
"ExpressionAttributeNames": expression_attr_names,
155155
}
156156

157-
self.table.update_item(**kwargs)
157+
self.table.update_item(**kwargs) # type: ignore
158158

159159
def _delete_record(self, data_record: DataRecord) -> None:
160160
logger.debug(f"Deleting record for idempotency key: {data_record.idempotency_key}")

aws_lambda_powertools/utilities/parameters/appconfig.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from ...shared import constants
1414
from ...shared.functions import resolve_env_var_choice
15-
from .base import DEFAULT_PROVIDERS, BaseProvider
15+
from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider
1616

1717
CLIENT_ID = str(uuid4())
1818

@@ -110,6 +110,7 @@ def get_app_config(
110110
application: Optional[str] = None,
111111
transform: Optional[str] = None,
112112
force_fetch: bool = False,
113+
max_age: int = DEFAULT_MAX_AGE_SECS,
113114
**sdk_options
114115
) -> Union[str, list, dict, bytes]:
115116
"""
@@ -127,6 +128,8 @@ def get_app_config(
127128
Transforms the content from a JSON object ('json') or base64 binary string ('binary')
128129
force_fetch: bool, optional
129130
Force update even before a cached item has expired, defaults to False
131+
max_age: int
132+
Maximum age of the cached value
130133
sdk_options: dict, optional
131134
Dictionary of options that will be passed to the Parameter Store get_parameter API call
132135
@@ -165,4 +168,6 @@ def get_app_config(
165168

166169
sdk_options["ClientId"] = CLIENT_ID
167170

168-
return DEFAULT_PROVIDERS["appconfig"].get(name, transform=transform, force_fetch=force_fetch, **sdk_options)
171+
return DEFAULT_PROVIDERS["appconfig"].get(
172+
name, max_age=max_age, transform=transform, force_fetch=force_fetch, **sdk_options
173+
)

aws_lambda_powertools/utilities/parameters/dynamodb.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55

6-
from typing import Any, Dict, Optional
6+
from typing import Dict, Optional
77

88
import boto3
99
from boto3.dynamodb.conditions import Key
@@ -141,11 +141,6 @@ class DynamoDBProvider(BaseProvider):
141141
c Parameter value c
142142
"""
143143

144-
table: Any = None
145-
key_attr = None
146-
sort_attr = None
147-
value_attr = None
148-
149144
def __init__(
150145
self,
151146
table_name: str,

aws_lambda_powertools/utilities/parameters/secrets.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import boto3
99
from botocore.config import Config
1010

11-
from .base import DEFAULT_PROVIDERS, BaseProvider
11+
from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider
1212

1313

1414
class SecretsProvider(BaseProvider):
@@ -94,7 +94,11 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]:
9494

9595

9696
def get_secret(
97-
name: str, transform: Optional[str] = None, force_fetch: bool = False, **sdk_options
97+
name: str,
98+
transform: Optional[str] = None,
99+
force_fetch: bool = False,
100+
max_age: int = DEFAULT_MAX_AGE_SECS,
101+
**sdk_options
98102
) -> Union[str, dict, bytes]:
99103
"""
100104
Retrieve a parameter value from AWS Secrets Manager
@@ -107,6 +111,8 @@ def get_secret(
107111
Transforms the content from a JSON object ('json') or base64 binary string ('binary')
108112
force_fetch: bool, optional
109113
Force update even before a cached item has expired, defaults to False
114+
max_age: int
115+
Maximum age of the cached value
110116
sdk_options: dict, optional
111117
Dictionary of options that will be passed to the get_secret_value call
112118
@@ -143,4 +149,6 @@ def get_secret(
143149
if "secrets" not in DEFAULT_PROVIDERS:
144150
DEFAULT_PROVIDERS["secrets"] = SecretsProvider()
145151

146-
return DEFAULT_PROVIDERS["secrets"].get(name, transform=transform, force_fetch=force_fetch, **sdk_options)
152+
return DEFAULT_PROVIDERS["secrets"].get(
153+
name, max_age=max_age, transform=transform, force_fetch=force_fetch, **sdk_options
154+
)

aws_lambda_powertools/utilities/parameters/ssm.py

+26-3
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,12 @@ def _get_multiple(self, path: str, decrypt: bool = False, recursive: bool = Fals
186186

187187

188188
def get_parameter(
189-
name: str, transform: Optional[str] = None, decrypt: bool = False, force_fetch: bool = False, **sdk_options
189+
name: str,
190+
transform: Optional[str] = None,
191+
decrypt: bool = False,
192+
force_fetch: bool = False,
193+
max_age: int = DEFAULT_MAX_AGE_SECS,
194+
**sdk_options
190195
) -> Union[str, list, dict, bytes]:
191196
"""
192197
Retrieve a parameter value from AWS Systems Manager (SSM) Parameter Store
@@ -201,6 +206,8 @@ def get_parameter(
201206
If the parameter values should be decrypted
202207
force_fetch: bool, optional
203208
Force update even before a cached item has expired, defaults to False
209+
max_age: int
210+
Maximum age of the cached value
204211
sdk_options: dict, optional
205212
Dictionary of options that will be passed to the Parameter Store get_parameter API call
206213
@@ -240,7 +247,9 @@ def get_parameter(
240247
# Add to `decrypt` sdk_options to we can have an explicit option for this
241248
sdk_options["decrypt"] = decrypt
242249

243-
return DEFAULT_PROVIDERS["ssm"].get(name, transform=transform, force_fetch=force_fetch, **sdk_options)
250+
return DEFAULT_PROVIDERS["ssm"].get(
251+
name, max_age=max_age, transform=transform, force_fetch=force_fetch, **sdk_options
252+
)
244253

245254

246255
def get_parameters(
@@ -249,6 +258,8 @@ def get_parameters(
249258
recursive: bool = True,
250259
decrypt: bool = False,
251260
force_fetch: bool = False,
261+
max_age: int = DEFAULT_MAX_AGE_SECS,
262+
raise_on_transform_error: bool = False,
252263
**sdk_options
253264
) -> Union[Dict[str, str], Dict[str, dict], Dict[str, bytes]]:
254265
"""
@@ -266,6 +277,11 @@ def get_parameters(
266277
If the parameter values should be decrypted
267278
force_fetch: bool, optional
268279
Force update even before a cached item has expired, defaults to False
280+
max_age: int
281+
Maximum age of the cached value
282+
raise_on_transform_error: bool, optional
283+
Raises an exception if any transform fails, otherwise this will
284+
return a None value for each transform that failed
269285
sdk_options: dict, optional
270286
Dictionary of options that will be passed to the Parameter Store get_parameters_by_path API call
271287
@@ -305,4 +321,11 @@ def get_parameters(
305321
sdk_options["recursive"] = recursive
306322
sdk_options["decrypt"] = decrypt
307323

308-
return DEFAULT_PROVIDERS["ssm"].get_multiple(path, transform=transform, force_fetch=force_fetch, **sdk_options)
324+
return DEFAULT_PROVIDERS["ssm"].get_multiple(
325+
path,
326+
max_age=max_age,
327+
transform=transform,
328+
raise_on_transform_error=raise_on_transform_error,
329+
force_fetch=force_fetch,
330+
**sdk_options
331+
)

aws_lambda_powertools/utilities/parser/models/apigw.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ class APIGatewayEventRequestContext(BaseModel):
6868
routeKey: Optional[str]
6969
operationName: Optional[str]
7070

71+
@root_validator
72+
def check_message_id(cls, values):
73+
message_id, event_type = values.get("messageId"), values.get("eventType")
74+
if message_id is not None and event_type != "MESSAGE":
75+
raise TypeError("messageId is available only when the `eventType` is `MESSAGE`")
76+
return values
77+
7178

7279
class APIGatewayProxyEventModel(BaseModel):
7380
version: Optional[str]
@@ -83,10 +90,3 @@ class APIGatewayProxyEventModel(BaseModel):
8390
stageVariables: Optional[Dict[str, str]]
8491
isBase64Encoded: bool
8592
body: str
86-
87-
@root_validator()
88-
def check_message_id(cls, values):
89-
message_id, event_type = values.get("messageId"), values.get("eventType")
90-
if message_id is not None and event_type != "MESSAGE":
91-
raise TypeError("messageId is available only when the `eventType` is `MESSAGE`")
92-
return values

poetry.lock

+7-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ flake8-debugger = "^4.0.0"
3939
flake8-fixme = "^1.1.1"
4040
flake8-isort = "^4.0.0"
4141
flake8-variables-names = "^0.0.4"
42-
isort = "^5.9.2"
42+
isort = "^5.9.3"
4343
pytest-cov = "^2.12.1"
4444
pytest-mock = "^3.5.1"
4545
pdoc3 = "^0.9.2"
@@ -49,7 +49,7 @@ radon = "^4.5.0"
4949
xenon = "^0.7.3"
5050
flake8-eradicate = "^1.1.0"
5151
flake8-bugbear = "^21.3.2"
52-
mkdocs-material = "^7.2.0"
52+
mkdocs-material = "^7.2.1"
5353
mkdocs-git-revision-date-plugin = "^0.3.1"
5454
mike = "^0.6.0"
5555
mypy = "^0.910"

tests/functional/idempotency/test_idempotency.py

-1
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,6 @@ def test_idempotent_lambda_expired_during_request(
395395
lambda_apigw_event,
396396
timestamp_expired,
397397
lambda_response,
398-
expected_params_update_item,
399398
hashed_idempotency_key,
400399
lambda_context,
401400
):

tests/functional/parser/test_apigw.py

+44
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import pytest
2+
from pydantic import ValidationError
3+
14
from aws_lambda_powertools.utilities.parser import envelopes, event_parser
25
from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventModel
36
from aws_lambda_powertools.utilities.typing import LambdaContext
@@ -100,3 +103,44 @@ def test_apigw_event():
100103
assert request_context.operationName is None
101104
assert identity.apiKey is None
102105
assert identity.apiKeyId is None
106+
107+
108+
def test_apigw_event_with_invalid_websocket_request():
109+
# GIVEN an event with an eventType != MESSAGE and has a messageId
110+
event = {
111+
"resource": "/",
112+
"path": "/",
113+
"httpMethod": "GET",
114+
"headers": {},
115+
"multiValueHeaders": {},
116+
"isBase64Encoded": False,
117+
"body": "Foo!",
118+
"requestContext": {
119+
"accountId": "1234",
120+
"apiId": "myApi",
121+
"httpMethod": "GET",
122+
"identity": {
123+
"sourceIp": "127.0.0.1",
124+
},
125+
"path": "/",
126+
"protocol": "Https",
127+
"requestId": "1234",
128+
"requestTime": "2018-09-07T16:20:46Z",
129+
"requestTimeEpoch": 1536992496000,
130+
"resourcePath": "/",
131+
"stage": "test",
132+
"eventType": "DISCONNECT",
133+
"messageId": "messageId",
134+
},
135+
}
136+
137+
# WHEN calling event_parser with APIGatewayProxyEventModel
138+
with pytest.raises(ValidationError) as err:
139+
handle_apigw_event(event, LambdaContext())
140+
141+
# THEN raise TypeError for invalid event
142+
errors = err.value.errors()
143+
assert len(errors) == 1
144+
expected_msg = "messageId is available only when the `eventType` is `MESSAGE`"
145+
assert errors[0]["msg"] == expected_msg
146+
assert expected_msg in str(err.value)

0 commit comments

Comments
 (0)