msgcenterpy 0.0.5__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 (33) hide show
  1. msgcenterpy/__init__.py +97 -0
  2. msgcenterpy/__pycache__/__init__.cpython-310.pyc +0 -0
  3. msgcenterpy/core/__init__.py +0 -0
  4. msgcenterpy/core/__pycache__/__init__.cpython-310.pyc +0 -0
  5. msgcenterpy/core/__pycache__/envelope.cpython-310.pyc +0 -0
  6. msgcenterpy/core/__pycache__/field_accessor.cpython-310.pyc +0 -0
  7. msgcenterpy/core/__pycache__/message_center.cpython-310.pyc +0 -0
  8. msgcenterpy/core/__pycache__/message_instance.cpython-310.pyc +0 -0
  9. msgcenterpy/core/__pycache__/type_converter.cpython-310.pyc +0 -0
  10. msgcenterpy/core/__pycache__/type_info.cpython-310.pyc +0 -0
  11. msgcenterpy/core/__pycache__/types.cpython-310.pyc +0 -0
  12. msgcenterpy/core/envelope.py +54 -0
  13. msgcenterpy/core/field_accessor.py +406 -0
  14. msgcenterpy/core/message_center.py +69 -0
  15. msgcenterpy/core/message_instance.py +193 -0
  16. msgcenterpy/core/type_converter.py +411 -0
  17. msgcenterpy/core/type_info.py +400 -0
  18. msgcenterpy/core/types.py +25 -0
  19. msgcenterpy/instances/__init__.py +0 -0
  20. msgcenterpy/instances/__pycache__/__init__.cpython-310.pyc +0 -0
  21. msgcenterpy/instances/__pycache__/json_schema_instance.cpython-310.pyc +0 -0
  22. msgcenterpy/instances/__pycache__/ros2_instance.cpython-310.pyc +0 -0
  23. msgcenterpy/instances/json_schema_instance.py +303 -0
  24. msgcenterpy/instances/ros2_instance.py +242 -0
  25. msgcenterpy/utils/__init__.py +0 -0
  26. msgcenterpy/utils/__pycache__/__init__.cpython-310.pyc +0 -0
  27. msgcenterpy/utils/__pycache__/decorator.cpython-310.pyc +0 -0
  28. msgcenterpy/utils/decorator.py +29 -0
  29. msgcenterpy-0.0.5.dist-info/METADATA +330 -0
  30. msgcenterpy-0.0.5.dist-info/RECORD +33 -0
  31. msgcenterpy-0.0.5.dist-info/WHEEL +5 -0
  32. msgcenterpy-0.0.5.dist-info/licenses/LICENSE +199 -0
  33. msgcenterpy-0.0.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,97 @@
1
+ """
2
+ MsgCenterPy - Unified Message Conversion System
3
+
4
+ A multi-format message conversion system supporting seamless conversion
5
+ between ROS2, Pydantic, Dataclass, JSON, Dict, YAML and JSON Schema.
6
+ """
7
+
8
+ __version__ = "0.0.5"
9
+ __license__ = "Apache-2.0"
10
+
11
+ from msgcenterpy.core.envelope import MessageEnvelope, create_envelope
12
+ from msgcenterpy.core.field_accessor import FieldAccessor
13
+ from msgcenterpy.core.message_center import MessageCenter
14
+
15
+ # Core imports
16
+ from msgcenterpy.core.message_instance import MessageInstance
17
+ from msgcenterpy.core.type_converter import StandardType, TypeConverter
18
+ from msgcenterpy.core.type_info import ConstraintType, TypeInfo
19
+ from msgcenterpy.core.types import ConversionError, MessageType, ValidationError
20
+
21
+ # Always available instance
22
+ from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance
23
+
24
+ # Optional ROS2 instance (with graceful fallback)
25
+ try:
26
+ from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
27
+
28
+ _HAS_ROS2 = True
29
+ except ImportError:
30
+ _HAS_ROS2 = False
31
+
32
+
33
+ # Convenience function
34
+ def get_message_center() -> MessageCenter:
35
+ """Get the MessageCenter singleton instance."""
36
+ return MessageCenter.get_instance()
37
+
38
+
39
+ # Main exports
40
+ __all__ = [
41
+ # Version info
42
+ "__version__",
43
+ "__license__",
44
+ ]
45
+
46
+
47
+ def get_version() -> str:
48
+ """Get the current version of MsgCenterPy."""
49
+ return __version__
50
+
51
+
52
+ def get_package_info() -> dict:
53
+ """Get package information."""
54
+ return {
55
+ "name": "msgcenterpy",
56
+ "version": __version__,
57
+ "description": "Unified message conversion system supporting ROS2, Pydantic, Dataclass, JSON, YAML, Dict, and JSON Schema inter-conversion",
58
+ "license": __license__,
59
+ "url": "https://github.com/ZGCA-Forge/MsgCenterPy",
60
+ "keywords": [
61
+ "message",
62
+ "conversion",
63
+ "ros2",
64
+ "pydantic",
65
+ "dataclass",
66
+ "json",
67
+ "yaml",
68
+ "mcp",
69
+ ],
70
+ }
71
+
72
+
73
+ def check_dependencies() -> dict:
74
+ """Check which optional dependencies are available."""
75
+ dependencies = {
76
+ "ros2": False,
77
+ "jsonschema": False,
78
+ }
79
+
80
+ # Check ROS2
81
+ try:
82
+ import rclpy # type: ignore
83
+ import rosidl_runtime_py # type: ignore
84
+
85
+ dependencies["ros2"] = True
86
+ except ImportError:
87
+ pass
88
+
89
+ # Check jsonschema
90
+ try:
91
+ import jsonschema # type: ignore
92
+
93
+ dependencies["jsonschema"] = True
94
+ except ImportError:
95
+ pass
96
+
97
+ return dependencies
File without changes
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, TypedDict
4
+
5
+ ENVELOPE_VERSION: str = "1"
6
+
7
+
8
+ class Properties(TypedDict, total=False):
9
+ ros_msg_cls_path: str
10
+ ros_msg_cls_namespace: str
11
+ json_schema: Dict[str, Any]
12
+
13
+
14
+ class FormatMetadata(TypedDict, total=False):
15
+ """Additional metadata for source format, optional.
16
+
17
+ Examples: field statistics, original type descriptions, field type mappings, etc.
18
+ """
19
+
20
+ current_format: str
21
+ source_cls_name: str
22
+ source_cls_module: str
23
+ properties: Properties
24
+
25
+
26
+ class MessageEnvelope(TypedDict, total=True):
27
+ """Unified message envelope format.
28
+
29
+ - version: Protocol version
30
+ - format: Source format (MessageType.value)
31
+ - type_info: Type information (applicable for ROS2, Pydantic, etc.)
32
+ - content: Normalized message content (dictionary)
33
+ - metadata: Additional metadata
34
+ """
35
+
36
+ version: str
37
+ format: str
38
+ content: Dict[str, Any]
39
+ metadata: FormatMetadata
40
+
41
+
42
+ def create_envelope(
43
+ *,
44
+ format_name: str,
45
+ content: Dict[str, Any],
46
+ metadata: FormatMetadata,
47
+ ) -> MessageEnvelope:
48
+ env: MessageEnvelope = {
49
+ "version": ENVELOPE_VERSION,
50
+ "format": format_name,
51
+ "content": content,
52
+ "metadata": metadata,
53
+ }
54
+ return env
@@ -0,0 +1,406 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict, Optional, cast
3
+
4
+ from msgcenterpy.core.type_converter import StandardType
5
+ from msgcenterpy.core.type_info import (
6
+ ConstraintType,
7
+ Consts,
8
+ TypeInfo,
9
+ TypeInfoPostProcessor,
10
+ )
11
+ from msgcenterpy.utils.decorator import experimental
12
+
13
+ TEST_MODE = True
14
+
15
+
16
+ class FieldAccessor:
17
+ """
18
+ 字段访问器,支持类型转换和约束验证的统一字段访问接口
19
+ 只需要getitem和setitem,外部必须通过字典的方式来赋值
20
+ """
21
+
22
+ @property
23
+ def parent_msg_center(self) -> Optional["FieldAccessor"]:
24
+ return self._parent
25
+
26
+ @property
27
+ def full_path_from_root(self) -> str:
28
+ if self._parent is None:
29
+ return self._field_name or "unknown"
30
+ else:
31
+ parent_path = self._parent.full_path_from_root
32
+ return f"{parent_path}.{self._field_name or 'unknown'}"
33
+
34
+ @property
35
+ def root_accessor_msg_center(self) -> "FieldAccessor":
36
+ """获取根访问器"""
37
+ current = self
38
+ while current._parent is not None:
39
+ current = current._parent
40
+ return current
41
+
42
+ @property
43
+ def value(self) -> Any:
44
+ return self._data
45
+
46
+ @value.setter
47
+ def value(self, data: Any) -> None:
48
+ if self._parent is not None and self._field_name is not None:
49
+ self._parent[self._field_name] = data
50
+
51
+ @property
52
+ def type_info(self) -> Optional[TypeInfo]:
53
+ if self._type_info is not None:
54
+ return self._type_info
55
+
56
+ # 如果是根accessor或者没有字段名,无法获取TypeInfo
57
+ if self._parent is None or self._field_name is None:
58
+ return None
59
+
60
+ # 调用类型信息提供者获取类型信息,调用是耗时的
61
+ if self._type_info_provider is None:
62
+ return None
63
+ type_info = self._type_info_provider.get_field_type_info(self._field_name, self._data, self._parent)
64
+
65
+ # 对TypeInfo进行后处理,添加默认约束
66
+ if type_info:
67
+ TypeInfoPostProcessor.post_process_type_info(type_info)
68
+ self._type_info = type_info
69
+
70
+ return type_info
71
+
72
+ """标记方便排除getitem/setitem,不要删除"""
73
+ _data: Any = None
74
+ _type_info_provider: "TypeInfoProvider" = None # type: ignore[assignment]
75
+ _parent: Optional["FieldAccessor"] = None
76
+ _field_name: str = None # type: ignore[assignment]
77
+ _cache: Dict[str, "FieldAccessor"] = None # type: ignore[assignment]
78
+ _type_info: Optional[TypeInfo] = None
79
+
80
+ def __init__(
81
+ self,
82
+ data: Any,
83
+ type_info_provider: "TypeInfoProvider",
84
+ parent: Optional["FieldAccessor"],
85
+ field_name: str,
86
+ ):
87
+ """
88
+ 初始化字段访问器
89
+
90
+ Args:
91
+ data: 要访问的数据对象
92
+ type_info_provider: 类型信息提供者
93
+ parent: 父字段访问器,用于嵌套访问
94
+ field_name: 当前访问器对应的字段名(用于构建路径)
95
+ """
96
+ self._data = data
97
+ self._type_info_provider = type_info_provider
98
+ self._parent = parent
99
+ self._field_name = field_name
100
+ self._cache: Dict[str, "FieldAccessor"] = {} # 缓存FieldAccessor而不是TypeInfo
101
+ self._type_info: Optional[TypeInfo] = None # 当前accessor的TypeInfo
102
+
103
+ def get_sub_type_info(self, field_name: str) -> Optional[TypeInfo]:
104
+ """获取字段的类型信息,通过获取字段的accessor"""
105
+ field_accessor = self[field_name]
106
+ return field_accessor.type_info
107
+
108
+ def __getitem__(self, field_name: str) -> "FieldAccessor":
109
+ """获取字段访问器,支持嵌套访问"""
110
+ # 检查缓存中是否有对应的 accessor
111
+ if self._cache is None:
112
+ self._cache = {}
113
+ if field_name in self._cache:
114
+ cached_accessor = self._cache[field_name]
115
+ # 更新 accessor 的数据源,以防数据已更改
116
+ if TEST_MODE:
117
+ raw_value = self._get_raw_value(field_name)
118
+ if cached_accessor.value != raw_value:
119
+ raise ValueError(
120
+ f"Cached accessor value mismatch for field '{field_name}': expected {raw_value}, got {cached_accessor.value}"
121
+ )
122
+ return cached_accessor
123
+ # 获取原始值并创建新的 accessor
124
+ raw_value = self._get_raw_value(field_name)
125
+ if self._type_info_provider is None:
126
+ raise RuntimeError("TypeInfoProvider not initialized")
127
+ accessor = FieldAccessorFactory.create_accessor(
128
+ data=raw_value,
129
+ type_info_provider=self._type_info_provider,
130
+ parent=self,
131
+ field_name=field_name,
132
+ )
133
+
134
+ self._cache[field_name] = accessor
135
+ return accessor
136
+
137
+ def __setitem__(self, field_name: str, value: Any) -> None:
138
+ """设置字段值,支持类型转换和验证"""
139
+ # 获取类型信息
140
+ if field_name in self._get_field_names():
141
+ type_info = self.get_sub_type_info(field_name)
142
+ if type_info is not None:
143
+ # 进行类型转换
144
+ converted_value = type_info.convert_value(value) # 这步自带validate
145
+ value = converted_value
146
+ # 对子field设置value,依然会上溯走set_raw_value,确保一致性
147
+ # 设置值
148
+ sub_accessor = self[field_name]
149
+ self._set_raw_value(field_name, value)
150
+ sub_accessor._data = self._get_raw_value(field_name) # 有可能内部还有赋值的处理
151
+ # 清除相关缓存
152
+ self.clear_cache(field_name)
153
+
154
+ def __contains__(self, field_name: str) -> bool:
155
+ return self._has_field(field_name)
156
+
157
+ def __getattr__(self, field_name: str) -> "FieldAccessor | Any":
158
+ """支持通过属性访问字段,用于嵌套访问如 accessor.pose.position.x"""
159
+ for cls in self.__class__.__mro__:
160
+ if field_name in cls.__dict__:
161
+ return cast(Any, super().__getattribute__(field_name))
162
+ return self[field_name]
163
+
164
+ def __setattr__(self, field_name: str, value: Any) -> None:
165
+ """支持通过属性设置字段值,用于嵌套赋值如 accessor.pose.position.x = 1.0"""
166
+ for cls in self.__class__.__mro__:
167
+ if field_name in cls.__dict__:
168
+ return super().__setattr__(field_name, value)
169
+ self[field_name] = value
170
+ return None
171
+
172
+ def clear_cache(self, field_name: Optional[str] = None) -> None:
173
+ """失效字段相关的缓存"""
174
+ if self._cache is not None and field_name is not None and field_name in self._cache:
175
+ self._cache[field_name].clear_type_info()
176
+
177
+ def clear_type_info(self) -> None:
178
+ """清空所有缓存"""
179
+ if self._type_info is not None:
180
+ self._type_info._outdated = True
181
+ self._type_info = None
182
+
183
+ def get_nested_field_accessor(self, path: str, separator: str = ".") -> "FieldAccessor":
184
+ parts = path.split(separator)
185
+ current = self
186
+ for part in parts:
187
+ current = self[part]
188
+ return current
189
+
190
+ def set_nested_value(self, path: str, value: Any, separator: str = ".") -> None:
191
+ current = self.get_nested_field_accessor(path, separator)
192
+ current.value = value
193
+
194
+ def _get_raw_value(self, field_name: str) -> Any:
195
+ """获取原始字段值(子类实现)"""
196
+ if hasattr(self._data, "__getitem__"):
197
+ return self._data[field_name]
198
+ elif hasattr(self._data, field_name):
199
+ return getattr(self._data, field_name)
200
+ else:
201
+ raise KeyError(f"Field {field_name} not found")
202
+
203
+ def _set_raw_value(self, field_name: str, value: Any) -> None:
204
+ """设置原始字段值(子类实现)"""
205
+ if hasattr(self._data, "__setitem__"):
206
+ self._data[field_name] = value
207
+ elif hasattr(self._data, field_name):
208
+ setattr(self._data, field_name, value)
209
+ else:
210
+ raise KeyError(f"Field {field_name} not found")
211
+
212
+ def _has_field(self, field_name: str) -> bool:
213
+ """检查字段是否存在(子类实现)"""
214
+ if hasattr(self._data, "__contains__"):
215
+ return field_name in self._data
216
+ else:
217
+ return field_name in self._get_field_names()
218
+
219
+ def _get_field_names(self) -> list[str]:
220
+ """获取所有字段名(子类实现)"""
221
+ if callable(getattr(self._data, "keys", None)):
222
+ # noinspection PyCallingNonCallable
223
+ return list(self._data.keys())
224
+ elif hasattr(self._data, "__dict__"):
225
+ return list(self._data.__dict__.keys())
226
+ elif hasattr(self._data, "__slots__"):
227
+ # noinspection PyTypeChecker
228
+ return list(self._data.__slots__)
229
+ else:
230
+ # 回退方案:使用dir()但过滤掉特殊方法
231
+ return [name for name in dir(self._data) if not name.startswith("_")]
232
+
233
+ def get_json_schema(self) -> Dict[str, Any]:
234
+ """原有的递归生成 JSON Schema 逻辑"""
235
+ # 获取当前访问器的类型信息
236
+ current_type_info = self.type_info
237
+
238
+ # 如果当前层级有类型信息,使用它生成基本schema
239
+ if current_type_info is not None:
240
+ schema = current_type_info.to_json_schema_property()
241
+ else:
242
+ # 如果没有类型信息,创建基本的object schema
243
+ schema = {"type": "object", "additionalProperties": True}
244
+
245
+ # 如果这是一个对象类型,需要递归处理其字段
246
+ if schema.get("type") == "object":
247
+ properties = {}
248
+ required_fields = []
249
+
250
+ # 获取所有字段名
251
+ field_names = self._get_field_names()
252
+
253
+ for field_name in field_names:
254
+ try:
255
+ # 获取字段的访问器
256
+ field_accessor = self[field_name]
257
+ field_type_info = field_accessor.type_info
258
+
259
+ if field_type_info is not None:
260
+ # 根据字段类型决定如何生成schema
261
+ if field_type_info.standard_type == StandardType.OBJECT:
262
+ # 对于嵌套对象,递归调用
263
+ field_schema = field_accessor.get_json_schema()
264
+ else:
265
+ # 对于基本类型,直接使用type_info转换
266
+ field_schema = field_type_info.to_json_schema_property()
267
+
268
+ properties[field_name] = field_schema
269
+
270
+ # 检查是否为必需字段
271
+ if field_type_info.has_constraint(ConstraintType.REQUIRED):
272
+ required_fields.append(field_name)
273
+
274
+ except Exception as e:
275
+ # 如果字段处理失败,记录警告但继续处理其他字段
276
+ print(f"Warning: Failed to generate schema for field '{field_name}': {e}")
277
+ continue
278
+
279
+ # 更新schema中的properties
280
+ if properties:
281
+ schema["properties"] = properties
282
+
283
+ # 设置必需字段
284
+ if required_fields:
285
+ schema["required"] = required_fields
286
+
287
+ # 如果没有字段信息,允许额外属性
288
+ if not properties:
289
+ schema["additionalProperties"] = True
290
+ else:
291
+ schema["additionalProperties"] = False
292
+
293
+ return schema
294
+
295
+ @experimental("Feature under development")
296
+ def update_from_dict(self, source_data: Dict[str, Any]) -> None:
297
+ """递归更新嵌套字典
298
+
299
+ Args:
300
+ source_data: 源数据字典
301
+ """
302
+ for key, new_value in source_data.items():
303
+ field_exists = self._has_field(key)
304
+ could_add = self._could_allow_new_field(key, new_value)
305
+ if field_exists:
306
+ current_field_accessor = self[key]
307
+ current_type_info = current_field_accessor.type_info
308
+ # 当前key: Object,交给子dict去迭代
309
+ if (
310
+ current_type_info
311
+ and current_type_info.standard_type == StandardType.OBJECT
312
+ and isinstance(new_value, dict)
313
+ ):
314
+ current_field_accessor.update_from_dict(new_value)
315
+ # 当前key: Array,每个值要交给子array去迭代
316
+ elif (
317
+ current_type_info
318
+ and hasattr(current_type_info.standard_type, "IS_ARRAY")
319
+ and current_type_info.standard_type.IS_ARRAY
320
+ and isinstance(new_value, list)
321
+ ):
322
+ # 存在情况Array嵌套,这里后续支持逐个赋值,可能需要利用iter进行赋值
323
+ self[key] = new_value
324
+ else:
325
+ # 不限制类型 或 python类型包含
326
+ if could_add or (current_type_info and issubclass(type(new_value), current_type_info.python_type)):
327
+ self[key] = new_value
328
+ else:
329
+ raise ValueError(f"{key}")
330
+ elif could_add:
331
+ self[key] = new_value
332
+
333
+ def _could_allow_new_field(self, field_name: str, field_value: Any) -> bool:
334
+ """检查是否应该允许添加新字段
335
+
336
+ 通过检查当前type_info中的TYPE_KEEP约束来判断:
337
+ - 如果有TYPE_KEEP且为True,说明类型结构固定,不允许添加新字段
338
+ - 如果没有TYPE_KEEP约束或为False,则允许添加新字段
339
+
340
+ Args:
341
+ field_name: 字段名
342
+ field_value: 字段值
343
+
344
+ Returns:
345
+ 是否允许添加该字段
346
+ """
347
+ parent_type_info = self.type_info
348
+ if parent_type_info is None:
349
+ return True # DEBUGGER NEEDED
350
+ type_keep_constraint = parent_type_info.get_constraint(ConstraintType.TYPE_KEEP)
351
+ if type_keep_constraint is not None and type_keep_constraint.value:
352
+ return False
353
+ return True
354
+
355
+
356
+ class TypeInfoProvider(ABC):
357
+ """Require All Message Instances Extends This get_field_typ_info"""
358
+
359
+ @abstractmethod
360
+ def get_field_type_info(
361
+ self, field_name: str, field_value: Any, field_accessor: "FieldAccessor"
362
+ ) -> Optional[TypeInfo]:
363
+ """获取指定字段的类型信息
364
+
365
+ Args:
366
+ field_name: 字段名,简单字段名如 'field'
367
+ field_value: 字段的当前值,用于动态类型推断,不能为None
368
+ field_accessor: 字段访问器,提供额外的上下文信息,不能为None
369
+
370
+ Returns:
371
+ 字段的TypeInfo,如果字段不存在则返回None
372
+ """
373
+ pass
374
+
375
+
376
+ class ROS2FieldAccessor(FieldAccessor):
377
+ def _get_raw_value(self, field_name: str) -> Any:
378
+ return getattr(self._data, field_name)
379
+
380
+ def _set_raw_value(self, field_name: str, value: Any) -> None:
381
+ return setattr(self._data, field_name, value)
382
+
383
+ def _has_field(self, field_name: str) -> bool:
384
+ return hasattr(self._data, field_name)
385
+
386
+ def _get_field_names(self) -> list[str]:
387
+ if hasattr(self._data, "_fields_and_field_types"):
388
+ # noinspection PyProtectedMember
389
+ fields_and_types: Dict[str, str] = cast(Dict[str, str], self._data._fields_and_field_types)
390
+ return list(fields_and_types.keys())
391
+ else:
392
+ return []
393
+
394
+
395
+ class FieldAccessorFactory:
396
+ @staticmethod
397
+ def create_accessor(
398
+ data: Any,
399
+ type_info_provider: TypeInfoProvider,
400
+ parent: Optional[FieldAccessor] = None,
401
+ field_name: str = Consts.ACCESSOR_ROOT_NODE,
402
+ ) -> FieldAccessor:
403
+ if hasattr(data, "_fields_and_field_types"):
404
+ return ROS2FieldAccessor(data, type_info_provider, parent, field_name)
405
+ else:
406
+ return FieldAccessor(data, type_info_provider, parent, field_name)
@@ -0,0 +1,69 @@
1
+ from typing import Any, Dict, Optional, Type
2
+
3
+ from msgcenterpy.core.envelope import MessageEnvelope, Properties
4
+ from msgcenterpy.core.message_instance import MessageInstance
5
+ from msgcenterpy.core.types import MessageType
6
+
7
+
8
+ class MessageCenter:
9
+ """Message Center singleton class that manages all message types and instances"""
10
+
11
+ _instance: Optional["MessageCenter"] = None
12
+
13
+ @classmethod
14
+ def get_instance(cls) -> "MessageCenter":
15
+ """Get MessageCenter singleton instance"""
16
+ if cls._instance is None:
17
+ cls._instance = cls()
18
+ return cls._instance
19
+
20
+ def __init__(self) -> None:
21
+ """Private constructor, use get_instance() to get singleton"""
22
+ self._type_registry: Dict[MessageType, Type[MessageInstance]] = {}
23
+ self._register_builtin_types()
24
+
25
+ def _register_builtin_types(self) -> None:
26
+ """Register built-in message types with lazy import to avoid circular dependencies"""
27
+ try:
28
+ from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
29
+
30
+ self._type_registry[MessageType.ROS2] = ROS2MessageInstance
31
+ except ImportError:
32
+ pass
33
+ try:
34
+ from msgcenterpy.instances.json_schema_instance import (
35
+ JSONSchemaMessageInstance,
36
+ )
37
+
38
+ self._type_registry[MessageType.JSON_SCHEMA] = JSONSchemaMessageInstance
39
+ except ImportError:
40
+ pass
41
+
42
+ def get_instance_class(self, message_type: MessageType) -> Type[MessageInstance]:
43
+ """Get instance class for the specified message type"""
44
+ instance_class = self._type_registry.get(message_type)
45
+ if not instance_class:
46
+ raise ValueError(f"Unsupported message type: {message_type}")
47
+ return instance_class
48
+
49
+ def convert(
50
+ self,
51
+ source: MessageInstance,
52
+ target_type: MessageType,
53
+ override_properties: Dict[str, Any],
54
+ **kwargs: Any,
55
+ ) -> MessageInstance:
56
+ """Convert message types"""
57
+ target_class = self.get_instance_class(target_type)
58
+ dict_data: MessageEnvelope = source.export_to_envelope()
59
+ if "properties" not in dict_data["metadata"]:
60
+ dict_data["metadata"]["properties"] = Properties()
61
+ dict_data["metadata"]["properties"].update(override_properties) # type: ignore[typeddict-item]
62
+ target_instance = target_class.import_from_envelope(dict_data)
63
+ return target_instance
64
+
65
+
66
+ # Module-level convenience function using singleton
67
+ def get_message_center() -> MessageCenter:
68
+ """Get message center singleton"""
69
+ return MessageCenter.get_instance()