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.
- msgcenterpy/__init__.py +97 -0
- msgcenterpy/__pycache__/__init__.cpython-310.pyc +0 -0
- msgcenterpy/core/__init__.py +0 -0
- msgcenterpy/core/__pycache__/__init__.cpython-310.pyc +0 -0
- msgcenterpy/core/__pycache__/envelope.cpython-310.pyc +0 -0
- msgcenterpy/core/__pycache__/field_accessor.cpython-310.pyc +0 -0
- msgcenterpy/core/__pycache__/message_center.cpython-310.pyc +0 -0
- msgcenterpy/core/__pycache__/message_instance.cpython-310.pyc +0 -0
- msgcenterpy/core/__pycache__/type_converter.cpython-310.pyc +0 -0
- msgcenterpy/core/__pycache__/type_info.cpython-310.pyc +0 -0
- msgcenterpy/core/__pycache__/types.cpython-310.pyc +0 -0
- msgcenterpy/core/envelope.py +54 -0
- msgcenterpy/core/field_accessor.py +406 -0
- msgcenterpy/core/message_center.py +69 -0
- msgcenterpy/core/message_instance.py +193 -0
- msgcenterpy/core/type_converter.py +411 -0
- msgcenterpy/core/type_info.py +400 -0
- msgcenterpy/core/types.py +25 -0
- msgcenterpy/instances/__init__.py +0 -0
- msgcenterpy/instances/__pycache__/__init__.cpython-310.pyc +0 -0
- msgcenterpy/instances/__pycache__/json_schema_instance.cpython-310.pyc +0 -0
- msgcenterpy/instances/__pycache__/ros2_instance.cpython-310.pyc +0 -0
- msgcenterpy/instances/json_schema_instance.py +303 -0
- msgcenterpy/instances/ros2_instance.py +242 -0
- msgcenterpy/utils/__init__.py +0 -0
- msgcenterpy/utils/__pycache__/__init__.cpython-310.pyc +0 -0
- msgcenterpy/utils/__pycache__/decorator.cpython-310.pyc +0 -0
- msgcenterpy/utils/decorator.py +29 -0
- msgcenterpy-0.0.5.dist-info/METADATA +330 -0
- msgcenterpy-0.0.5.dist-info/RECORD +33 -0
- msgcenterpy-0.0.5.dist-info/WHEEL +5 -0
- msgcenterpy-0.0.5.dist-info/licenses/LICENSE +199 -0
- msgcenterpy-0.0.5.dist-info/top_level.txt +1 -0
msgcenterpy/__init__.py
ADDED
|
@@ -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
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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()
|