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

Update project #326

Merged
merged 3 commits into from
Sep 19, 2024
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
35 changes: 17 additions & 18 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,25 @@ jobs:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v2
name: Configure pip caching
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
version: "0.4.12"
enable-cache: true

- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}

- name: Install dependencies
run: |
pip install -U -r requirements.txt
run: uv sync --frozen

- name: Run tests
run: |
scripts/test
run: scripts/test

- name: Run linters
run: |
scripts/lint
run: scripts/lint

- name: Run codecov
run: codecov
run: uv run codecov
4 changes: 1 addition & 3 deletions mangum/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ def __init__(
exclude_headers: Optional[List[str]] = None,
) -> None:
if lifespan not in ("auto", "on", "off"):
raise ConfigurationError(
"Invalid argument supplied for `lifespan`. Choices are: auto|on|off"
)
raise ConfigurationError("Invalid argument supplied for `lifespan`. Choices are: auto|on|off")

self.app = app
self.lifespan = lifespan
Expand Down
18 changes: 6 additions & 12 deletions mangum/handlers/alb.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ def encode_query_string_for_alb(params: QueryParams) -> bytes:
"them. You must decode them in your Lambda function."
"""
params = {
unquote_plus(key): unquote_plus(value)
if isinstance(value, str)
else tuple(unquote_plus(element) for element in value)
unquote_plus(key): (
unquote_plus(value) if isinstance(value, str) else tuple(unquote_plus(element) for element in value)
)
for key, value in params.items()
}
query_string = urlencode(params, doseq=True).encode()
Expand All @@ -83,14 +83,10 @@ def transform_headers(event: LambdaEvent) -> List[Tuple[bytes, bytes]]:

class ALB:
@classmethod
def infer(
cls, event: LambdaEvent, context: LambdaContext, config: LambdaConfig
) -> bool:
def infer(cls, event: LambdaEvent, context: LambdaContext, config: LambdaConfig) -> bool:
return "requestContext" in event and "elb" in event["requestContext"]

def __init__(
self, event: LambdaEvent, context: LambdaContext, config: LambdaConfig
) -> None:
def __init__(self, event: LambdaEvent, context: LambdaContext, config: LambdaConfig) -> None:
self.event = event
self.context = context
self.config = config
Expand Down Expand Up @@ -167,9 +163,7 @@ def __call__(self, response: Response) -> dict:
# headers otherwise.
multi_value_headers_enabled = "multiValueHeaders" in self.scope["aws.event"]
if multi_value_headers_enabled:
out["multiValueHeaders"] = handle_exclude_headers(
multi_value_headers, self.config
)
out["multiValueHeaders"] = handle_exclude_headers(multi_value_headers, self.config)
else:
out["headers"] = handle_exclude_headers(finalized_headers, self.config)

Expand Down
36 changes: 9 additions & 27 deletions mangum/handlers/api_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,18 @@ def _combine_headers_v2(
cookies.append(normalized_value)
else:
if normalized_key in output_headers:
normalized_value = (
f"{output_headers[normalized_key]},{normalized_value}"
)
normalized_value = f"{output_headers[normalized_key]},{normalized_value}"
output_headers[normalized_key] = normalized_value

return output_headers, cookies


class APIGateway:
@classmethod
def infer(
cls, event: LambdaEvent, context: LambdaContext, config: LambdaConfig
) -> bool:
def infer(cls, event: LambdaEvent, context: LambdaContext, config: LambdaConfig) -> bool:
return "resource" in event and "requestContext" in event

def __init__(
self, event: LambdaEvent, context: LambdaContext, config: LambdaConfig
) -> None:
def __init__(self, event: LambdaEvent, context: LambdaContext, config: LambdaConfig) -> None:
self.event = event
self.context = context
self.config = config
Expand Down Expand Up @@ -112,34 +106,26 @@ def scope(self) -> Scope:
}

def __call__(self, response: Response) -> dict:
finalized_headers, multi_value_headers = handle_multi_value_headers(
response["headers"]
)
finalized_headers, multi_value_headers = handle_multi_value_headers(response["headers"])
finalized_body, is_base64_encoded = handle_base64_response_body(
response["body"], finalized_headers, self.config["text_mime_types"]
)

return {
"statusCode": response["status"],
"headers": handle_exclude_headers(finalized_headers, self.config),
"multiValueHeaders": handle_exclude_headers(
multi_value_headers, self.config
),
"multiValueHeaders": handle_exclude_headers(multi_value_headers, self.config),
"body": finalized_body,
"isBase64Encoded": is_base64_encoded,
}


class HTTPGateway:
@classmethod
def infer(
cls, event: LambdaEvent, context: LambdaContext, config: LambdaConfig
) -> bool:
def infer(cls, event: LambdaEvent, context: LambdaContext, config: LambdaConfig) -> bool:
return "version" in event and "requestContext" in event

def __init__(
self, event: LambdaEvent, context: LambdaContext, config: LambdaConfig
) -> None:
def __init__(self, event: LambdaEvent, context: LambdaContext, config: LambdaConfig) -> None:
self.event = event
self.context = context
self.config = config
Expand Down Expand Up @@ -216,13 +202,9 @@ def __call__(self, response: Response) -> dict:
"cookies": cookies or None,
"isBase64Encoded": is_base64_encoded,
}
return {
key: value for key, value in response_out.items() if value is not None
}
return {key: value for key, value in response_out.items() if value is not None}

finalized_headers, multi_value_headers = handle_multi_value_headers(
response["headers"]
)
finalized_headers, multi_value_headers = handle_multi_value_headers(response["headers"])
finalized_body, is_base64_encoded = handle_base64_response_body(
response["body"], finalized_headers, self.config["text_mime_types"]
)
Expand Down
19 changes: 4 additions & 15 deletions mangum/handlers/lambda_at_edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,14 @@

class LambdaAtEdge:
@classmethod
def infer(
cls, event: LambdaEvent, context: LambdaContext, config: LambdaConfig
) -> bool:
return (
"Records" in event
and len(event["Records"]) > 0
and "cf" in event["Records"][0]
)
def infer(cls, event: LambdaEvent, context: LambdaContext, config: LambdaConfig) -> bool:
return "Records" in event and len(event["Records"]) > 0 and "cf" in event["Records"][0]

# FIXME: Since this is the last in the chain it doesn't get coverage by default,
# # just ignoring it for now.
# return None # pragma: nocover

def __init__(
self, event: LambdaEvent, context: LambdaContext, config: LambdaConfig
) -> None:
def __init__(self, event: LambdaEvent, context: LambdaContext, config: LambdaConfig) -> None:
self.event = event
self.context = context
self.config = config
Expand Down Expand Up @@ -61,10 +53,7 @@ def scope(self) -> Scope:
"type": "http",
"method": http_method,
"http_version": "1.1",
"headers": [
[k.encode(), v[0]["value"].encode()]
for k, v in cf_request["headers"].items()
],
"headers": [[k.encode(), v[0]["value"].encode()] for k, v in cf_request["headers"].items()],
"path": cf_request["uri"],
"raw_path": None,
"root_path": "",
Expand Down
4 changes: 1 addition & 3 deletions mangum/handlers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@ def handle_base64_response_body(
return output_body, is_base64_encoded


def handle_exclude_headers(
headers: Dict[str, Any], config: LambdaConfig
) -> Dict[str, Any]:
def handle_exclude_headers(headers: Dict[str, Any], config: LambdaConfig) -> Dict[str, Any]:
finalized_headers = {}
for header_key, header_value in headers.items():
if header_key in config["exclude_headers"]:
Expand Down
10 changes: 2 additions & 8 deletions mangum/protocols/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,11 @@ async def receive(self) -> Message:
return await self.app_queue.get() # pragma: no cover

async def send(self, message: Message) -> None:
if (
self.state is HTTPCycleState.REQUEST
and message["type"] == "http.response.start"
):
if self.state is HTTPCycleState.REQUEST and message["type"] == "http.response.start":
self.status = message["status"]
self.headers = message.get("headers", [])
self.state = HTTPCycleState.RESPONSE
elif (
self.state is HTTPCycleState.RESPONSE
and message["type"] == "http.response.body"
):
elif self.state is HTTPCycleState.RESPONSE and message["type"] == "http.response.body":

body = message.get("body", b"")
more_body = message.get("more_body", False)
Expand Down
8 changes: 2 additions & 6 deletions mangum/protocols/lifespan.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,11 @@ async def receive(self) -> Message:
async def send(self, message: Message) -> None:
"""Awaited by the application to send ASGI `lifespan` events."""
message_type = message["type"]
self.logger.info(
"%s: '%s' event received from application.", self.state, message_type
)
self.logger.info("%s: '%s' event received from application.", self.state, message_type)

if self.state is LifespanCycleState.CONNECTING:
if self.lifespan == "on":
raise LifespanFailure(
"Lifespan connection failed during startup and lifespan is 'on'."
)
raise LifespanFailure("Lifespan connection failed during startup and lifespan is 'on'.")

# If a message is sent before the startup event is received by the
# application, then assume that lifespan is unsupported.
Expand Down
20 changes: 6 additions & 14 deletions mangum/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,7 @@ def get_remaining_time_in_millis(self) -> int:


class ASGI(Protocol):
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
... # pragma: no cover
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: ... # pragma: no cover


LifespanMode: TypeAlias = Literal["auto", "on", "off"]
Expand All @@ -121,22 +120,15 @@ class LambdaConfig(TypedDict):


class LambdaHandler(Protocol):
def __init__(self, *args: Any) -> None:
... # pragma: no cover
def __init__(self, *args: Any) -> None: ... # pragma: no cover

@classmethod
def infer(
cls, event: LambdaEvent, context: LambdaContext, config: LambdaConfig
) -> bool:
... # pragma: no cover
def infer(cls, event: LambdaEvent, context: LambdaContext, config: LambdaConfig) -> bool: ... # pragma: no cover

@property
def body(self) -> bytes:
... # pragma: no cover
def body(self) -> bytes: ... # pragma: no cover

@property
def scope(self) -> Scope:
... # pragma: no cover
def scope(self) -> Scope: ... # pragma: no cover

def __call__(self, response: Response) -> dict:
... # pragma: no cover
def __call__(self, response: Response) -> dict: ... # pragma: no cover
51 changes: 51 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "mangum"
version = "0.17.0"
authors = [
{ name = "Jordan Eremieff", email = "[email protected]" },
{ name = "Marcelo Trylesinski", email = "[email protected]" },
]
description = "AWS Lambda support for ASGI applications"
readme = "README.md"
requires-python = ">=3.8"
classifiers = [
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Internet :: WWW/HTTP",
]
dependencies = ["typing_extensions"]

[tool.uv]
dev-dependencies = [
"pytest",
"codecov",
"pytest-cov",
"black",
"flake8",
"starlette",
"quart",
"mypy",
"brotli",
"brotli-asgi",
"mkdocs",
"mkdocs-material",
]

[project.urls]
Homepage = "https://github.com/Kludex/mangum"
Documentation = "https://mangum.fastapiexpert.com"
Changelog = "https://github.com/Kludex/mangum/blob/main/CHANGELOG.md"
Funding = "https://github.com/sponsors/Kludex"
Source = "https://github.com/Kludex/mangum"

[tool.black]
line-length = 120
13 changes: 0 additions & 13 deletions requirements.txt

This file was deleted.

11 changes: 3 additions & 8 deletions scripts/lint
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
#!/bin/sh -e

export PREFIX=""
if [ -d 'venv' ] ; then
export PREFIX="venv/bin/"
fi

set -x

${PREFIX}black mangum tests --check
${PREFIX}mypy mangum
${PREFIX}flake8 mangum tests
uv run black mangum tests --check
uv run mypy mangum
uv run flake8 mangum tests
3 changes: 1 addition & 2 deletions scripts/setup
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/bin/sh -ex

python3.7 -m venv venv
venv/bin/pip install -r requirements.txt
uv sync --frozen
Loading