Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python ticketing example #169

Merged
merged 9 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
18 changes: 18 additions & 0 deletions .github/workflows/pre-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ on:
description: 'sdk-java version (without prepending v). Leave empty if you do not want to update it.'
required: false
type: string
sdkPythonVersion:
description: 'sdk-python version (without prepending v). Leave empty if you do not want to update it.'
required: false
type: string
cdkVersion:
description: 'cdk version (without prepending v). Leave empty if you do not want to update it.'
required: false
Expand Down Expand Up @@ -67,6 +71,20 @@ jobs:
if: github.event.inputs.sdkJavaVersion != ''
run: ./.tools/run_jvm_tests.sh

# Bump Python SDK
- uses: actions/checkout@v3
if: github.event.inputs.sdkPythonVersion != ''
- uses: actions/setup-python@v5
if: github.event.inputs.sdkPythonVersion != ''
with:
python-version: "3.12"
- name: Bump Python SDK
if: github.event.inputs.sdkPythonVersion != ''
run: ./.tools/update_python_examples.sh ${{ inputs.sdkPythonVersion }}
gvdongen marked this conversation as resolved.
Show resolved Hide resolved
- name: Run Python tests
if: github.event.inputs.sdkPythonVersion != ''
run: ./.tools/run_python_tests.sh

- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
Expand Down
3 changes: 2 additions & 1 deletion .tools/run_python_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ function python_mypi_lint() {
deactivate
}

pushd $PROJECT_ROOT/templates/python && python_mypi_lint && popd || exit
pushd $PROJECT_ROOT/templates/python && python_mypi_lint && popd || exit
pushd $PROJECT_ROOT/patterns-use-cases/ticket-reservation/ticket-reservation-python && python_mypi_lint && popd || exit
15 changes: 15 additions & 0 deletions .tools/update_python_examples.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash

set -eufx -o pipefail

NEW_VERSION=$1
SELF_PATH=${BASH_SOURCE[0]:-"$(command -v -- "$0")"}
PROJECT_ROOT="$(dirname "$SELF_PATH")/.."

function search_and_replace_version() {
echo "upgrading Python version of $1 to $NEW_VERSION"
sed -i 's/restate_sdk==[0-9A-Za-z.-]*/restate_sdk=='"$NEW_VERSION"'/' "$1/requirements.txt"
}

search_and_replace_version $PROJECT_ROOT/templates/python
search_and_replace_version $PROJECT_ROOT/patterns-use-cases/ticket-reservation/ticket-reservation-python
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
venv
.venv
__pycache__/
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Restate Example: Ticket reservation system Python

This example shows a subset of a ticket booking system.

Restate is a system for easily building resilient applications using **distributed durable building blocks**.

❓ Learn more about Restate from the [Restate documentation](https://docs.restate.dev).

## Running the example

To set up the example, use the following sequence of commands.

Setup the virtual env:

```shell
python3 -m venv .venv
source venv/bin/activate
gvdongen marked this conversation as resolved.
Show resolved Hide resolved
```

Install the requirements:

```shell
pip install -r requirements.txt
```

Start the app as follows:

```shell
python3 -m hypercorn example/app:app
```

Start the Restate Server ([other options here]()):
gvdongen marked this conversation as resolved.
Show resolved Hide resolved

```shell
restate-server
```

Register the service:

```shell
restate dp register http://localhost:8000
```

Then add a ticket to Mary's cart:

```shell
curl localhost:8080/cart/Mary/add_ticket -H 'content-type: application/json' -d '"seat2B"'
```

Let Mary buy the ticket via:
```shell
curl -X POST localhost:8080/cart/Mary/checkout
```

That's it! We managed to run the example, add a ticket to the user session cart, and buy it!
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import restate

from example.cart_object import cart
from example.checkout_service import checkout
from example.ticket_object import ticket

app = restate.app(services=[cart, checkout, ticket])
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from datetime import timedelta

from restate.context import ObjectContext
from restate.object import VirtualObject

from example.checkout_service import handle
from example.ticket_object import reserve, mark_as_sold, unreserve

cart = VirtualObject("cart")


@cart.handler()
async def add_ticket(ctx: ObjectContext, ticket_id: str) -> bool:
reserved = await ctx.object_call(reserve, key=ticket_id, arg=None)

if reserved:
tickets = await ctx.get("tickets") or []
tickets.append(ticket_id)
ctx.set("tickets", tickets)

ctx.object_send(expire_ticket, key=ctx.key(), arg=ticket_id, send_delay=timedelta(minutes=15))

return reserved


@cart.handler()
async def checkout(ctx: ObjectContext) -> bool:
tickets = await ctx.get("tickets") or []

if len(tickets) == 0:
return False

success = await ctx.service_call(handle, arg={'user_id': ctx.key(),
gvdongen marked this conversation as resolved.
Show resolved Hide resolved
'tickets': tickets})

if success:
for ticket in tickets:
ctx.object_send(mark_as_sold, key=ticket, arg=None)

ctx.clear("tickets")

return success


@cart.handler()
async def expire_ticket(ctx: ObjectContext, ticket_id: str):
tickets = await ctx.get("tickets") or []

try:
ticket_index = tickets.index(ticket_id)
except ValueError:
ticket_index = -1

if ticket_index != -1:
tickets.pop(ticket_index)
ctx.set("tickets", tickets)

ctx.object_send(unreserve, key=ticket_id, arg=None)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import uuid
from typing import TypedDict, List
from restate.context import ObjectContext, Serde
from restate.service import Service

from example.utils.email_client import EmailClient
from example.utils.payment_client import PaymentClient


class Order(TypedDict):
user_id: str
tickets: List[str]


payment_client = PaymentClient()
email_client = EmailClient()

checkout = Service("checkout")


@checkout.handler()
async def handle(ctx: ObjectContext, order: Order) -> bool:
total_price = len(order['tickets']) * 40

idempotency_key = await ctx.run("idempotency_key", lambda: str(uuid.uuid4()))

async def pay():
return await payment_client.call(idempotency_key, total_price)
success = await ctx.run("payment", pay)

if success:
await ctx.run("send_success_email", lambda: email_client.notify_user_of_payment_success(order['user_id']))
gvdongen marked this conversation as resolved.
Show resolved Hide resolved
else:
await ctx.run("send_failure_email", lambda: email_client.notify_user_of_payment_failure(order['user_id']))

return success
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from restate.context import ObjectContext
from restate.object import VirtualObject


ticket = VirtualObject("ticket")


@ticket.handler()
async def reserve(ctx: ObjectContext) -> bool:
status = await ctx.get("status") or "AVAILABLE"

if status == "AVAILABLE":
ctx.set("status", "RESERVED")
return True
else:
return False


@ticket.handler()
async def unreserve(ctx: ObjectContext):
status = await ctx.get("status") or "AVAILABLE"

if status != "SOLD":
ctx.clear("status")


@ticket.handler()
async def mark_as_sold(ctx: ObjectContext):
status = await ctx.get("status") or "AVAILABLE"

if status == "RESERVED":
ctx.set("status", "SOLD")
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class EmailClient:

def __init__(self):
self.i = 0

def notify_user_of_payment_success(self, user_id: str):
print(f"Notifying user {user_id} of payment success")
# send the email
return True

def notify_user_of_payment_failure(self, user_id: str):
print(f"Notifying user {user_id} of payment failure")
# send the email
return True
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class PaymentClient:

def __init__(self):
self.i = 0

async def call(self, idempotency_key: str, amount: float) -> bool:
print(f"Payment call succeeded for idempotency key {idempotency_key} and amount {amount}")
# do the call
return True

async def failing_call(self, idempotency_key: str, amount: float) -> bool:
if self.i >= 2:
print(f"Payment call succeeded for idempotency key {idempotency_key} and amount {amount}")
i = 0
return True
else:
print(f"Payment call failed for idempotency key {idempotency_key} and amount {amount}. Retrying...")
self.i += 1
raise Exception("Payment call failed")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
restate_sdk==0.1.1
hypercorn
2 changes: 1 addition & 1 deletion templates/python/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
hypercorn
restate_sdk
restate_sdk==0.1.1
Loading