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

RFC: API Gateway Proxy Event utils #325

Closed
michaelbrewer opened this issue Mar 10, 2021 · 4 comments
Closed

RFC: API Gateway Proxy Event utils #325

michaelbrewer opened this issue Mar 10, 2021 · 4 comments
Assignees
Labels
Milestone

Comments

@michaelbrewer
Copy link
Contributor

michaelbrewer commented Mar 10, 2021

Key information

  • RFC PR: (leave this empty)
  • Related issue(s), if known:
  • Area: Utilities
  • Meet tenets: Yes

Summary

One paragraph explanation of the feature.

Add the ability to help map multiple API Gateway Proxy events to a single lambda much like how Chalice or Lambda Proxy does it, but in a very simple and light weight way and still be compatible with Powertools

Motivation

Simplify the work needed to setup API Gateway Proxy Lambdas that support multiple endpoints on a single lambda

Proposal

Build something like: https://github.com/vincentsarago/lambda-proxy

But also allow the develop to keep their existing handler with all of the powertools decotators supported kind of like how
#324 works.

app = APIGatewayProxy()


@app.post(uri="/merchant")
def create_merchant(merchant: dict) -> dict:
   # return a 200 OK response with JSON details
    return {"id": ...}

@app.get(uri="/merchant/{uid}", include_header=True)
def get_merchant(uid: str, headers: dict):
   return {"name":...}


def handler(event, context):
    return app.resolve(event, context)

Drawbacks

Why should we not do this?
Do we need additional dependencies? Impact performance/package size?
No additional dependencies

Rationale and alternatives

  • What other designs have been considered? Why not them?
  • What is the impact of not doing this?

Unresolved questions

Optional, stash area for topics that need further development e.g. TBD

@michaelbrewer michaelbrewer added RFC triage Pending triage from maintainers labels Mar 10, 2021
@michaelbrewer michaelbrewer changed the title RFC: APIGateway utils RFC: API Gateway Proxy Event utils Mar 11, 2021
@heitorlessa
Copy link
Contributor

Hey @michaelbrewer - Could you let us know when the RFC body is ready to review? It's missing a few sections like drawbacks, rationale, a more complete proposal besides the link, and any open questions you might have (if any), etc.

@michaelbrewer michaelbrewer changed the title RFC: API Gateway Proxy Event utils RFC: API Gateway Proxy Event utils [DRAFT] Mar 12, 2021
@michaelbrewer michaelbrewer changed the title RFC: API Gateway Proxy Event utils [DRAFT] RFC: API Gateway Proxy Event utils Mar 23, 2021
@michaelbrewer
Copy link
Contributor Author

@heitorlessa here is a super lightweight implementation:

from typing import Any, Dict, Tuple, Callable, List

from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEvent
from aws_lambda_powertools.utilities.typing import LambdaContext


class ApiGatewayResolver:
    def __init__(self):
        self._resolvers: List[Dict] = []

    def _register(
        self,
        func: Callable[[Any, Any], Tuple[int, str, str]],
        http_method: str,
        uri_starts_with: str,
        include_event: bool,
        include_context: bool,
        kwargs: Dict,
    ):
        kwargs["include_event"] = include_event
        kwargs["include_context"] = include_context
        self._resolvers.append(
            {
                "http_method": http_method,
                "uri_starts_with": uri_starts_with,
                "func": func,
                "config": kwargs,
            }
        )

    def get(self, uri: str, include_event: bool = False, include_context: bool = False, **kwargs):
        return self.route("GET", uri, include_event, include_context, **kwargs)

    def post(self, uri: str, include_event: bool = False, include_context: bool = False, **kwargs):
        return self.route("POST", uri, include_event, include_context, **kwargs)

    def put(self, uri: str, include_event: bool = False, include_context: bool = False, **kwargs):
        return self.route("PUT", uri, include_event, include_context, **kwargs)

    def delete(self, uri: str, include_event: bool = False, include_context: bool = False, **kwargs):
        return self.route("DELETE", uri, include_event, include_context, **kwargs)

    def route(
        self,
        method: str,
        uri: str,
        include_event: bool = False,
        include_context: bool = False,
        **kwargs,
    ):
        def register_resolver(func: Callable[[Any, Any], Tuple[int, str, str]]):
            self._register(func, method.upper(), uri, include_event, include_context, kwargs)
            return func

        return register_resolver

    def resolve(self, _event: dict, context: LambdaContext) -> Dict:
        event = APIGatewayProxyEvent(_event)
        path = _event.get("pathParameters", {}).get("proxy")
        resolver: Callable[[Any], Tuple[int, str, str]]
        config: Dict
        resolver, config = self._find_resolver(event.http_method.upper(), path)
        kwargs = self._kwargs(event, context, config)
        result = resolver(**kwargs)
        return {"statusCode": result[0], "headers": {"Content-Type": result[1]}, "body": result[2]}

    def _find_resolver(self, http_method: str, proxy_path: str) -> Tuple[Callable, Dict]:
        for resolver in self._resolvers:
            expected_method = resolver["http_method"]
            if http_method != expected_method:
                continue
            path_starts_with = resolver["uri_starts_with"]
            if proxy_path.startswith(path_starts_with):
                return resolver["func"], resolver["config"]

        raise ValueError(f"No resolver found for '{http_method}.{proxy_path}'")

    @staticmethod
    def _kwargs(event: APIGatewayProxyEvent, context: LambdaContext, config: Dict) -> Dict[str, Any]:
        kwargs: Dict[str, Any] = {}
        if config.get("include_event", False):
            kwargs["event"] = event
        if config.get("include_context", False):
            kwargs["context"] = context
        return kwargs

    def __call__(self, event, context) -> Any:
        return self.resolve(event, context)

And its usage:

import json
from typing import Tuple

from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEvent
from aws_lambda_powertools.utilities.event_handler.api_gateway import ApiGatewayResolver

app = ApiGatewayResolver()


@app.get("/foo")
def get_foo() -> Tuple[int, str, str]:
    # Matches on http GET and proxy path starting with "/foo"
    return 200, "text/html", "Hello"


@app.post("/make_foo", include_event=True)
def make_foo(event: APIGatewayProxyEvent) -> Tuple[int, str, str]:
    # Matches on http POST and proxy path starting with "/make_foo"
    post_data = json.loads(event.body or "{}")
    return 200, "application/json", json.dumps(post_data)


@app.delete("/delete", include_event=True)
def delete_foo(event: APIGatewayProxyEvent) -> Tuple[int, str, str]:
    # Matches on http DELETE and proxy path starting with "/delete"
    item_to_delete = event.path.removeprefix("/delete/")
    return 200, "application/json", json.dumps({"id": item_to_delete})

@michaelbrewer
Copy link
Contributor Author

I have updated the RFC PR include a couple more feature ideas:

While still keeping the code to a minimum

@heitorlessa heitorlessa added the pending-release Fix or implementation already in dev waiting to be released label Apr 26, 2021
@heitorlessa heitorlessa removed pending-release Fix or implementation already in dev waiting to be released triage Pending triage from maintainers labels May 6, 2021
@heitorlessa heitorlessa self-assigned this May 6, 2021
@heitorlessa heitorlessa added this to the 1.15.0 milestone May 6, 2021
@heitorlessa
Copy link
Contributor

This is now OUT, thanks a lot for the gigantic help here @michaelbrewer -- #423

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants