Skip to content
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
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ The adapter accepts various arguments for configuring lifespan, logging, HTTP, W
```python
handler = Mangum(
app,
enable_lifespan=True,
lifespan="auto",
log_level="info",
api_gateway_base_path=None,
text_mime_types=None,
Expand All @@ -80,17 +80,32 @@ handler = Mangum(

### Parameters

- `app` : ***ASGI application***
- `app` : **ASGI application**

An asynchronous callable that conforms to ASGI specification version 3.0. This will usually be a framework application instance that exposes a valid ASGI callable.

- `enable_lifespan` : **bool**
- `lifespan` : **str** (`auto`|`on`|`off`)

Specify whether or not to enable lifespan support. The adapter will automatically determine if lifespan is supported by the framework unless explicitly disabled.
Specify lifespan support option. Default: `auto`.

* `auto`
Application support for lifespan will be inferred.

Any error that occurs during startup will be logged and the request will continue being handled unless a `lifespan.startup.failed` event is sent by the application.

* `on`:
Application support for lifespan is explicitly set.

Any error that occurs during startup will be raised and a 500 response will be returned.

* `off`:
Application support for lifespan should be ignored.

The application will not enter the lifespan cycle context.

- `log_level` : **str**
- `log_level` : **str** (`info`|`critical`|`error`|`warning`|`debug`)

Level parameter for the logger.
Level parameter for the logger. Default: `info`.

- `api_gateway_base_path` : **str**

Expand Down
4 changes: 2 additions & 2 deletions docs/asgi.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")

application = get_asgi_application()

handler = Mangum(application, enable_lifespan=False)
handler = Mangum(application, lifespan="off")
```

This example looks a bit different than the others because it is based on Django's standard project configuration, but the ASGI behaviour is the same.
Expand All @@ -199,7 +199,7 @@ django.setup()
application = get_default_application()

wrapped_application = guarantee_single_callable(application)
handler = Mangum(wrapped_application, enable_lifespan=False)
handler = Mangum(wrapped_application, lifespan="off")
```

## Middleware
Expand Down
27 changes: 21 additions & 6 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ The adapter accepts various arguments for configuring lifespan, logging, HTTP, W
```python
handler = Mangum(
app,
enable_lifespan=True,
lifespan="auto",
log_level="info",
api_gateway_base_path=None,
text_mime_types=None,
Expand All @@ -80,17 +80,32 @@ handler = Mangum(

### Parameters

- `app` : ***ASGI application***
- `app` : **ASGI application**

An asynchronous callable that conforms to ASGI specification version 3.0. This will usually be a framework application instance that exposes a valid ASGI callable.

- `enable_lifespan` : **bool**
- `lifespan` : **str** (`auto`|`on`|`off`)

Specify whether or not to enable lifespan support. The adapter will automatically determine if lifespan is supported by the framework unless explicitly disabled.
Specify lifespan support option. Default: `auto`.

* `auto`
Application support for lifespan will be inferred.

Any error that occurs during startup will be logged and the request will continue being handled unless a `lifespan.startup.failed` event is sent by the application.

* `on`:
Application support for lifespan is explicitly set.

Any error that occurs during startup will be raised and a 500 response will be returned.

* `off`:
Application support for lifespan should be ignored.

The application will not enter the lifespan cycle context.

- `log_level` : **str**
- `log_level` : **str** (`info`|`critical`|`error`|`warning`|`debug`)

Level parameter for the logger.
Level parameter for the logger. Default: `info`.

- `api_gateway_base_path` : **str**

Expand Down
2 changes: 1 addition & 1 deletion docs/websockets.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ A connected client has sent a message. The adapter will retrieve the initial req

The client or the server disconnects from the API. The adapter will remove the connection from the backend.

### Backends
## Backends

A data source, such as a cloud database, is required in order to persist the connection identifiers in a 'serverless' environment. Any data source can be used as long as it is accessible remotely to the AWS Lambda function.

Expand Down
81 changes: 53 additions & 28 deletions mangum/adapter.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import base64
import asyncio
import urllib.parse
import typing
import logging
import os
import warnings
from contextlib import ExitStack
from dataclasses import dataclass

from mangum.lifespan import Lifespan
from mangum.types import ASGIApp
from mangum.protocols.lifespan import LifespanCycle
from mangum.protocols.http import HTTPCycle
from mangum.protocols.ws import WebSocketCycle
from mangum.protocols.websockets import WebSocketCycle
from mangum.websocket import WebSocket
from mangum.exceptions import ConfigurationError

Expand Down Expand Up @@ -52,23 +53,51 @@ def get_logger(log_level: str) -> logging.Logger:

@dataclass
class Mangum:
"""
Creates an adapter instance.

**Parameters:**

* **app** - An asynchronous callable that conforms to version 3.0 of the ASGI
specification. This will usually be an ASGI framework application instance.
* **lifespan** - A string to configure lifespan support. Choices are `auto`, `on`,
and `off`. Default is `auto`.
* **log_level** - A string to configure the log level. Choices are: `info`,
`critical`, `error`, `warning`, and `debug`. Default is `info`.
* **api_gateway_base_path** - Base path to strip from URL when using a custom
domain name.
* **text_mime_types** - A list of MIME types to include with the defaults that
should never return a binary response in API Gateway.
* **dsn** - A connection string required to configure a supported WebSocket backend.
* **api_gateway_endpoint_url** - A string endpoint url to use for API Gateway when
sending data to WebSocket connections. Default is `None`.
* **api_gateway_region_name** - A string region name to use for API Gateway when
sending data to WebSocket connections. Default is `AWS_REGION` environment variable.
"""

app: ASGIApp
enable_lifespan: bool = True
lifespan: str = "auto"
log_level: str = "info"
api_gateway_base_path: typing.Optional[str] = None
text_mime_types: typing.Optional[typing.List[str]] = None
dsn: typing.Optional[str] = None
api_gateway_endpoint_url: typing.Optional[str] = None
api_gateway_region_name: typing.Optional[str] = None
enable_lifespan: bool = True # Deprecated.

def __post_init__(self) -> None:
self.logger = get_logger(self.log_level)
if self.enable_lifespan:
loop = asyncio.get_event_loop()
self.lifespan = Lifespan(self.app)
loop.create_task(self.lifespan.run())
loop.run_until_complete(self.lifespan.startup())
if not self.enable_lifespan: # pragma: no cover
warnings.warn(
"The `enable_lifespan` parameter will be removed in a future release. "
"It is replaced by `lifespan` setting.",
DeprecationWarning,
stacklevel=2,
)
if self.lifespan not in ("auto", "on", "off"): # pragma: no cover
raise ConfigurationError(
"Invalid argument supplied for `lifespan`. Choices are: auto|on|off."
)

def __call__(self, event: dict, context: dict) -> dict:
response = self.handler(event, context)
Expand All @@ -84,16 +113,18 @@ def strip_base_path(self, path: str) -> str:
return urllib.parse.unquote(path or "/")

def handler(self, event: dict, context: dict) -> dict:
if "eventType" in event["requestContext"]:
response = self.handle_ws(event, context)
else:
is_http_api = "http" in event["requestContext"]
response = self.handle_http(event, context, is_http_api=is_http_api)

if self.enable_lifespan:
if self.lifespan.is_supported:
loop = asyncio.get_event_loop()
loop.run_until_complete(self.lifespan.shutdown())
with ExitStack() as stack:
if self.lifespan in ("auto", "on"):
asgi_cycle: typing.ContextManager = LifespanCycle(
self.app, self.lifespan
)
stack.enter_context(asgi_cycle)

if "eventType" in event["requestContext"]:
response = self.handle_ws(event, context)
else:
is_http_api = "http" in event["requestContext"]
response = self.handle_http(event, context, is_http_api=is_http_api)

return response

Expand Down Expand Up @@ -155,10 +186,7 @@ def handle_http(self, event: dict, context: dict, *, is_http_api: bool) -> dict:
text_mime_types = DEFAULT_TEXT_MIME_TYPES

asgi_cycle = HTTPCycle(
scope, text_mime_types=text_mime_types, log_level=self.log_level
)
asgi_cycle.put_message(
{"type": "http.request", "body": body, "more_body": False}
scope, body=body, text_mime_types=text_mime_types, log_level=self.log_level
)
response = asgi_cycle(self.app)

Expand Down Expand Up @@ -220,12 +248,9 @@ def handle_ws(self, event: dict, context: dict) -> dict:

elif event_type == "MESSAGE":
websocket.fetch()
asgi_cycle = WebSocketCycle(websocket, log_level=self.log_level)
asgi_cycle.put_message({"type": "websocket.connect"})
asgi_cycle.put_message(
{"type": "websocket.receive", "bytes": None, "text": event["body"]}
asgi_cycle = WebSocketCycle(
event["body"], websocket=websocket, log_level=self.log_level
)

response = asgi_cycle(self.app)

elif event_type == "DISCONNECT":
Expand Down
14 changes: 13 additions & 1 deletion mangum/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
class LifespanFailure(Exception):
"""Raise when an error occurs in a lifespan event"""
"""Raise when a lifespan failure event is sent by an application."""


class LifespanUnsupported(Exception):
"""Raise when lifespan events are not supported by an application."""


class UnexpectedMessage(Exception):
"""Raise when an unexpected message type is received during an ASGI cycle."""


class WebSocketClosed(Exception):
"""Raise when an application closes the connection during the handshake."""


class WebSocketError(Exception):
Expand Down
67 changes: 0 additions & 67 deletions mangum/lifespan.py

This file was deleted.

Loading