sp-rtk-base 0.2.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.
- sp_rtk_base/__init__.py +5 -0
- sp_rtk_base/api/__init__.py +3 -0
- sp_rtk_base/api/config.py +99 -0
- sp_rtk_base/api/destinations.py +161 -0
- sp_rtk_base/api/device.py +489 -0
- sp_rtk_base/api/events.py +74 -0
- sp_rtk_base/api/health.py +22 -0
- sp_rtk_base/api/metrics.py +67 -0
- sp_rtk_base/api/relay.py +135 -0
- sp_rtk_base/api/settings.py +124 -0
- sp_rtk_base/app.py +103 -0
- sp_rtk_base/cli/__init__.py +1 -0
- sp_rtk_base/cli/config_audit.py +601 -0
- sp_rtk_base/main.py +35 -0
- sp_rtk_base/models/__init__.py +3 -0
- sp_rtk_base/models/api_models.py +198 -0
- sp_rtk_base/models/config_models.py +300 -0
- sp_rtk_base/models/device_models.py +405 -0
- sp_rtk_base/py.typed +0 -0
- sp_rtk_base/services/__init__.py +123 -0
- sp_rtk_base/services/config_service.py +322 -0
- sp_rtk_base/services/device_service.py +473 -0
- sp_rtk_base/services/drivers/__init__.py +88 -0
- sp_rtk_base/services/drivers/base.py +296 -0
- sp_rtk_base/services/drivers/ublox.py +1029 -0
- sp_rtk_base/services/event_bridge.py +198 -0
- sp_rtk_base/services/metrics_service.py +182 -0
- sp_rtk_base/services/relay_service.py +233 -0
- sp_rtk_base/ui/__init__.py +3 -0
- sp_rtk_base/ui/components/__init__.py +7 -0
- sp_rtk_base/ui/components/status_card.py +37 -0
- sp_rtk_base/ui/layout.py +153 -0
- sp_rtk_base/ui/pages/__init__.py +3 -0
- sp_rtk_base/ui/pages/dashboard.py +412 -0
- sp_rtk_base/ui/pages/gps_config.py +647 -0
- sp_rtk_base/ui/pages/input.py +637 -0
- sp_rtk_base/ui/pages/outputs.py +402 -0
- sp_rtk_base/ui/pages/settings.py +111 -0
- sp_rtk_base/ui/pages/survey.py +1085 -0
- sp_rtk_base/ui/validators.py +109 -0
- sp_rtk_base-0.2.0.dist-info/METADATA +306 -0
- sp_rtk_base-0.2.0.dist-info/RECORD +44 -0
- sp_rtk_base-0.2.0.dist-info/WHEEL +4 -0
- sp_rtk_base-0.2.0.dist-info/entry_points.txt +4 -0
sp_rtk_base/__init__.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Config import/export API endpoints.
|
|
2
|
+
|
|
3
|
+
Provides YAML-based configuration export (download) and import (upload)
|
|
4
|
+
for backing up and restoring the full application configuration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
from fastapi import APIRouter, Depends, HTTPException, UploadFile
|
|
13
|
+
from fastapi.responses import Response
|
|
14
|
+
|
|
15
|
+
from sp_rtk_base.models.config_models import AppConfig
|
|
16
|
+
from sp_rtk_base.services import get_config_service
|
|
17
|
+
from sp_rtk_base.services.config_service import ConfigService
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
router = APIRouter(prefix="/api/config", tags=["config"])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.get("/export")
|
|
25
|
+
async def export_config(
|
|
26
|
+
config_svc: ConfigService = Depends(get_config_service),
|
|
27
|
+
) -> Response:
|
|
28
|
+
"""Export the full application configuration as a YAML file download.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
YAML file response with Content-Disposition attachment header.
|
|
32
|
+
"""
|
|
33
|
+
config = config_svc.get_config()
|
|
34
|
+
data = config.model_dump(mode="json", exclude_none=True)
|
|
35
|
+
yaml_text = yaml.dump(data, default_flow_style=False, sort_keys=False)
|
|
36
|
+
|
|
37
|
+
return Response(
|
|
38
|
+
content=yaml_text,
|
|
39
|
+
media_type="application/x-yaml",
|
|
40
|
+
headers={"Content-Disposition": "attachment; filename=sp-rtk-base-config.yaml"},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@router.post("/import")
|
|
45
|
+
async def import_config(
|
|
46
|
+
file: UploadFile,
|
|
47
|
+
config_svc: ConfigService = Depends(get_config_service),
|
|
48
|
+
) -> dict[str, str]:
|
|
49
|
+
"""Import application configuration from a YAML file upload.
|
|
50
|
+
|
|
51
|
+
Validates the uploaded YAML against the AppConfig schema before saving.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
file: The uploaded YAML configuration file.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Success message with destination/input counts.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
HTTPException: 400 if the file is not valid YAML or fails schema validation.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
content = await file.read()
|
|
64
|
+
text = content.decode("utf-8")
|
|
65
|
+
except Exception as exc:
|
|
66
|
+
raise HTTPException(
|
|
67
|
+
status_code=400, detail=f"Failed to read uploaded file: {exc}"
|
|
68
|
+
) from exc
|
|
69
|
+
|
|
70
|
+
if not text.strip():
|
|
71
|
+
raise HTTPException(status_code=400, detail="Uploaded file is empty")
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
data = yaml.safe_load(text)
|
|
75
|
+
except yaml.YAMLError as exc:
|
|
76
|
+
raise HTTPException(status_code=400, detail=f"Invalid YAML: {exc}") from exc
|
|
77
|
+
|
|
78
|
+
if not isinstance(data, dict):
|
|
79
|
+
raise HTTPException(
|
|
80
|
+
status_code=400, detail="YAML content must be a mapping (object)"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
config = AppConfig.model_validate(data)
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
raise HTTPException(
|
|
87
|
+
status_code=400, detail=f"Invalid configuration schema: {exc}"
|
|
88
|
+
) from exc
|
|
89
|
+
|
|
90
|
+
config_svc.save_config(config)
|
|
91
|
+
logger.info("Configuration imported successfully")
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"status": "ok",
|
|
95
|
+
"message": (
|
|
96
|
+
f"Configuration imported: {len(config.destinations)} destinations, "
|
|
97
|
+
f"input={'configured' if config.input else 'none'}"
|
|
98
|
+
),
|
|
99
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Destination management API endpoints.
|
|
2
|
+
|
|
3
|
+
Provides CRUD operations for relay destination profiles.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from fastapi import APIRouter, Depends
|
|
11
|
+
from fastapi.responses import JSONResponse
|
|
12
|
+
|
|
13
|
+
from sp_rtk_base.models.api_models import (
|
|
14
|
+
DestinationCreateRequest,
|
|
15
|
+
DestinationListResponse,
|
|
16
|
+
DestinationResponse,
|
|
17
|
+
DestinationUpdateRequest,
|
|
18
|
+
RelayActionResponse,
|
|
19
|
+
)
|
|
20
|
+
from sp_rtk_base.models.config_models import DestinationProfile, FilterProfile
|
|
21
|
+
from sp_rtk_base.services import get_config_service
|
|
22
|
+
from sp_rtk_base.services.config_service import ConfigService
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
router = APIRouter(prefix="/api/destinations", tags=["destinations"])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _profile_to_response(profile: DestinationProfile) -> DestinationResponse:
|
|
30
|
+
"""Convert a DestinationProfile to an API response model."""
|
|
31
|
+
return DestinationResponse(
|
|
32
|
+
name=profile.name,
|
|
33
|
+
type=profile.type,
|
|
34
|
+
enabled=profile.enabled,
|
|
35
|
+
config=profile.config,
|
|
36
|
+
filter=profile.filter.model_dump(),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@router.get("", response_model=DestinationListResponse)
|
|
41
|
+
async def list_destinations(
|
|
42
|
+
config_svc: ConfigService = Depends(get_config_service),
|
|
43
|
+
) -> DestinationListResponse:
|
|
44
|
+
"""List all configured destinations."""
|
|
45
|
+
destinations = config_svc.get_destinations()
|
|
46
|
+
items = [_profile_to_response(d) for d in destinations]
|
|
47
|
+
return DestinationListResponse(destinations=items, count=len(items))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@router.get("/{name}", response_model=DestinationResponse)
|
|
51
|
+
async def get_destination(
|
|
52
|
+
name: str,
|
|
53
|
+
config_svc: ConfigService = Depends(get_config_service),
|
|
54
|
+
) -> DestinationResponse | JSONResponse:
|
|
55
|
+
"""Get a single destination by name."""
|
|
56
|
+
dest = config_svc.get_destination(name)
|
|
57
|
+
if dest is None:
|
|
58
|
+
return JSONResponse(
|
|
59
|
+
status_code=404,
|
|
60
|
+
content={"status": "error", "message": f"Destination '{name}' not found"},
|
|
61
|
+
)
|
|
62
|
+
return _profile_to_response(dest)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@router.post("", response_model=DestinationResponse, status_code=201)
|
|
66
|
+
async def create_destination(
|
|
67
|
+
request: DestinationCreateRequest,
|
|
68
|
+
config_svc: ConfigService = Depends(get_config_service),
|
|
69
|
+
) -> DestinationResponse | JSONResponse:
|
|
70
|
+
"""Create a new destination profile.
|
|
71
|
+
|
|
72
|
+
The destination is saved to config but not added to the running
|
|
73
|
+
relay engine. Restart the relay or use the relay start endpoint
|
|
74
|
+
to apply changes.
|
|
75
|
+
"""
|
|
76
|
+
# Check for duplicate name
|
|
77
|
+
existing = config_svc.get_destination(request.name)
|
|
78
|
+
if existing is not None:
|
|
79
|
+
return JSONResponse(
|
|
80
|
+
status_code=409,
|
|
81
|
+
content={
|
|
82
|
+
"status": "error",
|
|
83
|
+
"message": f"Destination '{request.name}' already exists",
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
profile = DestinationProfile(
|
|
89
|
+
name=request.name,
|
|
90
|
+
type=request.type, # type: ignore[arg-type]
|
|
91
|
+
enabled=request.enabled,
|
|
92
|
+
config=request.config,
|
|
93
|
+
filter=FilterProfile.model_validate(request.filter),
|
|
94
|
+
)
|
|
95
|
+
config_svc.save_destination(profile)
|
|
96
|
+
logger.info("Created destination: %s", request.name)
|
|
97
|
+
return _profile_to_response(profile)
|
|
98
|
+
except Exception as exc:
|
|
99
|
+
return JSONResponse(
|
|
100
|
+
status_code=400,
|
|
101
|
+
content={"status": "error", "message": str(exc)},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@router.put("/{name}", response_model=DestinationResponse)
|
|
106
|
+
async def update_destination(
|
|
107
|
+
name: str,
|
|
108
|
+
request: DestinationUpdateRequest,
|
|
109
|
+
config_svc: ConfigService = Depends(get_config_service),
|
|
110
|
+
) -> DestinationResponse | JSONResponse:
|
|
111
|
+
"""Update an existing destination profile.
|
|
112
|
+
|
|
113
|
+
Only the provided fields are updated; unset fields retain
|
|
114
|
+
their current values.
|
|
115
|
+
"""
|
|
116
|
+
existing = config_svc.get_destination(name)
|
|
117
|
+
if existing is None:
|
|
118
|
+
return JSONResponse(
|
|
119
|
+
status_code=404,
|
|
120
|
+
content={"status": "error", "message": f"Destination '{name}' not found"},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
updated_data = existing.model_dump()
|
|
125
|
+
if request.enabled is not None:
|
|
126
|
+
updated_data["enabled"] = request.enabled
|
|
127
|
+
if request.config is not None:
|
|
128
|
+
updated_data["config"] = request.config
|
|
129
|
+
if request.filter is not None:
|
|
130
|
+
updated_data["filter"] = request.filter
|
|
131
|
+
|
|
132
|
+
updated = DestinationProfile.model_validate(updated_data)
|
|
133
|
+
config_svc.save_destination(updated)
|
|
134
|
+
logger.info("Updated destination: %s", name)
|
|
135
|
+
return _profile_to_response(updated)
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
return JSONResponse(
|
|
138
|
+
status_code=400,
|
|
139
|
+
content={"status": "error", "message": str(exc)},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@router.delete("/{name}", response_model=RelayActionResponse)
|
|
144
|
+
async def delete_destination(
|
|
145
|
+
name: str,
|
|
146
|
+
config_svc: ConfigService = Depends(get_config_service),
|
|
147
|
+
) -> RelayActionResponse | JSONResponse:
|
|
148
|
+
"""Delete a destination profile.
|
|
149
|
+
|
|
150
|
+
Removes the destination from saved config. If the relay is
|
|
151
|
+
running, the destination continues until the relay is restarted.
|
|
152
|
+
"""
|
|
153
|
+
removed = config_svc.remove_destination(name)
|
|
154
|
+
if not removed:
|
|
155
|
+
return JSONResponse(
|
|
156
|
+
status_code=404,
|
|
157
|
+
content={"status": "error", "message": f"Destination '{name}' not found"},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
logger.info("Deleted destination: %s", name)
|
|
161
|
+
return RelayActionResponse(status="ok", message=f"Destination '{name}' deleted")
|