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
32 changes: 8 additions & 24 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,37 +1,21 @@
name: Test

on:
pull_request:
workflow_dispatch:
push:
branches:
- main

permissions:
contents: read
checks: write
pull-requests: write

jobs:
lint-and-test:
name: "Lint and Test (Python ${{ matrix.python }})"
runs-on: ubuntu-latest
strategy:
matrix:
python: [ "3.10", "3.11", "3.12", "3.13" ]
env:
UV_PYTHON: ${{ matrix.python }}
steps:
- uses: actions/checkout@v4
- uses: extractions/setup-just@v2
- uses: actions/setup-python@v5
- uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python }}
- name: Build Rust module
uses: PyO3/maturin-action@v1
with:
args: --out dist --interpreter python${{ matrix.python }}
sccache: 'true'
container: off
- run: pip install -r requirements.txt
- run: pip install dist/*
enable-cache: true
- name: Install dependencies
run: just sync
- name: Build
run: just build
- name: Verify
run: just verify
53 changes: 31 additions & 22 deletions Justfile
Original file line number Diff line number Diff line change
@@ -1,38 +1,47 @@
# Justfile
# Use UV_PYTHON env variable to select either a python version or
# the complete python to your python interpreter

python := "python3"
default := "all"

default:
@echo "Available recipes:"
@echo " mypy - Run mypy for type checking"
@echo " pylint - Run pylint for linting"
@echo " test - Run pytest for testing"
@echo " verify - Run mypy, pylint, test"
set shell := ["bash", "-c"]

# Recipe to run mypy for type checking
mypy:
@echo "Running mypy..."
{{python}} -m mypy --check-untyped-defs --ignore-missing-imports python/restate/
{{python}} -m mypy --check-untyped-defs --ignore-missing-imports examples/
sync:
uv sync --all-extras --all-packages

# Recipe to run pylint for linting
pylint:
@echo "Running pylint..."
{{python}} -m pylint python/restate --ignore-paths='^.*.?venv.*$'
{{python}} -m pylint examples/ --ignore-paths='^.*\.?venv.*$'
format:
uv run ruff format
uv run ruff check --fix --fix-only

lint:
uv run ruff format --check
uv run ruff check

typecheck-pyright:
PYRIGHT_PYTHON_IGNORE_WARNINGS=1 uv run pyright python/
PYRIGHT_PYTHON_IGNORE_WARNINGS=1 uv run pyright examples/
PYRIGHT_PYTHON_IGNORE_WARNINGS=1 uv run pyright tests
PYRIGHT_PYTHON_IGNORE_WARNINGS=1 uv run pyright test-services/

typecheck-mypy:
uv run -m mypy --check-untyped-defs --ignore-missing-imports python/
uv run -m mypy --check-untyped-defs --ignore-missing-imports examples/
uv run -m mypy --check-untyped-defs --ignore-missing-imports tests/

typecheck: typecheck-pyright typecheck-mypy

test:
@echo "Running Python tests..."
{{python}} -m pytest tests/*
uv run -m pytest tests/*


# Recipe to run both mypy and pylint
verify: mypy pylint test
verify: format lint typecheck test
@echo "Type checking and linting completed successfully."

# Recipe to build the project
build:
@echo "Building the project..."
maturin build --release
#maturin build --release
uv build --all-packages

clean:
@echo "Cleaning the project"
Expand Down
9 changes: 8 additions & 1 deletion examples/concurrent_greeter.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,24 @@
from pydantic import BaseModel
from restate import wait_completed, Service, Context


# models
class GreetingRequest(BaseModel):
name: str


class Greeting(BaseModel):
messages: typing.List[str]


class Message(BaseModel):
role: str
content: str


concurrent_greeter = Service("concurrent_greeter")


@concurrent_greeter.handler()
async def greet(ctx: Context, req: GreetingRequest) -> Greeting:
claude = ctx.service_call(claude_sonnet, arg=Message(role="user", content=f"please greet {req.name}"))
Expand All @@ -45,17 +50,19 @@ async def greet(ctx: Context, req: GreetingRequest) -> Greeting:

# cancel the pending calls
for f in pending:
await f.cancel_invocation() # type: ignore
await f.cancel_invocation() # type: ignore

return Greeting(messages=greetings)


# not really interesting, just for this demo:


@concurrent_greeter.handler()
async def claude_sonnet(ctx: Context, req: Message) -> str:
return f"Bonjour {req.content[13:]}!"


@concurrent_greeter.handler()
async def open_ai(ctx: Context, req: Message) -> str:
return f"Hello {req.content[13:]}!"
8 changes: 2 additions & 6 deletions examples/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,13 @@

logging.basicConfig(level=logging.INFO)

app = restate.app(services=[greeter,
random_greeter,
counter,
payment,
pydantic_greeter,
concurrent_greeter])
app = restate.app(services=[greeter, random_greeter, counter, payment, pydantic_greeter, concurrent_greeter])

if __name__ == "__main__":
import hypercorn
import hypercorn.asyncio
import asyncio

conf = hypercorn.Config()
conf.bind = ["0.0.0.0:9080"]
asyncio.run(hypercorn.asyncio.serve(app, conf))
1 change: 1 addition & 0 deletions examples/greeter.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

greeter = Service("greeter")


@greeter.handler()
async def greet(ctx: Context, name: str) -> str:
logger.info("Received greeting request: %s", name)
Expand Down
4 changes: 4 additions & 0 deletions examples/pydantic_greeter.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,21 @@
from pydantic import BaseModel
from restate import Service, Context


# models
class GreetingRequest(BaseModel):
name: str


class Greeting(BaseModel):
message: str


# service

pydantic_greeter = Service("pydantic_greeter")


@pydantic_greeter.handler()
async def greet(ctx: Context, req: GreetingRequest) -> Greeting:
return Greeting(message=f"Hello {req.name}!")
15 changes: 9 additions & 6 deletions examples/random_greeter.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# https://github.com/restatedev/sdk-typescript/blob/main/LICENSE
#
"""example.py"""

from datetime import datetime

# pylint: disable=C0116
Expand All @@ -18,9 +19,9 @@

random_greeter = Service("random_greeter")


@random_greeter.handler()
async def greet(ctx: Context, name: str) -> str:

# ctx.random() returns a Python Random instance seeded deterministically.
# By using ctx.random() you don't write entries in the journal,
# but you still get the same generated values on retries.
Expand Down Expand Up @@ -48,8 +49,10 @@ async def greet(ctx: Context, name: str) -> str:
# end = await ctx.time()
# delta = datetime.timedelta(seconds=(end-start))

return (f"Hello {name} with "
f"random number {random_number}, "
f"random bytes {random_bytes!r} "
f"random uuid {random_uuid},"
f"now datetime {now_datetime}!")
return (
f"Hello {name} with "
f"random number {random_number}, "
f"random bytes {random_bytes!r} "
f"random uuid {random_uuid},"
f"now datetime {now_datetime}!"
)
2 changes: 2 additions & 0 deletions examples/virtual_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@

counter = VirtualObject("counter")


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


@counter.handler(kind="shared")
async def count(ctx: ObjectSharedContext) -> int:
return await ctx.get("counter") or 0
10 changes: 6 additions & 4 deletions examples/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

payment = Workflow("payment")


@payment.main()
async def pay(ctx: WorkflowContext, amount: int):
workflow_key = ctx.key()
Expand All @@ -44,16 +45,17 @@ def payment_gateway():
# Wait for the payment to be verified

match await select(result=ctx.promise("verify.payment").value(), timeout=ctx.sleep(TIMEOUT)):
case ['result', "approved"]:
case ["result", "approved"]:
ctx.set("status", "payment approved")
return { "success" : True }
case ['result', "declined"]:
return {"success": True}
case ["result", "declined"]:
ctx.set("status", "payment declined")
raise TerminalError(message="Payment declined", status_code=401)
case ['timeout', _]:
case ["timeout", _]:
ctx.set("status", "payment verification timed out")
raise TerminalError(message="Payment verification timed out", status_code=410)


@payment.handler()
async def payment_verified(ctx: WorkflowSharedContext, result: str):
promise = ctx.promise("verify.payment", type_hint=str)
Expand Down
20 changes: 19 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Source = "https://github.com/restatedev/sdk-python"

[project.optional-dependencies]
test = ["pytest", "hypercorn"]
lint = ["mypy", "pylint"]
lint = ["mypy>=1.11.2", "pyright>=1.1.390", "ruff>=0.6.9"]
harness = ["testcontainers", "hypercorn", "httpx"]
serde = ["dacite", "pydantic"]

Expand All @@ -34,3 +34,21 @@ build-backend = "maturin"
features = ["pyo3/extension-module"]
module-name = "restate._internal"
python-source = "python"

[tool.ruff]
line-length = 120
target-version = "py310"
include = [
"python/**/*.py",
"examples/**/*.py",
"tests/**/*.py",
"test-services//**/*.py",
]

[tool.ruff.lint]
ignore = ["E741"]

[tool.pytest.ini_options]
filterwarnings = [
"ignore:The @wait_container_is_ready decorator is deprecated:DeprecationWarning",
]
Loading