Skip to content
This repository was archived by the owner on Sep 22, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ plugins:
out: gen
- local: protoc-gen-connecpy
out: gen
# Optional: Enable experimental Client Transport API
# opt:
# - transport_api=true
```

Then run:
Expand All @@ -147,9 +150,16 @@ buf generate
protoc --python_out=./ --pyi_out=./ --connecpy_out=./ ./haberdasher.proto
```

### Generator Options

By default, naming follows PEP8 conventions. To use Google conventions, matching the output of grpc-python, add `--connecpy_opt=naming=google`.
By default, imports are generated absolutely based on the proto package name. To use relative import, add `--connecpy_opt=imports=relative`.

For experimental Client Transport API support (see Client Transport API section below), add `--connecpy_opt=transport_api=true`:
```sh
protoc --python_out=./ --pyi_out=./ --connecpy_out=./ --connecpy_opt=transport_api=true ./haberdasher.proto
```

### Server code (ASGI)

```python
Expand Down Expand Up @@ -566,6 +576,91 @@ On Windows, Content-Type: application/json, HTTP/2
curl --http2-prior-knowledge -X POST -H "Content-Type: application/json" -d '{\"inches\": 12}' -v http://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHat
```

## Client Transport API (Experimental)

The Client Transport API provides a protocol-agnostic way to create RPC clients that can work with both Connect and gRPC protocols. This allows you to switch between protocols without changing your client code.

**Note**: This feature must be explicitly enabled during code generation using the `transport_api=true` option (see Generator Options above).

### Features

- **Protocol Agnostic**: Write client code once, use with both Connect and gRPC
- **Type Safety**: Generated Protocol types ensure type-safe client interfaces
- **Seamless Integration**: Factory functions automatically handle protocol differences

### Usage

The protoc-gen-connecpy plugin automatically generates Client Transport API support alongside regular client code:

```python
# Using Connect transport
from connecpy.transport.client import ConnectTransportAsync
from example.haberdasher_connecpy import create_client
from example.haberdasher_pb2 import Size

async def connect_example():
transport = ConnectTransportAsync("http://localhost:3000", proto_json=True)
client = create_client(transport)

hat = await client.make_hat(Size(inches=12))
print(f"Got hat: {hat.color}")

# Using gRPC transport (requires grpcio)
from connecpy.transport.client import GrpcTransportAsync

async def grpc_example():
transport = GrpcTransportAsync("localhost:50051")
client = create_client(transport)

hat = await client.make_hat(Size(inches=12))
print(f"Got hat: {hat.color}")
```

### Synchronous API

The Client Transport API also supports synchronous clients:

```python
from connecpy.transport.client import ConnectTransport, GrpcTransport
from example.haberdasher_connecpy import create_client_sync

# Connect transport (sync)
transport = ConnectTransport("http://localhost:3000")
client = create_client_sync(transport)
hat = client.make_hat(Size(inches=12))

# gRPC transport (sync)
transport = GrpcTransport("localhost:50051")
client = create_client_sync(transport)
hat = client.make_hat(Size(inches=12))
```

### Advanced Configuration

Both transports support advanced configuration options:

```python
# Connect with compression and custom headers
transport = ConnectTransportAsync(
"http://localhost:3000",
proto_json=True,
accept_compression=["gzip", "br"],
send_compression="gzip",
timeout_ms=5000,
)

# gRPC with TLS
import grpc
credentials = grpc.ssl_channel_credentials()
transport = GrpcTransportAsync(
"api.example.com:443",
credentials=credentials,
options=[("grpc.max_receive_message_length", 10000000)],
)
```

**Note**: The Client Transport API is experimental and the interface may change in future versions. For production use, consider using the standard `HaberdasherClient` and `HaberdasherClientSync` classes directly.

## WSGI Support

Connecpy provides full WSGI support via the `ConnecpyWSGIApplication`. This synchronous application adapts our service endpoints to the WSGI specification. It reads requests from the WSGI `environ`, processes requests, and returns responses using `start_response`. This enables integration with WSGI servers and middleware.
Expand Down
60 changes: 59 additions & 1 deletion example/example/eliza_connecpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# source: example/eliza.proto

from collections.abc import AsyncIterator, Iterable, Iterator, Mapping
from typing import Protocol
from typing import ClassVar, Protocol

from connecpy.client import ConnecpyClient, ConnecpyClientSync
from connecpy.code import Code
Expand All @@ -21,6 +21,35 @@


class ElizaService(Protocol):
"""Service protocol for ElizaService."""

_service_info: ClassVar[dict] = {
"name": "connectrpc.eliza.v1.ElizaService",
"methods": {
"say": MethodInfo(
name="Say",
service_name="connectrpc.eliza.v1.ElizaService",
input=example_dot_eliza__pb2.SayRequest,
output=example_dot_eliza__pb2.SayResponse,
idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS,
),
"converse": MethodInfo(
name="Converse",
service_name="connectrpc.eliza.v1.ElizaService",
input=example_dot_eliza__pb2.ConverseRequest,
output=example_dot_eliza__pb2.ConverseResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
"introduce": MethodInfo(
name="Introduce",
service_name="connectrpc.eliza.v1.ElizaService",
input=example_dot_eliza__pb2.IntroduceRequest,
output=example_dot_eliza__pb2.IntroduceResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
},
}

async def say(
self, request: example_dot_eliza__pb2.SayRequest, ctx: RequestContext
) -> example_dot_eliza__pb2.SayResponse:
Expand Down Expand Up @@ -155,6 +184,35 @@ def introduce(


class ElizaServiceSync(Protocol):
"""Synchronous service protocol for ElizaService."""

_service_info: ClassVar[dict] = {
"name": "connectrpc.eliza.v1.ElizaService",
"methods": {
"say": MethodInfo(
name="Say",
service_name="connectrpc.eliza.v1.ElizaService",
input=example_dot_eliza__pb2.SayRequest,
output=example_dot_eliza__pb2.SayResponse,
idempotency_level=IdempotencyLevel.NO_SIDE_EFFECTS,
),
"converse": MethodInfo(
name="Converse",
service_name="connectrpc.eliza.v1.ElizaService",
input=example_dot_eliza__pb2.ConverseRequest,
output=example_dot_eliza__pb2.ConverseResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
"introduce": MethodInfo(
name="Introduce",
service_name="connectrpc.eliza.v1.ElizaService",
input=example_dot_eliza__pb2.IntroduceRequest,
output=example_dot_eliza__pb2.IntroduceResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
},
}

def say(
self, request: example_dot_eliza__pb2.SayRequest, ctx: RequestContext
) -> example_dot_eliza__pb2.SayResponse:
Expand Down
36 changes: 18 additions & 18 deletions example/example/eliza_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading