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.
- reticulum_telemetry_hub/api/__init__.py +23 -0
- reticulum_telemetry_hub/api/models.py +323 -0
- reticulum_telemetry_hub/api/service.py +836 -0
- reticulum_telemetry_hub/api/storage.py +528 -0
- reticulum_telemetry_hub/api/storage_base.py +156 -0
- reticulum_telemetry_hub/api/storage_models.py +118 -0
- reticulum_telemetry_hub/atak_cot/__init__.py +49 -0
- reticulum_telemetry_hub/atak_cot/base.py +277 -0
- reticulum_telemetry_hub/atak_cot/chat.py +506 -0
- reticulum_telemetry_hub/atak_cot/detail.py +235 -0
- reticulum_telemetry_hub/atak_cot/event.py +181 -0
- reticulum_telemetry_hub/atak_cot/pytak_client.py +569 -0
- reticulum_telemetry_hub/atak_cot/tak_connector.py +848 -0
- reticulum_telemetry_hub/config/__init__.py +25 -0
- reticulum_telemetry_hub/config/constants.py +7 -0
- reticulum_telemetry_hub/config/manager.py +515 -0
- reticulum_telemetry_hub/config/models.py +215 -0
- reticulum_telemetry_hub/embedded_lxmd/__init__.py +5 -0
- reticulum_telemetry_hub/embedded_lxmd/embedded.py +418 -0
- reticulum_telemetry_hub/internal_api/__init__.py +21 -0
- reticulum_telemetry_hub/internal_api/bus.py +344 -0
- reticulum_telemetry_hub/internal_api/core.py +690 -0
- reticulum_telemetry_hub/internal_api/v1/__init__.py +74 -0
- reticulum_telemetry_hub/internal_api/v1/enums.py +109 -0
- reticulum_telemetry_hub/internal_api/v1/manifest.json +8 -0
- reticulum_telemetry_hub/internal_api/v1/schemas.py +478 -0
- reticulum_telemetry_hub/internal_api/versioning.py +63 -0
- reticulum_telemetry_hub/lxmf_daemon/Handlers.py +122 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMF.py +252 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMPeer.py +898 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMRouter.py +4227 -0
- reticulum_telemetry_hub/lxmf_daemon/LXMessage.py +1006 -0
- reticulum_telemetry_hub/lxmf_daemon/LXStamper.py +490 -0
- reticulum_telemetry_hub/lxmf_daemon/__init__.py +10 -0
- reticulum_telemetry_hub/lxmf_daemon/_version.py +1 -0
- reticulum_telemetry_hub/lxmf_daemon/lxmd.py +1655 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/fields/field_telemetry_stream.py +6 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/__init__.py +3 -0
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/appearance.py +19 -19
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/peer.py +17 -13
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/__init__.py +65 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/acceleration.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/ambient_light.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/angular_velocity.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/battery.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/connection_map.py +258 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/generic.py +841 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/gravity.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/humidity.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/information.py +42 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/location.py +110 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/lxmf_propagation.py +429 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/magnetic_field.py +68 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/physical_link.py +53 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/pressure.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/proximity.py +37 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/received.py +75 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/rns_transport.py +209 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor.py +65 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_enum.py +27 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +58 -0
- reticulum_telemetry_hub/lxmf_telemetry/model/persistance/sensors/temperature.py +37 -0
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/sensors/time.py +36 -32
- {lxmf_telemetry → reticulum_telemetry_hub/lxmf_telemetry}/model/persistance/telemeter.py +26 -23
- reticulum_telemetry_hub/lxmf_telemetry/sampler.py +229 -0
- reticulum_telemetry_hub/lxmf_telemetry/telemeter_manager.py +409 -0
- reticulum_telemetry_hub/lxmf_telemetry/telemetry_controller.py +804 -0
- reticulum_telemetry_hub/northbound/__init__.py +5 -0
- reticulum_telemetry_hub/northbound/app.py +195 -0
- reticulum_telemetry_hub/northbound/auth.py +119 -0
- reticulum_telemetry_hub/northbound/gateway.py +310 -0
- reticulum_telemetry_hub/northbound/internal_adapter.py +302 -0
- reticulum_telemetry_hub/northbound/models.py +213 -0
- reticulum_telemetry_hub/northbound/routes_chat.py +123 -0
- reticulum_telemetry_hub/northbound/routes_files.py +119 -0
- reticulum_telemetry_hub/northbound/routes_rest.py +345 -0
- reticulum_telemetry_hub/northbound/routes_subscribers.py +150 -0
- reticulum_telemetry_hub/northbound/routes_topics.py +178 -0
- reticulum_telemetry_hub/northbound/routes_ws.py +107 -0
- reticulum_telemetry_hub/northbound/serializers.py +72 -0
- reticulum_telemetry_hub/northbound/services.py +373 -0
- reticulum_telemetry_hub/northbound/websocket.py +855 -0
- reticulum_telemetry_hub/reticulum_server/__main__.py +2237 -0
- reticulum_telemetry_hub/reticulum_server/command_manager.py +1268 -0
- reticulum_telemetry_hub/reticulum_server/command_text.py +399 -0
- reticulum_telemetry_hub/reticulum_server/constants.py +1 -0
- reticulum_telemetry_hub/reticulum_server/event_log.py +357 -0
- reticulum_telemetry_hub/reticulum_server/internal_adapter.py +358 -0
- reticulum_telemetry_hub/reticulum_server/outbound_queue.py +312 -0
- reticulum_telemetry_hub/reticulum_server/services.py +422 -0
- reticulumtelemetryhub-0.143.0.dist-info/METADATA +181 -0
- reticulumtelemetryhub-0.143.0.dist-info/RECORD +97 -0
- {reticulumtelemetryhub-0.1.0.dist-info → reticulumtelemetryhub-0.143.0.dist-info}/WHEEL +1 -1
- reticulumtelemetryhub-0.143.0.dist-info/licenses/LICENSE +277 -0
- lxmf_telemetry/model/fields/field_telemetry_stream.py +0 -7
- lxmf_telemetry/model/persistance/__init__.py +0 -3
- lxmf_telemetry/model/persistance/sensors/location.py +0 -69
- lxmf_telemetry/model/persistance/sensors/magnetic_field.py +0 -36
- lxmf_telemetry/model/persistance/sensors/sensor.py +0 -44
- lxmf_telemetry/model/persistance/sensors/sensor_enum.py +0 -24
- lxmf_telemetry/model/persistance/sensors/sensor_mapping.py +0 -9
- lxmf_telemetry/telemetry_controller.py +0 -124
- reticulum_server/main.py +0 -182
- reticulumtelemetryhub-0.1.0.dist-info/METADATA +0 -15
- reticulumtelemetryhub-0.1.0.dist-info/RECORD +0 -19
- {lxmf_telemetry → reticulum_telemetry_hub}/__init__.py +0 -0
- {lxmf_telemetry/model/persistance/sensors → reticulum_telemetry_hub/lxmf_telemetry}/__init__.py +0 -0
- {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)
|