Trouble sending back received objects

Hi All!

I’m working on a script that pulls down the latest commit on a stream, decomposes a Collection into its nested Base objects, does some transformation and sends all objects to a different branch.

At the moment I am having no trouble pulling the objects within a dummy file down, traversing the nested objects and appending all into an array.

However, if I try to immediately send the exact same objects straight back again, some objects fail with the following error:

C:\Users\...\AppData\Local\Programs\Python\Python39\lib\site-packages\specklepy\objects\units.py:34: SpeckleWarning: Invalid units: expected type str but received <class 'NoneType'> (None). Skipping - no units will be set.
  warn(
...

...
File "C:\Users\...\AppData\Local\Programs\Python\Python39\lib\site-packages\specklepy\objects\other.py", line 73, in <listcomp>
    return [self._value[i] for i in (3, 7, 11, 15)]
TypeError: 'NoneType' object is not subscriptable

The stream & objects in question can be found here: Speckle

Python receive code:


def pub_get_latest_objs(host_server: str, access_token: str, stream_id: str, branch_name: str) -> object:

    """gets referencedObject from latest commit"""

    client = SpeckleClient(host_server)
    client.authenticate_with_token(access_token)
    branch = client.branch.get(stream_id, branch_name)
    latest_commit_obj = branch.commits.items[0]

    target_object_id = latest_commit_obj.referencedObject
    transport = ServerTransport(client=client, stream_id=stream_id)
    received = operations.receive(
        obj_id=target_object_id, remote_transport=transport)

    return received

def pub_get_all_objects(commit_collection: object) -> list:
    """Returns a list of Base objects from a Commit referencedObject"""

    all_objects = []
    
    try:
        for collection in commit_collection.elements:
            for element in collection.elements:
                all_objects.append(element)
                if element.elements:
                    all_objects.extend(element.elements)
    except:
        print("No elements found, aborting process.")
    return all_objects

Python send code:

def pub_send_objs_to_branch(host_server: str, access_token: str,  stream_id: str, target_branch_name: str, source_objects: list):

    # authenticate
    client = SpeckleClient(host_server)
    client.authenticate_with_token(access_token)

    # get or create branch
    branches = client.branch.list(stream_id)
    has_res_branch = any(b.name == target_branch_name for b in branches)
    if not has_res_branch:
        client.branch.create(
            stream_id, name=target_branch_name, description="New Branch"
        )

    payload_objects = source_objects

    # append source objects to new Base
    payload_object = Base()
    payload_object["elements"] = payload_objects

    # send
    transport = ServerTransport(client=client, stream_id=stream_id)
    payload_object_id = operations.send(payload_object, [transport])

    commit_id = client.commit.create(
        stream_id,
        payload_object_id,
        target_branch_name,
        message="Commit",
    )

    return commit_id

Any help would be greatly appreciated!

2 Likes

Hi @thmsclrk , try updating specklepy to the latest (2.16.2), works with no issues: Speckle

2 Likes

For enhanced context, here.

As part of release 2.16, one of the initiatives has been a greater scope of supported objects and parameters from the Revit connector. Specifically, we used not to send parameters that were empty or null. We heard from yáll in the community that there are use cases where having all the properties present was desirable, even with empty values.

specklepy has been updated to handle this new era of nulls,. :partying_face::tada::pouring_liquid:

It looks like you have fallen between the stools of an updated Revit connector and an older specklepy.


side note:

my preferred way to handle branch creating and handling pre-existing-ness is:

def new_branch(stream_id: str, branch_name: str) -> str:
    try:
        client.branch.create(stream_id, name=branch_name)
    finally:
        return client.branch.get(stream_id, name=branch_name)

and my super basic configurable flat-list-ifier™:

def should_include(obj: Any) -> bool:
    """Define a logic for what objects to include transformed data set"""
    return any(
        [
            hasattr(obj, "displayValue"),
            hasattr(obj, "speckle_type")
            and obj.speckle_type == "Objects.Organization.Collection",
            hasattr(obj, "displayStyle"),
        ]
    )


def flatten(
    obj: Any,
    visited: Optional[Set[Any]] = None,
    include_func: Callable[[Any], bool] = should_include,
) -> Iterator[Any]:
    """Flattens nested object structures based on given criteria. """
    if visited is None:
        visited = set()

    if obj in visited:
        return

    visited.add(obj)

    if include_func(obj):
        yield obj

    props = obj.__dict__ if hasattr(obj, "__dict__") else {}

    for prop in props:
        value = getattr(obj, prop)

        if value is None:
            continue

        if isinstance(value, Base):
            yield from flatten(value, visited, include_func)

        elif isinstance(value, Mapping):
            for dict_value in value.values():
                if isinstance(dict_value, Base):
                    yield from flatten(dict_value, visited, include_func)

        elif isinstance(value, Iterable):
            for list_value in value:
                if isinstance(list_value, Base):
                    yield from flatten(list_value, visited, include_func)

### NB: This code is simplistic and may not cover how others host individual objects, are instances of others, or have more complex relationships between them, but it is the pattern I start with.

1 Like