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,152 +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- """Creates a scope object according to ASGI specification from a Lambda Event.
109-
110- https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope
111-
112- The event comes from various sources: AWS ALB, AWS API Gateway of different
113- versions and configurations(multivalue header, etc).
114- Thus, some heuristics is applied to guess an event type.
115-
116- """
117- request_context = event ["requestContext" ]
118-
119- if event .get ("multiValueHeaders" ):
120- headers = {
121- k .lower (): ", " .join (v ) if isinstance (v , list ) else ""
122- for k , v in event .get ("multiValueHeaders" , {}).items ()
123- }
124- elif event .get ("headers" ):
125- headers = {k .lower (): v for k , v in event .get ("headers" , {}).items ()}
126- else :
127- headers = {}
128-
129- # API Gateway v2
130- if event .get ("version" ) == "2.0" :
131- source_ip = request_context ["http" ]["sourceIp" ]
132- path = request_context ["http" ]["path" ]
133- http_method = request_context ["http" ]["method" ]
134- query_string = event .get ("rawQueryString" , "" ).encode ()
135-
136- if event .get ("cookies" ):
137- headers ["cookie" ] = "; " .join (event .get ("cookies" , []))
138-
139- # API Gateway v1 / ELB
140- else :
141- if "elb" in request_context :
142- # NOTE: trust only the most right side value
143- source_ip = headers .get ("x-forwarded-for" , "" ).split (", " )[- 1 ]
144- else :
145- source_ip = request_context .get ("identity" , {}).get ("sourceIp" )
146-
147- path = event ["path" ]
148- http_method = event ["httpMethod" ]
149-
150- if event .get ("multiValueQueryStringParameters" ):
151- query_string = urllib .parse .urlencode (
152- event .get ("multiValueQueryStringParameters" , {}), doseq = True
153- ).encode ()
154- elif event .get ("queryStringParameters" ):
155- query_string = urllib .parse .urlencode (
156- event .get ("queryStringParameters" , {})
157- ).encode ()
158- else :
159- query_string = b""
160-
161- server_name = headers .get ("host" , "mangum" )
162- if ":" not in server_name :
163- server_port = headers .get ("x-forwarded-port" , 80 )
164- else :
165- server_name , server_port = server_name .split (":" ) # pragma: no cover
166- server = (server_name , int (server_port ))
167- client = (source_ip , 0 )
168-
169- if not path : # pragma: no cover
170- path = "/"
171- elif self .api_gateway_base_path :
172- if path .startswith (self .api_gateway_base_path ):
173- path = path [len (self .api_gateway_base_path ) :]
174-
175- scope = {
176- "type" : "http" ,
177- "http_version" : "1.1" ,
178- "method" : http_method ,
179- "headers" : [[k .encode (), v .encode ()] for k , v in headers .items ()],
180- "path" : urllib .parse .unquote (path ),
181- "raw_path" : None ,
182- "root_path" : "" ,
183- "scheme" : headers .get ("x-forwarded-proto" , "https" ),
184- "query_string" : query_string ,
185- "server" : server ,
186- "client" : client ,
187- "asgi" : {"version" : "3.0" },
188- "aws.event" : event ,
189- "aws.context" : context ,
190- }
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 )
19173
192- return scope
74+ return handler . transform_response ( response )
0 commit comments