Skip to main content

Speckle Interoperability

RevitPy includes a full interop layer for bidirectional synchronisation with Speckle. The revitpy.interop module provides type mapping between RevitPy elements and Speckle objects, push/pull sync operations, property-level diffing, conflict-aware merging, and real-time commit subscriptions over WebSocket.

The Speckle integration is an optional dependency. Install it with:

pip install revitpy[speckle]

You can check at runtime whether the specklepy package is available:

from revitpy.interop import speckle_available

if speckle_available():
    print("specklepy is installed")

Quick Start

For simple one-shot operations, use the convenience functions at module level:

from revitpy.interop import push_to_speckle, pull_from_speckle, sync
from revitpy.interop import SyncMode, SyncDirection, SpeckleConfig

# Push elements to a stream
result = await push_to_speckle(
    elements,
    stream_id="abc123",
    branch="main",
    message="Updated wall layout",
)
print(f"Sent {result.objects_sent} objects, commit {result.commit_id}")

# Pull elements from a stream
elements = await pull_from_speckle(
    stream_id="abc123",
    branch="main",
    commit_id="def456",  # optional; latest commit if omitted
)

# Full bidirectional sync
result = await sync(
    elements,
    stream_id="abc123",
    mode=SyncMode.INCREMENTAL,
    direction=SyncDirection.BIDIRECTIONAL,
)

All three convenience functions accept an optional config parameter of type SpeckleConfig for connecting to a self-hosted server or providing an auth token.

SpeckleConfig

SpeckleConfig controls the connection to a Speckle server.

Field Type Default Description
server_url str "https://app.speckle.systems" Speckle server URL
token str \| None None Personal access token for authentication
default_stream str \| None None Default stream identifier
from revitpy.interop import SpeckleConfig

config = SpeckleConfig(
    server_url="https://speckle.mycompany.com",
    token="your-personal-access-token",
    default_stream="abc123",
)

SpeckleTypeMapper

SpeckleTypeMapper maintains a bidirectional registry of mappings between RevitPy element type names and Speckle object type identifiers. A default set of mappings is pre-loaded at construction time.

Default Mappings

RevitPy Type Speckle Type
WallElement Objects.BuiltElements.Wall:Wall
RoomElement Objects.BuiltElements.Room:Room
DoorElement Objects.BuiltElements.Door:Door
WindowElement Objects.BuiltElements.Window:Window
SlabElement Objects.BuiltElements.Floor:Floor
RoofElement Objects.BuiltElements.Roof:Roof
ColumnElement Objects.BuiltElements.Column:Column
BeamElement Objects.BuiltElements.Beam:Beam
StairElement Objects.BuiltElements.Stair:Stair
RailingElement Objects.BuiltElements.Railing:Railing

Registering Custom Mappings

Use register_mapping to add custom type mappings with an optional property map that controls how attribute names are translated between systems:

from revitpy.interop import SpeckleTypeMapper

mapper = SpeckleTypeMapper()

mapper.register_mapping(
    revitpy_type="CurtainWallElement",
    speckle_type="Objects.BuiltElements.CurtainWall:CurtainWall",
    property_map={
        "panel_count": "panelCount",
        "grid_spacing": "gridSpacing",
    },
)

The property_map dict maps RevitPy attribute names (keys) to Speckle property names (values). When no property_map is provided, all public non-callable attributes are copied automatically.

Converting Elements

Convert a RevitPy element to a Speckle-compatible dict with to_speckle, or convert a Speckle object dict back with from_speckle:

# RevitPy element -> Speckle dict
speckle_dict = mapper.to_speckle(wall_element)
# Returns: {"speckle_type": "Objects.BuiltElements.Wall:Wall", "id": ..., "name": ..., ...}

# Speckle dict -> RevitPy-compatible dict
revitpy_dict = mapper.from_speckle(speckle_dict)
# Returns: {"type": "WallElement", "speckle_type": ..., "id": ..., "name": ..., ...}

# Override target type explicitly
revitpy_dict = mapper.from_speckle(speckle_dict, target_type="WallElement")

Both methods raise TypeMappingError when no mapping is found for the element type.

Inspecting the Registry

# List all registered RevitPy types
mapper.registered_types
# ["WallElement", "RoomElement", "DoorElement", ...]

# List all registered Speckle types
mapper.registered_speckle_types
# ["Objects.BuiltElements.Wall:Wall", ...]

# Get a specific mapping
mapping = mapper.get_mapping("WallElement")
# Returns TypeMapping or None

# Get an UNMAPPED placeholder for an unregistered type
placeholder = mapper.get_unmapped_status("CustomElement")
# Returns TypeMapping with status=MappingStatus.UNMAPPED

TypeMapping Dataclass

Field Type Default Description
revitpy_type str RevitPy element type name
speckle_type str Speckle object type identifier
property_map dict[str, str] {} RevitPy-to-Speckle property name map
status MappingStatus MappingStatus.MAPPED Current mapping status

MappingStatus Enum

Value Description
MAPPED Mapping is fully established
UNMAPPED No mapping exists for this type
PARTIAL Some properties are mapped but not all
FAILED Mapping was attempted but failed

SpeckleClient

SpeckleClient is an async HTTP client that communicates with the Speckle server GraphQL API using httpx.AsyncClient.

Connecting

from revitpy.interop import SpeckleClient, SpeckleConfig

config = SpeckleConfig(
    server_url="https://speckle.mycompany.com",
    token="your-token",
)
client = SpeckleClient(config=config)

# Validate the connection (sends a serverInfo query)
await client.connect()
print(client.is_connected)  # True

# Always close when done
await client.close()

Listing Streams and Branches

# Get all visible streams
streams = await client.get_streams()
for s in streams:
    print(s["id"], s["name"], s["description"])

# Get a single stream by ID
stream = await client.get_stream("abc123")

# Get branches for a stream
branches = await client.get_branches("abc123")
for b in branches:
    print(b["name"], b["description"])

Working with Commits

# Get recent commits on a branch
commits = await client.get_commits(
    stream_id="abc123",
    branch="main",
    limit=10,
)
for commit in commits:
    print(commit.id, commit.message, commit.author, commit.total_objects)

Each commit is returned as a SpeckleCommit dataclass:

Field Type Default Description
id str Commit identifier
message str Commit message
author str Author name
created_at str ISO timestamp
source_application str "revitpy" Application that created the commit
total_objects int 0 Number of child objects

Sending and Receiving Objects

# Send objects and create a commit
commit = await client.send_objects(
    stream_id="abc123",
    objects=[{"speckle_type": "...", "id": "1", "name": "Wall-1"}],
    branch="main",
    message="Pushed walls from RevitPy",
)
print(commit.id)

# Receive objects from the latest commit on a branch
objects = await client.receive_objects(
    stream_id="abc123",
    branch="main",
)

# Receive objects from a specific commit
objects = await client.receive_objects(
    stream_id="abc123",
    commit_id="def456",
)

SpeckleSync

SpeckleSync orchestrates push, pull, and bidirectional sync operations. It delegates transport to SpeckleClient and type conversion to SpeckleTypeMapper.

Creating a Sync Instance

from revitpy.interop import SpeckleClient, SpeckleSync, SpeckleTypeMapper

client = SpeckleClient(config=config)
mapper = SpeckleTypeMapper()
syncer = SpeckleSync(client=client, mapper=mapper)

The mapper argument is optional; a default SpeckleTypeMapper is created when omitted. You can also pass a change_tracker for incremental sync support.

Push

Push local elements to a Speckle stream. Each element is converted via the mapper before sending:

result = await syncer.push(
    elements,
    stream_id="abc123",
    branch="main",
    message="Layout update",
)
print(f"Sent: {result.objects_sent}, Errors: {len(result.errors)}")
print(f"Commit: {result.commit_id}, Duration: {result.duration_ms:.0f}ms")

Pull

Pull objects from a stream and map them back to RevitPy-compatible dicts:

elements = await syncer.pull(
    stream_id="abc123",
    branch="main",
    commit_id="def456",  # optional
)
for elem in elements:
    print(elem["type"], elem["name"])

Bidirectional Sync

The sync method combines push and pull in a single operation, controlled by SyncDirection and SyncMode:

from revitpy.interop import SyncMode, SyncDirection

result = await syncer.sync(
    elements,
    stream_id="abc123",
    mode=SyncMode.INCREMENTAL,
    direction=SyncDirection.BIDIRECTIONAL,
)
print(f"Sent: {result.objects_sent}, Received: {result.objects_received}")

SyncDirection Enum

Value Description
PUSH Send local elements to the remote stream only
PULL Receive remote objects only
BIDIRECTIONAL Push then pull in a single operation

SyncMode Enum

Value Description
FULL Synchronise all elements regardless of change state
INCREMENTAL Only synchronise elements marked as changed by the change tracker
SELECTIVE Synchronise a specific subset of elements

SyncResult Dataclass

Field Type Default Description
direction SyncDirection Direction of the sync operation
objects_sent int 0 Number of objects pushed
objects_received int 0 Number of objects pulled
errors list[str] [] Error messages encountered
commit_id str \| None None Commit ID created during push
duration_ms float 0.0 Total operation time in milliseconds

SpeckleDiff

SpeckleDiff compares two lists of element dicts (local vs. remote) and produces a list of DiffEntry records describing additions, removals, and per-property modifications. Elements are matched by their id, element_id, or Id key.

from revitpy.interop import SpeckleDiff

differ = SpeckleDiff()

local = [{"id": "1", "name": "Wall-A", "height": 3.0}]
remote = [{"id": "1", "name": "Wall-A", "height": 4.0}, {"id": "2", "name": "Wall-B"}]

entries = differ.compare(local, remote)
for entry in entries:
    print(entry.element_id, entry.change_type, entry.property_name)
    # "1" "modified" "height" (local_value=3.0, remote_value=4.0)
    # "2" "removed"  None

# Quick boolean check
if differ.has_changes(local, remote):
    print("Models have diverged")

DiffEntry Dataclass

Field Type Default Description
element_id str Element identifier
change_type str One of "added", "removed", or "modified"
property_name str \| None None Property that differs (for "modified" entries)
local_value Any None Value in the local model
remote_value Any None Value in the remote stream

Change types are determined as follows:

  • added – element exists locally but not on the remote stream
  • removed – element exists on the remote stream but not locally
  • modified – element exists in both but one or more properties differ

SpeckleMerge

SpeckleMerge resolves differences between local and remote element sets using a configurable conflict resolution strategy. It uses SpeckleDiff internally.

Merging Element Sets

from revitpy.interop import SpeckleMerge, ConflictResolution

merger = SpeckleMerge(resolution=ConflictResolution.LOCAL_WINS)

result = merger.merge(
    local_elements=local,
    remote_elements=remote,
    diff_entries=None,  # computed automatically when None
)
print(f"Merged: {result.merged_count}, Conflicts: {result.conflict_count}")
print(f"Strategy: {result.resolution.value}")  # "local_wins"

When diff_entries is None, the merge method calls SpeckleDiff.compare internally. You can also pass pre-computed diff entries to avoid redundant computation.

Resolving Conflicts Manually

If the initial merge used LOCAL_WINS or REMOTE_WINS, conflicts are auto-resolved. For finer control, retrieve unresolved conflicts from the result and resolve them explicitly:

resolved = merger.resolve_conflicts(
    conflicts=result.conflicts,
    strategy=ConflictResolution.REMOTE_WINS,
)
for r in resolved:
    print(r["element_id"], r["property_name"], r["resolved_value"])

When ConflictResolution.MANUAL is used as the default strategy, merge() raises MergeConflictError if any conflicts exist, forcing the caller to handle each one.

ConflictResolution Enum

Value Description
LOCAL_WINS Keep local values when properties conflict
REMOTE_WINS Keep remote values when properties conflict
MANUAL Raise MergeConflictError on any conflict; caller must resolve

MergeResult Dataclass

Field Type Default Description
merged_count int 0 Number of entries successfully merged
conflict_count int 0 Number of property-level conflicts detected
conflicts list[DiffEntry] [] Conflict entries (change_type "modified")
resolution ConflictResolution ConflictResolution.LOCAL_WINS Strategy that was applied

SpeckleSubscriptions

SpeckleSubscriptions manages real-time GraphQL subscriptions over WebSocket to receive live commit notifications from Speckle streams.

from revitpy.interop import SpeckleClient, SpeckleSubscriptions

client = SpeckleClient(config=config)
subs = SpeckleSubscriptions(client=client)

# Subscribe to a stream/branch with a callback
async def on_commit(payload):
    print(f"New commit: {payload}")

await subs.subscribe(
    stream_id="abc123",
    branch="main",
    callback=on_commit,
)

# List active subscriptions
print(subs.active_subscriptions)  # ["abc123/main"]

# Unsubscribe from all branches on a stream
await subs.unsubscribe(stream_id="abc123")

# Close all subscriptions
await subs.close()

The subscribe method also accepts an optional event_manager (passed at construction time) for dispatching subscription events through a centralised event system.

Error Handling

All interop errors inherit from InteropError. Specific exception types let you handle different failure modes:

Exception Description
InteropError Base exception for all interop errors
SpeckleConnectionError Server is unreachable or returned an unexpected response
SpeckleSyncError Push or pull operation failed
TypeMappingError No mapping found for an element or Speckle type
MergeConflictError Unresolved conflicts remain when using MANUAL resolution
from revitpy.interop import (
    SpeckleConnectionError,
    SpeckleSyncError,
    TypeMappingError,
    MergeConflictError,
)

try:
    result = await push_to_speckle(elements, stream_id="abc123")
except SpeckleConnectionError as exc:
    print(f"Connection failed: {exc}")
except TypeMappingError as exc:
    print(f"Type not mapped: {exc}")
except SpeckleSyncError as exc:
    print(f"Sync error: {exc}")