From 85057887257d0382af5abb583ca8e3b644071ac5 Mon Sep 17 00:00:00 2001
From: Jordan Eremieff <1376648+jordaneremieff@users.noreply.github.com>
Date: Sun, 13 Feb 2022 00:08:21 +1100
Subject: [PATCH] :fire: Remove WebSocket support, reduce some of the noise
generally in preparation for refactoring. (#234)
---
docs/adapter.md | 5 +-
docs/index.md | 4 +-
docs/websockets.md | 300 -------------------
mangum/adapter.py | 43 +--
mangum/backends/__init__.py | 199 ------------
mangum/backends/base.py | 44 ---
mangum/backends/dynamodb.py | 93 ------
mangum/backends/postgresql.py | 45 ---
mangum/backends/redis.py | 27 --
mangum/backends/s3.py | 90 ------
mangum/backends/sqlite.py | 48 ---
mangum/exceptions.py | 8 -
mangum/handlers/__init__.py | 2 -
mangum/handlers/abstract_handler.py | 34 +--
mangum/handlers/aws_alb.py | 12 -
mangum/handlers/aws_api_gateway.py | 10 -
mangum/handlers/aws_cf_lambda_at_edge.py | 10 -
mangum/handlers/aws_http_gateway.py | 1 -
mangum/handlers/aws_ws_gateway.py | 75 -----
mangum/protocols/__init__.py | 8 +-
mangum/protocols/websockets.py | 198 ------------
mangum/types.py | 29 +-
mkdocs.yml | 25 +-
pytest.ini | 3 +-
requirements.txt | 11 -
tests/conftest.py | 191 ------------
tests/handlers/test_aws_alb.py | 2 -
tests/handlers/test_aws_api_gateway.py | 3 -
tests/handlers/test_aws_cf_lambda_at_edge.py | 3 -
tests/handlers/test_aws_http_gateway.py | 6 -
tests/handlers/test_aws_ws_gateway.py | 68 -----
tests/integration/__init__.py | 0
tests/integration/docker-compose.yml | 14 -
tests/integration/mock_server.py | 81 -----
tests/integration/test_backends.py | 237 ---------------
tests/test_http.py | 3 -
tests/test_websockets.py | 109 -------
37 files changed, 27 insertions(+), 2014 deletions(-)
delete mode 100644 docs/websockets.md
delete mode 100644 mangum/backends/__init__.py
delete mode 100644 mangum/backends/base.py
delete mode 100644 mangum/backends/dynamodb.py
delete mode 100644 mangum/backends/postgresql.py
delete mode 100644 mangum/backends/redis.py
delete mode 100644 mangum/backends/s3.py
delete mode 100644 mangum/backends/sqlite.py
delete mode 100644 mangum/handlers/aws_ws_gateway.py
delete mode 100644 mangum/protocols/websockets.py
delete mode 100644 tests/handlers/test_aws_ws_gateway.py
delete mode 100644 tests/integration/__init__.py
delete mode 100644 tests/integration/docker-compose.yml
delete mode 100644 tests/integration/mock_server.py
delete mode 100644 tests/integration/test_backends.py
delete mode 100644 tests/test_websockets.py
diff --git a/docs/adapter.md b/docs/adapter.md
index 3400d0f2..a94718d6 100644
--- a/docs/adapter.md
+++ b/docs/adapter.md
@@ -8,13 +8,10 @@ handler = Mangum(
lifespan="auto",
api_gateway_base_path=None,
text_mime_types=None,
- dsn=None,
- api_gateway_endpoint_url=None,
- api_gateway_region_name=None
)
```
-All arguments are optional, but some may be necessary for specific use-cases (e.g. dsn is only required for WebSocket support).
+All arguments are optional.
## Configuring an adapter instance
diff --git a/docs/index.md b/docs/index.md
index 84c564e6..e871acfb 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -14,9 +14,7 @@ Mangum is an adapter for using [ASGI](https://asgi.readthedocs.io/en/latest/) ap
## Features
-- API Gateway support for [HTTP](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html), [REST](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html), and [WebSocket](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api.html) APIs.
-
-- Multiple storage backend interfaces for managing WebSocket connections.
+- API Gateway support for [HTTP](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html) and [REST](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html) APIs.
- Compatibility with ASGI application frameworks, such as [Starlette](https://www.starlette.io/), [FastAPI](https://fastapi.tiangolo.com/), and [Quart](https://pgjones.gitlab.io/quart/).
diff --git a/docs/websockets.md b/docs/websockets.md
deleted file mode 100644
index 52fecd6b..00000000
--- a/docs/websockets.md
+++ /dev/null
@@ -1,300 +0,0 @@
-# WebSockets
-
-Mangum provides support for [WebSocket API](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api.html) events in API Gateway. The adapter class handles parsing the incoming requests and managing the ASGI cycle using a configured storage backend.
-
-```python
-import os
-
-from fastapi import FastAPI, WebSocket
-from fastapi.responses import HTMLResponse
-from mangum import Mangum
-
-DSN_URL = os.environ["DSN_URL"]
-WEBSOCKET_URL = os.environ["WEBSOCKET_URL"]
-HTML = """
-
-
-
- Chat
-
-
-
- WebSocket Chat
-
-
-
-
-
-
-
-""" % WEBSOCKET_URL
-
-app = FastAPI()
-
-@app.get("/")
-async def get():
- return HTMLResponse(HTML)
-
-@app.websocket("/")
-async def websocket_endpoint(websocket: WebSocket):
- await websocket.accept()
- while True:
- data = await websocket.receive_text()
- await websocket.send_text(f"Message text was: {data}")
-
-handler = Mangum(app, dsn=DSN_URL)
-```
-
-## Dependencies
-
-The WebSocket implementation requires the following extra packages:
-
-```
-pip install httpx boto3
-```
-
-## Configuring a storage backend
-
-A data source is required in order to persist the WebSocket client connections stored in API Gateway*. Any data source can be used as long as it is accessible remotely to the AWS Lambda function. All supported backends require a `dsn` connection string argument to configure the connection between the adapter and the data source.
-
-```python
-handler = Mangum(app, dsn="[postgresql|redis|dynamodb|s3|sqlite]://[...]")
-```
-
-*Read the section on ([handling events in API Gateway](https://mangum.io/websockets/#handling-api-gateway-events) for more information.)
-
-### Supported backends
-
-The following backends are currently supported:
-
- - `dynamodb`
- - `s3`
- - `postgresql`
- - `redis`
- - `sqlite` (for local debugging)
-
-#### DynamoDB
-
-The `DynamoDBBackend` uses a [DynamoDB](https://aws.amazon.com/dynamodb/) table to store the connection details.
-
-##### Usage
-
-```python
-handler = Mangum(
- app,
- dsn="dynamodb://mytable"
-)
-```
-
-###### Parameters
-
-The DynamoDB backend `dsn` uses the following connection string syntax:
-
-```
-dynamodb://[?region=&endpoint_url=]
-```
-
-- `table_name` (Required)
-
- The name of the table in DynamoDB.
-
-- `region_name`
-
- The region name of the DynamoDB table.
-
-- `endpoint_url`
-
- The endpoint url to use in DynamoDB calls. This is useful if you are debugging locally with a package such as [serverless-dynamodb-local](https://github.com/99xt/serverless-dynamodb-local).
-
-###### Dependencies
-
-This backend requires the following extra package:
-
-```
-pip install aioboto3
-```
-
-#### S3
-
-The `S3Backend` uses an [S3](https://aws.amazon.com/s3/) bucket as a key-value store to store the connection details.
-
-##### Usage
-
-```python
-handler = Mangum(
- app,
- dsn="s3://my-bucket-12345"
-)
-```
-
-###### Parameters
-
-The S3 backend `dsn` uses the following connection string syntax:
-
-```
-s3://[/key/...][?region=&endpoint_url=]
-```
-
-- `bucket` (Required)
-
- The name of the bucket in S3.
-
-- `region_name`
-
- The region name of the S3 bucket.
-
-- `endpoint_url`
-
- The endpoint url to use in S3 calls. This is useful if you are debugging locally with a package such as [serverless-s3-local](https://github.com/ar90n/serverless-s3-local).
-
-###### Dependencies
-
-This backend requires the following extra package:
-
-```
-pip install aioboto3
-```
-
-#### PostgreSQL
-
-The `PostgreSQLBackend` requires [psycopg2](https://github.com/psycopg/psycopg2) and access to a remote PostgreSQL database.
-
-##### Usage
-
-```python
-handler = Mangum(
- app,
- dsn="postgresql://myuser:mysecret@my.host:5432/mydb"
-)
-```
-
-###### Parameters
-
-The PostgreSQL backend `dsn` uses the following connection string syntax:
-
-```
-postgresql://[user[:password]@][host][:port][,...][/dbname][?param1=value1&...]
-```
-
-- `host` (Required)
-
- The network location of the PostgreSQL database
-
-Read more about the supported uri schemes and additional parameters [here](https://www.postgresql.org/docs/10/libpq-connect.html#LIBPQ-CONNSTRING).
-
-###### Dependencies
-
-This backend requires the following extra package:
-
-```
-pip install aiopg
-```
-
-#### Redis
-
-The `RedisBackend` requires [redis-py](https://github.com/andymccurdy/redis-py) and access to a Redis server.
-
-##### Usage
-
-```python
-handler = Mangum(
- app,
- dsn="redis://:mysecret@my.host:6379/0"
-)
-```
-
-##### Parameters
-
-The Redis backend `dsn` uses the following connection string syntax:
-
-```
-redis://[[user:]password@]host[:port][/database]
-```
-
-- `host` (Required)
-
- The network location of the Redis server.
-
-Read more about the supported uri schemes and additional parameters [here](https://www.iana.org/assignments/uri-schemes/prov/redis).
-
-###### Dependencies
-
-This backend requires the following extra package:
-
-```
-pip install aioredis
-```
-
-#### SQLite
-
-The `sqlite` backend uses a local [sqlite3](https://docs.python.org/3/library/sqlite3.html) database to store connection. It is intended for local debugging.
-
-##### Usage
-
-```python
-handler = Mangum(
- app,
- dsn="sqlite://mydbfile.sqlite3"
-)
-```
-
-##### Parameters
-
-The SQLite backend uses the following connection string syntax:
-
-```
-sqlite://[file_path].db
-```
-
-- `file_path` (Required)
-
- The file name or path to an sqlite3 database file. If one does not exist, then it will be created automatically.
-
-## State machine
-
-The `WebSocketCycle` is used by the adapter to communicate message events between the application and WebSocket client connections in API Gateway using a storage backend to persist the connection `scope`. It is a state machine that handles the ASGI request and response cycle for each individual message sent by a client.
-
-### WebSocketCycle
-
-::: mangum.protocols.websockets.WebSocketCycle
- :docstring:
- :members: run receive send
-
-#### API Gateway events
-
-There are three WebSocket events sent by API Gateway for a WebSocket API connection. Each event requires returning a response immediately, and the information required to create the connection scope is only available in the initial `CONNECT` event. Messages are only sent in `MESSAGE` events that occur after the initial connection is established, and they do not include the details of the initial connect event. Due to the stateless nature of AWS Lambda, a storage backend is required to persist the WebSocket connection details for the duration of a client connection.
-
-##### CONNECT
-
-A persistent connection between the client and a WebSocket API is being initiated. The adapter uses a supported WebSocket backend to store the connection id and initial request information.
-
-##### MESSAGE
-
-A connected client has sent a message. The adapter will retrieve the initial request information from the backend using the connection id to form the ASGI connection `scope` and run the ASGI application cycle.
-
-##### DISCONNECT
-
-The client or the server disconnects from the API. The adapter will remove the connection from the backend.
-
-### WebSocketCycleState
-
-::: mangum.protocols.websockets.WebSocketCycleState
- :docstring:
diff --git a/mangum/adapter.py b/mangum/adapter.py
index 52dffc00..7811feed 100644
--- a/mangum/adapter.py
+++ b/mangum/adapter.py
@@ -1,17 +1,11 @@
import logging
from contextlib import ExitStack
-from typing import (
- Any,
- Dict,
- Optional,
- TYPE_CHECKING,
-)
+from typing import Any, Dict, TYPE_CHECKING
from .exceptions import ConfigurationError
from .handlers import AbstractHandler
-from .protocols import HTTPCycle, WebSocketCycle, LifespanCycle
-from .backends import WebSocket
-from .types import ASGIApp, WsRequest
+from .protocols import HTTPCycle, LifespanCycle
+from .types import ASGIApp
if TYPE_CHECKING: # pragma: no cover
@@ -38,34 +32,21 @@ class Mangum:
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`.
- * **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 not return a binary response in API Gateway.
- * **dsn** - A connection string required to configure a supported WebSocket backend.
* **api_gateway_base_path** - A string specifying the part of the url path after
which the server routing begins.
- * **api_gateway_endpoint_url** - A string endpoint url to use for API Gateway when
- sending data to WebSocket connections. Default is to determine this automatically.
- * **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.
"""
def __init__(
self,
app: ASGIApp,
lifespan: str = "auto",
- dsn: Optional[str] = None,
api_gateway_base_path: str = "/",
- api_gateway_endpoint_url: Optional[str] = None,
- api_gateway_region_name: Optional[str] = None,
) -> None:
self.app = app
self.lifespan = lifespan
- self.dsn = dsn
self.api_gateway_base_path = api_gateway_base_path
- self.api_gateway_endpoint_url = api_gateway_endpoint_url
- self.api_gateway_region_name = api_gateway_region_name
if self.lifespan not in ("auto", "on", "off"):
raise ConfigurationError(
@@ -85,21 +66,7 @@ def __call__(self, event: Dict[str, Any], context: "LambdaContext") -> dict:
)
request = handler.request
- if isinstance(request, WsRequest):
- api_gateway_endpoint_url = (
- self.api_gateway_endpoint_url or handler.api_gateway_endpoint_url
- )
- websocket = WebSocket(
- dsn=self.dsn,
- api_gateway_endpoint_url=api_gateway_endpoint_url,
- api_gateway_region_name=self.api_gateway_region_name,
- )
- websocket_cycle = WebSocketCycle(
- request, handler.message_type, handler.connection_id, websocket
- )
- response = websocket_cycle(self.app, handler.body)
- else:
- http_cycle = HTTPCycle(request)
- response = http_cycle(self.app, handler.body)
+ http_cycle = HTTPCycle(request)
+ response = http_cycle(self.app, handler.body)
return handler.transform_response(response)
diff --git a/mangum/backends/__init__.py b/mangum/backends/__init__.py
deleted file mode 100644
index fd71c010..00000000
--- a/mangum/backends/__init__.py
+++ /dev/null
@@ -1,199 +0,0 @@
-import asyncio
-import logging
-from typing import Dict, Optional, Type
-import json
-from functools import partial
-from dataclasses import dataclass
-from urllib.parse import urlparse
-
-try:
- import httpx
- from httpx import AsyncClient, Response
-except ImportError: # pragma: no cover
- httpx = None # type: ignore
-
-try:
- import boto3
- from botocore.auth import SigV4Auth
- from botocore.awsrequest import AWSRequest
-except ImportError: # pragma: no cover
- boto3 = None # type: ignore
-
-
-from .base import WebSocketBackend
-from ..exceptions import WebSocketError, ConfigurationError
-from ..types import Scope
-
-
-def get_sigv4_headers(
- method: str,
- url: str,
- data: Optional[bytes] = None,
- region_name: Optional[str] = None,
-) -> Dict:
- if boto3 is None: # pragma: no cover
- raise WebSocketError("boto3 must be installed to use WebSockets.")
- session = boto3.Session()
- credentials = session.get_credentials()
- creds = credentials.get_frozen_credentials()
- region = region_name or session.region_name
-
- request = AWSRequest(method=method, url=url, data=data)
- SigV4Auth(creds, "execute-api", region).add_auth(request)
-
- return dict(request.headers)
-
-
-@dataclass
-class WebSocket:
- """
- A `WebSocket` connection handler interface for the
- selected `WebSocketBackend` subclass
- """
-
- dsn: Optional[str]
- api_gateway_endpoint_url: str
- api_gateway_region_name: Optional[str] = None
-
- def __post_init__(self) -> None:
- if not httpx: # pragma: no cover
- raise WebSocketError("httpx must be installed to use WebSockets.")
-
- if self.dsn is None:
- raise ConfigurationError(
- "The `dsn` parameter must be provided for WebSocket connections."
- )
-
- self.logger: logging.Logger = logging.getLogger("mangum.backends")
- parsed_dsn = urlparse(self.dsn)
- if not any((parsed_dsn.hostname, parsed_dsn.path)):
- raise ConfigurationError("Invalid value for `dsn` provided.")
-
- scheme = parsed_dsn.scheme
- self.logger.debug(
- f"Attempting WebSocket backend connection using scheme: {scheme}"
- )
-
- self._Backend: Type[WebSocketBackend]
- if scheme == "sqlite":
- self.logger.info(
- "The `SQLiteBackend` should be only be used for local "
- "debugging. It will not work in a deployed environment."
- )
- from mangum.backends.sqlite import SQLiteBackend
-
- self._Backend = SQLiteBackend
-
- elif scheme == "dynamodb":
- from mangum.backends.dynamodb import DynamoDBBackend
-
- self._Backend = DynamoDBBackend
-
- elif scheme == "s3":
- from mangum.backends.s3 import S3Backend
-
- self._Backend = S3Backend
-
- elif scheme in ("postgresql", "postgres"):
- from mangum.backends.postgresql import PostgreSQLBackend
-
- self._Backend = PostgreSQLBackend
-
- elif scheme == "redis":
- from mangum.backends.redis import RedisBackend
-
- self._Backend = RedisBackend
-
- else:
- raise ConfigurationError(f"{scheme} does not match a supported backend.")
-
- self.logger.info("WebSocket backend connection established.")
-
- async def load_scope(self, backend: WebSocketBackend, connection_id: str) -> Scope:
- loaded_scope = await backend.retrieve(connection_id)
- scope = json.loads(loaded_scope)
- scope.update(
- {
- "query_string": scope["query_string"].encode(),
- "headers": [[h[0].encode(), h[1].encode()] for h in scope["headers"]],
- "client": tuple(scope["client"]),
- "server": tuple(scope["server"]),
- }
- )
-
- return scope
-
- async def save_scope(
- self, backend: WebSocketBackend, connection_id: str, scope: Scope
- ) -> None:
- scope.update(
- {
- "query_string": scope["query_string"].decode(),
- "headers": [[h[0].decode(), h[1].decode()] for h in scope["headers"]],
- }
- )
- json_scope = json.dumps(scope)
- await backend.save(connection_id, json_scope=json_scope)
-
- async def on_connect(self, connection_id: str, initial_scope: Scope) -> None:
- self.logger.debug("Creating scope entry for %s", connection_id)
- async with self._Backend(self.dsn) as backend: # type: ignore
- await self.save_scope(backend, connection_id, initial_scope)
-
- async def on_message(self, connection_id: str) -> Scope:
- self.logger.debug("Retrieving scope entry for %s", connection_id)
- async with self._Backend(self.dsn) as backend: # type: ignore
- scope = await self.load_scope(backend, connection_id)
- return scope
-
- async def on_disconnect(self, connection_id: str) -> None:
- self.logger.debug("Deleting scope entry for %s", connection_id)
- async with self._Backend(self.dsn) as backend: # type: ignore
- await backend.delete(connection_id)
-
- async def post_to_connection(self, connection_id: str, body: bytes) -> None:
- async with AsyncClient() as client:
- await self._post_to_connection(connection_id, client=client, body=body)
-
- async def delete_connection(self, connection_id: str) -> None:
- async with AsyncClient() as client:
- await self._request_to_connection("DELETE", connection_id, client=client)
-
- async def _post_to_connection(
- self,
- connection_id: str,
- *,
- client: "AsyncClient",
- body: bytes,
- ) -> None: # pragma: no cover
- response = await self._request_to_connection(
- "POST", connection_id, client=client, body=body
- )
-
- if response.status_code == 410:
- await self.on_disconnect(connection_id)
- elif response.status_code != 200:
- raise WebSocketError(f"Error: {response.status_code}")
-
- async def _request_to_connection(
- self,
- method: str,
- connection_id: str,
- *,
- client: "AsyncClient",
- body: Optional[bytes] = None,
- ) -> "Response":
- loop = asyncio.get_event_loop()
- url = f"{self.api_gateway_endpoint_url}/{connection_id}"
- headers = await loop.run_in_executor(
- None,
- partial(
- get_sigv4_headers,
- method,
- url,
- body,
- self.api_gateway_region_name,
- ),
- )
-
- return await client.request(method, url, content=body, headers=headers)
diff --git a/mangum/backends/base.py b/mangum/backends/base.py
deleted file mode 100644
index cd0b3fb1..00000000
--- a/mangum/backends/base.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from dataclasses import dataclass
-from typing import Any
-
-
-@dataclass
-class WebSocketBackend: # pragma: no cover
- """
- Base class for implementing WebSocket backends to store API Gateway connections.
-
- Data source backends are required to implement configuration using the `dsn`
- connection string setting.
- """
-
- dsn: str
-
- async def __aenter__(self) -> "WebSocketBackend":
- """
- Establish the connection to a data source.
- """
- raise NotImplementedError()
-
- async def __aexit__(self, *exc_info: Any) -> None:
- """
- Closes the connection to a data source.
- """
- raise NotImplementedError()
-
- async def save(self, connection_id: str, *, json_scope: str) -> None:
- """
- Save the JSON scope for a connection.
- """
- raise NotImplementedError()
-
- async def retrieve(self, connection_id: str) -> str:
- """
- Retrieve the JSON scope for a connection.
- """
- raise NotImplementedError()
-
- async def delete(self, connection_id: str) -> None:
- """
- Delete the JSON scope for a connection.
- """
- raise NotImplementedError()
diff --git a/mangum/backends/dynamodb.py b/mangum/backends/dynamodb.py
deleted file mode 100644
index ddaeb9c6..00000000
--- a/mangum/backends/dynamodb.py
+++ /dev/null
@@ -1,93 +0,0 @@
-import os
-from typing import Any
-from urllib.parse import ParseResult, urlparse, parse_qs
-import logging
-
-import aioboto3
-from botocore.exceptions import ClientError
-
-from .base import WebSocketBackend
-from ..exceptions import WebSocketError
-
-logger = logging.getLogger("mangum.backends.dynamodb")
-
-
-def get_table_name(parsed_dsn: ParseResult) -> str:
- netloc = parsed_dsn.netloc
- _, _, hostinfo = netloc.rpartition("@")
- hostname, _, _ = hostinfo.partition(":")
- return hostname
-
-
-class DynamoDBBackend(WebSocketBackend):
- async def __aenter__(self) -> WebSocketBackend:
- parsed_dsn = urlparse(self.dsn)
- parsed_query = parse_qs(parsed_dsn.query)
- self.table_name = get_table_name(parsed_dsn)
- self.region_name = (
- parsed_query["region"][0]
- if "region" in parsed_query
- else os.environ["AWS_REGION"]
- )
- self.endpoint_url = (
- parsed_query["endpoint_url"][0] if "endpoint_url" in parsed_query else None
- )
-
- session = aioboto3.Session()
- self.resource = await session.resource(
- "dynamodb",
- region_name=self.region_name,
- endpoint_url=self.endpoint_url,
- ).__aenter__()
-
- create_table = False
-
- try:
- await self.resource.meta.client.describe_table(TableName=self.table_name)
- except ClientError as exc:
- if exc.response["Error"]["Code"] == "ResourceNotFoundException":
- logger.info(f"Table {self.table_name} not found, creating.")
- create_table = True
- else: # pragma: no cover
- await self.__aexit__(None, None, None)
- raise WebSocketError(exc)
-
- self.table = await self.resource.Table(self.table_name)
-
- if create_table:
- client = self.resource.meta.client
- await client.create_table(
- TableName=self.table_name,
- KeySchema=[{"AttributeName": "connectionId", "KeyType": "HASH"}],
- AttributeDefinitions=[
- {"AttributeName": "connectionId", "AttributeType": "S"}
- ],
- ProvisionedThroughput={
- "ReadCapacityUnits": 5,
- "WriteCapacityUnits": 5,
- },
- )
- await self.table.wait_until_exists()
-
- return self
-
- async def __aexit__(self, *exc_info: Any) -> None:
- await self.resource.__aexit__(*exc_info)
-
- async def save(self, connection_id: str, *, json_scope: str) -> None:
- await self.table.put_item(
- Item={"connectionId": connection_id, "initial_scope": json_scope},
- ConditionExpression="attribute_not_exists(connectionId)",
- )
-
- async def retrieve(self, connection_id: str) -> str:
- try:
- response = await self.table.get_item(Key={"connectionId": connection_id})
- item = response["Item"]
- except KeyError:
- raise WebSocketError(f"Connection not found: {connection_id}")
- initial_scope = item["initial_scope"]
- return initial_scope
-
- async def delete(self, connection_id: str) -> None:
- await self.table.delete_item(Key={"connectionId": connection_id})
diff --git a/mangum/backends/postgresql.py b/mangum/backends/postgresql.py
deleted file mode 100644
index e665b3bd..00000000
--- a/mangum/backends/postgresql.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from typing import Any
-
-import aiopg
-
-from .base import WebSocketBackend
-from ..exceptions import WebSocketError
-
-
-class PostgreSQLBackend(WebSocketBackend):
- async def __aenter__(self) -> WebSocketBackend:
- self.connection = await aiopg.connect(self.dsn)
- self.cursor = await self.connection.cursor()
-
- await self.cursor.execute(
- "create table if not exists mangum_websockets "
- "(id varchar(64) primary key, initial_scope text)"
- )
-
- return self
-
- async def __aexit__(self, *exc_info: Any) -> None:
- self.cursor.close()
- await self.connection.close()
-
- async def save(self, connection_id: str, *, json_scope: str) -> None:
- await self.cursor.execute(
- "insert into mangum_websockets values (%s, %s)",
- (connection_id, json_scope),
- )
-
- async def retrieve(self, connection_id: str) -> str:
- await self.cursor.execute(
- "select initial_scope from mangum_websockets where id = %s",
- (connection_id,),
- )
- row = await self.cursor.fetchone()
- if not row:
- raise WebSocketError(f"Connection not found: {connection_id}")
- initial_scope = row[0]
- return initial_scope
-
- async def delete(self, connection_id: str) -> None:
- await self.cursor.execute(
- "delete from mangum_websockets where id = %s", (connection_id,)
- )
diff --git a/mangum/backends/redis.py b/mangum/backends/redis.py
deleted file mode 100644
index 3c411742..00000000
--- a/mangum/backends/redis.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from typing import Any
-import aioredis
-
-from .base import WebSocketBackend
-from ..exceptions import WebSocketError
-
-
-class RedisBackend(WebSocketBackend):
- async def __aenter__(self) -> WebSocketBackend:
- self.connection = await aioredis.create_redis(self.dsn)
-
- return self
-
- async def __aexit__(self, *exc_info: Any) -> None:
- self.connection.close()
-
- async def save(self, connection_id: str, *, json_scope: str) -> None:
- await self.connection.set(connection_id, json_scope)
-
- async def retrieve(self, connection_id: str) -> str:
- scope = await self.connection.get(connection_id)
- if not scope:
- raise WebSocketError(f"Connection not found: {connection_id}")
- return scope
-
- async def delete(self, connection_id: str) -> None:
- await self.connection.delete(connection_id)
diff --git a/mangum/backends/s3.py b/mangum/backends/s3.py
deleted file mode 100644
index 35a0df46..00000000
--- a/mangum/backends/s3.py
+++ /dev/null
@@ -1,90 +0,0 @@
-import os
-import logging
-from typing import Any
-from urllib.parse import ParseResult, urlparse, parse_qs
-
-import aioboto3
-from botocore.exceptions import ClientError
-
-from .base import WebSocketBackend
-from ..exceptions import WebSocketError
-
-logger = logging.getLogger("mangum.backends.s3")
-
-
-def get_file_key(parsed_dsn: ParseResult) -> str:
- if parsed_dsn.path and parsed_dsn.path != "/":
- if not parsed_dsn.path.endswith("/"):
- return parsed_dsn.path + "/"
- return parsed_dsn.path
-
- return ""
-
-
-class S3Backend(WebSocketBackend):
- async def __aenter__(self) -> WebSocketBackend:
- parsed_dsn = urlparse(self.dsn)
- parsed_query = parse_qs(parsed_dsn.query)
- self.bucket = parsed_dsn.hostname
- self.key = get_file_key(parsed_dsn)
- self.region_name = (
- parsed_query["region"][0]
- if "region" in parsed_query
- else os.environ["AWS_REGION"]
- )
- self.endpoint_url = (
- parsed_query["endpoint_url"][0] if "endpoint_url" in parsed_query else None
- )
-
- session = aioboto3.Session()
- self.client = await session.client(
- "s3",
- region_name=self.region_name,
- endpoint_url=self.endpoint_url,
- ).__aenter__()
-
- create_bucket = False
-
- try:
- await self.client.head_bucket(Bucket=self.bucket)
- except ClientError as exc:
- error_code = int(exc.response["Error"]["Code"])
- if error_code == 403: # pragma: no cover
- logger.error("S3 bucket access forbidden!")
- elif error_code == 404:
- logger.info(f"Bucket {self.bucket} not found, creating.")
- create_bucket = True
-
- if create_bucket:
- await self.client.create_bucket(Bucket=self.bucket)
-
- return self
-
- async def __aexit__(self, *exc_info: Any) -> None:
- await self.client.__aexit__(*exc_info)
-
- async def save(self, connection_id: str, *, json_scope: str) -> None:
- await self.client.put_object(
- Body=json_scope.encode(),
- Bucket=self.bucket,
- Key=f"{self.key}{connection_id}",
- )
-
- async def retrieve(self, connection_id: str) -> str:
- try:
- s3_object = await self.client.get_object(
- Bucket=self.bucket, Key=f"{self.key}{connection_id}"
- )
- except self.client.exceptions.NoSuchKey:
- raise WebSocketError(f"Connection not found: {connection_id}")
-
- async with s3_object["Body"] as body:
- scope = await body.read()
- json_scope = scope.decode()
-
- return json_scope
-
- async def delete(self, connection_id: str) -> None:
- await self.client.delete_object(
- Bucket=self.bucket, Key=f"{self.key}{connection_id}"
- )
diff --git a/mangum/backends/sqlite.py b/mangum/backends/sqlite.py
deleted file mode 100644
index dc0c2bee..00000000
--- a/mangum/backends/sqlite.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from typing import Any
-from urllib.parse import urlparse
-
-import aiosqlite
-
-from .base import WebSocketBackend
-from ..exceptions import WebSocketError
-
-
-class SQLiteBackend(WebSocketBackend):
- async def __aenter__(self) -> WebSocketBackend:
- parsed_dsn = urlparse(self.dsn)
- self.connection = await aiosqlite.connect(parsed_dsn.path)
- await self.connection.execute(
- "create table if not exists mangum_websockets "
- "(id varchar(64) primary key, initial_scope text)"
- )
- await self.connection.commit()
-
- return self
-
- async def __aexit__(self, *exc_info: Any) -> None:
- await self.connection.close()
-
- async def save(self, connection_id: str, *, json_scope: str) -> None:
- await self.connection.execute(
- "insert into mangum_websockets values (?, ?)",
- (connection_id, json_scope),
- )
- await self.connection.commit()
-
- async def retrieve(self, connection_id: str) -> str:
- async with self.connection.execute(
- "select initial_scope from mangum_websockets where id = ?",
- (connection_id,),
- ) as cursor:
- row = await cursor.fetchone()
- if not row:
- raise WebSocketError(f"Connection not found: {connection_id}")
- scope = row[0]
-
- return scope
-
- async def delete(self, connection_id: str) -> None:
- await self.connection.execute(
- "delete from mangum_websockets where id = ?", (connection_id,)
- )
- await self.connection.commit()
diff --git a/mangum/exceptions.py b/mangum/exceptions.py
index 6bdc468b..d14234c9 100644
--- a/mangum/exceptions.py
+++ b/mangum/exceptions.py
@@ -10,13 +10,5 @@ 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):
- """Raise when an error occurs in a WebSocket event."""
-
-
class ConfigurationError(Exception):
"""Raise when an error occurs parsing configuration."""
diff --git a/mangum/handlers/__init__.py b/mangum/handlers/__init__.py
index cf157f6a..9ab3303d 100644
--- a/mangum/handlers/__init__.py
+++ b/mangum/handlers/__init__.py
@@ -3,7 +3,6 @@
from .aws_api_gateway import AwsApiGateway
from .aws_cf_lambda_at_edge import AwsCfLambdaAtEdge
from .aws_http_gateway import AwsHttpGateway
-from .aws_ws_gateway import AwsWsGateway
__all__ = [
"AbstractHandler",
@@ -11,5 +10,4 @@
"AwsApiGateway",
"AwsCfLambdaAtEdge",
"AwsHttpGateway",
- "AwsWsGateway",
]
diff --git a/mangum/handlers/abstract_handler.py b/mangum/handlers/abstract_handler.py
index 4c7ce7ab..2bf5c437 100644
--- a/mangum/handlers/abstract_handler.py
+++ b/mangum/handlers/abstract_handler.py
@@ -1,8 +1,8 @@
import base64
from abc import ABCMeta, abstractmethod
-from typing import Dict, Any, TYPE_CHECKING, Tuple, List, Union
+from typing import Dict, Any, TYPE_CHECKING, Tuple, List
-from ..types import Response, Request, WsRequest
+from ..types import Response, Request
if TYPE_CHECKING: # pragma: no cover
from awslambdaric.lambda_context import LambdaContext
@@ -19,7 +19,7 @@ def __init__(
@property
@abstractmethod
- def request(self) -> Union[Request, WsRequest]:
+ def request(self) -> Request:
"""
Parse an ASGI scope from the request event
"""
@@ -38,25 +38,6 @@ def transform_response(self, response: Response) -> Dict[str, Any]:
this handler
"""
- @property
- def message_type(self) -> str:
- request_context = self.trigger_event["requestContext"]
- return request_context["eventType"]
-
- @property
- def connection_id(self) -> str:
- request_context = self.trigger_event["requestContext"]
- return request_context["connectionId"]
-
- @property
- def api_gateway_endpoint_url(self) -> str:
- request_context = self.trigger_event["requestContext"]
- domain = request_context["domainName"]
- stage = request_context["stage"]
- api_gateway_endpoint_url = f"https://{domain}/{stage}/@connections"
-
- return api_gateway_endpoint_url
-
@staticmethod
def from_trigger(
trigger_event: Dict[str, Any],
@@ -77,15 +58,6 @@ def from_trigger(
from . import AwsAlb
return AwsAlb(trigger_event, trigger_context)
-
- if (
- "requestContext" in trigger_event
- and "connectionId" in trigger_event["requestContext"]
- ):
- from . import AwsWsGateway
-
- return AwsWsGateway(trigger_event, trigger_context)
-
if (
"Records" in trigger_event
and len(trigger_event["Records"]) > 0
diff --git a/mangum/handlers/aws_alb.py b/mangum/handlers/aws_alb.py
index f178c323..9e265cc5 100644
--- a/mangum/handlers/aws_alb.py
+++ b/mangum/handlers/aws_alb.py
@@ -40,17 +40,6 @@ def case_mutated_headers(multi_value_headers: Dict[str, List[str]]) -> Dict[str,
class AwsAlb(AbstractHandler):
- """
- Handles AWS Elastic Load Balancer, really Application Load Balancer events
- transforming them into ASGI Scope and handling responses
-
- See:
- 1. https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html
- 2. https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html # noqa: E501
- """
-
- TYPE = "AWS_ALB"
-
def _encode_query_string(self) -> bytes:
"""
Encodes the queryStringParameters.
@@ -128,7 +117,6 @@ def request(self) -> Request:
client=client,
trigger_event=self.trigger_event,
trigger_context=self.trigger_context,
- event_type=self.TYPE,
)
@property
diff --git a/mangum/handlers/aws_api_gateway.py b/mangum/handlers/aws_api_gateway.py
index c20558f2..aaebc7a6 100644
--- a/mangum/handlers/aws_api_gateway.py
+++ b/mangum/handlers/aws_api_gateway.py
@@ -13,15 +13,6 @@
class AwsApiGateway(AbstractHandler):
- """
- Handles AWS API Gateway events, transforming them into ASGI Scope and handling
- responses
-
- See: https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html
- """
-
- TYPE = "AWS_API_GATEWAY"
-
def __init__(
self,
trigger_event: Dict[str, Any],
@@ -78,7 +69,6 @@ def request(self) -> Request:
client=client,
trigger_event=self.trigger_event,
trigger_context=self.trigger_context,
- event_type=self.TYPE,
)
def _encode_query_string(self) -> bytes:
diff --git a/mangum/handlers/aws_cf_lambda_at_edge.py b/mangum/handlers/aws_cf_lambda_at_edge.py
index b43132c3..c9d2f314 100644
--- a/mangum/handlers/aws_cf_lambda_at_edge.py
+++ b/mangum/handlers/aws_cf_lambda_at_edge.py
@@ -6,15 +6,6 @@
class AwsCfLambdaAtEdge(AbstractHandler):
- """
- Handles AWS Elastic Load Balancer, really Application Load Balancer events
- transforming them into ASGI Scope and handling responses
-
- See: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html # noqa: E501
- """
-
- TYPE = "AWS_CF_LAMBDA_AT_EDGE"
-
@property
def request(self) -> Request:
event = self.trigger_event
@@ -49,7 +40,6 @@ def request(self) -> Request:
client=client,
trigger_event=self.trigger_event,
trigger_context=self.trigger_context,
- event_type=self.TYPE,
)
@property
diff --git a/mangum/handlers/aws_http_gateway.py b/mangum/handlers/aws_http_gateway.py
index f6372082..93543b34 100644
--- a/mangum/handlers/aws_http_gateway.py
+++ b/mangum/handlers/aws_http_gateway.py
@@ -84,7 +84,6 @@ def request(self) -> Request:
client=client,
trigger_event=self.trigger_event,
trigger_context=self.trigger_context,
- event_type=self.TYPE,
)
@property
diff --git a/mangum/handlers/aws_ws_gateway.py b/mangum/handlers/aws_ws_gateway.py
deleted file mode 100644
index 3fdf6e97..00000000
--- a/mangum/handlers/aws_ws_gateway.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from typing import Any, Dict, Tuple
-import base64
-
-
-from ..types import Response, WsRequest
-from .abstract_handler import AbstractHandler
-
-
-def get_server_and_headers(event: Dict[str, Any]) -> Tuple: # pragma: no cover
- if event.get("multiValueHeaders"):
- headers = {
- k.lower(): ", ".join(v) if isinstance(v, list) else ""
- for k, v in event.get("multiValueHeaders", {}).items()
- }
- elif event.get("headers"):
- headers = {k.lower(): v for k, v in event.get("headers", {}).items()}
- else:
- headers = {}
-
- # Subprotocols are not supported, so drop Sec-WebSocket-Protocol to be safe
- headers.pop("sec-websocket-protocol", None)
-
- server_name = headers.get("host", "mangum")
- if ":" not in server_name:
- server_port = headers.get("x-forwarded-port", 80)
- else:
- server_name, server_port = server_name.split(":")
- server = (server_name, int(server_port))
-
- return server, headers
-
-
-class AwsWsGateway(AbstractHandler):
- """
- Handles AWS API Gateway Websocket events, transforming
- them into ASGI Scope and handling responses
-
- See: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format # noqa: E501
- """
-
- TYPE = "AWS_WS_GATEWAY"
-
- @property
- def request(self) -> WsRequest:
- request_context = self.trigger_event["requestContext"]
- server, headers = get_server_and_headers(self.trigger_event)
- source_ip = request_context.get("identity", {}).get("sourceIp")
- client = (source_ip, 0)
- headers_list = [[k.encode(), v.encode()] for k, v in headers.items()]
-
- return WsRequest(
- headers=headers_list,
- path="/",
- scheme=headers.get("x-forwarded-proto", "wss"),
- query_string=b"",
- server=server,
- client=client,
- trigger_event=self.trigger_event,
- trigger_context=self.trigger_context,
- event_type=self.TYPE,
- )
-
- @property
- def body(self) -> bytes:
- body = self.trigger_event.get("body", b"") or b""
-
- if self.trigger_event.get("isBase64Encoded", False):
- return base64.b64decode(body)
- if not isinstance(body, bytes):
- body = body.encode()
-
- return body
-
- def transform_response(self, response: Response) -> Dict[str, Any]:
- return {"statusCode": response.status}
diff --git a/mangum/protocols/__init__.py b/mangum/protocols/__init__.py
index 27df010c..f8d83845 100644
--- a/mangum/protocols/__init__.py
+++ b/mangum/protocols/__init__.py
@@ -1,10 +1,4 @@
from .http import HTTPCycle
-from .websockets import WebSocketCycle
from .lifespan import LifespanCycleState, LifespanCycle
-__all__ = [
- "HTTPCycle",
- "WebSocketCycle",
- "LifespanCycleState",
- "LifespanCycle",
-]
+__all__ = ["HTTPCycle", "LifespanCycleState", "LifespanCycle"]
diff --git a/mangum/protocols/websockets.py b/mangum/protocols/websockets.py
deleted file mode 100644
index ef15725b..00000000
--- a/mangum/protocols/websockets.py
+++ /dev/null
@@ -1,198 +0,0 @@
-import enum
-import asyncio
-import copy
-import typing
-import logging
-from io import BytesIO
-from dataclasses import dataclass
-
-from ..backends import WebSocket
-from ..exceptions import UnexpectedMessage, WebSocketClosed, WebSocketError
-from ..types import ASGIApp, Message, WsRequest, Response
-
-
-class WebSocketCycleState(enum.Enum):
- """
- The state of the ASGI WebSocket connection.
-
- * **CONNECTING** - Initial state. The ASGI application instance will be run with the
- connection scope containing the `websocket` type.
- * **HANDSHAKE** - The ASGI `websocket` connection with the application has been
- established, and a `websocket.connect` event has been pushed to the application
- queue. The application will respond by accepting or rejecting the connection.
- If rejected, a 403 response will be returned to the client, and it will be removed
- from API Gateway.
- * **RESPONSE** - Handshake accepted by the application. Data received in the API
- Gateway message event will be sent to the application. A `websocket.receive` event
- will be pushed to the application queue.
- * **DISCONNECTING** - The ASGI connection cycle is complete and should be
- disconnected from the application. A `websocket.disconnect` event will be pushed to
- the queue, and a response will be returned to the client connection.
- * **CLOSED** - The application has sent a `websocket.close` message. This will
- either be in response to a `websocket.disconnect` event or occurs when a connection
- is rejected in response to a `websocket.connect` event.
- """
-
- CONNECTING = enum.auto()
- HANDSHAKE = enum.auto()
- RESPONSE = enum.auto()
- DISCONNECTING = enum.auto()
- CLOSED = enum.auto()
-
-
-@dataclass
-class WebSocketCycle:
- """
- Manages the application cycle for an ASGI `websocket` connection.
-
- * **websocket** - A `WebSocket` connection handler interface for the selected
- `WebSocketBackend` subclass. Contains the ASGI connection `scope` and client
- connection identifier.
- * **state** - An enumerated `WebSocketCycleState` type that indicates the state of
- the ASGI connection.
- * **app_queue** - An asyncio queue (FIFO) containing messages to be received by the
- application.
- """
-
- request: WsRequest
- message_type: str
- connection_id: str
- websocket: WebSocket
- state: WebSocketCycleState = WebSocketCycleState.CONNECTING
-
- def __post_init__(self) -> None:
- self.logger: logging.Logger = logging.getLogger("mangum.websocket")
- self.loop = asyncio.get_event_loop()
- self.app_queue: asyncio.Queue[typing.Dict[str, typing.Any]] = asyncio.Queue()
- self.body: BytesIO = BytesIO()
- self.response: Response = Response(200, [], b"")
-
- def __call__(self, app: ASGIApp, initial_body: bytes) -> Response:
- self.logger.debug("WebSocket cycle starting.")
- self.initial_body = initial_body
-
- if self.message_type == "CONNECT":
- scope = self.request.scope
- del scope["aws.event"]
- del scope["aws.context"]
- self.loop.run_until_complete(
- self.websocket.on_connect(self.connection_id, scope)
- )
- elif self.message_type == "MESSAGE":
- self.app_queue.put_nowait({"type": "websocket.connect"})
- asgi_instance = self.run(app)
- asgi_task = self.loop.create_task(asgi_instance)
- self.loop.run_until_complete(asgi_task)
- elif self.message_type == "DISCONNECT":
- self.loop.run_until_complete(
- self.websocket.on_disconnect(self.connection_id)
- )
-
- return self.response
-
- async def run(self, app: ASGIApp) -> None:
- """
- Calls the application with the `websocket` connection scope.
- """
- self.scope = await self.websocket.on_message(self.connection_id)
- scope = copy.copy(self.scope)
- scope.update(
- {
- "aws.event": self.request.trigger_event,
- "aws.context": self.request.trigger_context,
- }
- )
- try:
- await app(scope, self.receive, self.send)
- except WebSocketClosed:
- self.response.status = 403
- except UnexpectedMessage:
- self.response.status = 500
- except BaseException as exc:
- self.logger.error("Exception in ASGI application", exc_info=exc)
- self.response.status = 500
-
- async def receive(self) -> Message:
- """
- Awaited by the application to receive ASGI `websocket` events.
- """
- if self.state is WebSocketCycleState.CONNECTING:
-
- # Initial ASGI connection established. The next event returned by the queue
- # will be `websocket.connect` to initiate the handshake.
- self.state = WebSocketCycleState.HANDSHAKE
-
- elif self.state is WebSocketCycleState.HANDSHAKE:
-
- # ASGI connection handshake accepted. The next event returned by the queue
- # will be `websocket.receive` containing the message data from API Gateway.
- self.state = WebSocketCycleState.RESPONSE
-
- return await self.app_queue.get()
-
- async def send(self, message: Message) -> None:
- """
- Awaited by the application to send ASGI `websocket` events.
- """
- message_type = message["type"]
- self.logger.info(
- "%s: '%s' event received from application.", self.state, message_type
- )
-
- if self.state is WebSocketCycleState.HANDSHAKE and message_type in (
- "websocket.accept",
- "websocket.close",
- ):
-
- # API Gateway handles the WebSocket client handshake in the connect event,
- # and it cannot be negotiated by the application directly. The application
- # may choose to close the connection at this point. This process does not
- # support subprotocols.
- if message_type == "websocket.accept":
- await self.app_queue.put(
- {
- "type": "websocket.receive",
- "bytes": None,
- "text": self.initial_body.decode(),
- }
- )
- elif message_type == "websocket.close":
- self.state = WebSocketCycleState.CLOSED
- await self.websocket.delete_connection(self.connection_id)
- raise WebSocketClosed
-
- elif (
- self.state is WebSocketCycleState.RESPONSE
- and message_type == "websocket.close"
- ):
-
- # The application is explicitly closing the connection. It should be
- # disconnected and removed in API Gateway.
- await self.websocket.delete_connection(self.connection_id)
-
- elif (
- self.state is WebSocketCycleState.RESPONSE
- and message_type == "websocket.send"
- ):
-
- # The application requested to send some data in response to the
- # "websocket.receive" event. After this, a "websocket.disconnect"
- # event is pushed to let the application finish gracefully.
- # Then the lambda's execution is ended.
-
- if message.get("body") is not None:
- raise WebSocketError(
- "Application attemped to send a binary payload, "
- "but it's unsupported!"
- )
-
- message_text = message.get("text", "")
- body = message_text.encode()
-
- await self.websocket.post_to_connection(self.connection_id, body=body)
- await self.app_queue.put({"type": "websocket.disconnect", "code": 1000})
-
- else:
- raise UnexpectedMessage(
- f"{self.state}: Unexpected '{message_type}' event received."
- )
diff --git a/mangum/types.py b/mangum/types.py
index c1a42917..e37d4ea4 100644
--- a/mangum/types.py
+++ b/mangum/types.py
@@ -1,4 +1,4 @@
-from dataclasses import dataclass, field
+from dataclasses import dataclass
from typing import (
List,
Tuple,
@@ -47,17 +47,14 @@ class BaseRequest:
# Invocation event
trigger_event: Dict[str, Any]
trigger_context: Union["LambdaContext", Dict[str, Any]]
- event_type: str
- http_version: str = "1.1"
raw_path: Optional[str] = None
root_path: str = ""
- asgi: Dict[str, str] = field(default_factory=lambda: {"version": "3.0"})
@property
def scope(self) -> Scope:
return {
- "http_version": self.http_version,
+ "http_version": "1.1",
"headers": self.headers,
"path": self.path,
"raw_path": self.raw_path,
@@ -66,11 +63,9 @@ def scope(self) -> Scope:
"query_string": self.query_string,
"server": self.server,
"client": self.client,
- "asgi": self.asgi,
- # Meta data to pass along to the application in case they need it
+ "asgi": {"version": "3.0"},
"aws.event": self.trigger_event,
"aws.context": self.trigger_context,
- "aws.eventType": self.event_type,
}
@@ -92,24 +87,6 @@ def scope(self) -> Scope:
return scope
-@dataclass
-class WsRequest(BaseRequest):
- """
- A holder for an ASGI scope. Specific for usage with WebSocket connections.
-
- https://asgi.readthedocs.io/en/latest/specs/www.html#websocket-connection-scope
- """
-
- type: str = "websocket"
- subprotocols: List[str] = field(default_factory=lambda: [])
-
- @property
- def scope(self) -> Scope:
- scope = super().scope
- scope.update({"type": self.type, "subprotocols": self.subprotocols})
- return scope
-
-
@dataclass
class Response:
status: int
diff --git a/mkdocs.yml b/mkdocs.yml
index ae3e892d..e929207c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -3,25 +3,24 @@ site_description: AWS Lambda & API Gateway support for ASGI
site_url: http://mangum.io/
theme:
- name: 'material'
- palette:
- primary: 'brown'
- accent: 'orange'
+ name: "material"
+ palette:
+ primary: "brown"
+ accent: "orange"
repo_name: jordaneremieff/mangum
repo_url: https://github.com/jordaneremieff/mangum
edit_uri: ""
nav:
- - Introduction: 'index.md'
- - Adapter: 'adapter.md'
- - HTTP: 'http.md'
- - WebSockets: 'websockets.md'
- - Lifespan: 'lifespan.md'
- - ASGI Frameworks: 'asgi-frameworks.md'
- - External Links: 'external-links.md'
- - Contributing: 'contributing.md'
- - Release Notes: 'release-notes.md'
+ - Introduction: "index.md"
+ - Adapter: "adapter.md"
+ - HTTP: "http.md"
+ - Lifespan: "lifespan.md"
+ - ASGI Frameworks: "asgi-frameworks.md"
+ - External Links: "external-links.md"
+ - Contributing: "contributing.md"
+ - Release Notes: "release-notes.md"
markdown_extensions:
- mkautodoc
diff --git a/pytest.ini b/pytest.ini
index e5be7db1..f861f05e 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -2,5 +2,4 @@
log_cli = 1
log_cli_level = INFO
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
-log_cli_date_format=%Y-%m-%d %H:%M:%S
-addopts = --docker-compose-remove-volumes --docker-compose=tests/integration/docker-compose.yml
+log_cli_date_format=%Y-%m-%d %H:%M:%S
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index ec8d79fa..12d5e910 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,21 +6,10 @@ black
flake8
starlette
quart; python_version >= '3.7'
-moto[server]
mypy
brotli
brotli-asgi
awslambdaric-stubs
-requests
-pytest-docker-compose
-aioboto3
-aioredis==1.3.1
-aiosqlite
-aiopg
-sqlalchemy
-redis
-httpx
-respx
types-dataclasses; python_version < '3.7' # For mypy
# Docs
mkdocs
diff --git a/tests/conftest.py b/tests/conftest.py
index ab04eafc..aa172e6b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -253,197 +253,6 @@ def mock_lambda_at_edge_event(request):
)
-@pytest.fixture
-def mock_ws_connect_event() -> dict:
- return {
- "headers": {
- "Accept-Encoding": "gzip, deflate, br",
- "Accept-Language": "en-US,en;q=0.9",
- "Cache-Control": "no-cache",
- "Host": "test.execute-api.ap-southeast-1.amazonaws.com",
- "Origin": "https://test.execute-api.ap-southeast-1.amazonaws.com",
- "Pragma": "no-cache",
- "Sec-WebSocket-Extensions": "permessage-deflate; " "client_max_window_bits",
- "Sec-WebSocket-Key": "bnfeqmh9SSPr5Sg9DvFIBw==",
- "Sec-WebSocket-Version": "13",
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) "
- "AppleWebKit/537.36 (KHTML, like Gecko) "
- "Chrome/75.0.3770.100 Safari/537.36",
- "X-Amzn-Trace-Id": "Root=1-5d465cb6-78ddcac1e21f89203d004a89",
- "X-Forwarded-For": "192.168.100.1",
- "X-Forwarded-Port": "443",
- "X-Forwarded-Proto": "https",
- },
- "isBase64Encoded": False,
- "multiValueHeaders": {
- "Accept-Encoding": ["gzip, deflate, br"],
- "Accept-Language": ["en-US,en;q=0.9"],
- "Cache-Control": ["no-cache"],
- "Host": ["test.execute-api.ap-southeast-1.amazonaws.com"],
- "Origin": ["https://test.execute-api.ap-southeast-1.amazonaws.com"],
- "Pragma": ["no-cache"],
- "Sec-WebSocket-Extensions": [
- "permessage-deflate; " "client_max_window_bits"
- ],
- "Sec-WebSocket-Key": ["bnfeqmh9SSPr5Sg9DvFIBw=="],
- "Sec-WebSocket-Version": ["13"],
- "User-Agent": [
- "Mozilla/5.0 (Macintosh; Intel Mac OS X "
- "10_14_5) AppleWebKit/537.36 (KHTML, "
- "like Gecko) Chrome/75.0.3770.100 "
- "Safari/537.36"
- ],
- "X-Amzn-Trace-Id": ["Root=1-5d465cb6-78ddcac1e21f89203d004a89"],
- "X-Forwarded-For": ["192.168.100.1"],
- "X-Forwarded-Port": ["443"],
- "X-Forwarded-Proto": ["https"],
- },
- "requestContext": {
- "apiId": "test",
- "connectedAt": 1564892342293,
- "connectionId": "d4NsecoByQ0CH-Q=",
- "domainName": "test.execute-api.ap-southeast-1.amazonaws.com",
- "eventType": "CONNECT",
- "extendedRequestId": "d4NseGc4yQ0FsSA=",
- "identity": {
- "accessKey": None,
- "accountId": None,
- "caller": None,
- "cognitoAuthenticationProvider": None,
- "cognitoAuthenticationType": None,
- "cognitoIdentityId": None,
- "cognitoIdentityPoolId": None,
- "principalOrgId": None,
- "sourceIp": "192.168.100.1",
- "user": None,
- "userAgent": "Mozilla/5.0 (Macintosh; Intel "
- "Mac OS X 10_14_5) "
- "AppleWebKit/537.36 (KHTML, like "
- "Gecko) Chrome/75.0.3770.100 "
- "Safari/537.36",
- "userArn": None,
- },
- "messageDirection": "IN",
- "messageId": None,
- "requestId": "d4NseGc4yQ0FsSA=",
- "requestTime": "04/Aug/2019:04:19:02 +0000",
- "requestTimeEpoch": 1564892342293,
- "routeKey": "$connect",
- "stage": "Prod",
- },
- }
-
-
-@pytest.fixture
-def mock_ws_send_event() -> dict:
- return {
- "body": '{"action": "sendmessage", "data": "Hello world"}',
- "isBase64Encoded": False,
- "requestContext": {
- "apiId": "test",
- "connectedAt": 1564984321285,
- "connectionId": "d4NsecoByQ0CH-Q=",
- "domainName": "test.execute-api.ap-southeast-1.amazonaws.com",
- "eventType": "MESSAGE",
- "extendedRequestId": "d7uRtFvnyQ0FYmw=",
- "identity": {
- "accessKey": None,
- "accountId": None,
- "caller": None,
- "cognitoAuthenticationProvider": None,
- "cognitoAuthenticationType": None,
- "cognitoIdentityId": None,
- "cognitoIdentityPoolId": None,
- "principalOrgId": None,
- "sourceIp": "192.168.100.1",
- "user": None,
- "userAgent": None,
- "userArn": None,
- },
- "messageDirection": "IN",
- "messageId": "d7uRtfaKSQ0CE4Q=",
- "requestId": "d7uRtFvnyQ0FYmw=",
- "requestTime": "05/Aug/2019:05:52:10 +0000",
- "requestTimeEpoch": 1564984330952,
- "routeKey": "sendmessage",
- "stage": "Prod",
- },
- }
-
-
-@pytest.fixture
-def mock_ws_disconnect_event() -> dict:
- return {
- "headers": {
- "Host": "test.execute-api.ap-southeast-1.amazonaws.com",
- "x-api-key": "",
- "x-restapi": "",
- },
- "isBase64Encoded": False,
- "multiValueHeaders": {
- "Host": ["test.execute-api.ap-southeast-1.amazonaws.com"],
- "x-api-key": [""],
- "x-restapi": [""],
- },
- "requestContext": {
- "apiId": "test",
- "connectedAt": 1565140098258,
- "connectionId": "d4NsecoByQ0CH-Q=",
- "domainName": "test.execute-api.ap-southeast-1.amazonaws.com",
- "eventType": "DISCONNECT",
- "extendedRequestId": "eBql1FJmSQ0FrjA=",
- "identity": {
- "accessKey": None,
- "accountId": None,
- "caller": None,
- "cognitoAuthenticationProvider": None,
- "cognitoAuthenticationType": None,
- "cognitoIdentityId": None,
- "cognitoIdentityPoolId": None,
- "principalOrgId": None,
- "sourceIp": "101.164.35.219",
- "user": None,
- "userAgent": "Mozilla/5.0 (Macintosh; Intel "
- "Mac OS X 10_14_6) "
- "AppleWebKit/537.36 (KHTML, like "
- "Gecko) Chrome/75.0.3770.142 "
- "Safari/537.36",
- "userArn": None,
- },
- "messageDirection": "IN",
- "messageId": None,
- "requestId": "eBql1FJmSQ0FrjA=",
- "requestTime": "07/Aug/2019:01:08:27 +0000",
- "requestTimeEpoch": 1565140107779,
- "routeKey": "$disconnect",
- "stage": "Prod",
- },
- }
-
-
-@pytest.fixture
-def mock_websocket_app():
- async def app(scope, receive, send):
- if scope["type"] == "websocket":
- while True:
- message = await receive()
- if message["type"] == "websocket.connect":
- await send({"type": "websocket.accept", "subprotocol": None})
- elif message["type"] == "websocket.receive":
- await send({"type": "websocket.send", "text": "Hello world!"})
- elif message["type"] == "websocket.disconnect":
- close_code = message.get("code", 1000)
- await send({"type": "websocket.close", "code": close_code})
- return
-
- return app
-
-
-@pytest.fixture
-def sqlite3_dsn(tmp_path):
- return f"sqlite://{tmp_path}/mangum.sqlite3"
-
-
@pytest.fixture(scope="session", autouse=True)
def aws_credentials():
"""Mocked AWS Credentials for moto."""
diff --git a/tests/handlers/test_aws_alb.py b/tests/handlers/test_aws_alb.py
index 055c1286..4027efaf 100644
--- a/tests/handlers/test_aws_alb.py
+++ b/tests/handlers/test_aws_alb.py
@@ -209,7 +209,6 @@ def test_aws_alb_scope_real(
"asgi": {"version": "3.0"},
"aws.context": {},
"aws.event": event,
- "aws.eventType": "AWS_ALB",
"client": ("72.12.164.125", 0),
"headers": [
[
@@ -311,7 +310,6 @@ def test_aws_alb_response(
method, content_type, raw_res_body, res_body, res_base64_encoded
):
async def app(scope, receive, send):
- assert scope["aws.eventType"] == "AWS_ALB"
await send(
{
"type": "http.response.start",
diff --git a/tests/handlers/test_aws_api_gateway.py b/tests/handlers/test_aws_api_gateway.py
index eac4a2d3..58a6ca56 100644
--- a/tests/handlers/test_aws_api_gateway.py
+++ b/tests/handlers/test_aws_api_gateway.py
@@ -110,7 +110,6 @@ def test_aws_api_gateway_scope_basic():
"asgi": {"version": "3.0"},
"aws.context": {},
"aws.event": example_event,
- "aws.eventType": "AWS_API_GATEWAY",
"client": (None, 0),
"headers": [
[
@@ -211,7 +210,6 @@ def test_aws_api_gateway_scope_real(
"asgi": {"version": "3.0"},
"aws.context": {},
"aws.event": event,
- "aws.eventType": "AWS_API_GATEWAY",
"client": ("192.168.100.1", 0),
"headers": [
[
@@ -339,7 +337,6 @@ def test_aws_api_gateway_response(
method, content_type, raw_res_body, res_body, res_base64_encoded
):
async def app(scope, receive, send):
- assert scope["aws.eventType"] == "AWS_API_GATEWAY"
await send(
{
"type": "http.response.start",
diff --git a/tests/handlers/test_aws_cf_lambda_at_edge.py b/tests/handlers/test_aws_cf_lambda_at_edge.py
index 4692e791..f7b28061 100644
--- a/tests/handlers/test_aws_cf_lambda_at_edge.py
+++ b/tests/handlers/test_aws_cf_lambda_at_edge.py
@@ -141,7 +141,6 @@ def test_aws_cf_lambda_at_edge_scope_basic():
"asgi": {"version": "3.0"},
"aws.context": {},
"aws.event": example_event,
- "aws.eventType": "AWS_CF_LAMBDA_AT_EDGE",
"client": ("203.0.113.178", 0),
"headers": [
[b"x-forwarded-for", b"203.0.113.178"],
@@ -230,7 +229,6 @@ def test_aws_api_gateway_scope_real(
"asgi": {"version": "3.0"},
"aws.context": {},
"aws.event": event,
- "aws.eventType": "AWS_CF_LAMBDA_AT_EDGE",
"client": ("192.168.100.1", 0),
"headers": [
[b"accept-encoding", b"gzip,deflate"],
@@ -275,7 +273,6 @@ def test_aws_lambda_at_edge_response(
method, content_type, raw_res_body, res_body, res_base64_encoded
):
async def app(scope, receive, send):
- assert scope["aws.eventType"] == "AWS_CF_LAMBDA_AT_EDGE"
await send(
{
"type": "http.response.start",
diff --git a/tests/handlers/test_aws_http_gateway.py b/tests/handlers/test_aws_http_gateway.py
index bdedfc99..8c0c764f 100644
--- a/tests/handlers/test_aws_http_gateway.py
+++ b/tests/handlers/test_aws_http_gateway.py
@@ -201,7 +201,6 @@ def test_aws_http_gateway_scope_basic_v1():
"asgi": {"version": "3.0"},
"aws.context": {},
"aws.event": example_event,
- "aws.eventType": "AWS_HTTP_GATEWAY",
"client": ("IP", 0),
"headers": [[b"header1", b"value1"], [b"header2", b"value1, value2"]],
"http_version": "1.1",
@@ -305,7 +304,6 @@ def test_aws_http_gateway_scope_basic_v2():
"asgi": {"version": "3.0"},
"aws.context": {},
"aws.event": example_event,
- "aws.eventType": "AWS_HTTP_GATEWAY",
"client": ("IP", 0),
"headers": [
[b"header1", b"value1"],
@@ -381,7 +379,6 @@ def test_aws_http_gateway_scope_real_v1(
"asgi": {"version": "3.0"},
"aws.context": {},
"aws.event": event,
- "aws.eventType": "AWS_HTTP_GATEWAY",
"client": ("192.168.100.1", 0),
"headers": [
[b"accept-encoding", b"gzip, deflate"],
@@ -448,7 +445,6 @@ def test_aws_http_gateway_scope_real_v2(
"asgi": {"version": "3.0"},
"aws.context": {},
"aws.event": event,
- "aws.eventType": "AWS_HTTP_GATEWAY",
"client": ("192.168.100.1", 0),
"headers": [
[b"accept-encoding", b"gzip,deflate"],
@@ -512,7 +508,6 @@ def test_aws_http_gateway_response_v1(
"""
async def app(scope, receive, send):
- assert scope["aws.eventType"] == "AWS_HTTP_GATEWAY"
headers = []
if content_type is not None:
headers.append([b"content-type", content_type])
@@ -579,7 +574,6 @@ def test_aws_http_gateway_response_v2(
method, content_type, raw_res_body, res_body, res_base64_encoded
):
async def app(scope, receive, send):
- assert scope["aws.eventType"] == "AWS_HTTP_GATEWAY"
headers = []
if content_type is not None:
headers.append([b"content-type", content_type])
diff --git a/tests/handlers/test_aws_ws_gateway.py b/tests/handlers/test_aws_ws_gateway.py
deleted file mode 100644
index e4e4168c..00000000
--- a/tests/handlers/test_aws_ws_gateway.py
+++ /dev/null
@@ -1,68 +0,0 @@
-from mangum import Mangum
-
-
-def test_aws_ws_gateway_scope(
- sqlite3_dsn, mock_ws_connect_event, mock_ws_send_event, mock_ws_disconnect_event
-):
- def make_app(event):
- async def wrapped(scope, receive, send):
- assert scope == {
- "asgi": {"version": "3.0"},
- "aws.context": {},
- "aws.event": event,
- "aws.eventType": "AWS_WS_GATEWAY",
- "client": ("192.168.100.1", 0),
- "headers": [
- [b"accept-encoding", b"gzip, deflate, br"],
- [b"accept-language", b"en-US,en;q=0.9"],
- [b"cache-control", b"no-cache"],
- [b"host", b"test.execute-api.ap-southeast-1.amazonaws.com"],
- [
- b"origin",
- b"https://test.execute-api.ap-southeast-1.amazonaws.com",
- ],
- [b"pragma", b"no-cache"],
- [
- b"sec-websocket-extensions",
- b"permessage-deflate; client_max_window_bits",
- ],
- [b"sec-websocket-key", b"bnfeqmh9SSPr5Sg9DvFIBw=="],
- [b"sec-websocket-version", b"13"],
- [
- b"user-agent",
- b"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) "
- b"AppleWebKit/537.36 (KHTML, like Gecko) "
- b"Chrome/75.0.3770.100 Safari/537.36",
- ],
- [b"x-amzn-trace-id", b"Root=1-5d465cb6-78ddcac1e21f89203d004a89"],
- [b"x-forwarded-for", b"192.168.100.1"],
- [b"x-forwarded-port", b"443"],
- [b"x-forwarded-proto", b"https"],
- ],
- "http_version": "1.1",
- "path": "/",
- "query_string": b"",
- "raw_path": None,
- "root_path": "",
- "scheme": "https",
- "server": ("test.execute-api.ap-southeast-1.amazonaws.com", 443),
- "subprotocols": [],
- "type": "websocket",
- }
-
- return wrapped
-
- app = make_app(mock_ws_connect_event)
- handler = Mangum(app, lifespan="off", dsn=sqlite3_dsn)
- response = handler(mock_ws_connect_event, {})
- assert response == {"statusCode": 200}
-
- app = make_app(mock_ws_send_event)
- handler = Mangum(app, lifespan="off", dsn=sqlite3_dsn)
- response = handler(mock_ws_send_event, {})
- assert response == {"statusCode": 200}
-
- app = make_app(mock_ws_disconnect_event)
- handler = Mangum(app, lifespan="off", dsn=sqlite3_dsn)
- response = handler(mock_ws_disconnect_event, {})
- assert response == {"statusCode": 200}
diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/integration/docker-compose.yml b/tests/integration/docker-compose.yml
deleted file mode 100644
index c206d5d1..00000000
--- a/tests/integration/docker-compose.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-version: '3.1'
-
-services:
- postgres:
- image: postgres:13
- environment:
- POSTGRES_PASSWORD: mangum
- ports:
- - "5432"
-
- redis:
- image: redis:6.2
- ports:
- - "6379"
diff --git a/tests/integration/mock_server.py b/tests/integration/mock_server.py
deleted file mode 100644
index d5f47c31..00000000
--- a/tests/integration/mock_server.py
+++ /dev/null
@@ -1,81 +0,0 @@
-import signal
-import subprocess as sp
-import time
-import logging
-import shutil
-
-import pytest
-import requests
-import psycopg2
-import redis
-
-_proxy_bypass = {
- "http": None,
- "https": None,
-}
-
-
-def start_service(service_name, host, port):
- moto_svr_path = shutil.which("moto_server")
- args = [moto_svr_path, service_name, "-H", host, "-p", str(port)]
- process = sp.Popen(args, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE)
- url = f"http://{host}:{port}"
-
- for i in range(0, 30):
- output = process.poll()
- if output is not None:
- logging.info(f"moto_server exited status {output}")
- stdout, stderr = process.communicate()
- logging.info(f"moto_server stdout: {stdout}")
- logging.info(f"moto_server stderr: {stderr}")
- pytest.fail(f"Can not start service: {service_name}")
-
- try:
- # we need to bypass the proxies due to monkeypatches
- requests.get(url, timeout=5, proxies=_proxy_bypass)
- break
- except requests.exceptions.ConnectionError:
- time.sleep(0.5)
- else:
- stop_process(process) # pytest.fail doesn't call stop_process
- pytest.fail(f"Can not start service: {service_name}")
-
- return process
-
-
-def stop_process(process):
- try:
- process.send_signal(signal.SIGTERM)
- process.communicate(timeout=20)
- except sp.TimeoutExpired:
- process.kill()
- outs, errors = process.communicate(timeout=20)
- exit_code = process.returncode
- msg = f"Child process finished {exit_code} not in clean way: {outs} {errors}"
- raise RuntimeError(msg)
-
-
-def wait_postgres_server(dsn):
- for i in range(0, 30):
- try:
- conn = psycopg2.connect(dsn)
- conn.close()
- return
- except Exception as e:
- logging.debug(f"PostgreSQL unavailable yet: {e}")
- time.sleep(1)
- else:
- pytest.fail("Couldn't reach PostgreSQL server")
-
-
-def wait_redis_server(hostname, host_port):
- for i in range(0, 30):
- try:
- conn = redis.Redis(hostname, host_port)
- conn.close()
- return
- except Exception as e:
- logging.debug(f"Redis unavailable yet: {e}")
- time.sleep(1)
- else:
- pytest.fail("Couldn't reach Redis server")
diff --git a/tests/integration/test_backends.py b/tests/integration/test_backends.py
deleted file mode 100644
index 9d6d9a54..00000000
--- a/tests/integration/test_backends.py
+++ /dev/null
@@ -1,237 +0,0 @@
-import pytest
-import boto3
-import respx
-
-from mangum import Mangum
-from mangum.exceptions import WebSocketError, ConfigurationError
-from .mock_server import (
- start_service,
- stop_process,
- wait_postgres_server,
- wait_redis_server,
-)
-
-pytest_plugins = ["docker_compose"]
-
-
-@pytest.fixture(scope="session")
-def dynamodb2_server():
- host = "localhost"
- port = 5001
- url = f"http://{host}:{port}"
- process = start_service("dynamodb2", host, port)
- yield url
- stop_process(process)
-
-
-@pytest.fixture(scope="session")
-def s3_server():
- host = "localhost"
- port = 5002
- url = f"http://{host}:{port}"
- process = start_service("s3", host, port)
- yield url
- stop_process(process)
-
-
-@pytest.fixture(scope="module")
-def postgres_server(module_scoped_container_getter):
- container = module_scoped_container_getter.get("postgres")
- network = container.network_info[0]
- hostname = network.hostname
- host_port = network.host_port
- dsn = f"postgresql://postgres:mangum@{hostname}:{host_port}/postgres"
- wait_postgres_server(dsn)
-
- try:
- yield dsn
- finally:
- container.stop()
-
-
-@pytest.fixture(scope="module")
-def redis_server(module_scoped_container_getter):
- container = module_scoped_container_getter.get("redis")
- network = container.network_info[0]
- hostname = network.hostname
- host_port = int(network.host_port)
- dsn = f"redis://{hostname}:{host_port}"
- wait_redis_server(hostname, host_port)
-
- try:
- yield dsn
- finally:
- container.stop()
-
-
-@pytest.mark.parametrize(
- "dsn", ["???://unknown/", "postgresql://", None, "http://localhost"]
-)
-def test_invalid_dsn(mock_ws_connect_event, mock_websocket_app, dsn):
- handler = Mangum(mock_websocket_app, dsn=dsn)
- with pytest.raises(ConfigurationError):
- handler(mock_ws_connect_event, {})
-
-
-@respx.mock(assert_all_mocked=False)
-def test_sqlite_3_backend(
- tmp_path,
- mock_ws_connect_event,
- mock_ws_send_event,
- mock_ws_disconnect_event,
- mock_websocket_app,
-) -> None:
- dsn = f"sqlite://{tmp_path}/mangum.sqlite3"
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- with pytest.raises(WebSocketError):
- response = handler(mock_ws_send_event, {})
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_connect_event, {})
- assert response == {"statusCode": 200}
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_send_event, {})
- assert response == {"statusCode": 200}
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_disconnect_event, {})
- assert response == {"statusCode": 200}
-
-
-@respx.mock(assert_all_mocked=False)
-@pytest.mark.parametrize(
- "table_name",
- ["man", "mangum", "Mangum.Dev.001", "Mangum-Dev-001", "Mangum_Dev_002"],
-)
-def test_dynamodb_backend(
- dynamodb2_server, # noqa: F811
- mock_ws_connect_event,
- mock_ws_send_event,
- mock_ws_disconnect_event,
- mock_websocket_app,
- table_name,
-) -> None:
- region_name = "ap-southeast-1"
-
- dsn = (
- f"dynamodb://{table_name}?region={region_name}&endpoint_url={dynamodb2_server}"
- )
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_connect_event, {})
- assert response == {"statusCode": 200}
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_send_event, {})
- assert response == {"statusCode": 200}
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_disconnect_event, {})
- assert response == {"statusCode": 200}
-
- dynamodb_resource = boto3.resource(
- "dynamodb", region_name=region_name, endpoint_url=dynamodb2_server
- )
- table = dynamodb_resource.Table(table_name)
- table.delete_item(Key={"connectionId": "d4NsecoByQ0CH-Q="})
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- with pytest.raises(WebSocketError):
- response = handler(mock_ws_send_event, {})
-
-
-@respx.mock(assert_all_mocked=False)
-@pytest.mark.parametrize(
- "dsn",
- [
- "s3://mangum-bucket-12345",
- "s3://mangum-bucket-12345/",
- "s3://mangum-bucket-12345/mykey/",
- "s3://mangum-bucket-12345/mykey",
- ],
-)
-def test_s3_backend(
- s3_server, # noqa: F811
- mock_ws_connect_event,
- mock_ws_send_event,
- mock_ws_disconnect_event,
- mock_websocket_app,
- dsn,
-) -> None:
- dsn = f"{dsn}?region=ap-southeast-1&endpoint_url={s3_server}"
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- with pytest.raises(WebSocketError):
- handler(mock_ws_send_event, {})
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_connect_event, {})
- assert response == {"statusCode": 200}
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_send_event, {})
- assert response == {"statusCode": 200}
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_disconnect_event, {})
- assert response == {"statusCode": 200}
-
-
-@respx.mock(assert_all_mocked=False)
-def test_postgresql_backend(
- postgres_server,
- mock_ws_connect_event,
- mock_ws_send_event,
- mock_ws_disconnect_event,
- mock_websocket_app,
-):
- dsn = postgres_server
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- with pytest.raises(WebSocketError):
- handler(mock_ws_send_event, {})
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_connect_event, {})
- assert response == {"statusCode": 200}
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_send_event, {})
- assert response == {"statusCode": 200}
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_disconnect_event, {})
- assert response == {"statusCode": 200}
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_connect_event, {})
- assert response == {"statusCode": 200}
-
-
-@respx.mock(assert_all_mocked=False)
-def test_redis_backend(
- redis_server,
- mock_ws_connect_event,
- mock_ws_send_event,
- mock_ws_disconnect_event,
- mock_websocket_app,
-):
- dsn = redis_server
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- with pytest.raises(WebSocketError):
- handler(mock_ws_send_event, {})
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_connect_event, {})
- assert response == {"statusCode": 200}
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_send_event, {})
- assert response == {"statusCode": 200}
-
- handler = Mangum(mock_websocket_app, dsn=dsn)
- response = handler(mock_ws_disconnect_event, {})
- assert response == {"statusCode": 200}
diff --git a/tests/test_http.py b/tests/test_http.py
index 4328aba3..820fe324 100644
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -20,7 +20,6 @@ def test_http_response(mock_aws_api_gateway_event) -> None:
async def app(scope, receive, send):
assert scope == {
"asgi": {"version": "3.0"},
- "aws.eventType": "AWS_API_GATEWAY",
"aws.context": {},
"aws.event": {
"body": None,
@@ -273,7 +272,6 @@ def test_set_cookies_v2(mock_http_api_event_v2) -> None:
async def app(scope, receive, send):
assert scope == {
"asgi": {"version": "3.0"},
- "aws.eventType": "AWS_HTTP_GATEWAY",
"aws.context": {},
"aws.event": {
"version": "2.0",
@@ -391,7 +389,6 @@ def test_set_cookies_v1(mock_http_api_event_v1) -> None:
async def app(scope, receive, send):
assert scope == {
"asgi": {"version": "3.0"},
- "aws.eventType": "AWS_HTTP_GATEWAY",
"aws.context": {},
"aws.event": {
"version": "1.0",
diff --git a/tests/test_websockets.py b/tests/test_websockets.py
deleted file mode 100644
index ec16adcc..00000000
--- a/tests/test_websockets.py
+++ /dev/null
@@ -1,109 +0,0 @@
-import respx
-
-from mangum import Mangum
-
-
-@respx.mock(assert_all_mocked=False)
-def test_websocket_close(
- sqlite3_dsn, mock_ws_connect_event, mock_ws_send_event
-) -> None:
- async def app(scope, receive, send):
- if scope["type"] == "websocket":
- while True:
- message = await receive()
- if message["type"] == "websocket.connect":
- await send({"type": "websocket.close"})
-
- handler = Mangum(app, lifespan="off", dsn=sqlite3_dsn)
- response = handler(mock_ws_connect_event, {})
- assert response == {"statusCode": 200}
-
- response = handler(mock_ws_send_event, {})
- assert response == {"statusCode": 403}
-
-
-@respx.mock(assert_all_mocked=False)
-def test_websocket_disconnect(
- sqlite3_dsn, mock_ws_connect_event, mock_ws_send_event, mock_websocket_app
-) -> None:
- handler = Mangum(mock_websocket_app, lifespan="off", dsn=sqlite3_dsn)
- response = handler(mock_ws_connect_event, {})
- assert response == {"statusCode": 200}
-
- response = handler(mock_ws_send_event, {})
- assert response == {"statusCode": 200}
-
-
-def test_websocket_exception(
- sqlite3_dsn, mock_ws_connect_event, mock_ws_send_event
-) -> None:
- async def app(scope, receive, send):
- raise Exception()
-
- handler = Mangum(app, dsn=sqlite3_dsn)
- handler(mock_ws_connect_event, {})
-
- handler = Mangum(app, dsn=sqlite3_dsn)
- response = handler(mock_ws_send_event, {})
- assert response == {"statusCode": 500}
-
-
-def test_websocket_unexpected_message_error(
- sqlite3_dsn, mock_ws_connect_event, mock_ws_send_event
-) -> None:
- async def app(scope, receive, send):
- await send({"type": "websocket.oops", "subprotocol": None})
-
- handler = Mangum(app, dsn=sqlite3_dsn)
- handler(mock_ws_connect_event, {})
-
- handler = Mangum(app, dsn=sqlite3_dsn)
- response = handler(mock_ws_send_event, {})
- assert response == {"statusCode": 500}
-
-
-@respx.mock(assert_all_mocked=False)
-def test_websocket_without_body(
- sqlite3_dsn, mock_ws_connect_event, mock_ws_send_event, mock_websocket_app
-) -> None:
- handler = Mangum(mock_websocket_app, lifespan="off", dsn=sqlite3_dsn)
- response = handler(mock_ws_connect_event, {})
- assert response == {"statusCode": 200}
-
- del mock_ws_send_event["body"]
- response = handler(mock_ws_send_event, {})
- assert response == {"statusCode": 200}
-
-
-@respx.mock(assert_all_mocked=False)
-def test_base64_encoded_body_on_request(
- sqlite3_dsn, mock_ws_connect_event, mock_ws_send_event, mock_websocket_app
-):
- handler = Mangum(mock_websocket_app, dsn=sqlite3_dsn)
- response = handler(mock_ws_connect_event, {})
- assert response == {"statusCode": 200}
-
- mock_ws_send_event["body"] = b"bWFuZ3Vt="
- mock_ws_send_event["isBase64Encoded"] = True
- response = handler(mock_ws_send_event, {})
- assert response == {"statusCode": 200}
-
-
-def test_binary_response(
- sqlite3_dsn, mock_ws_connect_event, mock_ws_send_event, mock_websocket_app
-):
- async def app(scope, receive, send):
- if scope["type"] == "websocket":
- while True:
- message = await receive()
- if message["type"] == "websocket.connect":
- await send({"type": "websocket.accept"})
- elif message["type"] == "websocket.receive":
- await send({"type": "websocket.send", "body": b"bWFuZ3Vt="})
-
- handler = Mangum(app, dsn=sqlite3_dsn)
- response = handler(mock_ws_connect_event, {})
- assert response == {"statusCode": 200}
-
- response = handler(mock_ws_send_event, {})
- assert response == {"statusCode": 500}