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,119 @@
|
|
|
1
|
+
"""File and image routes for the northbound API."""
|
|
2
|
+
# pylint: disable=import-error
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
from fastapi import HTTPException
|
|
8
|
+
from fastapi import status
|
|
9
|
+
from fastapi.responses import FileResponse
|
|
10
|
+
|
|
11
|
+
from reticulum_telemetry_hub.api.service import ReticulumTelemetryHubAPI
|
|
12
|
+
|
|
13
|
+
from .services import NorthboundServices
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register_file_routes(
|
|
17
|
+
app: FastAPI,
|
|
18
|
+
*,
|
|
19
|
+
services: NorthboundServices,
|
|
20
|
+
api: ReticulumTelemetryHubAPI,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Register file and image routes on the FastAPI app.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
app (FastAPI): FastAPI application instance.
|
|
26
|
+
services (NorthboundServices): Aggregated services.
|
|
27
|
+
api (ReticulumTelemetryHubAPI): API service instance.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
None: Routes are registered on the application.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
@app.get("/File")
|
|
34
|
+
def list_files() -> list[dict]:
|
|
35
|
+
"""List stored files.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
list[dict]: File attachment entries.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
return [attachment.to_dict() for attachment in services.list_files()]
|
|
42
|
+
|
|
43
|
+
@app.get("/File/{file_id}")
|
|
44
|
+
def retrieve_file(file_id: int) -> dict:
|
|
45
|
+
"""Retrieve file metadata by ID.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
file_id (int): File record identifier.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
dict: File attachment payload.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
attachment = api.retrieve_file(file_id)
|
|
56
|
+
except KeyError as exc:
|
|
57
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
|
58
|
+
return attachment.to_dict()
|
|
59
|
+
|
|
60
|
+
@app.get("/File/{file_id}/raw")
|
|
61
|
+
def retrieve_file_raw(file_id: int) -> FileResponse:
|
|
62
|
+
"""Return raw file bytes by ID.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
file_id (int): File record identifier.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
FileResponse: File response payload.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
attachment = api.retrieve_file(file_id)
|
|
73
|
+
except KeyError as exc:
|
|
74
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
|
75
|
+
return FileResponse(path=attachment.path, media_type=attachment.media_type)
|
|
76
|
+
|
|
77
|
+
@app.get("/Image")
|
|
78
|
+
def list_images() -> list[dict]:
|
|
79
|
+
"""List stored images.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
list[dict]: Image attachment entries.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
return [attachment.to_dict() for attachment in services.list_images()]
|
|
86
|
+
|
|
87
|
+
@app.get("/Image/{file_id}")
|
|
88
|
+
def retrieve_image(file_id: int) -> dict:
|
|
89
|
+
"""Retrieve image metadata by ID.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
file_id (int): Image record identifier.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
dict: Image attachment payload.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
attachment = api.retrieve_image(file_id)
|
|
100
|
+
except KeyError as exc:
|
|
101
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
|
102
|
+
return attachment.to_dict()
|
|
103
|
+
|
|
104
|
+
@app.get("/Image/{file_id}/raw")
|
|
105
|
+
def retrieve_image_raw(file_id: int) -> FileResponse:
|
|
106
|
+
"""Return raw image bytes by ID.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
file_id (int): Image record identifier.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
FileResponse: Image response payload.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
attachment = api.retrieve_image(file_id)
|
|
117
|
+
except KeyError as exc:
|
|
118
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
|
119
|
+
return FileResponse(path=attachment.path, media_type=attachment.media_type)
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""Core REST routes for the northbound API."""
|
|
2
|
+
# pylint: disable=import-error
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Callable
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from fastapi import Body
|
|
11
|
+
from fastapi import Depends
|
|
12
|
+
from fastapi import FastAPI
|
|
13
|
+
from fastapi import HTTPException
|
|
14
|
+
from fastapi import Query
|
|
15
|
+
from fastapi import Response
|
|
16
|
+
from fastapi import status
|
|
17
|
+
|
|
18
|
+
from reticulum_telemetry_hub.api.service import ReticulumTelemetryHubAPI
|
|
19
|
+
from reticulum_telemetry_hub.lxmf_telemetry.telemetry_controller import (
|
|
20
|
+
TelemetryController,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from .models import ConfigRollbackPayload
|
|
24
|
+
from .models import MessagePayload
|
|
25
|
+
from .services import NorthboundServices
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def register_core_routes(
|
|
29
|
+
app: FastAPI,
|
|
30
|
+
*,
|
|
31
|
+
services: NorthboundServices,
|
|
32
|
+
api: ReticulumTelemetryHubAPI,
|
|
33
|
+
telemetry_controller: TelemetryController,
|
|
34
|
+
require_protected: Callable[[], None],
|
|
35
|
+
resolve_openapi_spec: Callable[[], Optional[Path]],
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Register core REST routes on the FastAPI app.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
app (FastAPI): FastAPI application instance.
|
|
41
|
+
services (NorthboundServices): Aggregated services.
|
|
42
|
+
api (ReticulumTelemetryHubAPI): API service instance.
|
|
43
|
+
telemetry_controller (TelemetryController): Telemetry controller instance.
|
|
44
|
+
require_protected (Callable[[], None]): Dependency for protected routes.
|
|
45
|
+
resolve_openapi_spec (Callable[[], Optional[Path]]): OpenAPI spec resolver.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
None: Routes are registered on the application.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@app.get("/openapi.yaml", include_in_schema=False)
|
|
52
|
+
def openapi_yaml() -> Response:
|
|
53
|
+
"""Return the OpenAPI YAML file if available.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Response: YAML content response.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
spec_path = resolve_openapi_spec()
|
|
60
|
+
if not spec_path:
|
|
61
|
+
raise HTTPException(
|
|
62
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
63
|
+
detail="OpenAPI spec not found",
|
|
64
|
+
)
|
|
65
|
+
return Response(spec_path.read_text(encoding="utf-8"), media_type="application/yaml")
|
|
66
|
+
|
|
67
|
+
@app.get("/Help")
|
|
68
|
+
def get_help_text() -> Response:
|
|
69
|
+
"""Return the list of supported commands.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Response: Plain text command list.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
return Response(services.help_text(), media_type="text/plain")
|
|
76
|
+
|
|
77
|
+
@app.get("/Examples")
|
|
78
|
+
def get_examples_text() -> Response:
|
|
79
|
+
"""Return command payload examples.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Response: Plain text examples.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
return Response(services.examples_text(), media_type="text/plain")
|
|
86
|
+
|
|
87
|
+
@app.get("/Status", dependencies=[Depends(require_protected)])
|
|
88
|
+
def get_status() -> dict:
|
|
89
|
+
"""Return dashboard status metrics.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
dict: Status payload.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
return services.status_snapshot()
|
|
96
|
+
|
|
97
|
+
@app.get("/Events", dependencies=[Depends(require_protected)])
|
|
98
|
+
def get_events() -> list[dict]:
|
|
99
|
+
"""Return recent events.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
list[dict]: Event entries.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
return services.list_events()
|
|
106
|
+
|
|
107
|
+
@app.get("/Telemetry")
|
|
108
|
+
def get_telemetry(
|
|
109
|
+
since: int = Query(alias="since"),
|
|
110
|
+
topic_id: Optional[str] = Query(default=None, alias="topic_id"),
|
|
111
|
+
) -> dict:
|
|
112
|
+
"""Return telemetry entries since a timestamp.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
since (int): Unix timestamp (seconds) for the earliest entries.
|
|
116
|
+
topic_id (Optional[str]): Optional topic filter.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
dict: Telemetry response payload.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
entries = services.telemetry_entries(since=since, topic_id=topic_id)
|
|
124
|
+
except KeyError as exc:
|
|
125
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
|
126
|
+
except ValueError as exc:
|
|
127
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
128
|
+
return {"entries": entries}
|
|
129
|
+
|
|
130
|
+
@app.get("/Config", dependencies=[Depends(require_protected)])
|
|
131
|
+
def get_config() -> Response:
|
|
132
|
+
"""Return the config.ini payload.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Response: Plain text configuration payload.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
return Response(api.get_config_text(), media_type="text/plain")
|
|
139
|
+
|
|
140
|
+
@app.put("/Config", dependencies=[Depends(require_protected)])
|
|
141
|
+
def apply_config(config_text: str = Body(media_type="text/plain")) -> dict:
|
|
142
|
+
"""Apply a new config.ini payload.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
config_text (str): Raw config.ini payload.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
dict: Apply result payload.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
result = api.apply_config_text(config_text)
|
|
153
|
+
except ValueError as exc:
|
|
154
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
155
|
+
services.record_event("config_applied", "Configuration applied")
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
@app.post("/Config/Validate", dependencies=[Depends(require_protected)])
|
|
159
|
+
def validate_config(config_text: str = Body(media_type="text/plain")) -> dict:
|
|
160
|
+
"""Validate a config.ini payload.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
config_text (str): Raw config.ini payload.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
dict: Validation result payload.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
return api.validate_config_text(config_text)
|
|
170
|
+
|
|
171
|
+
@app.post("/Config/Rollback", dependencies=[Depends(require_protected)])
|
|
172
|
+
def rollback_config(payload: Optional[ConfigRollbackPayload] = Body(default=None)) -> dict:
|
|
173
|
+
"""Rollback config.ini using a backup path.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
payload (Optional[ConfigRollbackPayload]): Rollback payload.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
dict: Rollback result payload.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
backup_path = payload.backup_path if payload else None
|
|
183
|
+
result = api.rollback_config_text(backup_path=backup_path)
|
|
184
|
+
services.record_event("config_rolled_back", "Configuration rolled back")
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
@app.post("/Command/FlushTelemetry", dependencies=[Depends(require_protected)])
|
|
188
|
+
def flush_telemetry() -> dict:
|
|
189
|
+
"""Flush stored telemetry entries.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
dict: Flush result payload.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
deleted = telemetry_controller.clear_telemetry()
|
|
196
|
+
services.record_event("telemetry_flushed", f"Telemetry flushed ({deleted} rows)")
|
|
197
|
+
return {"deleted": deleted}
|
|
198
|
+
|
|
199
|
+
@app.post("/Command/ReloadConfig", dependencies=[Depends(require_protected)])
|
|
200
|
+
def reload_config() -> dict:
|
|
201
|
+
"""Reload config.ini from disk.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
dict: Reloaded configuration payload.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
info = services.reload_config()
|
|
208
|
+
services.record_event("config_reloaded", "Configuration reloaded")
|
|
209
|
+
return info.to_dict()
|
|
210
|
+
|
|
211
|
+
@app.post("/Message", dependencies=[Depends(require_protected)])
|
|
212
|
+
def send_message(payload: MessagePayload) -> dict:
|
|
213
|
+
"""Send a message into the Reticulum Telemetry Hub."""
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
services.send_message(
|
|
217
|
+
payload.content,
|
|
218
|
+
topic_id=payload.topic_id,
|
|
219
|
+
destination=payload.destination,
|
|
220
|
+
)
|
|
221
|
+
except RuntimeError as exc:
|
|
222
|
+
raise HTTPException(
|
|
223
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
224
|
+
detail=str(exc),
|
|
225
|
+
) from exc
|
|
226
|
+
services.record_event(
|
|
227
|
+
"message_sent",
|
|
228
|
+
"Northbound message dispatched",
|
|
229
|
+
metadata={
|
|
230
|
+
"topic_id": payload.topic_id,
|
|
231
|
+
"destination": payload.destination,
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
return {"sent": True}
|
|
235
|
+
|
|
236
|
+
@app.get("/Command/DumpRouting", dependencies=[Depends(require_protected)])
|
|
237
|
+
def dump_routing() -> dict:
|
|
238
|
+
"""Return connected destination hashes.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
dict: Routing summary payload.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
return services.dump_routing()
|
|
245
|
+
|
|
246
|
+
@app.get("/Identities", dependencies=[Depends(require_protected)])
|
|
247
|
+
def list_identities() -> list[dict]:
|
|
248
|
+
"""Return identity moderation status entries.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
list[dict]: Identity status entries.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
return [status_entry.to_dict() for status_entry in services.list_identity_statuses()]
|
|
255
|
+
|
|
256
|
+
@app.post("/Client/{identity}/Ban", dependencies=[Depends(require_protected)])
|
|
257
|
+
def ban_identity(identity: str) -> dict:
|
|
258
|
+
"""Ban an identity.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
identity (str): Identity to ban.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
dict: Updated identity status.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
status_entry = api.ban_identity(identity)
|
|
268
|
+
services.record_event("identity_banned", f"Identity banned: {identity}")
|
|
269
|
+
return status_entry.to_dict()
|
|
270
|
+
|
|
271
|
+
@app.post("/Client/{identity}/Unban", dependencies=[Depends(require_protected)])
|
|
272
|
+
def unban_identity(identity: str) -> dict:
|
|
273
|
+
"""Unban an identity.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
identity (str): Identity to unban.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
dict: Updated identity status.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
status_entry = api.unban_identity(identity)
|
|
283
|
+
services.record_event("identity_unbanned", f"Identity unbanned: {identity}")
|
|
284
|
+
return status_entry.to_dict()
|
|
285
|
+
|
|
286
|
+
@app.post("/Client/{identity}/Blackhole", dependencies=[Depends(require_protected)])
|
|
287
|
+
def blackhole_identity(identity: str) -> dict:
|
|
288
|
+
"""Blackhole an identity.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
identity (str): Identity to blackhole.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
dict: Updated identity status.
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
status_entry = api.blackhole_identity(identity)
|
|
298
|
+
services.record_event("identity_blackholed", f"Identity blackholed: {identity}")
|
|
299
|
+
return status_entry.to_dict()
|
|
300
|
+
|
|
301
|
+
@app.post("/RTH")
|
|
302
|
+
def rth_join(identity: str = Query(alias="identity")) -> bool:
|
|
303
|
+
"""Join the Reticulum Telemetry Hub.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
identity (str): Identity to register.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
bool: ``True`` when the identity is recorded.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
return api.join(identity)
|
|
313
|
+
|
|
314
|
+
@app.put("/RTH")
|
|
315
|
+
def rth_leave(identity: str = Query(alias="identity")) -> bool:
|
|
316
|
+
"""Leave the Reticulum Telemetry Hub.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
identity (str): Identity to remove.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
bool: ``True`` when the identity is removed.
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
return api.leave(identity)
|
|
326
|
+
|
|
327
|
+
@app.get("/Client", dependencies=[Depends(require_protected)])
|
|
328
|
+
def list_clients() -> list[dict]:
|
|
329
|
+
"""List clients.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
list[dict]: Client entries.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
return [client.to_dict() for client in services.list_clients()]
|
|
336
|
+
|
|
337
|
+
@app.get("/api/v1/app/info")
|
|
338
|
+
def app_info() -> dict:
|
|
339
|
+
"""Return application metadata.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
dict: Application info payload.
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
return services.app_info().to_dict()
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Subscriber routes for the northbound API."""
|
|
2
|
+
# pylint: disable=import-error
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
from fastapi import Depends
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
from fastapi import HTTPException
|
|
11
|
+
from fastapi import Query
|
|
12
|
+
from fastapi import status
|
|
13
|
+
|
|
14
|
+
from reticulum_telemetry_hub.api.service import ReticulumTelemetryHubAPI
|
|
15
|
+
|
|
16
|
+
from .models import SubscriberPayload
|
|
17
|
+
from .serializers import build_subscriber
|
|
18
|
+
from .serializers import serialize_subscriber
|
|
19
|
+
from .services import NorthboundServices
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def register_subscriber_routes(
|
|
23
|
+
app: FastAPI,
|
|
24
|
+
*,
|
|
25
|
+
services: NorthboundServices,
|
|
26
|
+
api: ReticulumTelemetryHubAPI,
|
|
27
|
+
require_protected: Callable[[], None],
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Register subscriber routes on the FastAPI app.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
app (FastAPI): FastAPI application instance.
|
|
33
|
+
services (NorthboundServices): Aggregated services.
|
|
34
|
+
api (ReticulumTelemetryHubAPI): API service instance.
|
|
35
|
+
require_protected (Callable[[], None]): Dependency for protected routes.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
None: Routes are registered on the application.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@app.get("/Subscriber/{subscriber_id}", dependencies=[Depends(require_protected)])
|
|
42
|
+
def retrieve_subscriber(subscriber_id: str) -> dict:
|
|
43
|
+
"""Retrieve a subscriber by ID.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
subscriber_id (str): Subscriber identifier.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
dict: Subscriber payload.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
subscriber = api.retrieve_subscriber(subscriber_id)
|
|
54
|
+
except KeyError as exc:
|
|
55
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
|
56
|
+
return serialize_subscriber(subscriber)
|
|
57
|
+
|
|
58
|
+
@app.post("/Subscriber/Add", dependencies=[Depends(require_protected)])
|
|
59
|
+
def add_subscriber(payload: SubscriberPayload) -> dict:
|
|
60
|
+
"""Add a subscriber mapping.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
payload (SubscriberPayload): Subscriber payload.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
dict: Subscriber payload.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
subscriber = build_subscriber(payload)
|
|
70
|
+
try:
|
|
71
|
+
created = api.add_subscriber(subscriber)
|
|
72
|
+
except ValueError as exc:
|
|
73
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
74
|
+
services.record_event("subscriber_added", f"Subscriber added: {created.subscriber_id}")
|
|
75
|
+
return serialize_subscriber(created)
|
|
76
|
+
|
|
77
|
+
@app.post("/Subscriber", dependencies=[Depends(require_protected)])
|
|
78
|
+
def create_subscriber(payload: SubscriberPayload) -> dict:
|
|
79
|
+
"""Create a subscriber.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
payload (SubscriberPayload): Subscriber payload.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
dict: Subscriber payload.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
subscriber = build_subscriber(payload)
|
|
89
|
+
try:
|
|
90
|
+
created = api.create_subscriber(subscriber)
|
|
91
|
+
except ValueError as exc:
|
|
92
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
93
|
+
services.record_event("subscriber_created", f"Subscriber created: {created.subscriber_id}")
|
|
94
|
+
return serialize_subscriber(created)
|
|
95
|
+
|
|
96
|
+
@app.delete("/Subscriber", dependencies=[Depends(require_protected)])
|
|
97
|
+
def delete_subscriber(subscriber_id: str = Query(alias="id")) -> dict:
|
|
98
|
+
"""Delete a subscriber.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
subscriber_id (str): Subscriber identifier.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
dict: Deleted subscriber payload.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
subscriber = api.delete_subscriber(subscriber_id)
|
|
109
|
+
except KeyError as exc:
|
|
110
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
|
111
|
+
services.record_event("subscriber_deleted", f"Subscriber deleted: {subscriber.subscriber_id}")
|
|
112
|
+
return serialize_subscriber(subscriber)
|
|
113
|
+
|
|
114
|
+
@app.get("/Subscriber", dependencies=[Depends(require_protected)])
|
|
115
|
+
def list_subscribers() -> list[dict]:
|
|
116
|
+
"""List subscribers.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
list[dict]: Subscriber entries.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
return [serialize_subscriber(subscriber) for subscriber in services.list_subscribers()]
|
|
123
|
+
|
|
124
|
+
@app.patch("/Subscriber", dependencies=[Depends(require_protected)])
|
|
125
|
+
def patch_subscriber(payload: SubscriberPayload) -> dict:
|
|
126
|
+
"""Update a subscriber.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
payload (SubscriberPayload): Subscriber update payload.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
dict: Updated subscriber payload.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
if not payload.subscriber_id:
|
|
136
|
+
raise HTTPException(
|
|
137
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
138
|
+
detail="SubscriberID is required",
|
|
139
|
+
)
|
|
140
|
+
try:
|
|
141
|
+
subscriber = api.patch_subscriber(
|
|
142
|
+
payload.subscriber_id,
|
|
143
|
+
**payload.model_dump(by_alias=True, exclude_unset=True),
|
|
144
|
+
)
|
|
145
|
+
except ValueError as exc:
|
|
146
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
147
|
+
except KeyError as exc:
|
|
148
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
|
149
|
+
services.record_event("subscriber_updated", f"Subscriber updated: {subscriber.subscriber_id}")
|
|
150
|
+
return serialize_subscriber(subscriber)
|