Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.1.0 (2026-08-12)

- `CreateLogEntries` now accepts a payload that is already encoded as JSON `bytes` and sends those bytes
as the request body unchanged. Callers that serialize an entry for their own reasons, such as validating
it before upload, can pass the result through instead of encoding the same payload twice.

## 1.0.0 (2026-07-31)

- Use `msgspec` for all JSON encoding and decoding, with a `ujson` fallback for types `msgspec` cannot serialize natively (such as `numpy` scalars and arrays).
Expand Down
32 changes: 32 additions & 0 deletions python/mujintestwebstackclient/test_webstackclient.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# -*- coding: utf-8 -*-

import msgspec
import pytest
import requests_mock
import random
import sys
import copy
import graphql

from unittest import mock

from mujinwebstackclient.webstackclient import WebstackClient
from mujinwebstackclient.webstackclientutils import QueryIterator, GetMaximumQueryLimit
from mujinwebstackclient.webstackgraphclientutils import GraphQueryIterator
Expand Down Expand Up @@ -489,3 +492,32 @@ def test_LazyQueryStandardListOperations():
del scenes[start:end]
del expectedScenes[start:end]
assert scenes == expectedScenes


def test_CreateLogEntriesAcceptsPreEncodedPayloads():
"""A payload handed over as JSON bytes must reach the request body unchanged.

Callers that already serialized an entry, such as to validate it before uploading, rely on this to avoid
encoding a second copy of every payload they send.
"""
webstackclient = WebstackClient.__new__(WebstackClient)
webstackclient._webclient = mock.Mock()
webstackclient._webclient.EncodeJSON.side_effect = msgspec.json.Encoder().encode

logEntry = {
'occurredAt': '2026-08-12T00:00:00Z',
'version': 1,
'soukoExecutionTask': {'taskId': 'aeon_cycleCount:C6E3KZP5K1LQMQL5', 'taskType': 'aeon_cycleCount'},
}

webstackclient.CreateLogEntries(logEntries=[('SoukoExecutionTask', logEntry, {})])
filesFromDict = webstackclient._webclient.APICall.call_args.kwargs['files']

webstackclient._webclient.reset_mock()
webstackclient.CreateLogEntries(logEntries=[('SoukoExecutionTask', msgspec.json.encode(logEntry), {})])
filesFromBytes = webstackclient._webclient.APICall.call_args.kwargs['files']

# Both forms put the same bytes on the wire, so a caller can switch to the encoded form freely.
assert filesFromBytes == filesFromDict
# The encoded payload went out as given rather than being run through the encoder again.
webstackclient._webclient.EncodeJSON.assert_not_called()
2 changes: 1 addition & 1 deletion python/mujinwebstackclient/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = '1.0.0'
__version__ = '1.1.0'

# Do not forget to update CHANGELOG.md
13 changes: 10 additions & 3 deletions python/mujinwebstackclient/webstackclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from email.utils import parsedate

import six
from typing import List, Tuple, Any, Dict # noqa: F401
from typing import List, Tuple, Any, Dict, Union # noqa: F401

# Mujin imports
from . import WebstackClientError
Expand Down Expand Up @@ -675,10 +675,17 @@ def DeleteJobs(self, timeout=5):
#

def CreateLogEntries(self, logEntries, timeout=5):
# type: (List[Tuple[str, Any, Dict[str, bytes]]], float) -> Any
# type: (List[Tuple[str, Union[bytes, Any], Dict[str, bytes]]], float) -> Any
"""Uploads log entries given as (log type, payload, attachments) tuples.

A payload may be passed as the JSON bytes of an already-encoded entry.
Callers may have already serialized it to e.g. ensure that it serializes correctly,
and if they have, it makes sense to re-use that already serialized data.
"""
files = []
for logType, logEntry, attachments in logEntries:
files.append(('logEntry/%s' % logType, ('', self._webclient.EncodeJSON(logEntry), 'application/json')))
body = logEntry if isinstance(logEntry, bytes) else self._webclient.EncodeJSON(logEntry)
files.append(('logEntry/%s' % logType, ('', body, 'application/json')))
if attachments is not None:
for attachmentName, attachmentData in six.iteritems(attachments):
files.append(('attachment', (attachmentName, attachmentData)))
Expand Down
Loading