Description
Why is this needed?
My apologies if there something already exists that addresses this issue. I searched all of the code and I just could not figure out a reusable way to handle parameter injection without rolling my own system.
Request body and query string parsing as well as other aspects of request parsing are not reusable. For example, a pydantic based parameter injection system can be put in place at some point in the stack to make it more reusable. I've implemented this in a work project using a custom decorator
from functools import wraps
from typing import Optional, Type
import inspect
import json
from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEvent
from pydantic import BaseModel, ValidationError
from lib.common.app import app
# NOTE: this utility does not map these types to the open api specification.
def inject_params(
query_model: Optional[Type[BaseModel]] = None,
body_model: Optional[Type[BaseModel]] = None,
):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
_ = args
api_event: APIGatewayProxyEvent = app.current_event
query_params = None
body_params = None
if query_model:
query_dict = api_event.query_string_parameters or {}
try:
query_params = query_model(**query_dict)
except ValidationError as e:
# Raise validation error to be handled by existing middleware
raise e
if body_model:
try:
body_dict = json.loads(api_event.body or "{}")
body_params = body_model(**body_dict)
except (ValidationError, json.JSONDecodeError) as e:
# Raise validation error to be handled by existing middleware
raise e
# Dynamically inject only the parameters that the handler expects
handler_sig = inspect.signature(func)
if "query_params" in handler_sig.parameters and query_params is not None:
kwargs["query_params"] = query_params
if "body_params" in handler_sig.parameters and body_params is not None:
kwargs["body_params"] = body_params
return func(*args, **kwargs)
return wrapper
return decorator
Example usage:
from http import HTTPStatus
from get_things import get_things
from lib.common.api_client.models.responses import PaginatedThingResponse
from lib.common.app import app, logger, metrics
from lib.common.aws import JsonResponse # custom class I implemented, ignore for the scope of this issue
from lib.common.dtos import GetThingsRequest
from lib.common.middleware.inject_params import inject_params
from lib.common.utils import run_async
@app.get("/thing")
@inject_params(query_model=GetThingsRequest)
def handler(
query_params: GetThingsRequest,
) -> JsonResponse[PaginatedThingResponse]:
response: PaginatedThingResponse = run_async(get_things, query_params)
return JsonResponse(response, status_code=HTTPStatus.OK)
So this allows for reusable request parsing with pydantic. The documented parsing strategy at this time is something that otherwise has to be repeated in every handler. Since this framework really leans into pydantic already I think this kind of functionality should exist. My implementation doesn't cover header parsing or anything else, but it could be extended to support it.
I'd be happy to make an open source contribution if it makes sense. If so, where would be the best place?
Which area does this relate to?
No response
Suggestion
No response
Acknowledgment
- This request meets Powertools for AWS Lambda (Python) Tenets
- Should this be considered in other Powertools for AWS Lambda languages? i.e. Java, TypeScript, and .NET
Metadata
Metadata
Assignees
Type
Projects
Status