benzene.aws

Host Benzene handlers on AWS Lambda — API Gateway (HTTP), SQS, SNS, S3, EventBridge, DynamoDB Streams, Kinesis, Kafka/MSK, and direct Lambda-invoke event sources in one function — plus SNS/SQS/EventBridge/Kinesis/Lambda outbound clients and a self-hosted SQS consumer. Distribution: benzene-aws (depends on benzene-core, benzene-http).

pip install benzene-aws            # add [boto3] for the real outbound clients

Overview

One host, inner bindings selected by event shape (transport-bindings §1):

Topic for the attribute-carrying transports (SQS, SNS, Kafka) comes from the topic message attribute / header. The channel-less sources (S3, EventBridge, DynamoDB, Kinesis) have no metadata channel on the wire, so their topic comes from an injectable convention configured on the host — or, for DynamoDB, from the record's eventName (dynamodb:insert). A direct invoke's topic is exactly what the caller sent, no convention involved.

AwsLambdaApp

from benzene.aws import AwsLambdaApp, to_lambda_handler

app = AwsLambdaApp(http_router=router, registry=registry)   # shares one pipeline across sources
handler = to_lambda_handler(app)                            # def handler(event, context)

benzene.aws also exports the per-source decoders behind these bindings for custom wiring: s3_record_envelope, eventbridge_envelope, dynamodb_record_envelope, kinesis_record_envelope, kafka_records / kafka_record_envelope, invoke_envelope, and the event_source classifier.

Outbound clients

All five implement benzene.core.MessageSender and use boto3 (a lazy, optional import). SNS/SQS have a native attribute channel, so the Benzene topic rides in the topic message attribute and headers as attributes. EventBridge/Kinesis have no metadata channel, so the sender embeds the whole Benzene envelope {topic, headers, body} inside the payload it serializes — keeping correlation/trace propagation intact. Lambda's invoke Payload is the envelope already. A send failure maps to service-unavailable, never a raise.

Azure Functions and Kubernetes services have no equivalent native "invoke another function directly" primitive — the cross-platform way to reach the same synchronous-call outcome is over HTTP or gRPC (benzene.http.HttpMessageSender / benzene.grpc.GrpcMessageSender), which is why LambdaMessageSender is AWS-only.

Self-hosted SQS consumer

Distinct from the Lambda SQS trigger above, benzene.aws.sqs_consumer polls a queue itself — the shape a long-running worker or a Kubernetes Deployment needs, rather than being invoked by a Lambda event source mapping. It mirrors benzene.kafka's self-hosted consumer.

from benzene.aws import SqsConsumerApp, run_sqs_consumer_loop

app = SqsConsumerApp.from_definition(definition)
await app.handle_message(message)                      # one receive_message() dict -> Result
await run_sqs_consumer_loop(app, client, queue_url)    # long-poll -> dispatch -> delete on success

Testing

benzene.aws.testing provides AwsLambdaTestHost with one send_* per source — send_http, send_sqs / send_sqs_event, send_sns, send_s3, send_eventbridge, send_dynamodb, send_kinesis, send_kafka, send_invoke — and a native-event builder behind each (ApiGatewayRequestBuilder, SqsEventBuilder, SnsEventBuilder, S3EventBuilder, EventBridgeEventBuilder, DynamoDbStreamBuilder, KinesisEventBuilder, KafkaLambdaEventBuilder; a direct invoke needs no builder, its Payload already is the envelope). The partial-batch sources (send_sqs*, send_dynamodb, send_kinesis) return an SqsBatchResponse — assert on response.batch_item_failures — mirroring how send_http returns a response object. send_invoke returns the decoded Result directly (benzene.core.decode_response) — the same thing a real LambdaMessageSender would resolve to on the caller's side.

The self-hosted SQS consumer has its own harness: SqsConsumerTestHost (send_sqs_consumer → the mapped Result), the SqsMessageBuilder (a receive_message()-shaped dict, distinct from the Lambda SqsEventBuilder), and RecordingSqsClient (a replay client whose deleted list a test asserts the loop's at-least-once behaviour against — a failed message is not deleted). Specialize the shared harness with create_test_host(StartUp).build_aws() / .build_sqs_consumer(). See Hosting on AWS Lambda and the runnable examples/aws_orders.

See also