11import base64
2- import typing
2+ from typing import Any , Callable , ContextManager , Dict , Optional , List , TYPE_CHECKING
33import logging
44import urllib .parse
55
66from dataclasses import dataclass , InitVar
77from contextlib import ExitStack
88
9- from mangum .types import ASGIApp , Scope
9+ from mangum .handlers import AbstractHandler
10+ from mangum .response import Response
11+ from mangum .types import ASGIApp , ScopeDict
1012from mangum .protocols .lifespan import LifespanCycle
1113from mangum .protocols .http import HTTPCycle
1214from mangum .exceptions import ConfigurationError
1315
14- if typing . TYPE_CHECKING : # pragma: no cover
16+ if TYPE_CHECKING : # pragma: no cover
1517 from awslambdaric .lambda_context import LambdaContext
1618
1719DEFAULT_TEXT_MIME_TYPES = [
20+ "text/" ,
1821 "application/json" ,
1922 "application/javascript" ,
2023 "application/xml" ,
2124 "application/vnd.api+json" ,
2225]
2326
24- LOG_LEVELS = {
25- "critical" : logging .CRITICAL ,
26- "error" : logging .ERROR ,
27- "warning" : logging .WARNING ,
28- "info" : logging .INFO ,
29- "debug" : logging .DEBUG ,
30- }
27+ logger = logging .getLogger ("mangum" )
3128
3229
33- @dataclass
3430class Mangum :
3531 """
3632 Creates an adapter instance.
@@ -41,153 +37,38 @@ class Mangum:
4137 and `off`. Default is `auto`.
4238 * **log_level** - A string to configure the log level. Choices are: `info`,
4339 `critical`, `error`, `warning`, and `debug`. Default is `info`.
44- * **api_gateway_base_path** - Base path to strip from URL when using a custom
45- domain name.
4640 * **text_mime_types** - A list of MIME types to include with the defaults that
4741 should not return a binary response in API Gateway.
4842 """
4943
5044 app : ASGIApp
5145 lifespan : str = "auto"
52- log_level : str = "info"
53- api_gateway_base_path : typing .Optional [str ] = None
54- text_mime_types : InitVar [typing .Optional [typing .List [str ]]] = None
5546
56- def __post_init__ (self , text_mime_types : typing .Optional [typing .List [str ]]) -> None :
47+ def __init__ (self , app : ASGIApp ,
48+ lifespan : str = "auto" ,
49+ ** handler_kwargs ,
50+ ):
51+ self .app = app
52+ self .lifespan = lifespan
53+ self .handler_kwargs = handler_kwargs
54+
5755 if self .lifespan not in ("auto" , "on" , "off" ):
5856 raise ConfigurationError (
5957 "Invalid argument supplied for `lifespan`. Choices are: auto|on|off"
6058 )
6159
62- if self .log_level not in ("critical" , "error" , "warning" , "info" , "debug" ):
63- raise ConfigurationError (
64- "Invalid argument supplied for `log_level`. "
65- "Choices are: critical|error|warning|info|debug"
66- )
67-
68- self .logger = logging .getLogger ("mangum" )
69- self .logger .setLevel (LOG_LEVELS [self .log_level ])
70-
71- should_prefix_base_path = (
72- self .api_gateway_base_path
73- and not self .api_gateway_base_path .startswith ("/" )
74- )
75- if should_prefix_base_path :
76- self .api_gateway_base_path = f"/{ self .api_gateway_base_path } "
77-
78- if text_mime_types :
79- text_mime_types += DEFAULT_TEXT_MIME_TYPES
80- else :
81- text_mime_types = DEFAULT_TEXT_MIME_TYPES
82- self .text_mime_types = text_mime_types
83-
8460 def __call__ (self , event : dict , context : "LambdaContext" ) -> dict :
85- self . logger .debug ("Event received." )
61+ logger .debug ("Event received." )
8662
8763 with ExitStack () as stack :
8864 if self .lifespan != "off" :
89- lifespan_cycle : typing . ContextManager = LifespanCycle (
65+ lifespan_cycle : ContextManager = LifespanCycle (
9066 self .app , self .lifespan
9167 )
9268 stack .enter_context (lifespan_cycle )
9369
94- is_binary = event .get ("isBase64Encoded" , False )
95- initial_body = event .get ("body" ) or b""
96- if is_binary :
97- initial_body = base64 .b64decode (initial_body )
98- elif not isinstance (initial_body , bytes ):
99- initial_body = initial_body .encode ()
100-
101- scope = self .create_scope (event , context )
102- http_cycle = HTTPCycle (scope , text_mime_types = self .text_mime_types )
103- response = http_cycle (self .app , initial_body )
104-
105- return response
106-
107- def create_scope (self , event : dict , context : "LambdaContext" ) -> Scope :
108- """
109- Creates a scope object according to ASGI specification from a Lambda Event.
110-
111- https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope
112-
113- The event comes from various sources: AWS ALB, AWS API Gateway of different
114- versions and configurations(multivalue header, etc).
115- Thus, some heuristics is applied to guess an event type.
116-
117- """
118- request_context = event ["requestContext" ]
119-
120- if event .get ("multiValueHeaders" ):
121- headers = {
122- k .lower (): ", " .join (v ) if isinstance (v , list ) else ""
123- for k , v in event .get ("multiValueHeaders" , {}).items ()
124- }
125- elif event .get ("headers" ):
126- headers = {k .lower (): v for k , v in event .get ("headers" , {}).items ()}
127- else :
128- headers = {}
129-
130- # API Gateway v2
131- if event .get ("version" ) == "2.0" :
132- source_ip = request_context ["http" ]["sourceIp" ]
133- path = request_context ["http" ]["path" ]
134- http_method = request_context ["http" ]["method" ]
135- query_string = event .get ("rawQueryString" , "" ).encode ()
136-
137- if event .get ("cookies" ):
138- headers ["cookie" ] = "; " .join (event .get ("cookies" , []))
139-
140- # API Gateway v1 / ELB
141- else :
142- if "elb" in request_context :
143- # NOTE: trust only the most right side value
144- source_ip = headers .get ("x-forwarded-for" , "" ).split (", " )[- 1 ]
145- else :
146- source_ip = request_context .get ("identity" , {}).get ("sourceIp" )
147-
148- path = event ["path" ]
149- http_method = event ["httpMethod" ]
150-
151- if event .get ("multiValueQueryStringParameters" ):
152- query_string = urllib .parse .urlencode (
153- event .get ("multiValueQueryStringParameters" , {}), doseq = True
154- ).encode ()
155- elif event .get ("queryStringParameters" ):
156- query_string = urllib .parse .urlencode (
157- event .get ("queryStringParameters" , {})
158- ).encode ()
159- else :
160- query_string = b""
161-
162- server_name = headers .get ("host" , "mangum" )
163- if ":" not in server_name :
164- server_port = headers .get ("x-forwarded-port" , 80 )
165- else :
166- server_name , server_port = server_name .split (":" ) # pragma: no cover
167- server = (server_name , int (server_port ))
168- client = (source_ip , 0 )
169-
170- if not path : # pragma: no cover
171- path = "/"
172- elif self .api_gateway_base_path :
173- if path .startswith (self .api_gateway_base_path ):
174- path = path [len (self .api_gateway_base_path ) :]
175-
176- scope = {
177- "type" : "http" ,
178- "http_version" : "1.1" ,
179- "method" : http_method ,
180- "headers" : [[k .encode (), v .encode ()] for k , v in headers .items ()],
181- "path" : urllib .parse .unquote (path ),
182- "raw_path" : None ,
183- "root_path" : "" ,
184- "scheme" : headers .get ("x-forwarded-proto" , "https" ),
185- "query_string" : query_string ,
186- "server" : server ,
187- "client" : client ,
188- "asgi" : {"version" : "3.0" },
189- "aws.event" : event ,
190- "aws.context" : context ,
191- }
70+ handler = AbstractHandler .from_trigger (event , context , ** self .handler_kwargs )
71+ http_cycle = HTTPCycle (handler .scope .as_dict ())
72+ response = http_cycle (self .app , handler .body )
19273
193- return scope
74+ return handler . transform_response ( response )
0 commit comments