Skip to content

RFC: API Gateway Proxy Event utils #325

Closed
@michaelbrewer

Description

@michaelbrewer

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

Activity

changed the title [-]RFC: APIGateway utils[/-] [+]RFC: API Gateway Proxy Event utils[/+] on Mar 11, 2021
heitorlessa

heitorlessa commented on Mar 12, 2021

@heitorlessa
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.

changed the title [-]RFC: API Gateway Proxy Event utils[/-] [+]RFC: API Gateway Proxy Event utils [DRAFT][/+] on Mar 12, 2021
changed the title [-]RFC: API Gateway Proxy Event utils [DRAFT][/-] [+]RFC: API Gateway Proxy Event utils[/+] on Mar 23, 2021
michaelbrewer

michaelbrewer commented on Mar 27, 2021

@michaelbrewer
ContributorAuthor

@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

michaelbrewer commented on Apr 10, 2021

@michaelbrewer
ContributorAuthor

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

While still keeping the code to a minimum

removed
pending-releaseFix or implementation already in dev waiting to be released
triagePending triage from maintainers
on May 6, 2021
self-assigned this
on May 6, 2021
added this to the 1.15.0 milestone on May 6, 2021
heitorlessa

heitorlessa commented on May 6, 2021

@heitorlessa
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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

Status

Triage

Relationships

None yet

    Development

    No branches or pull requests

      Participants

      @heitorlessa@michaelbrewer

      Issue actions

        RFC: API Gateway Proxy Event utils · Issue #325 · aws-powertools/powertools-lambda-python