Skip to content
Closed
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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,39 @@ To use this SDK, add the dependency to your project:
pip install restate_sdk
```

### Ingress client

The ingress client factory uses httpx by default. Install its optional dependency with:

```shell
python -m pip install "restate-sdk[client]"
```

```python
from restate import create_client

async with create_client("http://localhost:8080") as client:
...
```

To use httpx2, install `restate-sdk[client-httpx2]` and select it explicitly:

```python
async with create_client("http://localhost:8080", backend="httpx2") as client:
...
```

The factory enables HTTP/2 and closes the HTTP client on exit. It requires the
selected package and does not fall back to another package.

You can also pass an existing async HTTP client to `restate.client.Client`.
The `HttpClient` and `HttpResponse` protocols in `restate.client_types` describe
the required interface. They are also available from `restate`. Both
`httpx.AsyncClient` and `httpx2.AsyncClient` satisfy this interface. `Client`
keeps the supplied object and leaves its lifetime to the caller. Importing
`Client` requires neither HTTP package. Transport errors keep the supplied
client's exception types.

## Versions

The compatibility with Restate is described in the following table. sdk-python versions < 0.10 are deprecated and no longer supported.
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ lint = ["mypy>=1.11.2", "pyright>=1.1.390", "ruff>=0.6.9"]
harness = ["testcontainers", "hypercorn", "httpx"]
serde = ["dacite", "pydantic", "msgspec"]
client = ["httpx[http2]"]
client-httpx2 = ["httpx2[http2]>=2.12,<3"]
adk = ["google-adk>=1.20.0"]
openai = ["openai-agents>=0.6.1"]
pydantic_ai = ["pydantic-ai-slim>=2.0,<3"]
Expand Down
19 changes: 4 additions & 15 deletions python/restate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from .context import Context, ObjectContext, ObjectSharedContext
from .context import WorkflowContext, WorkflowSharedContext
from .retry_policy import InvocationRetryPolicy
from .client_types import RestateClient, HttpError, RestateClientSendHandle
from .client_types import HttpClient, HttpResponse, RestateClient, HttpError, RestateClientSendHandle

# pylint: disable=line-too-long
from .context import (
Expand Down Expand Up @@ -76,20 +76,7 @@ def test_harness(
raise ImportError("Install restate-sdk[harness] to use this feature") # type: ignore


try:
from .client import create_client
except ImportError:
# we don't have the appropriate dependencies installed

@asynccontextmanager
async def create_client(
ingress: str,
headers: typing.Optional[dict] = None,
journal_value_codec: typing.Optional[JournalValueCodec] = None,
) -> typing.AsyncGenerator[RestateClient, None]:
"""a dummy client constructor to raise ImportError. Install restate-sdk[client] to use this feature"""
raise ImportError("Install restate-sdk[client] to use this feature")
yield # type: ignore
from .client import create_client


__all__ = [
Expand Down Expand Up @@ -128,5 +115,7 @@ async def create_client(
"RestateClient",
"RestateClientSendHandle",
"HttpError",
"HttpClient",
"HttpResponse",
"create_client",
]
32 changes: 28 additions & 4 deletions python/restate/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@
"""

from datetime import timedelta
import httpx
import typing
from contextlib import asynccontextmanager

from .client_types import RestateClient, RestateClientSendHandle, RestateScopedClient, HttpError
from .client_types import HttpClient, RestateClient, RestateClientSendHandle, RestateScopedClient, HttpError

from .context import HandlerType
from .entry_codec import JournalValueCodec
Expand All @@ -35,7 +34,7 @@ class Client(RestateClient):

def __init__(
self,
client: httpx.AsyncClient,
client: HttpClient,
headers: typing.Optional[dict] = None,
journal_codec: typing.Optional[JournalValueCodec] = None,
):
Expand Down Expand Up @@ -486,6 +485,8 @@ async def create_client(
ingress: str,
headers: typing.Optional[dict] = None,
journal_value_codec: typing.Optional[JournalValueCodec] = None,
*,
backend: typing.Literal["httpx", "httpx2"] = "httpx",
) -> typing.AsyncGenerator[RestateClient, None]:
"""
Create a new Restate client.
Expand All @@ -496,6 +497,29 @@ async def create_client(
journal_value_codec: Optional journal value codec. When set, request bodies are encoded and
call responses decoded through it. It must match the codec configured on the endpoint.
NOTE: This is experimental and may change in future releases.
backend: HTTP package to use. Defaults to httpx. Install restate-sdk[client] for httpx
or restate-sdk[client-httpx2] for httpx2. The selected package must be installed.
"""
async with httpx.AsyncClient(base_url=ingress, headers=headers, http2=True) as http_client:
if backend not in ("httpx", "httpx2"):
msg = f"Unsupported HTTP backend: {backend!r}. Expected 'httpx' or 'httpx2'."
raise ValueError(msg)

http_client_context: typing.AsyncContextManager[HttpClient]
try:
if backend == "httpx":
import httpx

http_client_context = httpx.AsyncClient(base_url=ingress, headers=headers, http2=True)
else:
import httpx2

http_client_context = httpx2.AsyncClient(base_url=ingress, headers=headers, http2=True)
except ModuleNotFoundError as exc:
if exc.name != backend:
raise
extra = "client" if backend == "httpx" else "client-httpx2"
msg = f'The {backend} backend requires {backend}. Install it with: python -m pip install "restate-sdk[{extra}]"'
raise ImportError(msg) from exc

async with http_client_context as http_client:
yield Client(http_client, headers, journal_codec=journal_value_codec)
22 changes: 22 additions & 0 deletions python/restate/client_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@
O = typing.TypeVar("O")


class HttpResponse(typing.Protocol):
"""The buffered HTTP response used by the ingress client."""

@property
def status_code(self) -> int: ...

@property
def reason_phrase(self) -> str: ...

@property
def text(self) -> str: ...

@property
def content(self) -> bytes: ...


class HttpClient(typing.Protocol):
"""An HTTP client that can send ingress requests asynchronously."""

async def post(self, url: str, *, headers: dict[str, str], content: bytes) -> HttpResponse: ...


class RestateClientSendHandle:
"""
A handle for a send operation.
Expand Down
Loading
Loading