Latest SpecklePy SDK

I’m new to developing inside the “latest” SpecklePy SDK. Do we have any examples that I can reference?

I was poking around and got as far as the following

from specklepy.api.client import SpeckleClient
from specklepy.api import operations
from specklepy.objects.base import Base
from specklepy.transports.server import ServerTransport

def find_mesh_descendants(obj):
    mesh_objects = []
    stack = [obj]
    while stack:
        current = stack.pop()
        # Check if current is a mesh object
        if (
            isinstance(current, Base)
            and hasattr(current, "speckle_type")
            and "OBJECTS.GEOMETRY.MESH" in current.speckle_type.upper()
        ):
            mesh_objects.append(current)
        # Recursively check children
        for attr in dir(current):
            value = getattr(current, attr)
            if isinstance(value, list):
                stack.extend([v for v in value if isinstance(v, Base)])
            elif isinstance(value, Base):
                stack.append(value)
    return mesh_objects


client = SpeckleClient(host="speckle_server_url")
client.authenticate_with_token("speckle_token")

# Get all streams (projects) and get the project id
projects = client.active_user.get_projects()
project_id = next(
    (proj.id for proj in projects.items if proj.name == "John's First Project"),
    None
)
print(f"Selected Project ID: {project_id}")
transport = ServerTransport(client=client, stream_id=project_id)


# Get all models in the project and find the model id
models = client.model.get_models(project_id)
model_id = next(
    (mod.id for mod in models.items if mod.name == "castle"),
    None
)

# Get the model object
the_model = client.model.get(model_id, project_id)

At this point I was stuck as to how to iterate through objects in the model then calling find_mesh_descendants
From me looking around I believe this might be a combination of the model id and transport. Does anyone have an example on how this is done? Thank you for your time

Please see below how I retrieved the parent object id via the version of the model, which has a property reference object. This reference object is the parent object id that allows you iterate through the descendants

from specklepy.api.client import SpeckleClient
from specklepy.api import operations
from specklepy.objects.base import Base
from specklepy.transports.server import ServerTransport

def find_mesh_descendants(obj):
    mesh_objects = []
    stack = [obj]
    while stack:
        current = stack.pop()
        # Check if current is a mesh object
        if (
            isinstance(current, Base)
            and hasattr(current, "speckle_type")
            and "OBJECTS.GEOMETRY.MESH" in current.speckle_type.upper()
        ):
            mesh_objects.append(current)
        # Recursively check children
        for attr in dir(current):
            value = getattr(current, attr)
            if isinstance(value, list):
                stack.extend([v for v in value if isinstance(v, Base)])
            elif isinstance(value, Base):
                stack.append(value)
    return mesh_objects


client = SpeckleClient(host="**my_host**")
client.authenticate_with_token("**my_token**")



# retrieve the project id
projects = client.active_user.get_projects()
project_id = next(
    (proj.id for proj in projects.items if proj.name == "John's First Project"),
    None
)

#display the project id
print(f"Selected Project ID: {project_id}")
transport = ServerTransport(client=client, stream_id=project_id)


# retrieve the model id
models = client.model.get_models(project_id)
model_id = next(
    (mod.id for mod in models.items if mod.name == "castle"),
    None
)
print(f"Selected Model ID: {model_id}")

# get the versions of the model
print("Retrieving model versions...")
myversions = client.model.get_with_versions(
    model_id=model_id,
    project_id=project_id,
    versions_limit=25,
    )


# get the latest version 
print(f"retrieving the latest version of the model {model_id}...")
my_version = max(myversions.versions.items, key=lambda v: v.created_at)

print(f"retrieving the parent object id {my_version.referenced_object} from the version {my_version.id}...")
object_id = my_version.referenced_object

my_object = operations.receive(object_id, transport)


mesh_descendants = find_mesh_descendants(my_object)
for mesh_obj in mesh_descendants:
    print(f"Mesh ID: {mesh_obj.id}, speckle_type: {mesh_obj.speckle_type}")
    print("Vertices:", getattr(mesh_obj, "vertices", None))
    print("Faces:", getattr(mesh_obj, "faces", None))

Hi @shiangoli

Sounds like you’ve got something working now?

Your last screenshot seems about right, you’re traversing each node of the graph and finding meshes.


We do have some tooling in SpecklePy already to help you traverse through the objects. You’ll see connectors like our Blender connector using this…

from specklepy.objects.graph_traversal.default_traversal import create_default_traversal_function

def foo(root_object: Base):
  traversal_function = create_default_traversal_function()

  for traversal_item in traversal_function.traverse(root_object):
    # This is the function that all Speckle Connectors use to convert objects
    # This will iterate through all "geometry" bearking objects
    # Normally, that's immediate children of a `Collection`
    # Only "elements" and "@elements" properties are traversed "deeply"
   
    obj = traversal_item.current #The current base object
    parent = traversal_item.parent # The Parent context
      

However, this does behave different from your function, in won’t return all meshes directly if they’re wrapped in a display_value list of an object e..g DataObject. In those cases, it it will return the DataObjects themselves. Only “loose” meshes that are not in a display_value list will be returned directly, as Meshes.

So if you only care about the raw meshes, then you should probably stick with your function.
If you care about matching the traversal behaviour of Speckle Connectors, and are happy handling both raw Mesh objects, and objects that have a display_value: list[Mesh] property, then the traversal function is avaiable.

@Jedd’s already covered the vital distinction between your custom traversal and the create_default_traversal_function used by the connectors and when you’d pick one over the other.

A few extra notes from my side:

  • In SpecklePy 3.x, “streams” are deprecated. Everything is now projects/models/versions. Where you see stream_id in old examples, that should be your project_id. e.g.

  • To get the latest version, you can call: client.model.get_with_versions(model_id, project_id, versions_limit=1)

    Even if you don’t limit take versions.items[0]. If you fetch more than one, [0] is always the newest, so no need to sort by date.

  • The referenced_object on a version is the root object ID you pass to operations.receive() with your ServerTransport. That’s the missing link between “I have a model” and “I can traverse its contents”.

  • Traversal vs. flat list comes down to use case:

    • Traversal (like Jedd’s example) preserves hierarchy and matches connector behaviour, returning DataObjects if they wrap meshes in displayValue.
    • A flat list approach (like your find_mesh_descendants) is more straightforward when you want all meshes regardless of context.
  • ServerTransport.get_object() isn’t implemented for single objects: operations.receive() will always pull the root and its closure, so any filtering is done in your traversal logic after the fact.

Expect some mixed old/new syntax in the wild for now; updated SpecklePy 3.x dev docs are inbound to make these patterns more straightforward.

Hi @Jedd and @jonathon this is very useful and thank you for sharing. It certainly works and atm I’m getting to grips with the sdk with the view to look at some useful use cases. These use cases will certainly have context in mind and will be experimenting with these in the near future

1 Like