1- import base64
2- import typing
31import logging
4- import urllib .parse
5-
6- from dataclasses import dataclass , InitVar
72from contextlib import ExitStack
8-
9- from mangum .types import ASGIApp , Scope
10- from mangum .protocols .lifespan import LifespanCycle
11- from mangum .protocols .http import HTTPCycle
12- from mangum .exceptions import ConfigurationError
13-
14- if typing .TYPE_CHECKING : # pragma: no cover
3+ from typing import (
4+ Any ,
5+ ContextManager ,
6+ Dict ,
7+ TYPE_CHECKING ,
8+ )
9+
10+ from .exceptions import ConfigurationError
11+ from .handlers import AbstractHandler
12+ from .protocols import HTTPCycle , LifespanCycle
13+ from .types import ASGIApp
14+
15+ if TYPE_CHECKING : # pragma: no cover
1516 from awslambdaric .lambda_context import LambdaContext
1617
1718DEFAULT_TEXT_MIME_TYPES = [
19+ "text/" ,
1820 "application/json" ,
1921 "application/javascript" ,
2022 "application/xml" ,
2123 "application/vnd.api+json" ,
2224]
2325
24- LOG_LEVELS = {
25- "critical" : logging .CRITICAL ,
26- "error" : logging .ERROR ,
27- "warning" : logging .WARNING ,
28- "info" : logging .INFO ,
29- "debug" : logging .DEBUG ,
30- }
26+ logger = logging .getLogger ("mangum" )
3127
3228
33- @dataclass
3429class Mangum :
3530 """
3631 Creates an adapter instance.
@@ -41,153 +36,40 @@ class Mangum:
4136 and `off`. Default is `auto`.
4237 * **log_level** - A string to configure the log level. Choices are: `info`,
4338 `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.
4639 * **text_mime_types** - A list of MIME types to include with the defaults that
4740 should not return a binary response in API Gateway.
4841 """
4942
5043 app : ASGIApp
5144 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
5545
56- def __post_init__ (self , text_mime_types : typing .Optional [typing .List [str ]]) -> None :
46+ def __init__ (
47+ self ,
48+ app : ASGIApp ,
49+ lifespan : str = "auto" ,
50+ ** handler_kwargs : Dict [str , Any ],
51+ ):
52+ self .app = app
53+ self .lifespan = lifespan
54+ self .handler_kwargs = handler_kwargs
55+
5756 if self .lifespan not in ("auto" , "on" , "off" ):
5857 raise ConfigurationError (
5958 "Invalid argument supplied for `lifespan`. Choices are: auto|on|off"
6059 )
6160
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-
8461 def __call__ (self , event : dict , context : "LambdaContext" ) -> dict :
85- self . logger .debug ("Event received." )
62+ logger .debug ("Event received." )
8663
8764 with ExitStack () as stack :
8865 if self .lifespan != "off" :
89- lifespan_cycle : typing .ContextManager = LifespanCycle (
90- self .app , self .lifespan
91- )
66+ lifespan_cycle : ContextManager = LifespanCycle (self .app , self .lifespan )
9267 stack .enter_context (lifespan_cycle )
9368
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- }
69+ handler = AbstractHandler .from_trigger (
70+ event , context , ** self .handler_kwargs
71+ )
72+ http_cycle = HTTPCycle (handler .scope )
73+ response = http_cycle (self .app , handler .body )
19274
193- return scope
75+ return handler . transform_response ( response )
0 commit comments