ReticulumTelemetryHub 0.1.0__py3-none-any.whl → 0.143.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 (108) hide show
  1. reticulum_telemetry_hub/api/__init__.py +23 -0
  2. reticulum_telemetry_hub/api/models.py +323 -0
  3. reticulum_telemetry_hub/api/service.py +836 -0
  4. reticulum_telemetry_hub/api/storage.py +528 -0
  5. reticulum_telemetry_hub/api/storage_base.py +156 -0
  6. reticulum_telemetry_hub/api/storage_models.py +118 -0
  7. reticulum_telemetry_hub/atak_cot/__init__.py +49 -0
  8. reticulum_telemetry_hub/atak_cot/base.py +277 -0
  9. reticulum_telemetry_hub/atak_cot/chat.py +506 -0
  10. reticulum_telemetry_hub/atak_cot/detail.py +235 -0
  11. reticulum_telemetry_hub/atak_cot/event.py +181 -0
  12. reticulum_telemetry_hub/atak_cot/pytak_client.py +569 -0
  13. reticulum_telemetry_hub/atak_cot/tak_connector.py +848 -0
  14. reticulum_telemetry_hub/config/__init__.py +25 -0
  15. reticulum_telemetry_hub/config/constants.py +7 -0
  16. reticulum_telemetry_hub/config/manager.py +515 -0
  17. reticulum_telemetry_hub/config/models.py +215 -0
  18. reticulum_telemetry_hub/embedded_lxmd/__init__.py +5 -0
  19. reticulum_telemetry_hub/embedded_lxmd/embedded.py +418 -0
  20. reticulum_telemetry_hub/internal_api/__init__.py +21 -0
  21. reticulum_telemetry_hub/internal_api/bus.py +344 -0
  22. reticulum_telemetry_hub/internal_api/core.py +690 -0
  23. reticulum_telemetry_hub/internal_api/v1/__init__.py +74 -0
  24. reticulum_telemetry_hub/internal_api/v1/enums.py +109 -0
  25. reticulum_telemetry_hub/internal_api/v1/manifest.json +8 -0
  26. reticulum_telemetry_hub/internal_api/v1/schemas.py +478 -0
  27. reticulum_telemetry_hub/internal_api/versioning.py +63 -0
  28. reticulum_telemetry_hub/lxmf_daemon/Handlers.py +122 -0
  29. reticulum_telemetry_hub/lxmf_daemon/LXMF.py +252 -0
  30. reticulum_telemetry_hub/lxmf_daemon/LXMPeer.py +898 -0
  31. reticulum_telemetry_hub/lxmf_daemon/LXMRouter.py +4227 -0
  32. reticulum_telemetry_hub/lxmf_daemon/LXMessage.py +1006 -0
  33. reticulum_telemetry_hub/lxmf_daemon/LXStamper.py +490 -0
  34. reticulum_telemetry_hub/lxmf_daemon/__init__.py +10 -0
  35. reticulum_telemetry_hub/lxmf_daemon/_version.py +1 -0
  36. reticulum_telemetry_hub/lxmf_daemon/lxmd.py +1655 -0
  37. reticulum_telemetry_hub/lxmf_telemetry/model/fields/field_telemetry_stream.py +6 -0
  38. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/__init__.py +3 -0
  39. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/appearance.py +19 -19
  40. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/peer.py +17 -13
  41. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/__init__.py +65 -0
  42. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/acceleration.py +68 -0
  43. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/ambient_light.py +37 -0
  44. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/angular_velocity.py +68 -0
  45. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/battery.py +68 -0
  46. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/connection_map.py +258 -0
  47. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/generic.py +841 -0
  48. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/gravity.py +68 -0
  49. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/humidity.py +37 -0
  50. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/information.py +42 -0
  51. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/location.py +110 -0
  52. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/lxmf_propagation.py +429 -0
  53. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/magnetic_field.py +68 -0
  54. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/physical_link.py +53 -0
  55. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/pressure.py +37 -0
  56. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/proximity.py +37 -0
  57. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/received.py +75 -0
  58. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/rns_transport.py +209 -0
  59. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor.py +65 -0
  60. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_enum.py +27 -0
  61. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +58 -0
  62. reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/temperature.py +37 -0
  63. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/sensors/time.py +36 -32
  64. {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/telemeter.py +26 -23
  65. reticulum_telemetry_hub/lxmf_telemetry/sampler.py +229 -0
  66. reticulum_telemetry_hub/lxmf_telemetry/telemeter_manager.py +409 -0
  67. reticulum_telemetry_hub/lxmf_telemetry/telemetry_controller.py +804 -0
  68. reticulum_telemetry_hub/northbound/__init__.py +5 -0
  69. reticulum_telemetry_hub/northbound/app.py +195 -0
  70. reticulum_telemetry_hub/northbound/auth.py +119 -0
  71. reticulum_telemetry_hub/northbound/gateway.py +310 -0
  72. reticulum_telemetry_hub/northbound/internal_adapter.py +302 -0
  73. reticulum_telemetry_hub/northbound/models.py +213 -0
  74. reticulum_telemetry_hub/northbound/routes_chat.py +123 -0
  75. reticulum_telemetry_hub/northbound/routes_files.py +119 -0
  76. reticulum_telemetry_hub/northbound/routes_rest.py +345 -0
  77. reticulum_telemetry_hub/northbound/routes_subscribers.py +150 -0
  78. reticulum_telemetry_hub/northbound/routes_topics.py +178 -0
  79. reticulum_telemetry_hub/northbound/routes_ws.py +107 -0
  80. reticulum_telemetry_hub/northbound/serializers.py +72 -0
  81. reticulum_telemetry_hub/northbound/services.py +373 -0
  82. reticulum_telemetry_hub/northbound/websocket.py +855 -0
  83. reticulum_telemetry_hub/reticulum_server/__main__.py +2237 -0
  84. reticulum_telemetry_hub/reticulum_server/command_manager.py +1268 -0
  85. reticulum_telemetry_hub/reticulum_server/command_text.py +399 -0
  86. reticulum_telemetry_hub/reticulum_server/constants.py +1 -0
  87. reticulum_telemetry_hub/reticulum_server/event_log.py +357 -0
  88. reticulum_telemetry_hub/reticulum_server/internal_adapter.py +358 -0
  89. reticulum_telemetry_hub/reticulum_server/outbound_queue.py +312 -0
  90. reticulum_telemetry_hub/reticulum_server/services.py +422 -0
  91. reticulumtelemetryhub-0.143.0.dist-info/METADATA +181 -0
  92. reticulumtelemetryhub-0.143.0.dist-info/RECORD +97 -0
  93. {reticulumtelemetryhub-0.1.0.dist-info → reticulumtelemetryhub-0.143.0.dist-info}/WHEEL +1 -1
  94. reticulumtelemetryhub-0.143.0.dist-info/licenses/LICENSE +277 -0
  95. lxmf_telemetry/model/fields/field_telemetry_stream.py +0 -7
  96. lxmf_telemetry/model/persistance/__init__.py +0 -3
  97. lxmf_telemetry/model/persistance/sensors/location.py +0 -69
  98. lxmf_telemetry/model/persistance/sensors/magnetic_field.py +0 -36
  99. lxmf_telemetry/model/persistance/sensors/sensor.py +0 -44
  100. lxmf_telemetry/model/persistance/sensors/sensor_enum.py +0 -24
  101. lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +0 -9
  102. lxmf_telemetry/telemetry_controller.py +0 -124
  103. reticulum_server/main.py +0 -182
  104. reticulumtelemetryhub-0.1.0.dist-info/METADATA +0 -15
  105. reticulumtelemetryhub-0.1.0.dist-info/RECORD +0 -19
  106. {lxmf_telemetry → reticulum_telemetry_hub}/__init__.py +0 -0
  107. {lxmf_telemetry/model/persistance/sensors → reticulum_telemetry_hub/lxmf_telemetry}/__init__.py +0 -0
  108. {reticulum_server → reticulum_telemetry_hub/reticulum_server}/__init__.py +0 -0
@@ -0,0 +1,235 @@
1
+ """Detail payload helpers for ATAK Cursor on Target events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import xml.etree.ElementTree as ET
6
+ from dataclasses import dataclass, field
7
+ from typing import Optional, Union
8
+
9
+ from reticulum_telemetry_hub.atak_cot.base import Contact
10
+ from reticulum_telemetry_hub.atak_cot.base import Group
11
+ from reticulum_telemetry_hub.atak_cot.base import Status
12
+ from reticulum_telemetry_hub.atak_cot.base import Takv
13
+ from reticulum_telemetry_hub.atak_cot.base import Track
14
+ from reticulum_telemetry_hub.atak_cot.base import Uid
15
+ from reticulum_telemetry_hub.atak_cot.chat import Chat
16
+ from reticulum_telemetry_hub.atak_cot.chat import ChatGroup
17
+ from reticulum_telemetry_hub.atak_cot.chat import Link
18
+ from reticulum_telemetry_hub.atak_cot.chat import Marti
19
+ from reticulum_telemetry_hub.atak_cot.chat import ServerDestination
20
+ from reticulum_telemetry_hub.atak_cot.chat import Remarks
21
+
22
+
23
+ @dataclass
24
+ class Detail: # pylint: disable=too-many-instance-attributes
25
+ """Additional information such as contact, group, and movement."""
26
+
27
+ contact: Optional[Contact] = None
28
+ group: Optional[Group] = None
29
+ groups: list[Group] = field(default_factory=list)
30
+ track: Optional[Track] = None
31
+ takv: Optional[Takv] = None
32
+ chat: Optional[Chat] = None
33
+ chat_group: Optional[ChatGroup] = None
34
+ uid: Optional[Uid] = None
35
+ links: list[Link] = field(default_factory=list)
36
+ remarks: Optional[Union[str, Remarks]] = None
37
+ marti: Optional[Marti] = None
38
+ status: Optional[Status] = None
39
+ server_destination: bool = False
40
+
41
+ @classmethod
42
+ # pylint: disable=too-many-locals,too-many-branches
43
+ def from_xml(cls, elem: ET.Element) -> "Detail":
44
+ """Create a :class:`Detail` from a ``<detail>`` element."""
45
+
46
+ contact_el = elem.find("contact")
47
+ group_elems = elem.findall("__group")
48
+ track_el = elem.find("track")
49
+ takv_el = elem.find("takv")
50
+ chat_el = elem.find("__chat")
51
+ chatgrp_el = elem.find("chatgrp")
52
+ uid_el = elem.find("uid")
53
+ link_elems = elem.findall("link")
54
+ remarks_el = elem.find("remarks")
55
+ marti_el = elem.find("marti")
56
+ server_destination_el = elem.find("__serverdestination")
57
+ status_el = elem.find("status")
58
+ groups = [Group.from_xml(item) for item in group_elems]
59
+ primary_group = groups[0] if groups else None
60
+ extra_groups = groups[1:] if len(groups) > 1 else []
61
+ remarks: Optional[Union[str, Remarks]] = None
62
+ if remarks_el is not None:
63
+ if remarks_el.attrib:
64
+ remarks = Remarks.from_xml(remarks_el)
65
+ else:
66
+ remarks = remarks_el.text
67
+ return cls(
68
+ contact=(Contact.from_xml(contact_el) if contact_el is not None else None),
69
+ group=primary_group,
70
+ groups=extra_groups,
71
+ track=(Track.from_xml(track_el) if track_el is not None else None),
72
+ takv=Takv.from_xml(takv_el) if takv_el is not None else None,
73
+ chat=Chat.from_xml(chat_el) if chat_el is not None else None,
74
+ chat_group=(
75
+ ChatGroup.from_xml(chatgrp_el) if chatgrp_el is not None else None
76
+ ),
77
+ uid=Uid.from_xml(uid_el) if uid_el is not None else None,
78
+ links=[Link.from_xml(item) for item in link_elems],
79
+ remarks=remarks,
80
+ marti=Marti.from_xml(marti_el) if marti_el is not None else None,
81
+ status=Status.from_xml(status_el) if status_el is not None else None,
82
+ server_destination=server_destination_el is not None,
83
+ )
84
+
85
+ def to_element(self) -> Optional[ET.Element]: # pylint: disable=too-many-branches
86
+ """Return an XML detail element or ``None`` if empty."""
87
+
88
+ if not any(
89
+ [
90
+ self.contact,
91
+ self.group,
92
+ self.groups,
93
+ self.track,
94
+ self.takv,
95
+ self.chat,
96
+ self.chat_group,
97
+ self.uid,
98
+ self.links,
99
+ self.remarks,
100
+ self.marti,
101
+ self.status,
102
+ self.server_destination,
103
+ ]
104
+ ):
105
+ return None
106
+ detail_el = ET.Element("detail")
107
+ if self.takv:
108
+ detail_el.append(self.takv.to_element())
109
+ if self.contact:
110
+ detail_el.append(self.contact.to_element())
111
+ if self.group:
112
+ detail_el.append(self.group.to_element())
113
+ for group in self.groups:
114
+ detail_el.append(group.to_element())
115
+ if self.track:
116
+ detail_el.append(self.track.to_element())
117
+ if self.chat:
118
+ detail_el.append(self.chat.to_element())
119
+ if self.chat_group:
120
+ detail_el.append(self.chat_group.to_element())
121
+ if self.uid:
122
+ detail_el.append(self.uid.to_element())
123
+ for link in self.links:
124
+ detail_el.append(link.to_element())
125
+ if self.remarks:
126
+ if isinstance(self.remarks, Remarks):
127
+ detail_el.append(self.remarks.to_element())
128
+ else:
129
+ remarks_el = ET.SubElement(detail_el, "remarks")
130
+ remarks_el.text = self.remarks
131
+ if self.marti:
132
+ marti_element = self.marti.to_element()
133
+ if marti_element is not None:
134
+ detail_el.append(marti_element)
135
+ if self.server_destination:
136
+ detail_el.append(ServerDestination.to_element())
137
+ if self.status:
138
+ detail_el.append(self.status.to_element())
139
+ return detail_el
140
+
141
+ def to_dict(self) -> dict: # pylint: disable=too-many-branches
142
+ """Return a dictionary containing populated fields only."""
143
+
144
+ data: dict = {}
145
+ if self.contact:
146
+ data["contact"] = self.contact.to_dict()
147
+ if self.group:
148
+ data["group"] = self.group.to_dict()
149
+ if self.groups:
150
+ data["groups"] = [group.to_dict() for group in self.groups]
151
+ if self.track:
152
+ data["track"] = self.track.to_dict()
153
+ if self.takv:
154
+ data["takv"] = self.takv.to_dict()
155
+ if self.chat:
156
+ data["chat"] = self.chat.to_dict()
157
+ if self.chat_group:
158
+ data["chat_group"] = self.chat_group.to_dict()
159
+ if self.uid:
160
+ data["uid"] = self.uid.to_dict()
161
+ if self.links:
162
+ data["links"] = [link.to_dict() for link in self.links]
163
+ if self.remarks:
164
+ data["remarks"] = (
165
+ self.remarks.to_dict()
166
+ if isinstance(self.remarks, Remarks)
167
+ else self.remarks
168
+ )
169
+ if self.marti:
170
+ marti_dict = self.marti.to_dict()
171
+ if marti_dict:
172
+ data["marti"] = marti_dict
173
+ if self.status:
174
+ data["status"] = self.status.to_dict()
175
+ if self.server_destination:
176
+ data["server_destination"] = True
177
+ return data
178
+
179
+ @classmethod
180
+ def from_dict(cls, data: dict) -> "Detail": # pylint: disable=too-many-locals
181
+ """Create a :class:`Detail` from a dictionary."""
182
+
183
+ contact = None
184
+ if "contact" in data:
185
+ contact = Contact.from_dict(data["contact"])
186
+ group = None
187
+ if "group" in data:
188
+ group = Group.from_dict(data["group"])
189
+ groups_data = data.get("groups", [])
190
+ groups = [Group.from_dict(item) for item in groups_data]
191
+ track = None
192
+ if "track" in data:
193
+ track = Track.from_dict(data["track"])
194
+ takv = None
195
+ if "takv" in data:
196
+ takv = Takv.from_dict(data["takv"])
197
+ chat = None
198
+ if "chat" in data:
199
+ chat = Chat.from_dict(data["chat"])
200
+ chat_group = None
201
+ if "chat_group" in data:
202
+ chat_group = ChatGroup.from_dict(data["chat_group"])
203
+ uid = None
204
+ if "uid" in data:
205
+ uid = Uid.from_dict(data["uid"])
206
+ links_data = data.get("links", [])
207
+ links = [Link.from_dict(item) for item in links_data]
208
+ remarks_data = data.get("remarks")
209
+ remarks = None
210
+ if isinstance(remarks_data, dict):
211
+ remarks = Remarks.from_dict(remarks_data)
212
+ elif remarks_data is not None:
213
+ remarks = str(remarks_data)
214
+ marti = None
215
+ if "marti" in data:
216
+ marti = Marti.from_dict(data.get("marti", {}))
217
+ status = None
218
+ if "status" in data:
219
+ status = Status.from_dict(data.get("status", {}))
220
+ server_destination = data.get("server_destination", False) is True
221
+ return cls(
222
+ contact=contact,
223
+ group=group,
224
+ groups=groups,
225
+ track=track,
226
+ takv=takv,
227
+ chat=chat,
228
+ chat_group=chat_group,
229
+ uid=uid,
230
+ links=links,
231
+ remarks=remarks,
232
+ marti=marti,
233
+ status=status,
234
+ server_destination=server_destination,
235
+ )
@@ -0,0 +1,181 @@
1
+ """ATAK Cursor on Target event container and serialization helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import gzip
6
+ import json
7
+ import xml.etree.ElementTree as ET
8
+ from dataclasses import dataclass
9
+ from typing import Union, cast
10
+
11
+ import msgpack
12
+
13
+ from reticulum_telemetry_hub.atak_cot.base import Point
14
+ from reticulum_telemetry_hub.atak_cot.detail import Detail
15
+
16
+ Packable = Union["Event", dict]
17
+
18
+
19
+ def _ensure_packable(obj: Packable) -> dict:
20
+ """Return a dictionary representation regardless of input type."""
21
+
22
+ if isinstance(obj, Event):
23
+ return obj.to_dict()
24
+ if isinstance(obj, dict):
25
+ return obj
26
+ raise TypeError(f"Unsupported packable type: {type(obj)!r}")
27
+
28
+
29
+ def pack_data(obj: Packable) -> bytes:
30
+ """Return a compressed msgpack representation of ``obj`` or an Event."""
31
+
32
+ packed = msgpack.packb(_ensure_packable(obj), use_bin_type=True)
33
+ packed_bytes = cast(bytes, packed)
34
+ return gzip.compress(packed_bytes)
35
+
36
+
37
+ def unpack_data(data: bytes) -> dict:
38
+ """Inverse of :func:`pack_data` returning the original object."""
39
+
40
+ return msgpack.unpackb(gzip.decompress(data), strict_map_key=False)
41
+
42
+
43
+ @dataclass
44
+ class Event: # pylint: disable=too-many-instance-attributes
45
+ """Top level CoT event object."""
46
+
47
+ version: str
48
+ uid: str
49
+ type: str
50
+ how: str
51
+ time: str
52
+ start: str
53
+ stale: str
54
+ point: Point
55
+ access: str | None = None
56
+ detail: Detail | None = None
57
+
58
+ @classmethod
59
+ def from_xml(cls, xml: Union[str, bytes]) -> "Event":
60
+ """Parse an entire ``<event>`` XML string."""
61
+
62
+ if isinstance(xml, bytes):
63
+ xml = xml.decode("utf-8")
64
+ return cls.from_element(ET.fromstring(xml))
65
+
66
+ @classmethod
67
+ def from_element(cls, root: ET.Element) -> "Event":
68
+ """Construct an event from an ``<event>`` element."""
69
+
70
+ point_el = root.find("point")
71
+ detail_el = root.find("detail")
72
+ point = (
73
+ Point.from_xml(point_el) if point_el is not None else Point(0, 0, 0, 0, 0)
74
+ )
75
+ detail = Detail.from_xml(detail_el) if detail_el is not None else None
76
+ return cls(
77
+ version=root.get("version", ""),
78
+ uid=root.get("uid", ""),
79
+ type=root.get("type", ""),
80
+ how=root.get("how", ""),
81
+ time=root.get("time", ""),
82
+ start=root.get("start", ""),
83
+ stale=root.get("stale", ""),
84
+ point=point,
85
+ access=root.get("access"),
86
+ detail=detail,
87
+ )
88
+
89
+ @classmethod
90
+ def from_dict(cls, obj: dict) -> "Event":
91
+ """Construct an :class:`Event` from a dictionary rooted at ``event``."""
92
+
93
+ event_obj = obj.get("event") if isinstance(obj.get("event"), dict) else obj
94
+ point = Point.from_dict(event_obj.get("point", {}))
95
+ detail_obj = event_obj.get("detail")
96
+ detail = Detail.from_dict(detail_obj) if detail_obj else None
97
+ return cls(
98
+ version=event_obj.get("version", ""),
99
+ uid=event_obj.get("uid", ""),
100
+ type=event_obj.get("type", ""),
101
+ how=event_obj.get("how", ""),
102
+ time=event_obj.get("time", ""),
103
+ start=event_obj.get("start", ""),
104
+ stale=event_obj.get("stale", ""),
105
+ point=point,
106
+ access=event_obj.get("access"),
107
+ detail=detail,
108
+ )
109
+
110
+ @classmethod
111
+ def from_json(cls, data: str) -> "Event":
112
+ """Construct an Event from a JSON string."""
113
+
114
+ return cls.from_dict(json.loads(data))
115
+
116
+ def to_element(self) -> ET.Element:
117
+ """Return an XML element representing the event."""
118
+
119
+ attrib = {
120
+ "version": self.version,
121
+ "uid": self.uid,
122
+ "type": self.type,
123
+ "how": self.how,
124
+ "time": self.time,
125
+ "start": self.start,
126
+ "stale": self.stale,
127
+ }
128
+ if self.access:
129
+ attrib["access"] = self.access
130
+ event_el = ET.Element("event", attrib)
131
+ event_el.append(self.point.to_element())
132
+ detail_el = self.detail.to_element() if self.detail else None
133
+ if detail_el is not None:
134
+ event_el.append(detail_el)
135
+ return event_el
136
+
137
+ def to_xml(self) -> str:
138
+ """Return a Unicode XML string representing the event."""
139
+
140
+ return ET.tostring(self.to_element(), encoding="unicode")
141
+
142
+ def to_xml_bytes(self) -> bytes:
143
+ """Return UTF-8 encoded XML bytes representing the event."""
144
+
145
+ return ET.tostring(self.to_element())
146
+
147
+ def to_dict(self) -> dict:
148
+ """Return a dictionary representation of the event with an ``event`` root."""
149
+
150
+ event_data = {
151
+ "version": self.version,
152
+ "uid": self.uid,
153
+ "type": self.type,
154
+ "how": self.how,
155
+ "time": self.time,
156
+ "start": self.start,
157
+ "stale": self.stale,
158
+ "point": self.point.to_dict(),
159
+ }
160
+ if self.access:
161
+ event_data["access"] = self.access
162
+ if self.detail:
163
+ event_data["detail"] = self.detail.to_dict()
164
+ return {"event": event_data}
165
+
166
+ def to_json(self) -> str:
167
+ """Return a JSON representation of the event."""
168
+
169
+ return json.dumps(self.to_dict())
170
+
171
+ def to_datapack(self) -> bytes:
172
+ """Return a compressed datapack representation of the event."""
173
+
174
+ return pack_data(self)
175
+
176
+ @classmethod
177
+ def from_datapack(cls, data: bytes) -> "Event":
178
+ """Recreate an :class:`Event` instance from datapack bytes."""
179
+
180
+ unpacked = unpack_data(data)
181
+ return cls.from_dict(unpacked)