Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(apigateway): support @app.not_found() syntax & housekeeping #926

Merged
merged 7 commits into from
Dec 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions aws_lambda_powertools/event_handler/api_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ def _remove_prefix(self, path: str) -> str:
@staticmethod
def _path_starts_with(path: str, prefix: str):
"""Returns true if the `path` starts with a prefix plus a `/`"""
if not isinstance(prefix, str) or len(prefix) == 0:
if not isinstance(prefix, str) or prefix == "":
return False

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

raise

def not_found(self, func: Callable):
def not_found(self, func: Optional[Callable] = None):
if func is None:
michaelbrewer marked this conversation as resolved.
Show resolved Hide resolved
return self.exception_handler(NotFoundError)
return self.exception_handler(NotFoundError)(func)

def exception_handler(self, exc_class: Type[Exception]):
Expand Down
19 changes: 13 additions & 6 deletions aws_lambda_powertools/shared/functions.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
from typing import Any, Optional, Union


def strtobool(value):
def strtobool(value: str) -> bool:
"""Convert a string representation of truth to True or False.

True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
'value' is anything else.

> note:: Copied from distutils.util.
"""
value = value.lower()
if value in ("y", "yes", "t", "true", "on", "1"):
return 1
elif value in ("n", "no", "f", "false", "off", "0"):
return 0
else:
raise ValueError("invalid truth value %r" % (value,))
return True
if value in ("n", "no", "f", "false", "off", "0"):
return False
raise ValueError(f"invalid truth value {value!r}")


def resolve_truthy_env_var_choice(env: str, choice: Optional[bool] = None) -> bool:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ def session(self) -> List[ChallengeResult]:
@property
def client_metadata(self) -> Optional[Dict[str, str]]:
"""One or more key-value pairs that you can provide as custom input to the Lambda function that you
specify for the create auth challenge trigger.."""
specify for the create auth challenge trigger."""
return self["request"].get("clientMetadata")


Expand Down
4 changes: 2 additions & 2 deletions aws_lambda_powertools/utilities/data_classes/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def get_header_value(
name_lower = name.lower()

return next(
# Iterate over the dict and do a case insensitive key comparison
# Iterate over the dict and do a case-insensitive key comparison
(value for key, value in headers.items() if key.lower() == name_lower),
# Default value is returned if no matches was found
default_value,
Expand Down Expand Up @@ -116,7 +116,7 @@ def get_header_value(
default_value: str, optional
Default value if no value was found by name
case_sensitive: bool
Whether to use a case sensitive look up
Whether to use a case-sensitive look up
Returns
-------
str, optional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,5 @@ class IdempotencyPersistenceLayerError(Exception):

class IdempotencyKeyError(Exception):
"""
Payload does not contain a idempotent key
Payload does not contain an idempotent key
"""
6 changes: 3 additions & 3 deletions aws_lambda_powertools/utilities/idempotency/idempotency.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Any, Callable, Dict, Optional, cast

from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
from aws_lambda_powertools.shared.constants import IDEMPOTENCY_DISABLED_ENV
from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.shared.types import AnyCallableT
from aws_lambda_powertools.utilities.idempotency.base import IdempotencyHandler
from aws_lambda_powertools.utilities.idempotency.config import IdempotencyConfig
Expand Down Expand Up @@ -58,7 +58,7 @@ def idempotent(
>>> return {"StatusCode": 200}
"""

if os.getenv(IDEMPOTENCY_DISABLED_ENV):
if os.getenv(constants.IDEMPOTENCY_DISABLED_ENV):
return handler(event, context)

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

@functools.wraps(function)
def decorate(*args, **kwargs):
if os.getenv(IDEMPOTENCY_DISABLED_ENV):
if os.getenv(constants.IDEMPOTENCY_DISABLED_ENV):
return function(*args, **kwargs)

payload = kwargs.get(data_keyword_argument)
Expand Down
18 changes: 17 additions & 1 deletion tests/functional/event_handler/test_api_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -1142,10 +1142,26 @@ def handle_not_found(exc: NotFoundError) -> Response:
return Response(status_code=404, content_type=content_types.TEXT_PLAIN, body="I am a teapot!")

# WHEN calling the event handler
# AND not route is found
# AND no route is found
result = app(LOAD_GW_EVENT, {})

# THEN call the exception_handler
assert result["statusCode"] == 404
assert result["headers"]["Content-Type"] == content_types.TEXT_PLAIN
assert result["body"] == "I am a teapot!"


def test_exception_handler_not_found_alt():
# GIVEN a resolver with `@app.not_found()`
app = ApiGatewayResolver()

@app.not_found()
def handle_not_found(_) -> Response:
return Response(status_code=404, content_type=content_types.APPLICATION_JSON, body="{}")

# WHEN calling the event handler
# AND no route is found
result = app(LOAD_GW_EVENT, {})

# THEN call the @app.not_found() function
assert result["statusCode"] == 404
5 changes: 5 additions & 0 deletions tests/functional/idempotency/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ def persistence_store(config):
return DynamoDBPersistenceLayer(table_name=TABLE_NAME, boto_config=config)


@pytest.fixture
def persistence_store_compound(config):
return DynamoDBPersistenceLayer(table_name=TABLE_NAME, boto_config=config, key_attr="id", sort_key_attr="sk")


@pytest.fixture
def idempotency_config(config, request, default_jmespath):
return IdempotencyConfig(
Expand Down
46 changes: 46 additions & 0 deletions tests/functional/idempotency/test_idempotency.py
Original file line number Diff line number Diff line change
Expand Up @@ -1148,3 +1148,49 @@ def collect_payment(payment: Payment):

# THEN idempotency key assertion happens at MockPersistenceLayer
assert result == payment.transaction_id


@pytest.mark.parametrize("idempotency_config", [{"use_local_cache": False}], indirect=True)
def test_idempotent_lambda_compound_already_completed(
idempotency_config: IdempotencyConfig,
persistence_store_compound: DynamoDBPersistenceLayer,
lambda_apigw_event,
timestamp_future,
hashed_idempotency_key,
serialized_lambda_response,
deserialized_lambda_response,
lambda_context,
):
"""
Test idempotent decorator having a DynamoDBPersistenceLayer with a compound key
"""

stubber = stub.Stubber(persistence_store_compound.table.meta.client)
stubber.add_client_error("put_item", "ConditionalCheckFailedException")
ddb_response = {
"Item": {
"id": {"S": "idempotency#"},
"sk": {"S": hashed_idempotency_key},
"expiration": {"N": timestamp_future},
"data": {"S": serialized_lambda_response},
"status": {"S": "COMPLETED"},
}
}
expected_params = {
"TableName": TABLE_NAME,
"Key": {"id": "idempotency#", "sk": hashed_idempotency_key},
"ConsistentRead": True,
}
stubber.add_response("get_item", ddb_response, expected_params)

stubber.activate()

@idempotent(config=idempotency_config, persistence_store=persistence_store_compound)
def lambda_handler(event, context):
raise ValueError

lambda_resp = lambda_handler(lambda_apigw_event, lambda_context)
assert lambda_resp == deserialized_lambda_response

stubber.assert_no_pending_responses()
stubber.deactivate()
20 changes: 19 additions & 1 deletion tests/functional/test_shared_functions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from aws_lambda_powertools.shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice
import pytest

from aws_lambda_powertools.shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice, strtobool


def test_resolve_env_var_choice_explicit_wins_over_env_var():
Expand All @@ -9,3 +11,19 @@ def test_resolve_env_var_choice_explicit_wins_over_env_var():
def test_resolve_env_var_choice_env_wins_over_absent_explicit():
assert resolve_truthy_env_var_choice(env="true") == 1
assert resolve_env_var_choice(env="something") == "something"


@pytest.mark.parametrize("true_value", ["y", "yes", "t", "true", "on", "1"])
def test_strtobool_true(true_value):
assert strtobool(true_value)


@pytest.mark.parametrize("false_value", ["n", "no", "f", "false", "off", "0"])
def test_strtobool_false(false_value):
assert strtobool(false_value) is False


def test_strtobool_value_error():
with pytest.raises(ValueError) as exp:
strtobool("fail")
assert str(exp.value) == "invalid truth value 'fail'"