Skip to content

Commit bdf307b

Browse files
author
Michael Brewer
authored
fix(apigateway): support @app.not_found() syntax & housekeeping (#926)
1 parent cced6c4 commit bdf307b

File tree

10 files changed

+111
-17
lines changed

10 files changed

+111
-17
lines changed

Diff for: aws_lambda_powertools/event_handler/api_gateway.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,7 @@ def _remove_prefix(self, path: str) -> str:
579579
@staticmethod
580580
def _path_starts_with(path: str, prefix: str):
581581
"""Returns true if the `path` starts with a prefix plus a `/`"""
582-
if not isinstance(prefix, str) or len(prefix) == 0:
582+
if not isinstance(prefix, str) or prefix == "":
583583
return False
584584

585585
return path.startswith(prefix + "/")
@@ -633,7 +633,9 @@ def _call_route(self, route: Route, args: Dict[str, str]) -> ResponseBuilder:
633633

634634
raise
635635

636-
def not_found(self, func: Callable):
636+
def not_found(self, func: Optional[Callable] = None):
637+
if func is None:
638+
return self.exception_handler(NotFoundError)
637639
return self.exception_handler(NotFoundError)(func)
638640

639641
def exception_handler(self, exc_class: Type[Exception]):

Diff for: aws_lambda_powertools/shared/functions.py

+13-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
from typing import Any, Optional, Union
22

33

4-
def strtobool(value):
4+
def strtobool(value: str) -> bool:
5+
"""Convert a string representation of truth to True or False.
6+
7+
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
8+
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
9+
'value' is anything else.
10+
11+
> note:: Copied from distutils.util.
12+
"""
513
value = value.lower()
614
if value in ("y", "yes", "t", "true", "on", "1"):
7-
return 1
8-
elif value in ("n", "no", "f", "false", "off", "0"):
9-
return 0
10-
else:
11-
raise ValueError("invalid truth value %r" % (value,))
15+
return True
16+
if value in ("n", "no", "f", "false", "off", "0"):
17+
return False
18+
raise ValueError(f"invalid truth value {value!r}")
1219

1320

1421
def resolve_truthy_env_var_choice(env: str, choice: Optional[bool] = None) -> bool:

Diff for: aws_lambda_powertools/utilities/data_classes/cognito_user_pool_event.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,7 @@ def session(self) -> List[ChallengeResult]:
687687
@property
688688
def client_metadata(self) -> Optional[Dict[str, str]]:
689689
"""One or more key-value pairs that you can provide as custom input to the Lambda function that you
690-
specify for the create auth challenge trigger.."""
690+
specify for the create auth challenge trigger."""
691691
return self["request"].get("clientMetadata")
692692

693693

Diff for: aws_lambda_powertools/utilities/data_classes/common.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def get_header_value(
3838
name_lower = name.lower()
3939

4040
return next(
41-
# Iterate over the dict and do a case insensitive key comparison
41+
# Iterate over the dict and do a case-insensitive key comparison
4242
(value for key, value in headers.items() if key.lower() == name_lower),
4343
# Default value is returned if no matches was found
4444
default_value,
@@ -116,7 +116,7 @@ def get_header_value(
116116
default_value: str, optional
117117
Default value if no value was found by name
118118
case_sensitive: bool
119-
Whether to use a case sensitive look up
119+
Whether to use a case-sensitive look up
120120
Returns
121121
-------
122122
str, optional

Diff for: aws_lambda_powertools/utilities/idempotency/exceptions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,5 @@ class IdempotencyPersistenceLayerError(Exception):
4747

4848
class IdempotencyKeyError(Exception):
4949
"""
50-
Payload does not contain a idempotent key
50+
Payload does not contain an idempotent key
5151
"""

Diff for: aws_lambda_powertools/utilities/idempotency/idempotency.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Any, Callable, Dict, Optional, cast
88

99
from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
10-
from aws_lambda_powertools.shared.constants import IDEMPOTENCY_DISABLED_ENV
10+
from aws_lambda_powertools.shared import constants
1111
from aws_lambda_powertools.shared.types import AnyCallableT
1212
from aws_lambda_powertools.utilities.idempotency.base import IdempotencyHandler
1313
from aws_lambda_powertools.utilities.idempotency.config import IdempotencyConfig
@@ -58,7 +58,7 @@ def idempotent(
5858
>>> return {"StatusCode": 200}
5959
"""
6060

61-
if os.getenv(IDEMPOTENCY_DISABLED_ENV):
61+
if os.getenv(constants.IDEMPOTENCY_DISABLED_ENV):
6262
return handler(event, context)
6363

6464
config = config or IdempotencyConfig()
@@ -127,7 +127,7 @@ def process_order(customer_id: str, order: dict, **kwargs):
127127

128128
@functools.wraps(function)
129129
def decorate(*args, **kwargs):
130-
if os.getenv(IDEMPOTENCY_DISABLED_ENV):
130+
if os.getenv(constants.IDEMPOTENCY_DISABLED_ENV):
131131
return function(*args, **kwargs)
132132

133133
payload = kwargs.get(data_keyword_argument)

Diff for: tests/functional/event_handler/test_api_gateway.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -1142,10 +1142,26 @@ def handle_not_found(exc: NotFoundError) -> Response:
11421142
return Response(status_code=404, content_type=content_types.TEXT_PLAIN, body="I am a teapot!")
11431143

11441144
# WHEN calling the event handler
1145-
# AND not route is found
1145+
# AND no route is found
11461146
result = app(LOAD_GW_EVENT, {})
11471147

11481148
# THEN call the exception_handler
11491149
assert result["statusCode"] == 404
11501150
assert result["headers"]["Content-Type"] == content_types.TEXT_PLAIN
11511151
assert result["body"] == "I am a teapot!"
1152+
1153+
1154+
def test_exception_handler_not_found_alt():
1155+
# GIVEN a resolver with `@app.not_found()`
1156+
app = ApiGatewayResolver()
1157+
1158+
@app.not_found()
1159+
def handle_not_found(_) -> Response:
1160+
return Response(status_code=404, content_type=content_types.APPLICATION_JSON, body="{}")
1161+
1162+
# WHEN calling the event handler
1163+
# AND no route is found
1164+
result = app(LOAD_GW_EVENT, {})
1165+
1166+
# THEN call the @app.not_found() function
1167+
assert result["statusCode"] == 404

Diff for: tests/functional/idempotency/conftest.py

+5
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ def persistence_store(config):
165165
return DynamoDBPersistenceLayer(table_name=TABLE_NAME, boto_config=config)
166166

167167

168+
@pytest.fixture
169+
def persistence_store_compound(config):
170+
return DynamoDBPersistenceLayer(table_name=TABLE_NAME, boto_config=config, key_attr="id", sort_key_attr="sk")
171+
172+
168173
@pytest.fixture
169174
def idempotency_config(config, request, default_jmespath):
170175
return IdempotencyConfig(

Diff for: tests/functional/idempotency/test_idempotency.py

+46
Original file line numberDiff line numberDiff line change
@@ -1148,3 +1148,49 @@ def collect_payment(payment: Payment):
11481148

11491149
# THEN idempotency key assertion happens at MockPersistenceLayer
11501150
assert result == payment.transaction_id
1151+
1152+
1153+
@pytest.mark.parametrize("idempotency_config", [{"use_local_cache": False}], indirect=True)
1154+
def test_idempotent_lambda_compound_already_completed(
1155+
idempotency_config: IdempotencyConfig,
1156+
persistence_store_compound: DynamoDBPersistenceLayer,
1157+
lambda_apigw_event,
1158+
timestamp_future,
1159+
hashed_idempotency_key,
1160+
serialized_lambda_response,
1161+
deserialized_lambda_response,
1162+
lambda_context,
1163+
):
1164+
"""
1165+
Test idempotent decorator having a DynamoDBPersistenceLayer with a compound key
1166+
"""
1167+
1168+
stubber = stub.Stubber(persistence_store_compound.table.meta.client)
1169+
stubber.add_client_error("put_item", "ConditionalCheckFailedException")
1170+
ddb_response = {
1171+
"Item": {
1172+
"id": {"S": "idempotency#"},
1173+
"sk": {"S": hashed_idempotency_key},
1174+
"expiration": {"N": timestamp_future},
1175+
"data": {"S": serialized_lambda_response},
1176+
"status": {"S": "COMPLETED"},
1177+
}
1178+
}
1179+
expected_params = {
1180+
"TableName": TABLE_NAME,
1181+
"Key": {"id": "idempotency#", "sk": hashed_idempotency_key},
1182+
"ConsistentRead": True,
1183+
}
1184+
stubber.add_response("get_item", ddb_response, expected_params)
1185+
1186+
stubber.activate()
1187+
1188+
@idempotent(config=idempotency_config, persistence_store=persistence_store_compound)
1189+
def lambda_handler(event, context):
1190+
raise ValueError
1191+
1192+
lambda_resp = lambda_handler(lambda_apigw_event, lambda_context)
1193+
assert lambda_resp == deserialized_lambda_response
1194+
1195+
stubber.assert_no_pending_responses()
1196+
stubber.deactivate()

Diff for: tests/functional/test_shared_functions.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from aws_lambda_powertools.shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice
1+
import pytest
2+
3+
from aws_lambda_powertools.shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice, strtobool
24

35

46
def test_resolve_env_var_choice_explicit_wins_over_env_var():
@@ -9,3 +11,19 @@ def test_resolve_env_var_choice_explicit_wins_over_env_var():
911
def test_resolve_env_var_choice_env_wins_over_absent_explicit():
1012
assert resolve_truthy_env_var_choice(env="true") == 1
1113
assert resolve_env_var_choice(env="something") == "something"
14+
15+
16+
@pytest.mark.parametrize("true_value", ["y", "yes", "t", "true", "on", "1"])
17+
def test_strtobool_true(true_value):
18+
assert strtobool(true_value)
19+
20+
21+
@pytest.mark.parametrize("false_value", ["n", "no", "f", "false", "off", "0"])
22+
def test_strtobool_false(false_value):
23+
assert strtobool(false_value) is False
24+
25+
26+
def test_strtobool_value_error():
27+
with pytest.raises(ValueError) as exp:
28+
strtobool("fail")
29+
assert str(exp.value) == "invalid truth value 'fail'"

0 commit comments

Comments
 (0)