compas-eve 2.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. compas_eve/__init__.py +34 -0
  2. compas_eve/__main__.py +4 -0
  3. compas_eve/codecs/__init__.py +166 -0
  4. compas_eve/codecs/conversions.py +23 -0
  5. compas_eve/core.py +270 -0
  6. compas_eve/event_emitter.py +253 -0
  7. compas_eve/ghpython/__init__.py +61 -0
  8. compas_eve/ghpython/background.py +276 -0
  9. compas_eve/ghpython/components/Ce_BackgroundTask/code.py +64 -0
  10. compas_eve/ghpython/components/Ce_BackgroundTask/icon.png +0 -0
  11. compas_eve/ghpython/components/Ce_BackgroundTask/metadata.json +35 -0
  12. compas_eve/ghpython/components/Ce_Message/code.py +62 -0
  13. compas_eve/ghpython/components/Ce_Message/icon.png +0 -0
  14. compas_eve/ghpython/components/Ce_Message/metadata.json +25 -0
  15. compas_eve/ghpython/components/Ce_MqttConnect/code.py +39 -0
  16. compas_eve/ghpython/components/Ce_MqttConnect/icon.png +0 -0
  17. compas_eve/ghpython/components/Ce_MqttConnect/metadata.json +40 -0
  18. compas_eve/ghpython/components/Ce_Publish/code.py +44 -0
  19. compas_eve/ghpython/components/Ce_Publish/icon.png +0 -0
  20. compas_eve/ghpython/components/Ce_Publish/metadata.json +40 -0
  21. compas_eve/ghpython/components/Ce_Subscribe/code.py +65 -0
  22. compas_eve/ghpython/components/Ce_Subscribe/icon.png +0 -0
  23. compas_eve/ghpython/components/Ce_Subscribe/metadata.json +41 -0
  24. compas_eve/ghpython/components/Ce_ZenohConnect/code.py +37 -0
  25. compas_eve/ghpython/components/Ce_ZenohConnect/icon.png +0 -0
  26. compas_eve/ghpython/components/Ce_ZenohConnect/metadata.json +30 -0
  27. compas_eve/ghpython/components/___init__.py +0 -0
  28. compas_eve/ghpython/yak_template/icon.png +0 -0
  29. compas_eve/ghpython/yak_template/manifest.yml +27 -0
  30. compas_eve/memory/__init__.py +129 -0
  31. compas_eve/mqtt/__init__.py +3 -0
  32. compas_eve/mqtt/mqtt_paho.py +222 -0
  33. compas_eve/proto/message.proto +9 -0
  34. compas_eve/proto/message_pb2.py +41 -0
  35. compas_eve/ros/__init__.py +3 -0
  36. compas_eve/ros/ros_transport.py +184 -0
  37. compas_eve/zenoh/__init__.py +3 -0
  38. compas_eve/zenoh/zenoh_transport.py +184 -0
  39. compas_eve-2.4.0.dist-info/METADATA +212 -0
  40. compas_eve-2.4.0.dist-info/RECORD +44 -0
  41. compas_eve-2.4.0.dist-info/WHEEL +5 -0
  42. compas_eve-2.4.0.dist-info/entry_points.txt +2 -0
  43. compas_eve-2.4.0.dist-info/licenses/LICENSE +21 -0
  44. compas_eve-2.4.0.dist-info/top_level.txt +1 -0
compas_eve/__init__.py ADDED
@@ -0,0 +1,34 @@
1
+ __author__ = ["Gonzalo Casas"]
2
+ __copyright__ = "COMPAS Association"
3
+ __license__ = "MIT License"
4
+ __email__ = "casas@arch.ethz.ch"
5
+ __version__ = "2.4.0"
6
+
7
+ from .event_emitter import EventEmitterMixin # noqa: F401 needed here to avoid a circular import
8
+ from .core import (
9
+ Message,
10
+ Publisher,
11
+ Subscriber,
12
+ EchoSubscriber,
13
+ Transport,
14
+ Topic,
15
+ get_default_transport,
16
+ set_default_transport,
17
+ )
18
+ from .codecs import MessageCodec
19
+ from .memory import InMemoryTransport
20
+
21
+ set_default_transport(InMemoryTransport())
22
+
23
+ __all__ = [
24
+ "Message",
25
+ "Publisher",
26
+ "Subscriber",
27
+ "EchoSubscriber",
28
+ "Topic",
29
+ "Transport",
30
+ "MessageCodec",
31
+ "get_default_transport",
32
+ "set_default_transport",
33
+ "InMemoryTransport",
34
+ ]
compas_eve/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ import compas_eve
2
+
3
+ if __name__ == "__main__":
4
+ print("COMPAS EVE v{} is installed!".format(compas_eve.__version__))
@@ -0,0 +1,166 @@
1
+ from typing import Any
2
+ from typing import Optional
3
+ from typing import Union
4
+
5
+ from compas.data import json_dumps
6
+ from compas.data import json_loads
7
+
8
+ from compas_eve.core import Message
9
+
10
+ try:
11
+ import compas_pb
12
+
13
+ COMPAS_PB_AVAILABLE = True
14
+ except ImportError:
15
+ COMPAS_PB_AVAILABLE = False
16
+
17
+
18
+ __all__ = ["MessageCodec", "JsonMessageCodec", "ProtobufMessageCodec"]
19
+
20
+
21
+ class MessageCodec(object):
22
+ """Abstract base class for message codecs.
23
+
24
+ A codec is responsible for encoding and decoding messages
25
+ to/from a specific representation format (e.g., JSON, Protocol Buffers).
26
+ """
27
+
28
+ def encode(self, message: Union[Message, dict, Any]) -> Union[bytes, str]:
29
+ """Encode a message to the codec's representation format.
30
+
31
+ Parameters
32
+ ----------
33
+ message
34
+ Message to encode. Can be a Message instance, a dict, or
35
+ an object implementing the COMPAS data framework.
36
+
37
+ Returns
38
+ -------
39
+ bytes or str
40
+ Encoded representation of the message.
41
+ """
42
+ raise NotImplementedError("Subclasses must implement encode()")
43
+
44
+ def decode(self, encoded_data: bytes) -> Union[Message, dict, Any]:
45
+ """Decode data from the codec's representation format.
46
+
47
+ Parameters
48
+ ----------
49
+ encoded_data
50
+ Encoded data to decode.
51
+
52
+ Returns
53
+ -------
54
+ Union[Message, dict, Any]
55
+ Decoded message after reconstruction from the encoded data.
56
+ """
57
+ raise NotImplementedError("Subclasses must implement decode()")
58
+
59
+
60
+ class JsonMessageCodec(MessageCodec):
61
+ """JSON codec for message serialization.
62
+
63
+ This codec uses the COMPAS framework's JSON serialization functions
64
+ to encode and decode message data. It can handle Message objects,
65
+ COMPAS Data objects, and regular dictionaries.
66
+ """
67
+
68
+ def encode(self, message: Union[Message, dict, Any]) -> str:
69
+ """Encode a message to JSON string.
70
+
71
+ Parameters
72
+ ----------
73
+ message
74
+ Message to encode. Can be a Message instance, a dict, or
75
+ an object implementing the COMPAS data framework.
76
+
77
+ Returns
78
+ -------
79
+ str
80
+ JSON string representation of the message.
81
+ """
82
+ # Extract data from the message
83
+ try:
84
+ return json_dumps(message.data)
85
+ except (KeyError, AttributeError):
86
+ try:
87
+ return json_dumps(message)
88
+ except (KeyError, AttributeError):
89
+ return json_dumps(dict(message))
90
+
91
+ def decode(self, encoded_data: bytes, message_type: type) -> Message:
92
+ """Decode JSON message payloads to message object.
93
+
94
+ Parameters
95
+ ----------
96
+ encoded_data
97
+ Message bytes to decode into a JSON string.
98
+ message_type
99
+ The message type class to use for parsing.
100
+
101
+ Returns
102
+ -------
103
+ Message
104
+ Decoded message object.
105
+ """
106
+ data = json_loads(encoded_data.decode())
107
+ if hasattr(data, "__data__"):
108
+ return data
109
+ else:
110
+ return message_type.parse(data)
111
+
112
+
113
+ class ProtobufMessageCodec(MessageCodec):
114
+ """Protocol Buffers codec for message serialization.
115
+
116
+ This codec uses the compas_pb package to encode and decode message data
117
+ using Protocol Buffers binary format.
118
+
119
+ Note
120
+ ----
121
+ This codec requires the `compas_pb` package to be installed.
122
+ If `compas_pb` is not available, attempting to encode or decode
123
+ will raise an [ImportError][].
124
+ """
125
+
126
+ def __init__(self):
127
+ super(ProtobufMessageCodec, self).__init__()
128
+ if not COMPAS_PB_AVAILABLE:
129
+ raise ImportError("The ProtobufMessageCodec requires 'compas_pb' to be installed. Please install it with: pip install compas_pb")
130
+
131
+ def encode(self, message: Union[Message, dict, Any]) -> bytes:
132
+ """Encode a message to Protocol Buffers binary format.
133
+
134
+ Parameters
135
+ ----------
136
+ message
137
+ Message to encode. Can be a Message instance, a dict, or
138
+ an object implementing the COMPAS data framework.
139
+
140
+ Returns
141
+ -------
142
+ bytes
143
+ Protocol Buffers binary representation of the message.
144
+ """
145
+ if not COMPAS_PB_AVAILABLE:
146
+ raise ImportError("The ProtobufMessageCodec requires 'compas_pb' to be installed. Please install it with: pip install compas_pb")
147
+ return compas_pb.pb_dump_bts(message)
148
+
149
+ def decode(self, encoded_data: bytes, message_type: Optional[type] = None) -> object:
150
+ """Decode Protocol Buffers binary data to message object.
151
+
152
+ Parameters
153
+ ----------
154
+ encoded_data
155
+ Protocol Buffers binary data to decode.
156
+ message_type
157
+ The message type class (not used for protobuf as it's encoded in the data).
158
+
159
+ Returns
160
+ -------
161
+ object
162
+ Decoded message object.
163
+ """
164
+ if not COMPAS_PB_AVAILABLE:
165
+ raise ImportError("The ProtobufMessageCodec requires 'compas_pb' to be installed. Please install it with: pip install compas_pb")
166
+ return compas_pb.pb_load_bts(encoded_data)
@@ -0,0 +1,23 @@
1
+ from compas_pb.core import _deserialize_any
2
+ from compas_pb.core import _serializer_any
3
+ from compas_pb.registry import pb_deserializer
4
+ from compas_pb.registry import pb_serializer
5
+
6
+ from compas_eve import Message
7
+ from compas_eve.proto import message_pb2
8
+
9
+
10
+ @pb_serializer(Message)
11
+ def message_to_pb(message: Message) -> message_pb2.Message:
12
+ pb = message_pb2.Message()
13
+ for k, v in message.data.items():
14
+ pb.data[k].CopyFrom(_serializer_any(v))
15
+ return pb
16
+
17
+
18
+ @pb_deserializer(message_pb2.Message)
19
+ def message_from_pb(pb: message_pb2.Message) -> Message:
20
+ message = Message()
21
+ for k, v in pb.data.items():
22
+ message[k] = _deserialize_any(v)
23
+ return message
compas_eve/core.py ADDED
@@ -0,0 +1,270 @@
1
+ from typing import Any
2
+ from typing import Callable
3
+ from typing import Dict
4
+ from typing import Optional
5
+ from typing import Type
6
+ from typing import Union
7
+
8
+ DEFAULT_TRANSPORT = None
9
+
10
+
11
+ def get_default_transport() -> Optional["Transport"]:
12
+ """Retrieve the default transport implementation to be used system-wide.
13
+
14
+ Returns
15
+ -------
16
+ Transport
17
+ Instance of a transport class. By default, `compas_eve` uses
18
+ [InMemoryTransport][compas_eve.memory.InMemoryTransport].
19
+ """
20
+ return DEFAULT_TRANSPORT
21
+
22
+
23
+ def set_default_transport(transport: Optional["Transport"]) -> None:
24
+ """Assign a default transport implementation to be used system-wide.
25
+
26
+ Parameters
27
+ ----------
28
+ transport
29
+ Instance of a transport class. By default, `compas_eve` uses
30
+ [InMemoryTransport][compas_eve.memory.InMemoryTransport].
31
+ """
32
+ global DEFAULT_TRANSPORT
33
+ DEFAULT_TRANSPORT = transport
34
+
35
+
36
+ class Transport(object):
37
+ """Defines the base interface for different transport implementations.
38
+
39
+ Parameters
40
+ ----------
41
+ codec
42
+ The codec to use for encoding and decoding messages.
43
+ If not provided, defaults to [JsonMessageCodec][compas_eve.codecs.JsonMessageCodec].
44
+ """
45
+
46
+ def __init__(self, codec: Optional[Any] = None, *args: Any, **kwargs: Any) -> None:
47
+ super(Transport, self).__init__(*args, **kwargs)
48
+ from compas_eve.codecs import JsonMessageCodec
49
+
50
+ self._id_counter = 0
51
+ if codec is None:
52
+ codec = JsonMessageCodec()
53
+ self.codec = codec
54
+
55
+ @property
56
+ def id_counter(self) -> int:
57
+ """Generate an auto-incremental ID starting from 1."""
58
+ self._id_counter += 1
59
+ return self._id_counter
60
+
61
+ def publish(self, topic: "Topic", message: Union["Message", dict], **options: Any) -> None:
62
+ pass
63
+
64
+ def subscribe(self, topic: "Topic", callback: Callable) -> Optional[str]:
65
+ pass
66
+
67
+ def unsubscribe(self, topic: "Topic") -> None:
68
+ pass
69
+
70
+ def advertise(self, topic: "Topic") -> Optional[str]:
71
+ pass
72
+
73
+ def unadvertise(self, topic: "Topic") -> None:
74
+ pass
75
+
76
+
77
+ class Message(object):
78
+ """Message objects used for publishing and subscribing to/from topics.
79
+
80
+ A message is fundamentally a dictionary and behaves as one."""
81
+
82
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
83
+ super(Message, self).__init__()
84
+ self.data = {}
85
+ self.data.update(*args, **kwargs)
86
+
87
+ def ToString(self) -> str:
88
+ return str(self)
89
+
90
+ def __str__(self) -> str:
91
+ return str(self.data)
92
+
93
+ def __getattr__(self, name: str) -> Any:
94
+ return self.data[name]
95
+
96
+ def __setattr__(self, key: str, value: Any) -> None:
97
+ if key == "data" or key in self.__dict__:
98
+ super(Message, self).__setattr__(key, value)
99
+ else:
100
+ self.data[key] = value
101
+
102
+ def __getitem__(self, key: str) -> Any:
103
+ return self.data[key]
104
+
105
+ def __setitem__(self, key: str, value: Any) -> None:
106
+ self.data[key] = value
107
+
108
+ def __jsondump__(self, minimal: bool = False) -> Dict[str, Any]:
109
+ return self.data
110
+
111
+ @classmethod
112
+ def parse(cls, value: Dict[str, Any]) -> "Message":
113
+ instance = cls(**value)
114
+ return instance
115
+
116
+
117
+ class Topic(object):
118
+ """A topic is like a mailbox where messages can be sent and received.
119
+
120
+ Topics are described by a name, a type of message they accept, and a
121
+ set of options.
122
+
123
+ Attributes
124
+ ----------
125
+ name
126
+ Name of the topic.
127
+ message_type
128
+ Class defining the message structure, or a backend-native message type
129
+ identifier. Use [Message][] for a generic, non-type-checked message
130
+ implementation. Defaults to [Message][].
131
+ options
132
+ A dictionary of options.
133
+ """
134
+
135
+ # TODO: Add documentation/examples of possible options
136
+
137
+ def __init__(self, name: str, message_type: Optional[Union[Type[Message], str]] = None, **options: Any) -> None:
138
+ self.name = name
139
+ self.message_type = message_type or Message
140
+ self.options = options
141
+
142
+
143
+ class Publisher(object):
144
+ """Publisher for a specific topic.
145
+
146
+ Parameters
147
+ ----------
148
+ topic
149
+ The topic to publish messages to. If a string is provided, a new topic instance
150
+ will be created using the string as topic name.
151
+ transport
152
+ The transport to use for publishing. If not provided, the default transport will be used.
153
+ """
154
+
155
+ def __init__(self, topic: Union[Topic, str], transport: Optional[Transport] = None) -> None:
156
+ self.topic = topic if isinstance(topic, Topic) else Topic(topic)
157
+ self.transport = transport or get_default_transport()
158
+ self._advertise_id = None
159
+
160
+ @property
161
+ def is_advertised(self) -> bool:
162
+ """Indicate if the publisher has announced its topic as advertised or not.
163
+
164
+ Returns
165
+ -------
166
+ bool
167
+ True if advertised as publisher of this topic, False otherwise.
168
+ """
169
+ return self._advertise_id is not None
170
+
171
+ def message_published(self, message: Union[Message, dict]) -> None:
172
+ """Handler called when a message has been published."""
173
+ pass
174
+
175
+ def publish(self, message: Union[Message, dict], **options: Any) -> None:
176
+ """Publish a message to the topic.
177
+
178
+ Parameters
179
+ ----------
180
+ message
181
+ The message to publish.
182
+ **options
183
+ Transport-specific options passed through to the underlying transport.
184
+ For example, ``retain=True`` on MQTT and InMemory transports.
185
+ """
186
+ # TODO: check if message type matches self.topic.message_type declared
187
+ if not self.is_advertised:
188
+ self.advertise()
189
+
190
+ self.transport.publish(self.topic, message, **options)
191
+ self.message_published(message)
192
+
193
+ def advertise(self) -> None:
194
+ """Advertise the publisher for the topic."""
195
+ if self.is_advertised:
196
+ return
197
+
198
+ self._advertise_id = self.transport.advertise(self.topic)
199
+
200
+ def unadvertise(self) -> None:
201
+ """Unadvertise the publisher for the topic."""
202
+ if not self.is_advertised:
203
+ return
204
+
205
+ self.transport.unadvertise(self.topic)
206
+ self._advertise_id = None
207
+
208
+
209
+ class Subscriber(object):
210
+ """Subscriber for a specific topic.
211
+
212
+ Parameters
213
+ ----------
214
+ topic
215
+ The topic to subscribe to. If a string is provided, a new topic instance
216
+ will be created using the string as topic name.
217
+ transport
218
+ The transport to use for subscribing. If not provided, the default transport will be used.
219
+ """
220
+
221
+ def __init__(self, topic: Union[Topic, str], callback: Optional[Callable] = None, transport: Optional[Transport] = None) -> None:
222
+ self.transport = transport or get_default_transport()
223
+ self.topic = topic if isinstance(topic, Topic) else Topic(topic)
224
+ self._subscribe_id = None
225
+ self._callback = callback
226
+
227
+ def message_received(self, message: Union[Message, dict]) -> None:
228
+ """Handler called whenever a new message is received.
229
+
230
+ By default, this implementation will simply invoke the callback
231
+ used on init (if any), but sub-classes can override this behavior."""
232
+ if self._callback:
233
+ self._callback(message)
234
+
235
+ @property
236
+ def is_subscribed(self) -> bool:
237
+ """Indicate if the instace is currently subscribed to its topic or not."""
238
+ return self._subscribe_id is not None
239
+
240
+ def subscribe(self) -> None:
241
+ if self._subscribe_id:
242
+ return
243
+
244
+ self._subscribe_id = self.transport.subscribe(self.topic, self.message_received)
245
+
246
+ def unsubscribe(self) -> None:
247
+ """Unregister the subscriber from its topic."""
248
+ if not self._subscribe_id:
249
+ return
250
+
251
+ self.transport.unsubscribe_by_id(self._subscribe_id)
252
+ self._subscribe_id = None
253
+
254
+
255
+ class EchoSubscriber(Subscriber):
256
+ """Simple subscriber that prints received messages on the console (ie. `stdout`).
257
+
258
+ Parameters
259
+ ----------
260
+ topic
261
+ The topic to subscribe to. If a string is provided, a new topic instance
262
+ will be created using the string as topic name.
263
+ """
264
+
265
+ def __init__(self, topic: Union[Topic, str], transport: Optional[Transport] = None) -> None:
266
+ super(EchoSubscriber, self).__init__(topic, callback=self.echo, transport=transport)
267
+
268
+ def echo(self, message: Union[Message, dict]) -> None:
269
+ """Print received messages to the console."""
270
+ print(str(message))