Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

::: connectrpc.client
::: connectrpc.code
::: connectrpc.exceptions
::: connectrpc.errors
::: connectrpc.interceptor
::: connectrpc.method
::: connectrpc.request
Expand Down
2 changes: 1 addition & 1 deletion docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ async def main():

## More Examples

For more detailed examples, see the [Usage Guide](../usage.md).
For more detailed examples, see the [Usage Guide](./usage.md).
257 changes: 209 additions & 48 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,97 @@
# Getting Started

## Installation
Connect is a slim library for building HTTP APIs consumable anywhere, including browsers.
You define your service with a Protocol Buffer schema, and Connect generates type-safe server
and client code. Fill in your server's business logic and you're done — no hand-written
marshaling, routing, or client code required!

### Basic Client
This fifteen-minute walkthrough helps you create a small Connect service in Python.
It demonstrates what you'll be writing by hand, what Connect generates for you,
and how to call your new API.

For basic client functionality:
## Prerequisites

- [uv](https://docs.astral.sh/uv/#installation) installed. Any package manager including pip can also be used.
- [The Buf CLI](https://buf.build/docs/installation) installed, and include it in the `$PATH`.
- We'll also use [cURL](https://curl.se/). It's available from Homebrew and most Linux package managers.

## Setup python environment

First, we'll setup the python environment and dependencies.

=== "ASGI"

```bash
uv init
uv add connect-python uvicorn
```

=== "WSGI"

```bash
uv init
uv add connect-python gunicorn
```

## Define a service

Now we're ready to write the Protocol Buffer schema that defines our service. In your shell,

```bash
pip install connect-python
mkdir -p proto/greet/v1
touch proto/greet/v1/greet.proto
```

### Code Generation
Open `proto/greet/v1/greet.proto` in your editor and add:

For code generation additionally install the protoc plugin:
```protobuf
syntax = "proto3";

```bash
pip install protoc-gen-connect-python
package greet.v1;

message GreetRequest {
string name = 1;
}

message GreetResponse {
string greeting = 1;
}

service GreetService {
rpc Greet(GreetRequest) returns (GreetResponse) {}
}
```

## Code Generation
This file declares the `greet.v1` Protobuf package, a service called `GreetService`, and a single method
called `Greet` with its request and response structures. These package, service, and method names will
reappear soon in our HTTP API's URLs.

With a protobuf definition in hand, you can generate a client. This is
easiest using buf, but you can also use protoc directly.
## Generate code

Install the compiler (eg `pip install protoc-gen-connect-python`), and
it can be referenced as `protoc-gen-connect_python`.
We're going to generate our code using [Buf](https://buf.build/), a modern replacement for Google's protobuf compiler.

### Using Buf (Recommended)
First, scaffold a basic [buf.yaml](https://buf.build/docs/configuration/v2/buf-yaml) by running `buf config init`.
Then, edit `buf.yaml` to use our `proto` directory:

A reasonable `buf.gen.yaml`:
```yaml hl_lines="2 3"
version: v2
modules:
- path: proto
lint:
use:
- DEFAULT
breaking:
use:
- FILE
```

We will use [remote plugins](https://buf.build/docs/bsr/remote-plugins/usage), a feature of the
[Buf Schema Registry](https://buf.build/docs/tutorials/getting-started-with-bsr) for generating code. Tell buf how to
generate code by creating a buf.gen.yaml:

```bash
touch buf.gen.yaml
```

```yaml
version: v2
Expand All @@ -37,59 +100,157 @@ plugins:
out: .
- remote: buf.build/protocolbuffers/pyi
out: .
- local: .venv/bin/protoc-gen-connect_python
- remote: buf.build/connectrpc/python
out: .
```

### Using protoc
With those configuration files in place, you can lint your schema and generate code:

```bash
protoc --plugin=protoc-gen-connect-python=.venv/bin/protoc-gen-connect-python \
--connect-python_out=. \
--python_out=. \
--pyi_out=. \
your_service.proto
buf lint
buf generate
```

In the `greet` package, you should now see some generated Python:

```
greet
└── v1
├── greet_connect.py
└── greet_pb2.py
└── greet_pb2.pyi
```

The package `greet/v1` contains `greet_pb2.py` and `greet_pb2.pyi` which were generated by
the [protocolbuffers/python](https://buf.build/protocolbuffers/python) and
[protocolbuffers/pyi](https://buf.build/protocolbuffers/pyi) and contain `GreetRequest`
and `GreetResponse` structs and the associated marshaling code. `greet_connect.py` was
generated by [connectrpc/python](https://buf.build/connectrpc/python) and contains the
WSGI and ASGI service interfaces and client code to access a Connect server. Feel free to
poke around if you're interested - `greet_connect.py` is standard Python code.

## Implement service

## Example Service Definition
The code we've generated takes care of the boring boilerplate, but we still need to implement our greeting logic.
In the generated code, this is represented as the `greet_connect.GreetService` and `greet_connect.GreetServiceSync`
interfaces for async ASGI and sync WSGI servers respectively. Since the interface is so small, we can do everything
in one Python file. `touch server.py` and add:

If you have a proto definition like this:
=== "ASGI"

```proto
service ElizaService {
rpc Say(SayRequest) returns (SayResponse) {}
rpc Converse(stream ConverseRequest) returns (stream ConverseResponse) {}
rpc Introduce(IntroduceRequest) returns (stream IntroduceResponse) {}
rpc Pontificate(stream PontificateRequest) returns (PontificateResponse) {}
```python
from greet.v1.greet_connect import GreetService, GreetServiceASGIApplication
from greet.v1.greet_pb2 import GreetResponse

class Greeter(GreetService):
async def greet(self, request, ctx):
print("Request headers: ", ctx.request_headers())
response = GreetResponse(greeting=f"Hello, {request.name}!")
ctx.response_headers()["greet-version"] = "v1"
return response

app = GreetServiceASGIApplication(Greeter())
```

=== "WSGI"

```python
from greet.v1.greet_connect import GreetServiceSync, GreetServiceWSGIApplication
from greet.v1.greet_pb2 import GreetResponse

class Greeter(GreetServiceSync):
def greet(self, request, ctx):
print("Request headers: ", ctx.request_headers())
response = GreetResponse(greeting=f"Hello, {request.name}!")
ctx.response_headers()["greet-version"] = "v1"
return response

app = GreetServiceWSGIApplication(Greeter())
```

In a separate terminal window, you can now start your server:

=== "ASGI"

```bash
uv run uvicorn server:app
```

=== "WSGI"

```bash
uv run gunicorn server:app
```

## Make requests

The simplest way to consume your new API is an HTTP/1.1 POST with a JSON payload. If you have a recent version of
cURL installed, it's a one-liner:

```bash
curl \
--header "Content-Type: application/json" \
--data '{"name": "Jane"}' \
http://localhost:8000/greet.v1.GreetService/Greet
```

This responds:

```json
{
"greeting": "Hello, Jane!"
}
```

## Generated Client
We can also make requests using Connect's generated client. `touch client.py` and add:

=== "Async"

```python
import asyncio

from greet.v1.greet_connect import GreetServiceClient
from greet.v1.greet_pb2 import GreetRequest

Then the generated client will have methods like this (optional arguments have been elided for clarity):
async def main():
client = GreetServiceClient("http://localhost:8000")
res = await client.greet(GreetRequest(name="Jane"))
print(res.greeting)

```python
class ElizaServiceClient:
def __init__(self, url: str):
...
if __name__ == "__main__":
asyncio.run(main())
```

# Unary (no streams)
def say(self, req: eliza_pb2.SayRequest) -> eliza_pb2.SayResponse:
...
=== "Sync"

# Bidirectional (both sides stream)
def converse(self, req: Iterator[eliza_pb2.ConverseRequest]) -> Iterator[eliza_pb2.SayResponse]:
...
```python
from greet.v1.greet_connect import GreetServiceClientSync
from greet.v1.greet_pb2 import GreetRequest

# Server streaming (client sends one message, server sends a stream)
def introduce(self, req: eliza_pb2.IntroduceRequest) -> Iterator[eliza_pb2.IntroduceResponse]:
...
def main():
client = GreetServiceClientSync("http://localhost:8000")
res = client.greet(GreetRequest(name="Jane"))
print(res.greeting)

# Client streaming (client sends a stream, server sends one message back)
def pontificate(self, req: Iterator[eliza_pb2.PontificateRequest]) -> eliza_pb2.PontificateResponse:
...
if __name__ == "__main__":
main()
```

With your server still running in a separate terminal window, you can now run your client:

```bash
uv run python client.py
```

Congratulations — you've built your first Connect service! 🎉

## So what?

With just a few lines of hand-written code, you've built a real API server that supports both the and Connect protocol.
Unlike a hand-written REST service, you didn't need to design a URL hierarchy, hand-write request and response structs,
manage your own marshaling, or parse typed values out of query parameters. More importantly, your users got an idiomatic,
type-safe client without any extra work on your part.

## Next Steps

- Learn about [Usage](./usage.md) patterns
Expand Down
7 changes: 6 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ site_name: Connect Documentation

theme:
name: material
features:
- content.code.copy
- content.tabs.link

plugins:
- mkdocstrings:
Expand All @@ -25,4 +28,6 @@ markdown_extensions:
pygments_lang_class: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.superfences
- pymdownx.superfences:
- pymdownx.tabbed:
alternate_style: true
3 changes: 3 additions & 0 deletions src/connectrpc/_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ def __iter__(self) -> Iterator[str]:
def __len__(self) -> int:
return len(self._store)

def __repr__(self) -> str:
return repr(list(self.allitems()))

def add(self, key: str, value: str) -> None:
"""Add a header, appending to existing values without overwriting.

Expand Down