From dbf5483cfe2be46a6663a4d63371f74cdc9bc4a9 Mon Sep 17 00:00:00 2001 From: Ross Schlaikjer Date: Wed, 12 Aug 2026 23:35:46 +0900 Subject: [PATCH 1/3] Allow uploading preencoded entries directly --- python/mujinwebstackclient/webstackclient.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/python/mujinwebstackclient/webstackclient.py b/python/mujinwebstackclient/webstackclient.py index 88d197a..78ce824 100644 --- a/python/mujinwebstackclient/webstackclient.py +++ b/python/mujinwebstackclient/webstackclient.py @@ -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 @@ -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))) From 1946427bb3a073fdd3d82a0987bbadaccadef6f7 Mon Sep 17 00:00:00 2001 From: Ross Schlaikjer Date: Wed, 12 Aug 2026 23:35:55 +0900 Subject: [PATCH 2/3] Changelog, version --- CHANGELOG.md | 6 ++++++ python/mujinwebstackclient/version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index def7f7c..fa98e76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/python/mujinwebstackclient/version.py b/python/mujinwebstackclient/version.py index ce4357f..0308f96 100644 --- a/python/mujinwebstackclient/version.py +++ b/python/mujinwebstackclient/version.py @@ -1,3 +1,3 @@ -__version__ = '1.0.0' +__version__ = '1.1.0' # Do not forget to update CHANGELOG.md From 59d9d79d46d07c51d8cd99fa8808b50da87dd3fe Mon Sep 17 00:00:00 2001 From: Ross Schlaikjer Date: Wed, 12 Aug 2026 23:36:01 +0900 Subject: [PATCH 3/3] Add a test --- .../test_webstackclient.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/python/mujintestwebstackclient/test_webstackclient.py b/python/mujintestwebstackclient/test_webstackclient.py index 48fae7b..95f876a 100644 --- a/python/mujintestwebstackclient/test_webstackclient.py +++ b/python/mujintestwebstackclient/test_webstackclient.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import msgspec import pytest import requests_mock import random @@ -7,6 +8,8 @@ import copy import graphql +from unittest import mock + from mujinwebstackclient.webstackclient import WebstackClient from mujinwebstackclient.webstackclientutils import QueryIterator, GetMaximumQueryLimit from mujinwebstackclient.webstackgraphclientutils import GraphQueryIterator @@ -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()