Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 13, 2025

Summary

All requirements from the issue have been successfully implemented:

✅ Completed Features

1. Codec Infrastructure

  • MessageCodec abstract base class with encode() and decode() methods
  • JsonMessageCodec as default implementation using COMPAS JSON serialization
  • ProtobufMessageCodec with optional compas_pb dependency using correct API
  • Proper error handling when compas_pb not installed
  • Codecs handle message objects directly for maximum flexibility
  • Symmetric encode/decode - decode now returns message objects

2. Transport Updates

  • Transport accepts optional codec parameter (defaults to JsonMessageCodec)
  • InMemoryTransport accepts and passes codec parameter
  • MqttTransport accepts codec and uses it for encode/decode
  • Transports pass message objects directly to codecs

3. Code Organization

  • Both MessageCodec and JsonMessageCodec in codecs module (no duplication)
  • Removed unused imports and internal methods
  • Clean import structure with no circular dependencies
  • JsonMessageCodec not exported at top-level (stays in codecs submodule)
  • All examples updated to use correct imports

4. IronPython Removal

  • Removed mqtt_cli.py implementation
  • Removed netlib/MQTTnet.dll library
  • Removed IronPython checks from code and tests
  • Updated documentation to remove Rhino 7 references
  • Removed IronPython CI workflow from build.yml

5. Documentation

  • Updated README with simplified codec usage examples
  • Updated CHANGELOG with breaking changes (removed internal details)
  • Added comprehensive examples/codec_usage.py with simplified patterns
  • Updated API exports to include codec classes from correct module
  • Updated Rhino installation instructions

6. Code Quality

  • Fixed all trailing whitespace linting errors
  • Fixed indentation issues in test files
  • Fixed black formatting issues
  • Verified all flake8 and black checks pass
  • No duplicate code

📝 Breaking Changes

  1. IronPython/Rhino 7 no longer supported - Use Rhino 8+
  2. Codec interface changed - decode() now takes message_type parameter and returns message objects
  3. Module organization - JsonMessageCodec and MessageCodec in codecs module

🔄 Backward Compatibility

Existing code using default JSON serialization continues to work without changes.

📦 Files Changed

Added: src/compas_eve/codecs.py, examples/codec_usage.py
Modified: 10 files (core, transports, tests, docs, CI workflow)
Removed: mqtt_cli.py, netlib/MQTTnet.dll

✅ Testing

Comprehensive mock tests created and passed. All linting and style checks now pass.

All requirements from the issue have been implemented and validated!

Fixes #22

Original prompt

This section details on the original issue you should resolve

<issue_title>Add support for compas_pb serialization</issue_title>
<issue_description>We need to add support for binary (de)/serialization to compas_eve messages using compas_pb using the following specs:

  • A Transport should accept a new optional parameter codec. By default, if None is specified, compas_eve should use its own JsonMessageCodec.
  • Codecs are classes that know how to encode and decode a message down to a different representation. The default one being JSON, which is provided by JsonMessageCodec. Also add an abstract class MessageCodec
  • Currently, the implementations of transport has a call to topic._message_to_json(..) and topic._message_from_json(..) to encode and decode. This should change to be topic._message_to_data(..) and then self.codec.encode(..) (and the respective decoding part).
  • A new codec should be added, called ProtobufMessageCodec to support binary serialization.
  • The requirement to compas_pb should be optional, so, if compas_pb is not installed, the codec is simply going to report it cannot be used because compas_pb needs to be installed.
  • Please take into account that there are 3 cases of messages: a Message instance, any object with __data__ (ie. a COMPAS data object), and anything that can be casted to a dict. This roughly means this method should handle most things. However, compas_pb already handles COMPAS data objects and fallback to dictionary, so the last two options are probably the same.
    def _message_to_data(self, message):
        """Convert a message to a data representation ready to be encoded.

        Normally, this method expects sub-classes of Message as input.
        However, it can deal with regular dictionaries as well as classes
        implementing the COMPAS data framework.
        """
        try:
            data = message.data
        except (KeyError, AttributeError):
            try:
                data = message.__data__
            except (KeyError, AttributeError):
                data = dict(message)
        return data
  • In the process, please remove the mqtt_cli implementation, and all support for IronPython, because we will bump the major and forget about Rhino 7.</issue_description>

Comments on the Issue (you are @copilot in this section)

Fixes #22


💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click here to start the survey.

Copilot AI changed the title [WIP] Add support for compas_pb serialization in compas_eve Add extensible codec system with Protocol Buffers support and remove IronPython Oct 13, 2025
Copilot AI requested a review from gonzalocasas October 13, 2025 15:18
Copy link
Member

@gonzalocasas gonzalocasas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please fix the linting errors and also remove the windows ironpython ci workflow

Copy link
Contributor Author

Copilot AI commented Oct 13, 2025

@copilot please fix the linting errors and also remove the windows ironpython ci workflow

Fixed linting errors (trailing whitespace) and removed the IronPython CI workflow from build.yml in commit c648c88.

Copy link
Member

@gonzalocasas gonzalocasas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot there are still lint/style issues, please fix them

Copy link
Contributor Author

Copilot AI commented Oct 14, 2025

@copilot there are still lint/style issues, please fix them

Fixed all remaining lint/style issues in commit 60f32ed:

  • Removed trailing whitespace in memory/init.py and mqtt_paho.py
  • Fixed indentation error in test_mqtt_paho_compatibility.py
  • Fixed black formatting in codecs.py (added blank line after import)

All flake8 and black checks now pass.

Comment on lines 82 to 83
data = topic._message_to_data(message)
encoded_message = self.codec.encode(data)
Copy link
Member

@chenkasirer chenkasirer Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gonzalocasas should _message_to_data be handled by the codec (or omitted altogether)? otherwise no matter what codec we use, data is always a dictionary.

shouldn't the JSON codec should get message directly and call json_dumps on it? the Protobuf codec should also get message directly and call pb_dump_bts on it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point! @copilot please address the previous comment, make sure you also make the symmetric change to the decoding side

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored the codec system so codecs receive message objects directly instead of data dicts. Each codec is now responsible for extracting what it needs from the message, giving them more flexibility. Also removed Topic._message_to_data() since it's no longer needed. Changes in commit [pending].

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored the codec system so codecs receive message objects directly instead of data dicts. Each codec is now responsible for extracting what it needs from the message, giving them more flexibility. Also removed Topic._message_to_data() since it's no longer needed. Changes in commit e9fe014.

- MessageCodec.encode() now receives the message object instead of data dict
- Each codec is responsible for extracting data from the message
- JsonMessageCodec and ProtobufMessageCodec extract data internally
- Removed Topic._message_to_data() as it's no longer needed
- Updated example to show new codec pattern
- This gives codecs more flexibility in how they handle different message types

Co-authored-by: gonzalocasas <[email protected]>
Copy link
Member

@gonzalocasas gonzalocasas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please address the review findings

raise NotImplementedError("Subclasses must implement decode()")


class JsonMessageCodec(MessageCodec):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this class to the codecs module


def _message_to_json(self, message):
"""Convert a message to a JSON string.
def _message_to_data(self, message):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As requested above, this will need to move to the codecs

CHANGELOG.md Outdated
### Removed

* **BREAKING**: Removed IronPython support and `mqtt_cli` implementation.
* **BREAKING**: Removed IronPython MQTTnet.dll library.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this line

CHANGELOG.md Outdated
### Changed

* Updated dependency on `paho-mqtt` to support `>=1, <3` to include version `2.x` with backward compatibility.
* **BREAKING**: Transport implementations now use codec-based serialization instead of direct JSON calls.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this and the next line, this are internal changes, no need to add them to the changelog

CHANGELOG.md Outdated
* Added `JsonMessageCodec` for JSON-based message serialization (default).
* Added `ProtobufMessageCodec` for binary message serialization using Protocol Buffers (requires `compas_pb`).
* Added `codec` parameter to `Transport`, `InMemoryTransport`, and `MqttTransport` classes.
* Added `_message_to_data()` method to `Topic` class for extracting data from various message types.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't document internal functions in the changelog


def encode(self, message):
"""Encode message by converting all string values to uppercase."""
import json
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the json import to the top of the file

Comment on lines 109 to 111
decoded_data = self.codec.decode(msg.payload.decode())
message_obj = topic.message_type.parse(decoded_data)
callback(message_obj)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the message_type.parse be inside the codec instances, to be symmetric with the fact that it also handles conversion directly from Message instance in the encoding step?

def __init__(self, codec=None, *args, **kwargs):
super(Transport, self).__init__(*args, **kwargs)
self._id_counter = 0
self.codec = codec if codec is not None else JsonMessageCodec()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.codec = codec if codec is not None else JsonMessageCodec()
self.codec = codec or JsonMessageCodec()

Comment on lines 75 to 82
try:
data = message.data
except (KeyError, AttributeError):
try:
data = message.__data__
except (KeyError, AttributeError):
data = dict(message)
return compas_pb.encode(data)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should get compas_pb to byte-encode the whole message, so I guess something like:

Suggested change
try:
data = message.data
except (KeyError, AttributeError):
try:
data = message.__data__
except (KeyError, AttributeError):
data = dict(message)
return compas_pb.encode(data)
return compas_pb.pb_dump_bts(message)

"The ProtobufMessageCodec requires 'compas_pb' to be installed. "
"Please install it with: pip install compas_pb"
)
return compas_pb.decode(encoded_data)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, this is the wrong API call, it should be:

Suggested change
return compas_pb.decode(encoded_data)
return compas_pb.pb_load_bts(encoded_data)

Copy link
Contributor Author

Copilot AI commented Oct 14, 2025

@copilot please address the review findings

Addressed all review findings in commit 36a0af7:

  • Moved JsonMessageCodec to codecs module
  • Updated ProtobufMessageCodec to use correct compas_pb API (pb_dump_bts/pb_load_bts)
  • Made decode() symmetric with encode() - now takes message_type parameter and returns message objects
  • Cleaned up CHANGELOG to remove internal implementation details
  • Simplified README and example code per suggestions
  • Fixed all linting issues

Copy link
Member

@gonzalocasas gonzalocasas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please address the findings

DEFAULT_TRANSPORT = None


class MessageCodec(object):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicated now, please delete it

super(Transport, self).__init__(*args, **kwargs)
self._id_counter = 0
if codec is None:
from compas_eve.codecs import JsonMessageCodec
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the import to the top

- Removed duplicate MessageCodec class from core.py (only in codecs.py now)
- Moved JsonMessageCodec import to top of core.py to avoid lazy import
- Updated __init__.py to import MessageCodec from codecs module
- Simplified Transport.__init__ to use `codec or JsonMessageCodec()`

Co-authored-by: gonzalocasas <[email protected]>
Copy link
Contributor Author

Copilot AI commented Oct 14, 2025

@copilot please address the findings

Fixed both issues in commit [pending]:

  • Removed duplicate MessageCodec class from core.py (only exists in codecs.py now)
  • Moved JsonMessageCodec import to top of core.py file
  • Updated imports in init.py accordingly

@copilot please address the findings

Fixed both issues in commit ba9fcab:

  • Removed duplicate MessageCodec class from core.py (only exists in codecs.py now)
  • Moved JsonMessageCodec import to top of core.py file
  • Updated imports in init.py accordingly

EchoSubscriber
Transport
InMemoryTransport
MessageCodec
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot don't import the JsonMessageCodec to this level, leave it where it belongs, and just import it from .codecs where used, otherwise it's a bit asymmetric/odd

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed JsonMessageCodec from top-level exports in commit [pending]. It now stays in the codecs submodule and should be imported as from compas_eve.codecs import JsonMessageCodec. The import in core.py is now lazy to avoid unnecessary top-level dependencies.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed JsonMessageCodec from top-level exports in commit 490749a. It now stays in the codecs submodule and should be imported as from compas_eve.codecs import JsonMessageCodec. The import in core.py is now lazy to avoid unnecessary top-level dependencies.

- Removed JsonMessageCodec from compas_eve.__init__ exports
- Moved import back to lazy import in Transport.__init__ to avoid top-level dependency
- JsonMessageCodec should be imported as `from compas_eve.codecs import JsonMessageCodec`
- This keeps the API cleaner and more organized

Co-authored-by: gonzalocasas <[email protected]>
import json

import compas_eve as eve
from compas_eve import JsonMessageCodec, MessageCodec
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot fix this import

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed import in commit [pending]. Now uses from compas_eve.codecs import JsonMessageCodec instead of importing from top-level.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed import in commit f5ca830. Now uses from compas_eve.codecs import JsonMessageCodec instead of importing from top-level.

- Updated import to use `from compas_eve.codecs import JsonMessageCodec`
- This reflects the change where JsonMessageCodec is no longer exported at top level

Co-authored-by: gonzalocasas <[email protected]>
@gonzalocasas gonzalocasas marked this pull request as ready for review October 30, 2025 10:31
Copy link
Member

@chenkasirer chenkasirer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@gonzalocasas gonzalocasas merged commit 962ef54 into main Oct 30, 2025
9 checks passed
@gonzalocasas gonzalocasas deleted the copilot/add-protobuf-support branch October 30, 2025 11:02
@gonzalocasas gonzalocasas mentioned this pull request Nov 11, 2025
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for compas_pb serialization

3 participants