Skip to content

Commit d1cd5cb

Browse files
authored
docs(api_gateway): new event handler for API Gateway and ALB (#418)
1 parent 59b3adf commit d1cd5cb

File tree

5 files changed

+829
-317
lines changed

5 files changed

+829
-317
lines changed

aws_lambda_powertools/event_handler/api_gateway.py

+195-73
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import base64
22
import json
3+
import logging
34
import re
45
import zlib
56
from enum import Enum
@@ -10,6 +11,8 @@
1011
from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent
1112
from aws_lambda_powertools.utilities.typing import LambdaContext
1213

14+
logger = logging.getLogger(__name__)
15+
1316

1417
class ProxyEventType(Enum):
1518
"""An enumerations of the supported proxy event types."""
@@ -28,47 +31,47 @@ class CORSConfig(object):
2831
2932
Simple cors example using the default permissive cors, not this should only be used during early prototyping
3033
31-
>>> from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
32-
>>>
33-
>>> app = ApiGatewayResolver()
34-
>>>
35-
>>> @app.get("/my/path", cors=True)
36-
>>> def with_cors():
37-
>>> return {"message": "Foo"}
34+
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
35+
36+
app = ApiGatewayResolver()
37+
38+
@app.get("/my/path", cors=True)
39+
def with_cors():
40+
return {"message": "Foo"}
3841
3942
Using a custom CORSConfig where `with_cors` used the custom provided CORSConfig and `without_cors`
4043
do not include any cors headers.
4144
42-
>>> from aws_lambda_powertools.event_handler.api_gateway import (
43-
>>> ApiGatewayResolver, CORSConfig
44-
>>> )
45-
>>>
46-
>>> cors_config = CORSConfig(
47-
>>> allow_origin="https://wwww.example.com/",
48-
>>> expose_headers=["x-exposed-response-header"],
49-
>>> allow_headers=["x-custom-request-header"],
50-
>>> max_age=100,
51-
>>> allow_credentials=True,
52-
>>> )
53-
>>> app = ApiGatewayResolver(cors=cors_config)
54-
>>>
55-
>>> @app.get("/my/path", cors=True)
56-
>>> def with_cors():
57-
>>> return {"message": "Foo"}
58-
>>>
59-
>>> @app.get("/another-one")
60-
>>> def without_cors():
61-
>>> return {"message": "Foo"}
45+
from aws_lambda_powertools.event_handler.api_gateway import (
46+
ApiGatewayResolver, CORSConfig
47+
)
48+
49+
cors_config = CORSConfig(
50+
allow_origin="https://wwww.example.com/",
51+
expose_headers=["x-exposed-response-header"],
52+
allow_headers=["x-custom-request-header"],
53+
max_age=100,
54+
allow_credentials=True,
55+
)
56+
app = ApiGatewayResolver(cors=cors_config)
57+
58+
@app.get("/my/path", cors=True)
59+
def with_cors():
60+
return {"message": "Foo"}
61+
62+
@app.get("/another-one")
63+
def without_cors():
64+
return {"message": "Foo"}
6265
"""
6366

6467
_REQUIRED_HEADERS = ["Authorization", "Content-Type", "X-Amz-Date", "X-Api-Key", "X-Amz-Security-Token"]
6568

6669
def __init__(
6770
self,
6871
allow_origin: str = "*",
69-
allow_headers: List[str] = None,
70-
expose_headers: List[str] = None,
71-
max_age: int = None,
72+
allow_headers: Optional[List[str]] = None,
73+
expose_headers: Optional[List[str]] = None,
74+
max_age: Optional[int] = None,
7275
allow_credentials: bool = False,
7376
):
7477
"""
@@ -77,13 +80,13 @@ def __init__(
7780
allow_origin: str
7881
The value of the `Access-Control-Allow-Origin` to send in the response. Defaults to "*", but should
7982
only be used during development.
80-
allow_headers: str
83+
allow_headers: Optional[List[str]]
8184
The list of additional allowed headers. This list is added to list of
8285
built in allowed headers: `Authorization`, `Content-Type`, `X-Amz-Date`,
8386
`X-Api-Key`, `X-Amz-Security-Token`.
84-
expose_headers: str
87+
expose_headers: Optional[List[str]]
8588
A list of values to return for the Access-Control-Expose-Headers
86-
max_age: int
89+
max_age: Optional[int]
8790
The value for the `Access-Control-Max-Age`
8891
allow_credentials: bool
8992
A boolean value that sets the value of `Access-Control-Allow-Credentials`
@@ -170,6 +173,7 @@ def _compress(self):
170173
"""Compress the response body, but only if `Accept-Encoding` headers includes gzip."""
171174
self.response.headers["Content-Encoding"] = "gzip"
172175
if isinstance(self.response.body, str):
176+
logger.debug("Converting string response to bytes before compressing it")
173177
self.response.body = bytes(self.response.body, "utf-8")
174178
gzip = zlib.compressobj(9, zlib.DEFLATED, zlib.MAX_WBITS | 16)
175179
self.response.body = gzip.compress(self.response.body) + gzip.flush()
@@ -190,6 +194,7 @@ def build(self, event: BaseProxyEvent, cors: CORSConfig = None) -> Dict[str, Any
190194
self._route(event, cors)
191195

192196
if isinstance(self.response.body, bytes):
197+
logger.debug("Encoding bytes response with base64")
193198
self.response.base64_encoded = True
194199
self.response.body = base64.b64encode(self.response.body).decode()
195200
return {
@@ -207,27 +212,26 @@ class ApiGatewayResolver:
207212
--------
208213
Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator
209214
210-
>>> from aws_lambda_powertools import Tracer
211-
>>> from aws_lambda_powertools.event_handler.api_gateway import (
212-
>>> ApiGatewayResolver
213-
>>> )
214-
>>>
215-
>>> tracer = Tracer()
216-
>>> app = ApiGatewayResolver()
217-
>>>
218-
>>> @app.get("/get-call")
219-
>>> def simple_get():
220-
>>> return {"message": "Foo"}
221-
>>>
222-
>>> @app.post("/post-call")
223-
>>> def simple_post():
224-
>>> post_data: dict = app.current_event.json_body
225-
>>> return {"message": post_data["value"]}
226-
>>>
227-
>>> @tracer.capture_lambda_handler
228-
>>> def lambda_handler(event, context):
229-
>>> return app.resolve(event, context)
215+
```python
216+
from aws_lambda_powertools import Tracer
217+
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
218+
219+
tracer = Tracer()
220+
app = ApiGatewayResolver()
230221
222+
@app.get("/get-call")
223+
def simple_get():
224+
return {"message": "Foo"}
225+
226+
@app.post("/post-call")
227+
def simple_post():
228+
post_data: dict = app.current_event.json_body
229+
return {"message": post_data["value"]}
230+
231+
@tracer.capture_lambda_handler
232+
def lambda_handler(event, context):
233+
return app.resolve(event, context)
234+
```
231235
"""
232236

233237
current_event: BaseProxyEvent
@@ -247,32 +251,144 @@ def __init__(self, proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent, cors:
247251
self._cors = cors
248252
self._cors_methods: Set[str] = {"OPTIONS"}
249253

250-
def get(self, rule: str, cors: bool = False, compress: bool = False, cache_control: str = None):
251-
"""Get route decorator with GET `method`"""
254+
def get(self, rule: str, cors: bool = True, compress: bool = False, cache_control: str = None):
255+
"""Get route decorator with GET `method`
256+
257+
Examples
258+
--------
259+
Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator
260+
261+
```python
262+
from aws_lambda_powertools import Tracer
263+
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
264+
265+
tracer = Tracer()
266+
app = ApiGatewayResolver()
267+
268+
@app.get("/get-call")
269+
def simple_get():
270+
return {"message": "Foo"}
271+
272+
@tracer.capture_lambda_handler
273+
def lambda_handler(event, context):
274+
return app.resolve(event, context)
275+
```
276+
"""
252277
return self.route(rule, "GET", cors, compress, cache_control)
253278

254-
def post(self, rule: str, cors: bool = False, compress: bool = False, cache_control: str = None):
255-
"""Post route decorator with POST `method`"""
279+
def post(self, rule: str, cors: bool = True, compress: bool = False, cache_control: str = None):
280+
"""Post route decorator with POST `method`
281+
282+
Examples
283+
--------
284+
Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator
285+
286+
```python
287+
from aws_lambda_powertools import Tracer
288+
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
289+
290+
tracer = Tracer()
291+
app = ApiGatewayResolver()
292+
293+
@app.post("/post-call")
294+
def simple_post():
295+
post_data: dict = app.current_event.json_body
296+
return {"message": post_data["value"]}
297+
298+
@tracer.capture_lambda_handler
299+
def lambda_handler(event, context):
300+
return app.resolve(event, context)
301+
```
302+
"""
256303
return self.route(rule, "POST", cors, compress, cache_control)
257304

258-
def put(self, rule: str, cors: bool = False, compress: bool = False, cache_control: str = None):
259-
"""Put route decorator with PUT `method`"""
305+
def put(self, rule: str, cors: bool = True, compress: bool = False, cache_control: str = None):
306+
"""Put route decorator with PUT `method`
307+
308+
Examples
309+
--------
310+
Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator
311+
312+
```python
313+
from aws_lambda_powertools import Tracer
314+
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
315+
316+
tracer = Tracer()
317+
app = ApiGatewayResolver()
318+
319+
@app.put("/put-call")
320+
def simple_post():
321+
put_data: dict = app.current_event.json_body
322+
return {"message": put_data["value"]}
323+
324+
@tracer.capture_lambda_handler
325+
def lambda_handler(event, context):
326+
return app.resolve(event, context)
327+
```
328+
"""
260329
return self.route(rule, "PUT", cors, compress, cache_control)
261330

262-
def delete(self, rule: str, cors: bool = False, compress: bool = False, cache_control: str = None):
263-
"""Delete route decorator with DELETE `method`"""
331+
def delete(self, rule: str, cors: bool = True, compress: bool = False, cache_control: str = None):
332+
"""Delete route decorator with DELETE `method`
333+
334+
Examples
335+
--------
336+
Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator
337+
338+
```python
339+
from aws_lambda_powertools import Tracer
340+
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
341+
342+
tracer = Tracer()
343+
app = ApiGatewayResolver()
344+
345+
@app.delete("/delete-call")
346+
def simple_delete():
347+
return {"message": "deleted"}
348+
349+
@tracer.capture_lambda_handler
350+
def lambda_handler(event, context):
351+
return app.resolve(event, context)
352+
```
353+
"""
264354
return self.route(rule, "DELETE", cors, compress, cache_control)
265355

266-
def patch(self, rule: str, cors: bool = False, compress: bool = False, cache_control: str = None):
267-
"""Patch route decorator with PATCH `method`"""
356+
def patch(self, rule: str, cors: bool = True, compress: bool = False, cache_control: str = None):
357+
"""Patch route decorator with PATCH `method`
358+
359+
Examples
360+
--------
361+
Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator
362+
363+
```python
364+
from aws_lambda_powertools import Tracer
365+
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
366+
367+
tracer = Tracer()
368+
app = ApiGatewayResolver()
369+
370+
@app.patch("/patch-call")
371+
def simple_patch():
372+
patch_data: dict = app.current_event.json_body
373+
patch_data["value"] = patched
374+
375+
return {"message": patch_data}
376+
377+
@tracer.capture_lambda_handler
378+
def lambda_handler(event, context):
379+
return app.resolve(event, context)
380+
```
381+
"""
268382
return self.route(rule, "PATCH", cors, compress, cache_control)
269383

270-
def route(self, rule: str, method: str, cors: bool = False, compress: bool = False, cache_control: str = None):
384+
def route(self, rule: str, method: str, cors: bool = True, compress: bool = False, cache_control: str = None):
271385
"""Route decorator includes parameter `method`"""
272386

273387
def register_resolver(func: Callable):
388+
logger.debug(f"Adding route using rule {rule} and method {method.upper()}")
274389
self._routes.append(Route(method, self._compile_regex(rule), func, cors, compress, cache_control))
275390
if cors:
391+
logger.debug(f"Registering method {method.upper()} to Allow Methods in CORS")
276392
self._cors_methods.add(method.upper())
277393
return func
278394

@@ -308,9 +424,12 @@ def _compile_regex(rule: str):
308424
def _to_proxy_event(self, event: Dict) -> BaseProxyEvent:
309425
"""Convert the event dict to the corresponding data class"""
310426
if self._proxy_type == ProxyEventType.APIGatewayProxyEvent:
427+
logger.debug("Converting event to API Gateway REST API contract")
311428
return APIGatewayProxyEvent(event)
312429
if self._proxy_type == ProxyEventType.APIGatewayProxyEventV2:
430+
logger.debug("Converting event to API Gateway HTTP API contract")
313431
return APIGatewayProxyEventV2(event)
432+
logger.debug("Converting event to ALB contract")
314433
return ALBEvent(event)
315434

316435
def _resolve(self) -> ResponseBuilder:
@@ -322,17 +441,21 @@ def _resolve(self) -> ResponseBuilder:
322441
continue
323442
match: Optional[re.Match] = route.rule.match(path)
324443
if match:
444+
logger.debug("Found a registered route. Calling function")
325445
return self._call_route(route, match.groupdict())
326446

447+
logger.debug(f"No match found for path {path} and method {method}")
327448
return self._not_found(method)
328449

329450
def _not_found(self, method: str) -> ResponseBuilder:
330451
"""Called when no matching route was found and includes support for the cors preflight response"""
331452
headers = {}
332453
if self._cors:
454+
logger.debug("CORS is enabled, updating headers.")
333455
headers.update(self._cors.to_dict())
334456

335-
if method == "OPTIONS": # Preflight
457+
if method == "OPTIONS": # Pre-flight
458+
logger.debug("Pre-flight request detected. Returning CORS with null response")
336459
headers["Access-Control-Allow-Methods"] = ",".join(sorted(self._cors_methods))
337460
return ResponseBuilder(Response(status_code=204, content_type=None, headers=headers, body=None))
338461

@@ -361,11 +484,10 @@ def _to_response(result: Union[Dict, Response]) -> Response:
361484
"""
362485
if isinstance(result, Response):
363486
return result
364-
elif isinstance(result, dict):
365-
return Response(
366-
status_code=200,
367-
content_type="application/json",
368-
body=json.dumps(result, separators=(",", ":"), cls=Encoder),
369-
)
370-
else: # Tuple[int, str, Union[bytes, str]]
371-
return Response(*result)
487+
488+
logger.debug("Simple response detected, serializing return before constructing final response")
489+
return Response(
490+
status_code=200,
491+
content_type="application/json",
492+
body=json.dumps(result, separators=(",", ":"), cls=Encoder),
493+
)

0 commit comments

Comments
 (0)