ASGI specs, helper code, and adapters for bridging synchronous and asynchronous Python web applications
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Complete type definitions for ASGI protocols, events, and applications, enabling full type safety in async web applications. This module provides comprehensive TypeScript-style type annotations for all ASGI components.
Core ASGI application and protocol type definitions that define the interface contracts for ASGI servers and applications.
# Application Types
ASGIApplication = Union[ASGI2Application, ASGI3Application]
ASGI2Application = Callable[[Scope], ASGI2Protocol]
ASGI3Application = Callable[[Scope, ASGIReceiveCallable, ASGISendCallable], Awaitable[None]]
ASGI2Protocol = Callable[[ASGIReceiveCallable, ASGISendCallable], Awaitable[None]]
# Channel Types
ASGIReceiveCallable = Callable[[], Awaitable[ASGIReceiveEvent]]
ASGISendCallable = Callable[[ASGISendEvent], Awaitable[None]]
# Event Types
ASGIReceiveEvent = Union[HTTPRequestEvent, HTTPDisconnectEvent, WebSocketConnectEvent, WebSocketReceiveEvent, WebSocketDisconnectEvent, LifespanStartupEvent, LifespanShutdownEvent]
ASGISendEvent = Union[HTTPResponseStartEvent, HTTPResponseBodyEvent, HTTPResponseTrailersEvent, HTTPServerPushEvent, WebSocketAcceptEvent, WebSocketSendEvent, WebSocketCloseEvent, LifespanStartupCompleteEvent, LifespanStartupFailedEvent, LifespanShutdownCompleteEvent, LifespanShutdownFailedEvent]ASGI scope definitions that contain request/connection information passed to applications.
# Main Scope Union
Scope = Union[HTTPScope, WebSocketScope, LifespanScope]
# HTTP Scope
HTTPScope = TypedDict('HTTPScope', {
'type': Literal['http'],
'asgi': ASGIVersions,
'http_version': str,
'method': str,
'scheme': str,
'path': str,
'raw_path': bytes,
'query_string': bytes,
'root_path': str,
'headers': Iterable[Tuple[bytes, bytes]],
'client': Optional[Tuple[str, int]],
'server': Optional[Tuple[str, Optional[int]]],
'state': NotRequired[Dict[str, Any]],
'extensions': Optional[Dict[str, Dict[object, object]]]
})
# WebSocket Scope
WebSocketScope = TypedDict('WebSocketScope', {
'type': Literal['websocket'],
'asgi': ASGIVersions,
'http_version': str,
'scheme': str,
'path': str,
'raw_path': bytes,
'query_string': bytes,
'root_path': str,
'headers': Iterable[Tuple[bytes, bytes]],
'client': Optional[Tuple[str, int]],
'server': Optional[Tuple[str, Optional[int]]],
'subprotocols': Iterable[str],
'state': NotRequired[Dict[str, Any]],
'extensions': Optional[Dict[str, Dict[object, object]]]
})
# Lifespan Scope
LifespanScope = TypedDict('LifespanScope', {
'type': Literal['lifespan'],
'asgi': ASGIVersions,
'state': NotRequired[Dict[str, Any]],
'extensions': Optional[Dict[str, Dict[object, object]]]
})
# Legacy WWW Scope (deprecated)
WWWScope = Union[HTTPScope, WebSocketScope]Type definitions for HTTP protocol events exchanged between servers and applications.
# HTTP Request Events
HTTPRequestEvent = TypedDict('HTTPRequestEvent', {
'type': Literal['http.request'],
'body': bytes,
'more_body': bool
})
HTTPDisconnectEvent = TypedDict('HTTPDisconnectEvent', {
'type': Literal['http.disconnect']
})
# HTTP Response Events
HTTPResponseStartEvent = TypedDict('HTTPResponseStartEvent', {
'type': Literal['http.response.start'],
'status': int,
'headers': Iterable[Tuple[bytes, bytes]],
'trailers': bool
})
HTTPResponseBodyEvent = TypedDict('HTTPResponseBodyEvent', {
'type': Literal['http.response.body'],
'body': bytes,
'more_body': bool
})
HTTPResponseTrailersEvent = TypedDict('HTTPResponseTrailersEvent', {
'type': Literal['http.response.trailers'],
'headers': Iterable[Tuple[bytes, bytes]],
'more_trailers': bool
})
# HTTP Server Push (HTTP/2)
HTTPServerPushEvent = TypedDict('HTTPServerPushEvent', {
'type': Literal['http.response.push'],
'path': str,
'headers': Iterable[Tuple[bytes, bytes]]
})
# HTTP Response Path Send
HTTPResponsePathsendEvent = TypedDict('HTTPResponsePathsendEvent', {
'type': Literal['http.response.pathsend'],
'path': str
})Type definitions for WebSocket protocol events and connection management.
# WebSocket Connection Events
WebSocketConnectEvent = TypedDict('WebSocketConnectEvent', {
'type': Literal['websocket.connect']
})
WebSocketAcceptEvent = TypedDict('WebSocketAcceptEvent', {
'type': Literal['websocket.accept'],
'subprotocol': Optional[str],
'headers': Iterable[Tuple[bytes, bytes]]
})
# WebSocket Message Events
WebSocketReceiveEvent = TypedDict('WebSocketReceiveEvent', {
'type': Literal['websocket.receive'],
'bytes': Optional[bytes],
'text': Optional[str]
})
WebSocketSendEvent = TypedDict('WebSocketSendEvent', {
'type': Literal['websocket.send'],
'bytes': Optional[bytes],
'text': Optional[str]
})
# WebSocket Close Events
WebSocketDisconnectEvent = TypedDict('WebSocketDisconnectEvent', {
'type': Literal['websocket.disconnect'],
'code': int
})
WebSocketCloseEvent = TypedDict('WebSocketCloseEvent', {
'type': Literal['websocket.close'],
'code': int,
'reason': Optional[str]
})
# WebSocket Response Events (for compatibility)
WebSocketResponseStartEvent = TypedDict('WebSocketResponseStartEvent', {
'type': Literal['websocket.http.response.start'],
'status': int,
'headers': Iterable[Tuple[bytes, bytes]]
})
WebSocketResponseBodyEvent = TypedDict('WebSocketResponseBodyEvent', {
'type': Literal['websocket.http.response.body'],
'body': bytes,
'more_body': bool
})Type definitions for application lifespan management events.
# Lifespan Startup Events
LifespanStartupEvent = TypedDict('LifespanStartupEvent', {
'type': Literal['lifespan.startup']
})
LifespanStartupCompleteEvent = TypedDict('LifespanStartupCompleteEvent', {
'type': Literal['lifespan.startup.complete']
})
LifespanStartupFailedEvent = TypedDict('LifespanStartupFailedEvent', {
'type': Literal['lifespan.startup.failed'],
'message': str
})
# Lifespan Shutdown Events
LifespanShutdownEvent = TypedDict('LifespanShutdownEvent', {
'type': Literal['lifespan.shutdown']
})
LifespanShutdownCompleteEvent = TypedDict('LifespanShutdownCompleteEvent', {
'type': Literal['lifespan.shutdown.complete']
})
LifespanShutdownFailedEvent = TypedDict('LifespanShutdownFailedEvent', {
'type': Literal['lifespan.shutdown.failed'],
'message': str
})ASGI version specifications and utility type definitions.
# ASGI Version Types
ASGIVersions = TypedDict('ASGIVersions', {
'spec_version': str,
'version': Union[Literal['2.0'], Literal['3.0']]
})
# Common ASGI version values:
# {'version': '3.0', 'spec_version': '2.3'}
# {'version': '2.1', 'spec_version': '2.1'}from asgiref.typing import ASGIApplication, HTTPScope, ASGIReceiveCallable, ASGISendCallable
from typing import cast
async def typed_http_app(scope: HTTPScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
"""Type-safe HTTP ASGI application."""
assert scope['type'] == 'http'
# Type-safe access to scope properties
method: str = scope['method']
path: str = scope['path']
headers: list[tuple[bytes, bytes]] = scope['headers']
# Type-safe message handling
request_message = await receive()
if request_message['type'] == 'http.request':
body: bytes = request_message['body']
more_body: bool = request_message['more_body']
# Type-safe response sending
await send({
'type': 'http.response.start',
'status': 200,
'headers': [[b'content-type', b'application/json']],
})
await send({
'type': 'http.response.body',
'body': b'{"message": "Type-safe response"}',
'more_body': False,
})
# Type annotation for the application
app: ASGIApplication = typed_http_appfrom asgiref.typing import WebSocketScope, ASGIReceiveCallable, ASGISendCallable
import json
async def typed_websocket_app(scope: WebSocketScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
"""Type-safe WebSocket ASGI application."""
assert scope['type'] == 'websocket'
# Type-safe scope access
path: str = scope['path']
subprotocols: list[str] = scope['subprotocols']
# Accept connection
await send({
'type': 'websocket.accept',
'subprotocol': subprotocols[0] if subprotocols else None,
})
# Message handling loop
while True:
message = await receive()
if message['type'] == 'websocket.disconnect':
code: int = message['code']
print(f"WebSocket disconnected with code: {code}")
break
elif message['type'] == 'websocket.receive':
# Type-safe message access
text_data: str | None = message.get('text')
bytes_data: bytes | None = message.get('bytes')
if text_data:
# Echo text message
await send({
'type': 'websocket.send',
'text': f"Echo: {text_data}",
})from asgiref.typing import ASGIApplication, Scope, ASGIReceiveCallable, ASGISendCallable
from typing import Callable
class TypedMiddleware:
"""Type-safe ASGI middleware."""
def __init__(self, app: ASGIApplication) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
"""Type-safe middleware call."""
if scope['type'] == 'http':
# Add custom header to HTTP responses
async def send_wrapper(message):
if message['type'] == 'http.response.start':
headers = list(message.get('headers', []))
headers.append([b'x-middleware', b'typed'])
message = {**message, 'headers': headers}
await send(message)
await self.app(scope, receive, send_wrapper)
else:
# Pass through other protocols unchanged
await self.app(scope, receive, send)
# Type-safe middleware factory
def create_typed_middleware(app: ASGIApplication) -> ASGIApplication:
"""Create type-safe middleware."""
return TypedMiddleware(app)from asgiref.typing import ASGIApplication, HTTPScope, WebSocketScope, LifespanScope
from typing import Union
class TypedASGIServer:
"""Type-safe ASGI server implementation."""
def __init__(self, app: ASGIApplication) -> None:
self.app = app
async def handle_http(self, scope: HTTPScope) -> None:
"""Handle HTTP requests with type safety."""
async def receive():
return {
'type': 'http.request',
'body': b'',
'more_body': False,
}
async def send(message):
if message['type'] == 'http.response.start':
status: int = message['status']
headers: list[tuple[bytes, bytes]] = message.get('headers', [])
print(f"HTTP {status} with {len(headers)} headers")
elif message['type'] == 'http.response.body':
body: bytes = message.get('body', b'')
print(f"HTTP body: {len(body)} bytes")
await self.app(scope, receive, send)
async def handle_websocket(self, scope: WebSocketScope) -> None:
"""Handle WebSocket connections with type safety."""
async def receive():
return {'type': 'websocket.connect'}
async def send(message):
if message['type'] == 'websocket.accept':
subprotocol: str | None = message.get('subprotocol')
print(f"WebSocket accepted with subprotocol: {subprotocol}")
await self.app(scope, receive, send)
async def handle_lifespan(self, scope: LifespanScope) -> None:
"""Handle lifespan events with type safety."""
async def receive():
return {'type': 'lifespan.startup'}
async def send(message):
if message['type'] == 'lifespan.startup.complete':
print("Application startup complete")
await self.app(scope, receive, send)from asgiref.typing import ASGIApplication, HTTPScope
from asgiref.testing import ApplicationCommunicator
async def test_typed_application(app: ASGIApplication) -> None:
"""Type-safe application testing."""
# Create type-safe scope
scope: HTTPScope = {
'type': 'http',
'asgi': {'version': '3.0', 'spec_version': '2.3'},
'http_version': '1.1',
'method': 'GET',
'scheme': 'http',
'path': '/',
'raw_path': b'/',
'query_string': b'',
'root_path': '',
'headers': [[b'host', b'example.com']],
'server': ('127.0.0.1', 8000),
'client': ('127.0.0.1', 12345),
'state': {},
'extensions': {},
}
# Type-safe testing
communicator = ApplicationCommunicator(app, scope)
try:
# Send typed request
await communicator.send_input({
'type': 'http.request',
'body': b'test data',
'more_body': False,
})
# Receive typed response
response_start = await communicator.receive_output()
assert response_start['type'] == 'http.response.start'
status: int = response_start['status']
headers: list[tuple[bytes, bytes]] = response_start.get('headers', [])
print(f"Response: {status} with {len(headers)} headers")
finally:
await communicator.stop()from asgiref.typing import Scope, HTTPScope, WebSocketScope, LifespanScope
from typing import TypeGuard
def is_http_scope(scope: Scope) -> TypeGuard[HTTPScope]:
"""Type guard for HTTP scopes."""
return scope['type'] == 'http'
def is_websocket_scope(scope: Scope) -> TypeGuard[WebSocketScope]:
"""Type guard for WebSocket scopes."""
return scope['type'] == 'websocket'
def is_lifespan_scope(scope: Scope) -> TypeGuard[LifespanScope]:
"""Type guard for Lifespan scopes."""
return scope['type'] == 'lifespan'
async def protocol_aware_app(scope: Scope, receive, send) -> None:
"""Application that uses type guards for protocol detection."""
if is_http_scope(scope):
# TypeScript-style type narrowing
method: str = scope['method'] # Type checker knows this is HTTPScope
path: str = scope['path']
print(f"HTTP {method} {path}")
elif is_websocket_scope(scope):
# Type checker knows this is WebSocketScope
subprotocols: list[str] = scope['subprotocols']
print(f"WebSocket with subprotocols: {subprotocols}")
elif is_lifespan_scope(scope):
# Type checker knows this is LifespanScope
print("Lifespan event")The type definitions provide:
Install with Tessl CLI
npx tessl i tessl/pypi-asgiref