Skip to main content

Resonate Python SDK

Welcome to the Resonate Python SDK guide! This SDK makes it possible to write Distributed Async Await applications with Python. This guide covers installation and features that the SDK offers.

Installation

How to install the Resonate Python SDK into your project.

To install the Resonate Python SDK, you can use any of your favorite package managers.

uv add resonate-sdk

uv add resonate-sdk

pip install resonate-sdk

pip install resonate-sdk

Initialization

How to initialize Resonate in your Worker / Application Node.

There are two ways to initialize Resonate, local and remote.

resonate = Resonate()

Local initialization

Local initialization means that Resonate uses local memory.

Quickly integrate into an existing application

This is ideal for getting started quickly or for integrating Resonate into an existing application without relying on dependencies.

from resonate import Resonate

resonate = Resonate()

Remote initialization

Remote initialization means that Resonate stores promises remotely in a Resonate Server and can receive messages from other Application Nodes.

Add reliability and scalability

This is ideal for building a distributed application that is reliable and can scale across multiple Application Nodes.

The quickest way to get started with Remote initialization is to use Resonate's default Poller to receive messages directly from a Resonate Server.

from resonate import Resonate
from resonate.stores.remote import RemoteStore
from resonate.task_sources.poller import Poller

resonate = Resonate(
store=RemoteStore(url="http://localhost:8001"), # Resonate Server address
message_source=Poller(url="http://localhost:8002"), # Message source configuration
)

Resonate Application Nodes can receive messages from many different transports, such as HTTP, RabbitMQ, RedPanda, etc... The Poller is a great starting place as it will long-poll for messages from the Resonate Server without any additional setup.

Registration

How to register a function with Resonate in the Python SDK.

There are two ways to register a function with Resonate: using the register method or using the @resonate.register decorator.

@resonate.register

Decorator

@resonate.register
def foo(ctx, args):
# ...
return

resonate.register()

Method

def foo(ctx, args):
# ...
return

resonate.register(foo)

Ephemeral-to-Durable

How to invoke a function in the ephemeral world with the Resonate Class.

To move from the ephemeral world to the durable world you use the Resonate Class to invoke functions.

There are two methods that you can use .run() and .rpc().

.run()

Resonate's .run() method will invoke the function in the same process. You can think of it as a "run right here" invocation. After invocation, the function is considered durable and can recover in another process if desired.

Decorated function invocation

@resonate.register
def foo(ctx, args):
# ...
return

handle = foo.run("invocation-id", args)
# do more stuff
result = handle.result()

Method registered invocation

def foo(ctx, args):
# ...
return

resonate.register(foo)
handle = resonate.run("invocation-id", foo, args)
# do more stuff
result = handle.result()

.rpc()

Resonate's .rpc() method (Remote Procedure Call) invokes the function in a remote process.

Remote initialization required

The RPC method requires that the Worker / Application Node is connected with a Resonate Server. It also assumes that the Worker / Application Node where the invoked function is running is also connected to a Resonate Server and can receive messages.

You can think of this API as a "run somewhere else" invocation (Asynchronous Remote Procedure Call). However, if the function is registered locally, it is still possible for the function to execute in the same process. After invocation, the function is considered durable and can recover in another process if desired.

The invocation returns a promise which can be awaited on at any point after.

process a

# process a
@resonate.register
def foo(ctx, args):
# ...
return

process b

# process b
handle = resonate.rpc("invocation-id", "foo", args)
# do more stuff
result = handle.result()

.set_dependency()

Resonate's .set_dependency() method allows you to set a dependency for the Application Node. You can then access the dependency in the function using the Context's .get_dependency() method.

Dependencies can only be added in the ephemeral world.

resonate.set_dependency("dependency-name", dependency)

The dependency can be accesses from any function in the Call Graph on that Application Node. This is useful for things like database connections or other resources that you want to share across functions.

.promises.resolve()

Resonate's .promise.resolve() method allows you to resolve a promise by ID.

This is useful for HITL use cases where you want to wait for a human to approve or reject a function execution. It works well in conjunction with Context's .promise() method.

resonate.promise.resolve(
id="promise-id"
# optional
result={}
)

Durable

How to use the Resonate Context object in the Python SDK.

Resonate's Context object enables you to invoke functions from inside a Durable Function. This is how you extend the Call Graph and create a world of Durable Functions. Inside a Durable Function you use the yield keyword to interact with the Context object.

.lfi()

Context's .lfi() method (Local Function Invocation) invokes a function in the same process in an asynchronous manner. That is — the invocation returns a promise which can be awaited on at any point after.

@resonate.register
def foo(ctx, args):
# ...
promise = yield ctx.lfi(bar, args)
# do more sture
result = yield promise
# ...


def bar(ctx, args):
# ...
return

.lfc()

Context's .lfc() method (Local Function Call) invokes a function in the same process in a synchronous manner. That is — the calling function blocks until the invoked function returns.

@resonate.register
def foo(ctx, args):
# ...
result = yield ctx.lfc(bar, args)
# do more stuff
# ...


def bar(ctx, args):
# ...
return

.rfi()

Context's .rfi() method (Remote Function Invocation) invokes a function in a remote process in an asynchronous manner. That is — the invocation returns a promise which can be awaited on at any point after.

process a

# process a
@resonate.register
def foo(ctx, args):
# ...
promise = yield ctx.rfi("bar", args)
# do more stuff
result = yield promise
# ...

process b

# process b
@resonate.register
def bar(ctx, args):
# ...
return

.rfc()

Context's .rfc() method (Remote Function Call) invokes a function in a remote process in a synchronous manner. That is — the calling function blocks until the invoked function returns.

process a

# process a
@resonate.register
def foo(ctx, args):
# ...
result = yield ctx.rfc("bar", args)
# do more stuff
# ...

process b

# process b
@resonate.register
def bar(ctx, args):
# ...
return

.get_dependency()

Context's .get_dependency() method allows you to get a dependency that was set in the ephemeral world using the .set_dependency() method and use it the Durable World.

@resonate.register
def foo(ctx, args):
# ...
dependency = ctx.get_dependency("dependency-name")
# do something with the dependency
# ...

.sleep()

Context's .sleep() method allows you to sleep inside a function. There is no limit to how long you can sleep. The sleep method accepts an integer value in seconds.

@resonate.register
def foo(ctx, args):
# ...
yield ctx.sleep(5)
# do more stuff
# ...

The previous function will sleep for 5 seconds and then continue execution.

.promise()

Context's .promise() method allows you to get or create a promise that can be awaited on. You can optionally pass an ID using the options method.

If no ID is provided, one is generated and a new promise is created. If an ID is provided and a promise alread exists with that ID, then the existing promise is returned.

This is very useful for HITL (Human-In-The-Loop) use cases where you want to block progress until a human has taken an action or provided data. It works well in conjunction with Resonate's .promise.resolve() method.

@resonate.register
def foo(ctx, args):
# ...
promise = yield ctx.promise("promise-id")
# do more stuff
result = yield promise
# ...

You can also pass custom data into the promise.

  • In a Pending state, custom data stores in the data field of the promise.
  • In a Resolved state the custom data stores in the result field of the promise.
  • In a Rejected state the custom data stores in the error field of the promise.
@resonate.register
def foo(ctx, args):
# ...
promise = yield ctx.promise({"key": "value"}).options(
id="promise-id",
)
# do more stuff
result = yield promise
# ...

.options()

Many of Context's methods support options on the call you are making.

Batch operations

How to use batch operations in the Resonate Python SDK.

Older version

The following documentation reflects version 0.2.4 of the Resonate Python SDK.

Resonate's transparent batching feature handles the coordination of otherwise concurrent executions to create batches, enabling you to write concurrent, non-coordinated code. For a deeper dive into transparent batching, check out the Transparent batching with the Resonate Python SDK blog post. To use transparent batching, follow these steps.

First, create a data structure that inherits what Resonate calls a Command interface. The data structure must include the data to be inserted into the database. The Command data structure stands in for a function execution invocation so that you still get a Durable Promise and await on result.

from dataclasses import dataclass
# ...
from resonate.commands import Command
# ...
# Define a data structure for the Resonate SDK to track and create batches of
@dataclass
class InsertUser(Command):
id: int

Then, create a handler that can process a batch of operations.

# ...
from resonate.context import Context
# ...
# Define a function that inserts a batch of rows into the database
# The main difference is that commit() is only called after all the Insert statements are executed
def _batch_handler(_: Context, users: list[InsertUser]):
# error handling ommitted for this example
for user in users:
conn.execute("INSERT INTO users (value) VALUES (?)", (user.id,))
conn.commit()
print(f"{len(users)} users have been inserted to database.")

Next, register the data structure and the handler with the Resonate Scheduler.

# ...
from resonate.scheduler import Scheduler
from resonate.storage import LocalPromiseStore
from resonate.retry_policy import never
# ...
# Create a Resonate Scheduler
resonate = Scheduler(LocalPromiseStore(), processor_threads=1)
# ...
# Register the batch handler and data structure with the Resonate Scheduler
resonate.register_command_handler(InsertUser, _batch_handler, retry_policy=never())

Finally, create a function that can be invoked over and over again and passes the data to Resonate to manage. Register it with the Resonate Scheduler, and then call that function with Resonate's run() method.

# ...
# Definte the top level function that uses batching
def create_user_batching(ctx: Context, u: int):
p = yield ctx.lfi(InsertUser(u))
yield p
# ...
# Register the top level functions with the Resonate Scheduler
resonate.register(create_user_batching, retry_policy=never())
# ...
def main() -> None:
# ...
# Create an array to hold the promises
promises = []

for u in range(10000):
p = resonate.run(f"insert-value-{u}", create_user_batching, u)
promises.append(p)

for p in promises:
p.result()