Skip to content

Commit 0b97dd8

Browse files
author
Michael Brewer
committed
Merge branch 'develop' into feat-makefile-mypy
2 parents f10cc4e + 43b828e commit 0b97dd8

File tree

9 files changed

+365
-78
lines changed

9 files changed

+365
-78
lines changed

aws_lambda_powertools/event_handler/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Event handler decorators for common Lambda events
33
"""
44

5+
from .api_gateway import ApiGatewayResolver
56
from .appsync import AppSyncResolver
67

7-
__all__ = ["AppSyncResolver"]
8+
__all__ = ["AppSyncResolver", "ApiGatewayResolver"]

aws_lambda_powertools/event_handler/api_gateway.py

+87-35
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import base64
22
import json
33
import logging
4+
import os
45
import re
6+
import traceback
57
import zlib
68
from enum import Enum
9+
from http import HTTPStatus
710
from typing import Any, Callable, Dict, List, Optional, Set, Union
811

12+
from aws_lambda_powertools.event_handler import content_types
13+
from aws_lambda_powertools.event_handler.exceptions import ServiceError
14+
from aws_lambda_powertools.shared import constants
15+
from aws_lambda_powertools.shared.functions import resolve_truthy_env_var_choice
916
from aws_lambda_powertools.shared.json_encoder import Encoder
1017
from aws_lambda_powertools.utilities.data_classes import ALBEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2
1118
from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent
@@ -25,43 +32,46 @@ class ProxyEventType(Enum):
2532
class CORSConfig(object):
2633
"""CORS Config
2734
28-
2935
Examples
3036
--------
3137
3238
Simple cors example using the default permissive cors, not this should only be used during early prototyping
3339
34-
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
40+
```python
41+
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
3542
36-
app = ApiGatewayResolver()
43+
app = ApiGatewayResolver()
3744
38-
@app.get("/my/path", cors=True)
39-
def with_cors():
40-
return {"message": "Foo"}
45+
@app.get("/my/path", cors=True)
46+
def with_cors():
47+
return {"message": "Foo"}
48+
```
4149
4250
Using a custom CORSConfig where `with_cors` used the custom provided CORSConfig and `without_cors`
4351
do not include any cors headers.
4452
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")
59-
def with_cors():
60-
return {"message": "Foo"}
53+
```python
54+
from aws_lambda_powertools.event_handler.api_gateway import (
55+
ApiGatewayResolver, CORSConfig
56+
)
57+
58+
cors_config = CORSConfig(
59+
allow_origin="https://wwww.example.com/",
60+
expose_headers=["x-exposed-response-header"],
61+
allow_headers=["x-custom-request-header"],
62+
max_age=100,
63+
allow_credentials=True,
64+
)
65+
app = ApiGatewayResolver(cors=cors_config)
66+
67+
@app.get("/my/path")
68+
def with_cors():
69+
return {"message": "Foo"}
6170
62-
@app.get("/another-one", cors=False)
63-
def without_cors():
64-
return {"message": "Foo"}
71+
@app.get("/another-one", cors=False)
72+
def without_cors():
73+
return {"message": "Foo"}
74+
```
6575
"""
6676

6777
_REQUIRED_HEADERS = ["Authorization", "Content-Type", "X-Amz-Date", "X-Api-Key", "X-Amz-Security-Token"]
@@ -237,20 +247,31 @@ def lambda_handler(event, context):
237247
current_event: BaseProxyEvent
238248
lambda_context: LambdaContext
239249

240-
def __init__(self, proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent, cors: CORSConfig = None):
250+
def __init__(
251+
self,
252+
proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent,
253+
cors: CORSConfig = None,
254+
debug: Optional[bool] = None,
255+
):
241256
"""
242257
Parameters
243258
----------
244259
proxy_type: ProxyEventType
245260
Proxy request type, defaults to API Gateway V1
246261
cors: CORSConfig
247262
Optionally configure and enabled CORS. Not each route will need to have to cors=True
263+
debug: Optional[bool]
264+
Enables debug mode, by default False. Can be also be enabled by "POWERTOOLS_EVENT_HANDLER_DEBUG"
265+
environment variable
248266
"""
249267
self._proxy_type = proxy_type
250268
self._routes: List[Route] = []
251269
self._cors = cors
252270
self._cors_enabled: bool = cors is not None
253271
self._cors_methods: Set[str] = {"OPTIONS"}
272+
self._debug = resolve_truthy_env_var_choice(
273+
choice=debug, env=os.getenv(constants.EVENT_HANDLER_DEBUG_ENV, "false")
274+
)
254275

255276
def get(self, rule: str, cors: bool = None, compress: bool = False, cache_control: str = None):
256277
"""Get route decorator with GET `method`
@@ -413,6 +434,8 @@ def resolve(self, event, context) -> Dict[str, Any]:
413434
dict
414435
Returns the dict response
415436
"""
437+
if self._debug:
438+
print(self._json_dump(event))
416439
self.current_event = self._to_proxy_event(event)
417440
self.lambda_context = context
418441
return self._resolve().build(self.current_event, self._cors)
@@ -466,19 +489,41 @@ def _not_found(self, method: str) -> ResponseBuilder:
466489

467490
return ResponseBuilder(
468491
Response(
469-
status_code=404,
470-
content_type="application/json",
492+
status_code=HTTPStatus.NOT_FOUND.value,
493+
content_type=content_types.APPLICATION_JSON,
471494
headers=headers,
472-
body=json.dumps({"message": "Not found"}),
495+
body=self._json_dump({"statusCode": HTTPStatus.NOT_FOUND.value, "message": "Not found"}),
473496
)
474497
)
475498

476499
def _call_route(self, route: Route, args: Dict[str, str]) -> ResponseBuilder:
477500
"""Actually call the matching route with any provided keyword arguments."""
478-
return ResponseBuilder(self._to_response(route.func(**args)), route)
479-
480-
@staticmethod
481-
def _to_response(result: Union[Dict, Response]) -> Response:
501+
try:
502+
return ResponseBuilder(self._to_response(route.func(**args)), route)
503+
except ServiceError as e:
504+
return ResponseBuilder(
505+
Response(
506+
status_code=e.status_code,
507+
content_type=content_types.APPLICATION_JSON,
508+
body=self._json_dump({"statusCode": e.status_code, "message": e.msg}),
509+
),
510+
route,
511+
)
512+
except Exception:
513+
if self._debug:
514+
# If the user has turned on debug mode,
515+
# we'll let the original exception propagate so
516+
# they get more information about what went wrong.
517+
return ResponseBuilder(
518+
Response(
519+
status_code=500,
520+
content_type=content_types.TEXT_PLAIN,
521+
body="".join(traceback.format_exc()),
522+
)
523+
)
524+
raise
525+
526+
def _to_response(self, result: Union[Dict, Response]) -> Response:
482527
"""Convert the route's result to a Response
483528
484529
2 main result types are supported:
@@ -493,6 +538,13 @@ def _to_response(result: Union[Dict, Response]) -> Response:
493538
logger.debug("Simple response detected, serializing return before constructing final response")
494539
return Response(
495540
status_code=200,
496-
content_type="application/json",
497-
body=json.dumps(result, separators=(",", ":"), cls=Encoder),
541+
content_type=content_types.APPLICATION_JSON,
542+
body=self._json_dump(result),
498543
)
544+
545+
def _json_dump(self, obj: Any) -> str:
546+
"""Does a concise json serialization or pretty print when in debug mode"""
547+
if self._debug:
548+
return json.dumps(obj, indent=4, cls=Encoder)
549+
else:
550+
return json.dumps(obj, separators=(",", ":"), cls=Encoder)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# use mimetypes library to be certain, e.g., mimetypes.types_map[".json"]
2+
3+
APPLICATION_JSON = "application/json"
4+
TEXT_PLAIN = "text/plain"
5+
TEXT_HTML = "text/html"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from http import HTTPStatus
2+
3+
4+
class ServiceError(Exception):
5+
"""API Gateway and ALB HTTP Service Error"""
6+
7+
def __init__(self, status_code: int, msg: str):
8+
"""
9+
Parameters
10+
----------
11+
status_code: int
12+
Http status code
13+
msg: str
14+
Error message
15+
"""
16+
self.status_code = status_code
17+
self.msg = msg
18+
19+
20+
class BadRequestError(ServiceError):
21+
"""API Gateway and ALB Bad Request Error (400)"""
22+
23+
def __init__(self, msg: str):
24+
super().__init__(HTTPStatus.BAD_REQUEST, msg)
25+
26+
27+
class UnauthorizedError(ServiceError):
28+
"""API Gateway and ALB Unauthorized Error (401)"""
29+
30+
def __init__(self, msg: str):
31+
super().__init__(HTTPStatus.UNAUTHORIZED, msg)
32+
33+
34+
class NotFoundError(ServiceError):
35+
"""API Gateway and ALB Not Found Error (404)"""
36+
37+
def __init__(self, msg: str = "Not found"):
38+
super().__init__(HTTPStatus.NOT_FOUND, msg)
39+
40+
41+
class InternalServerError(ServiceError):
42+
"""API Gateway and ALB Not Found Internal Server Error (500)"""
43+
44+
def __init__(self, message: str):
45+
super().__init__(HTTPStatus.INTERNAL_SERVER_ERROR, message)

aws_lambda_powertools/shared/constants.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010

1111
METRICS_NAMESPACE_ENV: str = "POWERTOOLS_METRICS_NAMESPACE"
1212

13+
EVENT_HANDLER_DEBUG_ENV: str = "POWERTOOLS_EVENT_HANDLER_DEBUG"
14+
1315
SAM_LOCAL_ENV: str = "AWS_SAM_LOCAL"
1416
CHALICE_LOCAL_ENV: str = "AWS_CHALICE_CLI_MODE"
1517
SERVICE_NAME_ENV: str = "POWERTOOLS_SERVICE_NAME"
1618
XRAY_TRACE_ID_ENV: str = "_X_AMZN_TRACE_ID"
1719

18-
19-
XRAY_SDK_MODULE = "aws_xray_sdk"
20-
XRAY_SDK_CORE_MODULE = "aws_xray_sdk.core"
20+
XRAY_SDK_MODULE: str = "aws_xray_sdk"
21+
XRAY_SDK_CORE_MODULE: str = "aws_xray_sdk.core"

aws_lambda_powertools/utilities/data_classes/appsync/scalar_types_utils.py

+15-9
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,21 @@ def _formatted_time(now: datetime.date, fmt: str, timezone_offset: int) -> str:
1919
str
2020
Returns string formatted time with optional timezone offset
2121
"""
22-
if timezone_offset == 0:
23-
return now.strftime(fmt + "Z")
22+
if timezone_offset != 0:
23+
now = now + datetime.timedelta(hours=timezone_offset)
24+
25+
datetime_str = now.strftime(fmt)
26+
if fmt.endswith(".%f"):
27+
datetime_str = datetime_str[:-3]
2428

25-
now = now + datetime.timedelta(hours=timezone_offset)
26-
fmt += "+" if timezone_offset > 0 else "-"
27-
fmt += str(abs(timezone_offset)).zfill(2)
28-
fmt += ":00:00"
29+
if timezone_offset == 0:
30+
postfix = "Z"
31+
else:
32+
postfix = "+" if timezone_offset > 0 else "-"
33+
postfix += str(abs(timezone_offset)).zfill(2)
34+
postfix += ":00:00"
2935

30-
return now.strftime(fmt)
36+
return datetime_str + postfix
3137

3238

3339
def make_id() -> str:
@@ -65,7 +71,7 @@ def aws_time(timezone_offset: int = 0) -> str:
6571
str
6672
Returns current time as AWSTime scalar string with optional timezone offset
6773
"""
68-
return _formatted_time(datetime.datetime.utcnow(), "%H:%M:%S", timezone_offset)
74+
return _formatted_time(datetime.datetime.utcnow(), "%H:%M:%S.%f", timezone_offset)
6975

7076

7177
def aws_datetime(timezone_offset: int = 0) -> str:
@@ -81,7 +87,7 @@ def aws_datetime(timezone_offset: int = 0) -> str:
8187
str
8288
Returns current time as AWSDateTime scalar string with optional timezone offset
8389
"""
84-
return _formatted_time(datetime.datetime.utcnow(), "%Y-%m-%dT%H:%M:%S", timezone_offset)
90+
return _formatted_time(datetime.datetime.utcnow(), "%Y-%m-%dT%H:%M:%S.%f", timezone_offset)
8591

8692

8793
def aws_timestamp() -> int:

0 commit comments

Comments
 (0)