Skip to content
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
tmp/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
82 changes: 82 additions & 0 deletions etc/run-integration-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail

# Run the sdk-test-suite integration tests locally.
#
# Prerequisites:
# - Docker running
#
# Usage:
# ./etc/run-integration-tests.sh # test original test-services
# ./etc/run-integration-tests.sh --cls # test class-based test-services
# ./etc/run-integration-tests.sh --skip-build # reuse existing image
# ./etc/run-integration-tests.sh --cls --skip-build # combine flags

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SDK_TEST_SUITE_VERSION="v3.4"
JAR_URL="https://github.com/restatedev/sdk-test-suite/releases/download/${SDK_TEST_SUITE_VERSION}/restate-sdk-test-suite.jar"
JAR_PATH="${REPO_ROOT}/tmp/restate-sdk-test-suite.jar"
RESTATE_IMAGE="${RESTATE_CONTAINER_IMAGE:-ghcr.io/restatedev/restate:main}"
REPORT_DIR="${REPO_ROOT}/tmp/test-report"

SKIP_BUILD=false
USE_CLS=false
for arg in "$@"; do
case "$arg" in
--skip-build) SKIP_BUILD=true ;;
--cls) USE_CLS=true ;;
esac
done

if [ "$USE_CLS" = true ]; then
SERVICE_IMAGE="restatedev/test-services-python-cls"
DOCKERFILE="${REPO_ROOT}/test-services-cls/Dockerfile"
EXCLUSIONS="${REPO_ROOT}/test-services-cls/exclusions.yaml"
ENV_FILE="${REPO_ROOT}/test-services-cls/.env"
echo "==> Using class-based test-services (test-services-cls/)"
else
SERVICE_IMAGE="restatedev/test-services-python"
DOCKERFILE="${REPO_ROOT}/test-services/Dockerfile"
EXCLUSIONS="${REPO_ROOT}/test-services/exclusions.yaml"
ENV_FILE="${REPO_ROOT}/test-services/.env"
echo "==> Using original test-services (test-services/)"
fi

# 1. Build the test-services Docker image
if [ "$SKIP_BUILD" = false ]; then
echo "==> Building test-services Docker image..."
docker build -f "${DOCKERFILE}" -t "${SERVICE_IMAGE}" "${REPO_ROOT}"
fi

# 2. Download the test suite JAR (if not cached)
mkdir -p "$(dirname "$JAR_PATH")"
if [ ! -f "$JAR_PATH" ]; then
echo "==> Downloading sdk-test-suite ${SDK_TEST_SUITE_VERSION}..."
curl -fSL -o "$JAR_PATH" "$JAR_URL"
fi

# 3. Pull restate image
echo "==> Pulling Restate image: ${RESTATE_IMAGE}"
docker pull "${RESTATE_IMAGE}"

# 4. Run the test suite via Docker (no local Java needed)
echo "==> Running integration tests..."
rm -rf "${REPORT_DIR}"
mkdir -p "${REPORT_DIR}"

docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "${JAR_PATH}:/opt/test-suite.jar:ro" \
-v "${EXCLUSIONS}:/opt/exclusions.yaml:ro" \
-v "${ENV_FILE}:/opt/service.env:ro" \
-v "${REPORT_DIR}:/opt/test-report" \
-e RESTATE_CONTAINER_IMAGE="${RESTATE_IMAGE}" \
--network host \
eclipse-temurin:21-jre \
java -jar /opt/test-suite.jar run \
--exclusions-file=/opt/exclusions.yaml \
--service-container-env-file=/opt/service.env \
--report-dir=/opt/test-report \
"${SERVICE_IMAGE}"

echo "==> Done. Test report: ${REPORT_DIR}"
Empty file added examples/README.md
Empty file.
79 changes: 79 additions & 0 deletions examples/class_based_greeter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
Class-based API example for the Restate Python SDK.

This example demonstrates the same services as the decorator-based examples,
but using the class-based API with @handler, @shared, and @main decorators.
"""

from datetime import timedelta

import restate
from restate.cls import Service, VirtualObject, Workflow, handler, shared, main, Context


class Greeter(Service):
"""A simple stateless greeting service."""

@handler
async def greet(self, name: str) -> str:
return f"Hello {name}!"


class Counter(VirtualObject):
"""A stateful counter backed by durable state."""

@handler
async def increment(self, value: int) -> int:
n: int = await Context.get("counter", type_hint=int) or 0
n += value
Context.set("counter", n)
return n

@shared
async def count(self) -> int:
return await Context.get("counter", type_hint=int) or 0


class PaymentWorkflow(Workflow):
"""A durable payment workflow with external verification."""

@main
async def pay(self, amount: int) -> str:
Context.set("status", "processing")

async def charge():
return f"charged ${amount}"

receipt = await Context.run_typed("charge", charge)
Context.set("status", "completed")
return receipt

@handler
async def status(self) -> str:
return await Context.get("status", type_hint=str) or "unknown"


class OrderProcessor(Service):
"""Demonstrates type-safe RPC between services using fluent proxies."""

@handler
async def process(self, customer: str) -> str:
# Call a service handler — IDE knows .greet() takes str, returns str
greeting = await Greeter.call().greet(customer)

# Call a virtual object — IDE knows .increment() takes int, returns int
count = await Counter.call(customer).increment(1)

# Fire-and-forget send (returns SendHandle, not a coroutine)
Counter.send(customer).increment(1) # type: ignore[unused-coroutine]

# Send with delay
Counter.send(customer, delay=timedelta(seconds=30)).increment(1) # type: ignore[unused-coroutine]

# Call a workflow
receipt = await PaymentWorkflow.call(f"order-{count}").pay(100)

return f"{greeting} (visit #{count}, {receipt})"


app = restate.app([Greeter, Counter, PaymentWorkflow, OrderProcessor])
6 changes: 6 additions & 0 deletions examples/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def main():
print("Hello from examples!")


if __name__ == "__main__":
main()
15 changes: 15 additions & 0 deletions examples/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[project]
name = "examples"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"hypercorn>=0.18.0",
"pydantic>=2.12.5",
"restate-sdk",
"uvicorn>=0.38.0",
]

[tool.uv.sources]
restate-sdk = { path = "../dist/restate_sdk-0.14.2-cp313-cp313-macosx_14_0_arm64.whl" }
Loading
Loading