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,23 @@
1
+ """High level Python API mirroring the ReticulumTelemetryHub OpenAPI spec."""
2
+
3
+ from .models import ChatAttachment
4
+ from .models import ChatMessage
5
+ from .models import Client
6
+ from .models import FileAttachment
7
+ from .models import IdentityStatus
8
+ from .models import ReticulumInfo
9
+ from .models import Subscriber
10
+ from .models import Topic
11
+ from .service import ReticulumTelemetryHubAPI
12
+
13
+ __all__ = [
14
+ "Topic",
15
+ "Subscriber",
16
+ "Client",
17
+ "FileAttachment",
18
+ "ChatAttachment",
19
+ "ChatMessage",
20
+ "IdentityStatus",
21
+ "ReticulumInfo",
22
+ "ReticulumTelemetryHubAPI",
23
+ ]
@@ -0,0 +1,323 @@
1
+ """Data models for the Reticulum Telemetry Hub API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict
6
+ from dataclasses import dataclass
7
+ from dataclasses import field
8
+ from datetime import datetime
9
+ from datetime import timedelta
10
+ from datetime import timezone
11
+ from pathlib import Path
12
+ from typing import Any
13
+ from typing import Dict
14
+ from typing import List
15
+ from typing import Optional
16
+
17
+
18
+ def _now() -> datetime:
19
+ """Return the current UTC timestamp with timezone information."""
20
+
21
+ return datetime.now(timezone.utc)
22
+
23
+
24
+ def _json_safe_key(key: Any) -> str:
25
+ """Return a JSON-safe dictionary key."""
26
+
27
+ if isinstance(key, (bytes, bytearray, memoryview)):
28
+ return bytes(key).hex()
29
+ if key is None:
30
+ return "null"
31
+ return str(key)
32
+
33
+
34
+ def _json_safe(value: Any) -> Any:
35
+ """Return a JSON-safe version of ``value``."""
36
+
37
+ if isinstance(value, dict):
38
+ return {_json_safe_key(k): _json_safe(v) for k, v in value.items()}
39
+ if isinstance(value, (list, tuple, set)):
40
+ return [_json_safe(item) for item in value]
41
+ if isinstance(value, (bytes, bytearray, memoryview)):
42
+ return bytes(value).hex()
43
+ if isinstance(value, datetime):
44
+ return value.isoformat()
45
+ if isinstance(value, Path):
46
+ return str(value)
47
+ if isinstance(value, (str, int, float, bool)) or value is None:
48
+ return value
49
+ return str(value)
50
+
51
+
52
+ @dataclass
53
+ class Topic:
54
+ """Topic subscription metadata for the Reticulum Telemetry Hub."""
55
+
56
+ topic_name: str
57
+ topic_path: str
58
+ topic_description: str = ""
59
+ topic_id: Optional[str] = None
60
+
61
+ def to_dict(self) -> dict:
62
+ """Serialize the topic into the external API schema."""
63
+
64
+ return {
65
+ "TopicID": self.topic_id,
66
+ "TopicName": self.topic_name,
67
+ "TopicPath": self.topic_path,
68
+ "TopicDescription": self.topic_description,
69
+ }
70
+
71
+ @classmethod
72
+ def from_dict(cls, data: Dict[str, Any]) -> "Topic":
73
+ """Create a Topic from a dictionary with flexible casing.
74
+
75
+ Args:
76
+ data (Dict[str, Any]): Source mapping using either title-case or
77
+ snake_case keys.
78
+
79
+ Returns:
80
+ Topic: Parsed topic instance with defaults for missing values.
81
+ """
82
+
83
+ return cls(
84
+ topic_name=data.get("TopicName") or data.get("topic_name") or "",
85
+ topic_path=data.get("TopicPath") or data.get("topic_path") or "",
86
+ topic_description=data.get("TopicDescription")
87
+ or data.get("topic_description")
88
+ or "",
89
+ topic_id=data.get("TopicID") or data.get("topic_id"),
90
+ )
91
+
92
+
93
+ @dataclass
94
+ class Subscriber:
95
+ """Subscription details linking destinations to topics."""
96
+
97
+ destination: str
98
+ topic_id: Optional[str] = None
99
+ reject_tests: Optional[int] = None
100
+ metadata: Dict[str, Any] = field(default_factory=dict)
101
+ subscriber_id: Optional[str] = None
102
+
103
+ def to_dict(self) -> dict:
104
+ """Serialize the subscriber into the external API schema."""
105
+
106
+ return {
107
+ "SubscriberID": self.subscriber_id,
108
+ "Destination": self.destination,
109
+ "TopicID": self.topic_id,
110
+ "RejectTests": self.reject_tests,
111
+ "Metadata": _json_safe(self.metadata or None),
112
+ }
113
+
114
+ @classmethod
115
+ def from_dict(cls, data: Dict[str, Any]) -> "Subscriber":
116
+ """Create a Subscriber from a dictionary with flexible casing.
117
+
118
+ Args:
119
+ data (Dict[str, Any]): Source mapping using either title-case or
120
+ snake_case keys.
121
+
122
+ Returns:
123
+ Subscriber: Parsed subscriber instance.
124
+ """
125
+
126
+ reject_tests = None
127
+ if "RejectTests" in data:
128
+ reject_tests = data.get("RejectTests")
129
+ elif "reject_tests" in data:
130
+ reject_tests = data.get("reject_tests")
131
+
132
+ return cls(
133
+ destination=data.get("Destination") or data.get("destination") or "",
134
+ topic_id=data.get("TopicID") or data.get("topic_id"),
135
+ reject_tests=reject_tests,
136
+ metadata=data.get("Metadata") or data.get("metadata") or {},
137
+ subscriber_id=data.get("SubscriberID") or data.get("subscriber_id"),
138
+ )
139
+
140
+
141
+ @dataclass
142
+ class Client:
143
+ """Connected client state and metadata."""
144
+
145
+ identity: str
146
+ last_seen: datetime = field(default_factory=_now)
147
+ display_name: Optional[str] = None
148
+ metadata: Dict[str, Any] = field(default_factory=dict)
149
+
150
+ def touch(self) -> None:
151
+ """Update ``last_seen`` to the latest timestamp in UTC."""
152
+
153
+ now = _now()
154
+ if now <= self.last_seen:
155
+ now = self.last_seen + timedelta(microseconds=1)
156
+ self.last_seen = now
157
+
158
+ def to_dict(self) -> dict:
159
+ """Serialize the client into primitive types for JSON responses."""
160
+
161
+ data = asdict(self)
162
+ data["last_seen"] = self.last_seen.isoformat()
163
+ data["metadata"] = _json_safe(self.metadata or {})
164
+ return data
165
+
166
+
167
+ @dataclass
168
+ # pylint: disable=too-many-instance-attributes
169
+ class ReticulumInfo:
170
+ """Application and environment metadata exposed by the API."""
171
+
172
+ is_transport_enabled: bool
173
+ is_connected_to_shared_instance: bool
174
+ reticulum_config_path: str
175
+ database_path: str
176
+ storage_path: str
177
+ file_storage_path: str
178
+ image_storage_path: str
179
+ app_name: str
180
+ rns_version: str
181
+ lxmf_version: str
182
+ app_version: str
183
+ app_description: str
184
+
185
+ def to_dict(self) -> dict:
186
+ """Serialize the info model to a dictionary."""
187
+
188
+ data = asdict(self)
189
+ data["name"] = self.app_name
190
+ data["version"] = self.app_version
191
+ data["description"] = self.app_description
192
+ data["storage_paths"] = {
193
+ "storage": self.storage_path,
194
+ "database": self.database_path,
195
+ "reticulum_config": self.reticulum_config_path,
196
+ "files": self.file_storage_path,
197
+ "images": self.image_storage_path,
198
+ }
199
+ return data
200
+
201
+
202
+ @dataclass
203
+ class FileAttachment:
204
+ """Metadata for files or images stored by the hub."""
205
+
206
+ name: str
207
+ path: str
208
+ category: str
209
+ size: int
210
+ media_type: Optional[str] = None
211
+ topic_id: Optional[str] = None
212
+ created_at: datetime = field(default_factory=_now)
213
+ updated_at: datetime = field(default_factory=_now)
214
+ file_id: Optional[int] = None
215
+
216
+ def to_dict(self) -> dict:
217
+ """Return a serialization friendly representation."""
218
+
219
+ return {
220
+ "FileID": self.file_id,
221
+ "Name": self.name,
222
+ "Path": self.path,
223
+ "Category": self.category,
224
+ "MediaType": self.media_type,
225
+ "TopicID": self.topic_id,
226
+ "Size": self.size,
227
+ "CreatedAt": self.created_at.isoformat(),
228
+ "UpdatedAt": self.updated_at.isoformat(),
229
+ }
230
+
231
+
232
+ @dataclass
233
+ class ChatAttachment:
234
+ """Attachment metadata associated with a chat message."""
235
+
236
+ file_id: int
237
+ category: str
238
+ name: str
239
+ size: int
240
+ media_type: Optional[str] = None
241
+
242
+ def to_dict(self) -> dict:
243
+ """Serialize the attachment reference for API responses."""
244
+
245
+ return {
246
+ "FileID": self.file_id,
247
+ "Category": self.category,
248
+ "Name": self.name,
249
+ "Size": self.size,
250
+ "MediaType": self.media_type,
251
+ }
252
+
253
+ @classmethod
254
+ def from_dict(cls, data: Dict[str, Any]) -> "ChatAttachment":
255
+ """Create a ChatAttachment from a serialized dictionary."""
256
+
257
+ return cls(
258
+ file_id=int(data.get("FileID") or data.get("file_id") or 0),
259
+ category=str(data.get("Category") or data.get("category") or ""),
260
+ name=str(data.get("Name") or data.get("name") or ""),
261
+ size=int(data.get("Size") or data.get("size") or 0),
262
+ media_type=data.get("MediaType") or data.get("media_type"),
263
+ )
264
+
265
+
266
+ @dataclass
267
+ class ChatMessage:
268
+ """Chat message metadata persisted by the hub."""
269
+
270
+ direction: str
271
+ scope: str
272
+ state: str
273
+ content: str
274
+ source: Optional[str] = None
275
+ destination: Optional[str] = None
276
+ topic_id: Optional[str] = None
277
+ attachments: List[ChatAttachment] = field(default_factory=list)
278
+ created_at: datetime = field(default_factory=_now)
279
+ updated_at: datetime = field(default_factory=_now)
280
+ message_id: Optional[str] = None
281
+
282
+ def to_dict(self) -> dict:
283
+ """Serialize the chat message into API-friendly fields."""
284
+
285
+ return {
286
+ "MessageID": self.message_id,
287
+ "Direction": self.direction,
288
+ "Scope": self.scope,
289
+ "State": self.state,
290
+ "Content": self.content,
291
+ "Source": self.source,
292
+ "Destination": self.destination,
293
+ "TopicID": self.topic_id,
294
+ "Attachments": [attachment.to_dict() for attachment in self.attachments],
295
+ "CreatedAt": self.created_at.isoformat(),
296
+ "UpdatedAt": self.updated_at.isoformat(),
297
+ }
298
+
299
+
300
+ @dataclass
301
+ class IdentityStatus:
302
+ """Current identity status for admin tooling."""
303
+
304
+ identity: str
305
+ status: str
306
+ last_seen: Optional[datetime] = None
307
+ display_name: Optional[str] = None
308
+ metadata: Dict[str, Any] = field(default_factory=dict)
309
+ is_banned: bool = False
310
+ is_blackholed: bool = False
311
+
312
+ def to_dict(self) -> dict:
313
+ """Serialize the identity status into JSON-friendly values."""
314
+
315
+ return {
316
+ "Identity": self.identity,
317
+ "DisplayName": self.display_name,
318
+ "Status": self.status,
319
+ "LastSeen": self.last_seen.isoformat() if self.last_seen else None,
320
+ "Metadata": _json_safe(self.metadata or {}),
321
+ "IsBanned": self.is_banned,
322
+ "IsBlackholed": self.is_blackholed,
323
+ }