Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(event-handler): add http ProxyEvent handler #369

Merged
merged 47 commits into from
Apr 28, 2021
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1695911
feat(event-handler): Add http ProxyEvent handler
michaelbrewer Mar 28, 2021
d0f21f1
feat(event-handler): Lightweight rule matching
michaelbrewer Mar 28, 2021
670f872
fix(event-handler): Python 3.6 support
michaelbrewer Mar 28, 2021
633bff1
refactor(event-handler): Add lambda_context and current_request to app
michaelbrewer Mar 28, 2021
ad88b0b
refactor(event-handler): Resolv Pycharm warnings
michaelbrewer Mar 28, 2021
8b72bd2
chore(event-handler): Refactoring
michaelbrewer Mar 29, 2021
d66f380
feat(event-handler): Ensure we reset routes in __init__
michaelbrewer Mar 29, 2021
7588af0
Merge branch 'develop' into feat-event-handler-apigw
michaelbrewer Mar 29, 2021
001e8a9
refactor(event-handler): Rename to recent_event
michaelbrewer Mar 29, 2021
e7a9e42
Merge branch 'develop' into feat-event-handler-apigw
michaelbrewer Mar 30, 2021
8af2072
Merge branch 'develop' into feat-event-handler-apigw
michaelbrewer Mar 31, 2021
8c35ce7
chore(event-handler): Refactor name
michaelbrewer Mar 31, 2021
4a30ddc
chore: Refactor
michaelbrewer Mar 31, 2021
d17c59b
Merge branch 'develop' into feat-event-handler-apigw
michaelbrewer Apr 1, 2021
6030340
feat(event-handler): Add mapping for api_gateway
michaelbrewer Apr 3, 2021
fe73699
Merge branch 'develop' into feat-event-handler-apigw
michaelbrewer Apr 9, 2021
c87a526
Merge branch 'develop' into feat-event-handler-apigw
michaelbrewer Apr 9, 2021
7abceae
Merge branch 'develop' into feat-event-handler-apigw
michaelbrewer Apr 9, 2021
a065474
Merge branch 'develop' into feat-event-handler-apigw
michaelbrewer Apr 9, 2021
1d6ea4d
feat(event-handler): Add cors support to apigw handler
michaelbrewer Apr 10, 2021
306ee73
feat(event-handler): apigw compress and base64encode
michaelbrewer Apr 10, 2021
daaf137
feat(event-handler): apigwy cache_control option
michaelbrewer Apr 10, 2021
6f6a55c
refactor(event-handler): Code cleanup
michaelbrewer Apr 10, 2021
1484ac9
tests(event-handler): Add missing binary handling
michaelbrewer Apr 10, 2021
9ee7702
fix(event-handler): Set Content-Encoding header for compress
michaelbrewer Apr 10, 2021
85b5ff8
feat(event-handler): Add PATCH decorator
michaelbrewer Apr 10, 2021
0cf5366
docs(event-handler): Add some docs to tests
michaelbrewer Apr 18, 2021
b5a057b
feat(event-handler): Rest API simplification with function returns a …
michaelbrewer Apr 18, 2021
e7e8d59
feat(event-handler): Add Response class
michaelbrewer Apr 19, 2021
0fe00f6
Merge branch 'develop' into feat-event-handler-apigw
michaelbrewer Apr 21, 2021
e57b59e
feat(event-handler): Use shared json Encoder
michaelbrewer Apr 21, 2021
569cdbd
fix(data-classes): Correct typing for json_body
michaelbrewer Apr 21, 2021
c1ea9b1
tests: Add shared test utils.load_event
michaelbrewer Apr 21, 2021
7940c46
docs(tests): Add more docs to tests
michaelbrewer Apr 21, 2021
f74307f
refactor(event-handler): Final housekeeping
michaelbrewer Apr 21, 2021
3370c14
Merge branch 'develop' into feat-event-handler-apigw
michaelbrewer Apr 22, 2021
4c20ceb
tests(event-handler): Fix import
michaelbrewer Apr 22, 2021
bb660b1
Merge branch 'develop' into feat-event-handler-apigw
michaelbrewer Apr 25, 2021
785877c
refactor: precise handling of headers
michaelbrewer Apr 26, 2021
c5709bc
refactor: add to_response to simplify logic
michaelbrewer Apr 26, 2021
318508f
feat(event-handler): Add a more complete implementation of cors
michaelbrewer Apr 27, 2021
f0e4f11
fix(event-handler): Default to false
michaelbrewer Apr 27, 2021
ee52aee
refactor: make some of the code-review changes
michaelbrewer Apr 27, 2021
be12f3e
feat(event-handler): Add auto generated preflight option
michaelbrewer Apr 28, 2021
2eeee5c
chore: bump ci
michaelbrewer Apr 28, 2021
fbccaa1
fix(event-handler): make python 3.6 compatible
michaelbrewer Apr 28, 2021
6ec444c
refactor: make more method as _
michaelbrewer Apr 28, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions aws_lambda_powertools/event_handler/api_gateway.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import base64
import json
import re
import zlib
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Tuple, Union

from aws_lambda_powertools.shared.json_encoder import Encoder
from aws_lambda_powertools.utilities.data_classes import ALBEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2
from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent
from aws_lambda_powertools.utilities.typing import LambdaContext


class ProxyEventType(Enum):
http_api_v1 = "APIGatewayProxyEvent"
http_api_v2 = "APIGatewayProxyEventV2"
alb_event = "ALBEvent"
api_gateway = http_api_v1


class Route:
def __init__(
self, method: str, rule: Any, func: Callable, cors: bool, compress: bool, cache_control: Optional[str]
):
self.method = method.upper()
self.rule = rule
self.func = func
self.cors = cors
self.compress = compress
self.cache_control = cache_control


class Response:
michaelbrewer marked this conversation as resolved.
Show resolved Hide resolved
def __init__(self, status_code: int, content_type: str, body: Union[str, bytes], headers: Dict = None):
self.status_code = status_code
self.body = body
self.base64_encoded = False
self.headers: Dict = headers or {}
self.headers.setdefault("Content-Type", content_type)

def add_cors(self, method: str):
self.headers["Access-Control-Allow-Origin"] = "*"
self.headers["Access-Control-Allow-Methods"] = method
self.headers["Access-Control-Allow-Credentials"] = "true"
michaelbrewer marked this conversation as resolved.
Show resolved Hide resolved

def add_cache_control(self, cache_control: str):
self.headers["Cache-Control"] = cache_control if self.status_code == 200 else "no-cache"

def compress(self):
self.headers["Content-Encoding"] = "gzip"
if isinstance(self.body, str):
self.body = bytes(self.body, "utf-8")
gzip = zlib.compressobj(9, zlib.DEFLATED, zlib.MAX_WBITS | 16)
self.body = gzip.compress(self.body) + gzip.flush()

def to_dict(self) -> Dict[str, Any]:
if isinstance(self.body, bytes):
self.base64_encoded = True
self.body = base64.b64encode(self.body).decode()
return {
"statusCode": self.status_code,
"headers": self.headers,
"body": self.body,
"isBase64Encoded": self.base64_encoded,
}


class ApiGatewayResolver:
michaelbrewer marked this conversation as resolved.
Show resolved Hide resolved
current_event: BaseProxyEvent
lambda_context: LambdaContext

def __init__(self, proxy_type: Enum = ProxyEventType.http_api_v1):
self._proxy_type = proxy_type
self._routes: List[Route] = []

def get(self, rule: str, cors: bool = False, compress: bool = False, cache_control: str = None):
return self.route(rule, "GET", cors, compress, cache_control)

def post(self, rule: str, cors: bool = False, compress: bool = False, cache_control: str = None):
return self.route(rule, "POST", cors, compress, cache_control)

def put(self, rule: str, cors: bool = False, compress: bool = False, cache_control: str = None):
return self.route(rule, "PUT", cors, compress, cache_control)

def delete(self, rule: str, cors: bool = False, compress: bool = False, cache_control: str = None):
return self.route(rule, "DELETE", cors, compress, cache_control)

def patch(self, rule: str, cors: bool = False, compress: bool = False, cache_control: str = None):
return self.route(rule, "PATCH", cors, compress, cache_control)

def route(self, rule: str, method: str, cors: bool = False, compress: bool = False, cache_control: str = None):
def register_resolver(func: Callable):
self._routes.append(Route(method, self._compile_regex(rule), func, cors, compress, cache_control))
return func

return register_resolver

def resolve(self, event, context) -> Dict[str, Any]:
self.current_event = self._to_data_class(event)
self.lambda_context = context
route, args = self._find_route(self.current_event.http_method.upper(), self.current_event.path)
response = self.to_response(route.func(**args))

if route.cors:
response.add_cors(route.method)
if route.cache_control:
response.add_cache_control(route.cache_control)
if route.compress and "gzip" in (self.current_event.get_header_value("accept-encoding") or ""):
response.compress()

return response.to_dict()

@staticmethod
def to_response(result: Union[Tuple[int, str, Union[bytes, str]], Dict, Response]) -> Response:
if isinstance(result, Response):
return result
elif isinstance(result, dict):
return Response(
status_code=200,
content_type="application/json",
body=json.dumps(result, separators=(",", ":"), cls=Encoder),
)
else: # Tuple[int, str, Union[bytes, str]]
return Response(*result)

@staticmethod
def _compile_regex(rule: str):
rule_regex: str = re.sub(r"(<\w+>)", r"(?P\1.+)", rule)
return re.compile("^{}$".format(rule_regex))

def _to_data_class(self, event: Dict) -> BaseProxyEvent:
if self._proxy_type == ProxyEventType.http_api_v1:
return APIGatewayProxyEvent(event)
if self._proxy_type == ProxyEventType.http_api_v2:
return APIGatewayProxyEventV2(event)
return ALBEvent(event)

def _find_route(self, method: str, path: str) -> Tuple[Route, Dict]:
for route in self._routes:
if method != route.method:
continue
match: Optional[re.Match] = route.rule.match(path)
if match:
return route, match.groupdict()

raise ValueError(f"No route found for '{method}.{path}'")

def __call__(self, event, context) -> Any:
return self.resolve(event, context)
8 changes: 0 additions & 8 deletions aws_lambda_powertools/utilities/data_classes/alb_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,6 @@ class ALBEvent(BaseProxyEvent):
def request_context(self) -> ALBEventRequestContext:
return ALBEventRequestContext(self._data)

@property
def http_method(self) -> str:
return self["httpMethod"]

@property
def path(self) -> str:
return self["path"]

@property
def multi_value_query_string_parameters(self) -> Optional[Dict[str, List[str]]]:
return self.get("multiValueQueryStringParameters")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,15 +217,6 @@ def version(self) -> str:
def resource(self) -> str:
return self["resource"]

@property
def path(self) -> str:
return self["path"]

@property
def http_method(self) -> str:
"""The HTTP method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT."""
return self["httpMethod"]

@property
def multi_value_headers(self) -> Dict[str, List[str]]:
return self["multiValueHeaders"]
Expand Down Expand Up @@ -446,3 +437,12 @@ def path_parameters(self) -> Optional[Dict[str, str]]:
@property
def stage_variables(self) -> Optional[Dict[str, str]]:
return self.get("stageVariables")

@property
def path(self) -> str:
return self.raw_path

@property
def http_method(self) -> str:
"""The HTTP method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT."""
return self.request_context.http.method
16 changes: 16 additions & 0 deletions aws_lambda_powertools/utilities/data_classes/common.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from typing import Any, Dict, Optional


Expand Down Expand Up @@ -57,8 +58,23 @@ def is_base64_encoded(self) -> Optional[bool]:

@property
def body(self) -> Optional[str]:
"""Submitted body of the request as a string"""
return self.get("body")

@property
def json_body(self) -> Any:
"""Parses the submitted body as json"""
return json.loads(self["body"])

@property
def path(self) -> str:
return self["path"]

@property
def http_method(self) -> str:
"""The HTTP method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT."""
return self["httpMethod"]

def get_query_string_value(self, name: str, default_value: Optional[str] = None) -> Optional[str]:
"""Get query string value by name

Expand Down
Loading