Repository: aws-powertools/powertools-lambda-python

Issue:
Event Handler: API Gateway current_event doesn't show all properties
When trying to access `request_context` when using either API Gateway v1 or v2 we can't have type hinting due to ALB being different - both inherit from [BaseProxyEvent](https://github.com/awslabs/aws-lambda-powertools-python/blob/d02dd2eed07f3d67ca0c02437c6a31b3360b5b41/aws_lambda_powertools/utilities/data_classes/common.py#L47).

Overloading the internal method [_to_proxy_event](https://github.com/awslabs/aws-lambda-powertools-python/blob/6fcedeca10e54312ddc9e00eb1ca8fe27064f7dc/aws_lambda_powertools/event_handler/api_gateway.py#L517) with all potential returns, even with `Literal[ProxyEventType.APIGatewayProxyEvent]` didn't seem to yield any result, as MyPy and Intelisense continues to think it's a `BaseProxyEvent`.

Need a deeper understanding of MyPy and type invariants to potentially solve this.

```python
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver

app = ApiGatewayResolver()


@app.get("/test")
def test_it():
	# this should autocomplete when not using ALB as the ProxyType argument in ApiGatewayResolver
	app.current_event.request_context.identity.account_id 
    ...


def handler(event, context):
    app.resolve(event, context)


app({}, None)
```


Discussion:
Forgot the instance variable had an explicit type of `BaseProxyEvent` hence why overloading didn't potentially work... shall try again at some point -- `current_event: BaseProxyEvent`
@heitorlessa any suggestions on how to implement this on Python? I am not sure how generics would work on Python.

Another option would be to add properties that cast to the preferred type `app.current_alb_event`, `app.current_rest_event` and `app.current_http_event`?
Because we have a parameter that indicates which Proxy Type to use, we can use [function overloading from typing](https://mypy.readthedocs.io/en/stable/more_types.html#function-overloading) to indicate the return type will be different based on the params received.

I'm considering to have an entire release just focused on MyPy stuff as more and more customers are raising issues on typing that could've been prevented, then enable MyPy at CI level -- it's a good thing, we just need a focused release or else other higher priorities will take place.

After the next release I'll write a plan for this on all areas we need. MyPy and Integ test for runtime botocore are my top concerns now
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Dumping it here for posterity.

None of the techniques I tried worked [due to being late-bound:](https://github.com/awslabs/aws-lambda-powertools-python/blob/develop/aws_lambda_powertools/event_handler/api_gateway.py#L232) Dynamically inject properties, Create BaseProxyEvent at runtime, Use generic to propagate, etc. 

The closest I've got working is using an Union of all Proxy Event types -- auto-completion works as expected, but it shows attributes that might not be there leading to an `AttributeError`.

## Late bound issue

As the class variable defines the type upfront, static analysis cannot evaluate what is the correct event as it's only resolved when a request is received:

```python
class BaseRouter(ABC):
    current_event: ProxyEventTypes  # Union[APIGatewayProxyEvent, APIGatewayProxyEventV2, ALBEvent]
    lambda_context: LambdaContext
    ...
```

```python
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
from aws_lambda_powertools.utilities.data_classes.api_gateway_proxy_event import APIGatewayProxyEvent

app = ApiGatewayResolver()

@app.get
def hello():
    request: APIGatewayProxyEvent = app.current_event  # explicit type annotation works
    request = app.current_event.route_key  # leads to an AttributeError despite auto-completion as it's a API GW V1 event
    return request.get_header_value(name="X-Id", default_value="Blah")
```

## Next steps

Three potential solutions in my head now:

1. **Create a separate class for each type of event, update the docs (ApiGatewayV1Resolver, etc.)** 
2. ~~Refactor the class instance that was created as part of Router's implementation, use a proper attribute or property w/ setter, so we can propagate the type correctly.~~ Reason: Doesn't solve the late-bound issue.
3. ~~Keep Union type, explicitly call out in the docs that it's best to use type annotation to have precise types -- Cons: Potential AttributeError and possibly methods that might not exist (need to check)~~ Reason: Too easy to get it wrong, catching every AttributeError can decrease performance too.


cc @cakepietoast @mploski as I need some brainstorming.

Resolving patch:
diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py
--- a/aws_lambda_powertools/event_handler/api_gateway.py
+++ b/aws_lambda_powertools/event_handler/api_gateway.py
@@ -47,9 +47,9 @@ class CORSConfig:
     Simple cors example using the default permissive cors, not this should only be used during early prototyping
 
     ```python
-    from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
+    from aws_lambda_powertools.event_handler import APIGatewayRestResolver
 
-    app = ApiGatewayResolver()
+    app = APIGatewayRestResolver()
 
     @app.get("/my/path", cors=True)
     def with_cors():
@@ -61,7 +61,7 @@ def with_cors():
 
     ```python
     from aws_lambda_powertools.event_handler.api_gateway import (
-        ApiGatewayResolver, CORSConfig
+        APIGatewayRestResolver, CORSConfig
     )
 
     cors_config = CORSConfig(
@@ -71,7 +71,7 @@ def with_cors():
         max_age=100,
         allow_credentials=True,
     )
-    app = ApiGatewayResolver(cors=cors_config)
+    app = APIGatewayRestResolver(cors=cors_config)
 
     @app.get("/my/path")
     def with_cors():
@@ -252,10 +252,10 @@ def get(self, rule: str, cors: Optional[bool] = None, compress: bool = False, ca
 
         ```python
         from aws_lambda_powertools import Tracer
-        from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
+        from aws_lambda_powertools.event_handler import APIGatewayRestResolver
 
         tracer = Tracer()
-        app = ApiGatewayResolver()
+        app = APIGatewayRestResolver()
 
         @app.get("/get-call")
         def simple_get():
@@ -277,10 +277,10 @@ def post(self, rule: str, cors: Optional[bool] = None, compress: bool = False, c
 
         ```python
         from aws_lambda_powertools import Tracer
-        from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
+        from aws_lambda_powertools.event_handler import APIGatewayRestResolver
 
         tracer = Tracer()
-        app = ApiGatewayResolver()
+        app = APIGatewayRestResolver()
 
         @app.post("/post-call")
         def simple_post():
@@ -303,10 +303,10 @@ def put(self, rule: str, cors: Optional[bool] = None, compress: bool = False, ca
 
         ```python
         from aws_lambda_powertools import Tracer
-        from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
+        from aws_lambda_powertools.event_handler import APIGatewayRestResolver
 
         tracer = Tracer()
-        app = ApiGatewayResolver()
+        app = APIGatewayRestResolver()
 
         @app.put("/put-call")
         def simple_put():
@@ -331,10 +331,10 @@ def delete(
 
         ```python
         from aws_lambda_powertools import Tracer
-        from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
+        from aws_lambda_powertools.event_handler import APIGatewayRestResolver
 
         tracer = Tracer()
-        app = ApiGatewayResolver()
+        app = APIGatewayRestResolver()
 
         @app.delete("/delete-call")
         def simple_delete():
@@ -358,10 +358,10 @@ def patch(
 
         ```python
         from aws_lambda_powertools import Tracer
-        from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
+        from aws_lambda_powertools.event_handler import APIGatewayRestResolver
 
         tracer = Tracer()
-        app = ApiGatewayResolver()
+        app = APIGatewayRestResolver()
 
         @app.patch("/patch-call")
         def simple_patch():
@@ -387,10 +387,10 @@ class ApiGatewayResolver(BaseRouter):
 
     ```python
     from aws_lambda_powertools import Tracer
-    from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
+    from aws_lambda_powertools.event_handler import APIGatewayRestResolver
 
     tracer = Tracer()
-    app = ApiGatewayResolver()
+    app = APIGatewayRestResolver()
 
     @app.get("/get-call")
     def simple_get():
@@ -731,3 +731,45 @@ def register_route(func: Callable):
             self._routes[(rule, methods, cors, compress, cache_control)] = func
 
         return register_route
+
+
+class APIGatewayRestResolver(ApiGatewayResolver):
+    current_event: APIGatewayProxyEvent
+
+    def __init__(
+        self,
+        cors: Optional[CORSConfig] = None,
+        debug: Optional[bool] = None,
+        serializer: Optional[Callable[[Dict], str]] = None,
+        strip_prefixes: Optional[List[str]] = None,
+    ):
+        """Amazon API Gateway REST and HTTP API v1 payload resolver"""
+        super().__init__(ProxyEventType.APIGatewayProxyEvent, cors, debug, serializer, strip_prefixes)
+
+
+class APIGatewayHttpResolver(ApiGatewayResolver):
+    current_event: APIGatewayProxyEventV2
+
+    def __init__(
+        self,
+        cors: Optional[CORSConfig] = None,
+        debug: Optional[bool] = None,
+        serializer: Optional[Callable[[Dict], str]] = None,
+        strip_prefixes: Optional[List[str]] = None,
+    ):
+        """Amazon API Gateway HTTP API v2 payload resolver"""
+        super().__init__(ProxyEventType.APIGatewayProxyEventV2, cors, debug, serializer, strip_prefixes)
+
+
+class ALBResolver(ApiGatewayResolver):
+    current_event: ALBEvent
+
+    def __init__(
+        self,
+        cors: Optional[CORSConfig] = None,
+        debug: Optional[bool] = None,
+        serializer: Optional[Callable[[Dict], str]] = None,
+        strip_prefixes: Optional[List[str]] = None,
+    ):
+        """Amazon Application Load Balancer (ALB) resolver"""
+        super().__init__(ProxyEventType.ALBEvent, cors, debug, serializer, strip_prefixes)


Test patch:
diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py
--- a/tests/functional/event_handler/test_api_gateway.py
+++ b/tests/functional/event_handler/test_api_gateway.py
@@ -12,7 +12,10 @@
 
 from aws_lambda_powertools.event_handler import content_types
 from aws_lambda_powertools.event_handler.api_gateway import (
+    ALBResolver,
+    APIGatewayHttpResolver,
     ApiGatewayResolver,
+    APIGatewayRestResolver,
     CORSConfig,
     ProxyEventType,
     Response,
@@ -47,13 +50,14 @@ def read_media(file_name: str) -> bytes:
 
 
 def test_alb_event():
-    # GIVEN a Application Load Balancer proxy type event
-    app = ApiGatewayResolver(proxy_type=ProxyEventType.ALBEvent)
+    # GIVEN an Application Load Balancer proxy type event
+    app = ALBResolver()
 
     @app.get("/lambda")
     def foo():
         assert isinstance(app.current_event, ALBEvent)
         assert app.lambda_context == {}
+        assert app.current_event.request_context.elb_target_group_arn is not None
         return Response(200, content_types.TEXT_HTML, "foo")
 
     # WHEN calling the event handler
@@ -68,12 +72,13 @@ def foo():
 
 def test_api_gateway_v1():
     # GIVEN a Http API V1 proxy type event
-    app = ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEvent)
+    app = APIGatewayRestResolver()
 
     @app.get("/my/path")
     def get_lambda() -> Response:
         assert isinstance(app.current_event, APIGatewayProxyEvent)
         assert app.lambda_context == {}
+        assert app.current_event.request_context.domain_name == "id.execute-api.us-east-1.amazonaws.com"
         return Response(200, content_types.APPLICATION_JSON, json.dumps({"foo": "value"}))
 
     # WHEN calling the event handler
@@ -106,12 +111,13 @@ def get_lambda() -> Response:
 
 def test_api_gateway_v2():
     # GIVEN a Http API V2 proxy type event
-    app = ApiGatewayResolver(proxy_type=ProxyEventType.APIGatewayProxyEventV2)
+    app = APIGatewayHttpResolver()
 
     @app.post("/my/path")
     def my_path() -> Response:
         assert isinstance(app.current_event, APIGatewayProxyEventV2)
         post_data = app.current_event.json_body
+        assert app.current_event.cookies[0] == "cookie1"
         return Response(200, content_types.TEXT_PLAIN, post_data["username"])
 
     # WHEN calling the event handler
diff --git a/tests/functional/test_metrics.py b/tests/functional/test_metrics.py
--- a/tests/functional/test_metrics.py
+++ b/tests/functional/test_metrics.py
@@ -496,7 +496,8 @@ def lambda_handler(evt, context):
     # THEN ColdStart metric and function_name and service dimension should be logged
     assert output["ColdStart"] == [1.0]
     assert output["function_name"] == "example_fn"
-    assert output['service'] == service
+    assert output["service"] == service
+
 
 def test_log_metrics_capture_cold_start_metric_no_service(capsys, namespace):
     # GIVEN Metrics is initialized without service
@@ -515,7 +516,7 @@ def lambda_handler(evt, context):
     # THEN ColdStart metric and function_name dimension should be logged
     assert output["ColdStart"] == [1.0]
     assert output["function_name"] == "example_fn"
-    assert output.get('service') is None
+    assert output.get("service") is None
 
 
 def test_emit_cold_start_metric_only_once(capsys, namespace, service, metric):