Packages & adoption levels

Benzene for Python is not a single package. It is a stack of small packages on PyPI, each one an adoption level. You install only the layers you use, so your application never ships Benzene code — or transitive dependencies — that it doesn't need, and the deployed footprint stays small. This mirrors the way .NET Benzene is split into separate NuGet packages, translated to the idiomatic Python equivalent.

The layers

Install Import What it gives you Depends on
benzene-results benzene.results Result and the status vocabulary — the return type of every handler nothing
benzene-core benzene.core handler registry + @message, the middleware pipeline, per-invocation DI, and the transport-neutral BenzeneMessage envelope benzene-results
benzene-http benzene.http the inbound HTTP (ASGI) binding and the Benzene↔HTTP status mapping benzene-core
benzene-gcp benzene.gcp the Google Cloud Functions host (HTTP + Pub/Sub bindings, Pub/Sub outbound client) benzene-core, benzene-http
benzene-aws benzene.aws the AWS Lambda host (API Gateway + SQS + SNS bindings, SNS/SQS outbound clients) benzene-core, benzene-http
benzene-azure benzene.azure the Azure Functions host (HTTP + Service Bus + Event Hub bindings, Service Bus outbound client) benzene-core, benzene-http
benzene-testing benzene.testing in-memory test host + test doubles (a dev/test dependency) benzene-core

Because Benzene uses a PEP 420 namespace package, all of these contribute submodules to one shared benzene namespace. Installing a higher layer pulls in the lower ones automatically:

pip install benzene-http      # also installs benzene-core and benzene-results

Which one do I need?

1 — Just the result model. You want to describe a domain operation's outcome with Benzene's vocabulary (say, to type a service-layer function) but you are not wiring up a pipeline or a transport yet.

pip install benzene-results
from benzene.results import Result

def reserve_seat(seat_id: str) -> Result:
    if seat_taken(seat_id):
        return Result.failure("conflict", "seat already taken")
    return Result.ok({"seat": seat_id})

2 — Run handlers, transport-neutral. You want to register handlers by topic and drive them through the BenzeneMessage envelope — in tests, from a custom host, or from another transport you are building.

pip install benzene-core
from benzene.core import BenzeneMessageApplication, Registry, message
from benzene.results import Result

@message("order:create")
async def create_order(request: dict) -> Result:
    return Result.created({"id": "ord_1", "sku": request["sku"]})

app = BenzeneMessageApplication(Registry().add(create_order))
response = await app.handle_async(
    {"topic": "order:create", "headers": {}, "body": '{"sku": "ABC"}'}
)

3 — Host over HTTP. You want those same handlers reachable behind a real HTTP server.

pip install benzene-http
from benzene.core import message
from benzene.results import Result
from benzene.http import BenzeneHttpApp, HttpRouter, http_endpoint

@http_endpoint("POST", "/orders")
@message("order:create")
async def create_order(request: dict) -> Result:
    return Result.created({"id": "ord_1", "sku": request["sku"]})

app = BenzeneHttpApp(HttpRouter().add(create_order))   # run: uvicorn module:app

Why split it this way?

A note on granularity (a deliberate divergence from .NET/TypeScript)

The .NET and TypeScript ports split the bottom of the stack much more finely — Benzene.Abstractions, Benzene.Abstractions.Messages, Benzene.Core.Messages, Benzene.Core.Middleware, Benzene.Dependencies, and so on. Those splits exist to satisfy C# assembly / dependency isolation, which has no equivalent cost in Python. Reproducing ten distributions where three carry the same meaning would hurt, not help, a Python developer.

So the Python port keeps the meaningful adoption seams as separate packages (results, core, http) and folds the assembly-only splits into them — for example, the DI container that .NET ships as Benzene.Dependencies lives in benzene.core.dependencies. If and when a genuinely optional concern appears (e.g. a bring-your-own-container adapter, or a validation adapter over pydantic), that earns its own package, because it is a real adoption choice rather than an assembly boundary.