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

Add cached properties #296

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
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
Next Next commit
Add cached properties
Cache properties to prevent methods from invoking multiple times per request
rwestergren committed May 28, 2023
commit 2e245fbf6ba9979992c1c9f3648b7ee25debf691
6 changes: 3 additions & 3 deletions mangum/handlers/alb.py
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
handle_base64_response_body,
handle_exclude_headers,
maybe_encode_body,
TypedCachedProperty,
)
from mangum.types import (
Response,
@@ -95,16 +96,15 @@ def __init__(
self.context = context
self.config = config

@property
@TypedCachedProperty
def body(self) -> bytes:
return maybe_encode_body(
self.event.get("body", b""),
is_base64=self.event.get("isBase64Encoded", False),
)

@property
@TypedCachedProperty
def scope(self) -> Scope:

headers = transform_headers(self.event)
list_headers = [list(x) for x in headers]
# Unique headers. If there are duplicates, it will use the last defined.
10 changes: 5 additions & 5 deletions mangum/handlers/api_gateway.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from typing import Dict, List, Tuple
from urllib.parse import urlencode

from mangum.handlers.utils import (
get_server_and_port,
handle_base64_response_body,
handle_exclude_headers,
handle_multi_value_headers,
maybe_encode_body,
strip_api_gateway_path,
TypedCachedProperty,
)
from mangum.types import (
Response,
@@ -78,14 +78,14 @@ def __init__(
self.context = context
self.config = config

@property
@TypedCachedProperty
def body(self) -> bytes:
return maybe_encode_body(
self.event.get("body", b""),
is_base64=self.event.get("isBase64Encoded", False),
)

@property
@TypedCachedProperty
def scope(self) -> Scope:
headers = _handle_multi_value_headers_for_request(self.event)
return {
@@ -144,14 +144,14 @@ def __init__(
self.context = context
self.config = config

@property
@TypedCachedProperty
def body(self) -> bytes:
return maybe_encode_body(
self.event.get("body", b""),
is_base64=self.event.get("isBase64Encoded", False),
)

@property
@TypedCachedProperty
def scope(self) -> Scope:
request_context = self.event["requestContext"]
event_version = self.event["version"]
6 changes: 3 additions & 3 deletions mangum/handlers/lambda_at_edge.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import Dict, List

from mangum.handlers.utils import (
handle_base64_response_body,
handle_exclude_headers,
handle_multi_value_headers,
maybe_encode_body,
TypedCachedProperty,
)
from mangum.types import Scope, Response, LambdaConfig, LambdaEvent, LambdaContext

@@ -31,15 +31,15 @@ def __init__(
self.context = context
self.config = config

@property
@TypedCachedProperty
def body(self) -> bytes:
cf_request_body = self.event["Records"][0]["cf"]["request"].get("body", {})
return maybe_encode_body(
cf_request_body.get("data"),
is_base64=cf_request_body.get("encoding", "") == "base64",
)

@property
@TypedCachedProperty
def scope(self) -> Scope:
cf_request = self.event["Records"][0]["cf"]["request"]
scheme_header = cf_request["headers"].get("cloudfront-forwarded-proto", [{}])
12 changes: 10 additions & 2 deletions mangum/handlers/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import base64
from typing import Any, Dict, List, Tuple, Union
from typing import Any, Dict, List, Tuple, Union, TypeVar, Callable, cast
from urllib.parse import unquote

from cached_property import cached_property
from mangum.types import Headers, LambdaConfig


F = TypeVar("F", bound=Callable[..., Any])


def maybe_encode_body(body: Union[str, bytes], *, is_base64: bool) -> bytes:
body = body or b""
if is_base64:
@@ -93,3 +96,8 @@ def handle_exclude_headers(
finalized_headers[header_key] = header_value

return finalized_headers


class TypedCachedProperty(cached_property):
def __init__(self, func: F) -> None:
super().__init__(func)
1 change: 0 additions & 1 deletion mangum/protocols/http.py
Original file line number Diff line number Diff line change
@@ -93,7 +93,6 @@ async def send(self, message: Message) -> None:
self.state is HTTPCycleState.RESPONSE
and message["type"] == "http.response.body"
):

body = message.get("body", b"")
more_body = message.get("more_body", False)
self.buffer.write(body)
2 changes: 0 additions & 2 deletions mangum/protocols/lifespan.py
Original file line number Diff line number Diff line change
@@ -98,14 +98,12 @@ async def run(self) -> None:
async def receive(self) -> Message:
"""Awaited by the application to receive ASGI `lifespan` events."""
if self.state is LifespanCycleState.CONNECTING:

# Connection established. The next event returned by the queue will be
# `lifespan.startup` to inform the application that the connection is
# ready to receive lfiespan messages.
self.state = LifespanCycleState.STARTUP

elif self.state is LifespanCycleState.STARTUP:

# Connection shutting down. The next event returned by the queue will be
# `lifespan.shutdown` to inform the application that the connection is now
# closing so that it may perform cleanup.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -11,3 +11,4 @@ brotli
brotli-asgi
mkdocs
mkdocs-material
cached-property
24 changes: 24 additions & 0 deletions tests/handlers/test_alb.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
2. https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html # noqa: E501
"""
from typing import Dict, List, Optional
from unittest import mock

import pytest

@@ -409,3 +410,26 @@ async def app(scope, receive, send):
"content-type": "text/plain; charset=utf-8",
}
assert response == expected_response


def test_aws_alb_scope_called_once() -> None:
async def app(scope, receive, send):
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [
[b"content-type", b"text/plain; charset=utf-8"],
],
}
)
await send({"type": "http.response.body", "body": b"Hello, world!"})

handler = Mangum(app, lifespan="off")
event = get_mock_aws_alb_event("GET", "/test", {}, None, None, False, False)

with mock.patch(
"mangum.handlers.alb.transform_headers",
) as mock_func:
handler(event, {})
mock_func.assert_called_once()
25 changes: 25 additions & 0 deletions tests/handlers/test_api_gateway.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import urllib.parse
from unittest import mock

import pytest

@@ -429,3 +430,27 @@ async def app(scope, receive, send):
"multiValueHeaders": {},
"body": "Hello world",
}


def test_aws_api_gateway_scope_called_once():
async def app(scope, receive, send):
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [
[b"content-type", b"text/plain; charset=utf-8"],
],
}
)
await send({"type": "http.response.body", "body": b"Hello world"})

event = get_mock_aws_api_gateway_event("GET", "/test", {}, None, False)

handler = Mangum(app, lifespan="off")

with mock.patch(
"mangum.handlers.api_gateway._handle_multi_value_headers_for_request",
) as mock_func:
handler(event, {})
mock_func.assert_called_once()
23 changes: 23 additions & 0 deletions tests/handlers/test_http_gateway.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import urllib.parse
from unittest import mock

import pytest

@@ -687,3 +688,25 @@ async def app(scope, receive, send):
"headers": {"content-type": content_type.decode()},
"body": utf_res_body,
}


def test_aws_http_gateway_scope_called_once():
async def app(scope, receive, send):
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [[b"header1", b"value1"], [b"header2", b"value1, value2"]],
}
)
await send({"type": "http.response.body", "body": b"Hello world"})

event = get_mock_aws_http_gateway_event_v2("GET", "/test", {}, None, False)

handler = Mangum(app, lifespan="off")

with mock.patch(
"mangum.handlers.api_gateway.strip_api_gateway_path",
) as mock_func:
handler(event, {})
mock_func.assert_called_once()
22 changes: 22 additions & 0 deletions tests/handlers/test_lambda_at_edge.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import urllib.parse
from unittest import mock

import pytest

@@ -373,3 +374,24 @@ async def app(scope, receive, send):
},
"body": "Hello world",
}


def test_aws_lambda_at_edge_scope_called_once():
async def app(scope, receive, send):
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [[b"content-type", b"text/plain; charset=utf-8"]],
}
)
await send({"type": "http.response.body", "body": b"Hello world"})

event = mock_lambda_at_edge_event("GET", "/test", {}, None, False)
handler = Mangum(app, lifespan="off")

with mock.patch.object(
LambdaAtEdge, "context", new_callable=mock.PropertyMock, create=True
) as mock_ctx:
handler(event, {})
assert mock_ctx.call_count == 2