Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

openrois-sdk

Python

Python client SDK for OpenRoIS, an open-source middleware implementing the OMG RoIS Framework 2.0.

Connect to an OpenRoIS engine, discover the components of every connected physical robot, virtual avatar, or AI service, and drive them through the standard RoIS interfaces. Asyncio only.

Alpha, pre-1.0, unstable API.

Installation

pip

pip install openrois-sdk

From Source

git clone https://github.com/openrois/openrois.git
pip install -e ./openrois/sdk/python

wss:// has not been exercised: every run used ws://. The gateway does not authenticate connections in the alpha releases, so use it on trusted networks only.

Usage

import asyncio
from collections import defaultdict

from openrois.interfaces import CompletedParams, RoISIdentifierType
from openrois.sdk import RoISClient, component_ref, component_type


async def main() -> None:
    async with RoISClient.connect("ws://localhost:8765") as client:
        # Find a component by type, and select it by ref in later calls.
        navigation = RoISIdentifierType(authority="OMG", code="Navigation")
        [nav] = await client.search(component_type(navigation))
        target = component_ref(nav)

        # Read state.
        status = await client.query("component_status", target)
        print(status)

        # React to events.
        client.on(
            "rois.event.notify_event",
            lambda event: print(event.event_type, event.results),
        )
        await client.subscribe("reached_target", target)

        # Record how each command ends, before running any.
        statuses: dict[str, str] = {}
        ended: defaultdict[str, asyncio.Event] = defaultdict(asyncio.Event)

        def on_completed(completed: CompletedParams) -> None:
            statuses[completed.command_id] = completed.status
            ended[completed.command_id].set()

        client.on("rois.command.completed", on_completed)

        # Reserve, configure, command, wait, release.
        await client.bind(nav)
        await client.set_parameter(
            nav,
            [{"name": "target_positions", "data_type_ref": "string[]", "value": '["kitchen"]'}],
        )
        [command_id] = await client.execute([{"component_ref": nav, "command_type": "start"}])
        # Wait here, because release aborts a running command, which then ends with ABORT.
        await asyncio.wait_for(ended[command_id].wait(), timeout=30)
        print(statuses[command_id])
        await client.release(nav)


asyncio.run(main())

Run it against the Docker Compose stack at the repository root, which serves simulated components through a gateway.

Waiting for a Command

RoIS has no operation that waits for a command, so neither does the client. execute() returns as soon as the engine accepts the commands. Each command ends later, with a rois.command.completed notification that names its id. set_parameter() works the same way.

A command that ends at once can complete before the reply reaches your code, so a listener added after execute() can miss it. The recipe has three steps:

  1. Add the listener before the call, once for the whole session.
  2. Record each status by command_id.
  3. Read the status with the ids in the reply.

The quickstart above does this. Its asyncio.Event for each id stays set, so a command that has already ended does not wait, and asyncio.wait_for limits the wait.

Wait before you release the component: release() aborts your running commands on it, and they end with ABORT. disconnect() aborts them too.

API

RoISClient has one method per operation of the RoIS method catalog. Each takes the operation's IDL parameters in IDL order, validates them and the result against the catalog models in openrois-interfaces, and returns the result's out parameters as interface models. Inputs take a model or a mapping.

RoIS interface Methods
System RoISClient.connect(url, *, request_timeout, connect_timeout), disconnect(), get_profile(condition=""), get_error_detail(error_id, condition="")
Command search(condition=""), bind(component_ref), bind_any(condition=""), release(component_ref), get_parameter(component_ref), set_parameter(component_ref, parameters), execute(command_unit_list), get_command_result(command_id, condition="")
Query query(query_type, condition="")
Event subscribe(event_type, condition=""), unsubscribe(subscribe_id), get_event_detail(event_id, condition="")

A streaming component is driven with these same methods. Carrying its media is planned.

  • get_profile() returns an EngineProfile with profile and component_profiles: the engine profile and the profile of every component it lists, keyed by fully qualified ref.
  • execute() takes a command_unit_list: commands that run in order, and {"branch_list": [...], "delay_time": ...} groups whose branches run at the same time, each branch in order. The commands need no command_id: the engine assigns one to each, and execute() returns them in order, branch by branch.
  • query() and subscribe() go to the one component that declares the query or event type and matches the condition.

One client is one session. Bindings and subscriptions end with it, and there is no automatic reconnect, because a silent reconnect would hide that loss. After close, connect again and bind and subscribe again.

Conditions

Every condition is a string in the OpenRoIS subset of CQL2-Text: = and LIKE comparisons, joined by AND, over component_ref and component_type. An empty string is no filter. The builders quote values for you:

Builder Writes
component_ref("reachy_real/head") component_ref = 'reachy_real/head'
component_type(RoISIdentifierType(authority="OMG", code="Navigation")) component_type = 'urn:x-rois:def:component:OMG::Navigation'
like(COMPONENT_REF, "reachy_real/%") component_ref LIKE 'reachy_real/%'
all_of(a, b) a AND b

Listening

on(name, listener) adds a listener and off(name, listener) removes it. Listeners of catalog notifications receive the validated params, as models from openrois.interfaces.

Name Listener receives
rois.event.notify_event NotifyEventParams, for every event of every subscription. Filter by subscribe_id or event_type
rois.command.completed CompletedParams, when a command ends
rois.system.notify_error NotifyErrorParams, when the engine reports an error
rois.system.profile_changed ProfileChangedParams, when components join or leave
close (code, reason) when the connection closes, from either side
error An exception, for a message that cannot be used or a listener that raises

A plain function runs inline in the task that reads the socket, in the order messages arrive, so it must not block. A coroutine function runs in a task of its own, so it can await a call on the client:

async def on_person(event: NotifyEventParams) -> None:
    print(await client.get_event_detail(event.event_id))

client.on("rois.event.notify_event", on_person)

A listener that raises, in either form, never stops the client: the exception goes to the error listeners, and the other listeners still run. With no error listener, a fault goes to the openrois.sdk.transport logger at level ERROR. It is never raised.

Closing

async with calls disconnect() when the block ends, and needs no await on connect(). Without it, call disconnect() yourself:

client = await RoISClient.connect("ws://localhost:8765")
await client.search()
await client.disconnect()

Error Handling

from openrois.interfaces import ReturnCode
from openrois.sdk import RoISError

try:
    await client.bind("robot_1/navigation")
except RoISError as error:
    if error.return_code is ReturnCode.OUT_OF_RESOURCES:
        ...  # Another application holds the reservation.
Error Raised when Carries
RoISError A RoIS operation returns a code other than OK return_code, method
TransportError The base of the three below. Raised itself for a call on a closed client
RoISConnectionError The WebSocket cannot be opened, or closes while a request waits. Also a builtin ConnectionError url
RequestTimeoutError No response within the request timeout. Also a builtin TimeoutError method, request_id
RpcError The engine returns a JSON-RPC error: a protocol fault, such as an unknown method code, data
pydantic.ValidationError Params fail validation before they are sent, or a result does not match the catalog

Timeouts

RoISClient.connect() takes the timeouts of the transport, in seconds:

client = await RoISClient.connect(
    "ws://localhost:8765", request_timeout=5.0, connect_timeout=2.0
)
Option Default Meaning
request_timeout 30.0 How long a request waits for its response
connect_timeout 10.0 How long the WebSocket has to open

A call can also be cancelled, with asyncio.timeout or asyncio.wait_for. The client keeps working, and it drops the reply when one arrives. A cancelled connect() ends at once and leaves the connection to close by itself.

The Transport

client.transport is the WebSocketTransport under the client, for JSON-RPC methods outside the catalog. Nothing sent through it is validated.

result = await client.transport.send("vendor.custom.method", {"level": 3})

Its notification event receives each notification as a mapping with jsonrpc, method and, when the message has them, params, in or outside the catalog.

It accepts messages of up to 16 MiB, where the default of the websockets package is 1 MiB, so that a large get_profile does not close the connection. A larger message closes the connection with code 1009, and the close listeners and the failed requests carry that code and its reason.

With websockets 15 or later, the client connects through the proxy that the environment names in variables such as http_proxy and https_proxy, for a local gateway too. Add the gateway to no_proxy to reach it directly. Earlier versions of websockets and the JavaScript client on its default socket ignore these variables.

Modules

Module Contents
openrois.sdk RoISClient, EngineProfile, the errors, and the condition builders
openrois.sdk.transport WebSocketTransport, TransportState and JsonRpcNotification
openrois.sdk.errors The errors

Tested Versions

The wire has no version handshake, so a mismatch shows only when a message carries a field the other side does not know. This release is tested against these versions together, which testing/conformance/frozen.txt pins:

openrois-sdk openrois-gateway openrois-engine openrois-interfaces openrois-components-core
0.1.0a1 0.1.0a1 0.1.0a2 0.1.0a6 0.1.0a1

Development

pip install -e ../../interfaces/python -e ".[dev]"
pytest
mypy
ruff check src/ tests/

tests/test_transport.py runs the client against testing/conformance/scripted_server.py, which it starts by itself, so it needs no stack.

tests/test_conformance.py runs the conformance list of testing/conformance/README.md, one test per scenario. C01 to C17 need a live gateway with the mock adapter behind it, such as the Docker Compose stack at the repository root. They run when OPENROIS_GATEWAY_URL names the gateway, and are skipped otherwise. C18 to C21 and the coverage tests need no stack, so they always run.

docker compose up --build -d   # from the repository root
OPENROIS_GATEWAY_URL=ws://127.0.0.1:8765 pytest

License

Apache-2.0. See LICENSE.

Metadata

Release files for openrois-sdk 0.1.0a1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for openrois-sdk 0.1.0a1
File Size Uploaded Provenance Actions
openrois_sdk-0.1.0a1.tar.gz 40.8 kB With provenance Details for openrois_sdk-0.1.0a1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for openrois-sdk 0.1.0a1
File Interpreter ABI Platform Actions
openrois_sdk-0.1.0a1-py3-none-any.whl Python 3 none any Details for openrois_sdk-0.1.0a1-py3-none-any.whl

Total release size: 66.3 kB

Release files / openrois_sdk-0.1.0a1.tar.gz

Download URL openrois_sdk-0.1.0a1.tar.gz
Size 40.8 kB
Tags Source
SHA-256 checksum
How to use checksums
6bae8f885edf07f64009cdb5dc93355ed146f486e5ed9207ce0bf03acd1bd876
BLAKE2b-256 checksum
How to use checksums
f88298e1eb53a2697d67552c73449901c85a7d2cf2ab99ff708b0fdbc4479070
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.

Transparency log

Release files / openrois_sdk-0.1.0a1-py3-none-any.whl

Download URL openrois_sdk-0.1.0a1-py3-none-any.whl
Size 25.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
13d5455071a07e8b423e705fb6f51a4205e4bae24ffcc385552069025f239b23
BLAKE2b-256 checksum
How to use checksums
309f4cbf6945825995a67e05a59ac92947d6542076d9c558774fe4ac25ce62c1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.

Transparency log

Security for openrois-sdk 0.1.0a1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for openrois_sdk-0.1.0a1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page