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,74 @@
|
|
|
1
|
+
"""Internal API v1 contract namespace."""
|
|
2
|
+
|
|
3
|
+
from .enums import CommandStatus
|
|
4
|
+
from .enums import CommandType
|
|
5
|
+
from .enums import ErrorCode
|
|
6
|
+
from .enums import EventType
|
|
7
|
+
from .enums import IssuerType
|
|
8
|
+
from .enums import MessageType
|
|
9
|
+
from .enums import NodeType
|
|
10
|
+
from .enums import Qos
|
|
11
|
+
from .enums import QueryType
|
|
12
|
+
from .enums import RetentionPolicy
|
|
13
|
+
from .enums import Severity
|
|
14
|
+
from .enums import SubscriberAction
|
|
15
|
+
from .enums import Visibility
|
|
16
|
+
from .schemas import ApiEnvelopeBase
|
|
17
|
+
from .schemas import CommandEnvelope
|
|
18
|
+
from .schemas import CommandResult
|
|
19
|
+
from .schemas import CreateTopicPayload
|
|
20
|
+
from .schemas import ErrorDetail
|
|
21
|
+
from .schemas import EventEnvelope
|
|
22
|
+
from .schemas import GetNodeStatusPayload
|
|
23
|
+
from .schemas import GetSubscribersPayload
|
|
24
|
+
from .schemas import GetTopicsPayload
|
|
25
|
+
from .schemas import Issuer
|
|
26
|
+
from .schemas import PublishMessagePayload
|
|
27
|
+
from .schemas import QueryCacheHint
|
|
28
|
+
from .schemas import QueryError
|
|
29
|
+
from .schemas import QueryEnvelope
|
|
30
|
+
from .schemas import QueryResult
|
|
31
|
+
from .schemas import QueryResultPayload
|
|
32
|
+
from .schemas import RegisterNodeMetadata
|
|
33
|
+
from .schemas import RegisterNodePayload
|
|
34
|
+
from .schemas import SubscribeTopicPayload
|
|
35
|
+
from .schemas import SUPPORTED_API_VERSION
|
|
36
|
+
|
|
37
|
+
CONTRACT_VERSION = "1.0"
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"ApiEnvelopeBase",
|
|
41
|
+
"CommandEnvelope",
|
|
42
|
+
"CommandResult",
|
|
43
|
+
"CommandStatus",
|
|
44
|
+
"CommandType",
|
|
45
|
+
"CONTRACT_VERSION",
|
|
46
|
+
"CreateTopicPayload",
|
|
47
|
+
"ErrorCode",
|
|
48
|
+
"ErrorDetail",
|
|
49
|
+
"EventEnvelope",
|
|
50
|
+
"EventType",
|
|
51
|
+
"GetNodeStatusPayload",
|
|
52
|
+
"GetSubscribersPayload",
|
|
53
|
+
"GetTopicsPayload",
|
|
54
|
+
"Issuer",
|
|
55
|
+
"IssuerType",
|
|
56
|
+
"MessageType",
|
|
57
|
+
"NodeType",
|
|
58
|
+
"PublishMessagePayload",
|
|
59
|
+
"QueryCacheHint",
|
|
60
|
+
"Qos",
|
|
61
|
+
"QueryError",
|
|
62
|
+
"QueryEnvelope",
|
|
63
|
+
"QueryResult",
|
|
64
|
+
"QueryType",
|
|
65
|
+
"QueryResultPayload",
|
|
66
|
+
"RegisterNodeMetadata",
|
|
67
|
+
"RegisterNodePayload",
|
|
68
|
+
"RetentionPolicy",
|
|
69
|
+
"Severity",
|
|
70
|
+
"SubscriberAction",
|
|
71
|
+
"SubscribeTopicPayload",
|
|
72
|
+
"SUPPORTED_API_VERSION",
|
|
73
|
+
"Visibility",
|
|
74
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Enumerations for the internal API v1 contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IssuerType(str, Enum):
|
|
9
|
+
"""Issuer types for internal API commands."""
|
|
10
|
+
|
|
11
|
+
API = "api"
|
|
12
|
+
RETICULUM = "reticulum"
|
|
13
|
+
INTERNAL = "internal"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CommandType(str, Enum):
|
|
17
|
+
"""Supported command types."""
|
|
18
|
+
|
|
19
|
+
REGISTER_NODE = "RegisterNode"
|
|
20
|
+
CREATE_TOPIC = "CreateTopic"
|
|
21
|
+
SUBSCRIBE_TOPIC = "SubscribeTopic"
|
|
22
|
+
PUBLISH_MESSAGE = "PublishMessage"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EventType(str, Enum):
|
|
26
|
+
"""Supported event types."""
|
|
27
|
+
|
|
28
|
+
NODE_REGISTERED = "NodeRegistered"
|
|
29
|
+
TOPIC_CREATED = "TopicCreated"
|
|
30
|
+
MESSAGE_PUBLISHED = "MessagePublished"
|
|
31
|
+
SUBSCRIBER_UPDATED = "SubscriberUpdated"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class QueryType(str, Enum):
|
|
35
|
+
"""Supported query types."""
|
|
36
|
+
|
|
37
|
+
GET_TOPICS = "GetTopics"
|
|
38
|
+
GET_SUBSCRIBERS = "GetSubscribers"
|
|
39
|
+
GET_NODE_STATUS = "GetNodeStatus"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ErrorCode(str, Enum):
|
|
43
|
+
"""Error codes for internal API responses."""
|
|
44
|
+
|
|
45
|
+
API_VERSION_UNSUPPORTED = "API_VERSION_UNSUPPORTED"
|
|
46
|
+
UNAUTHORIZED_COMMAND = "UNAUTHORIZED_COMMAND"
|
|
47
|
+
TOPIC_NOT_FOUND = "TOPIC_NOT_FOUND"
|
|
48
|
+
INVALID_QUERY = "INVALID_QUERY"
|
|
49
|
+
UNAUTHORIZED = "UNAUTHORIZED"
|
|
50
|
+
INTERNAL_ERROR = "INTERNAL_ERROR"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Severity(str, Enum):
|
|
54
|
+
"""Severity levels for errors."""
|
|
55
|
+
|
|
56
|
+
WARNING = "warning"
|
|
57
|
+
ERROR = "error"
|
|
58
|
+
FATAL = "fatal"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class NodeType(str, Enum):
|
|
62
|
+
"""Supported node types for RegisterNode."""
|
|
63
|
+
|
|
64
|
+
RETICULUM = "reticulum"
|
|
65
|
+
GATEWAY = "gateway"
|
|
66
|
+
SERVICE = "service"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class RetentionPolicy(str, Enum):
|
|
70
|
+
"""Retention policy for topics."""
|
|
71
|
+
|
|
72
|
+
EPHEMERAL = "ephemeral"
|
|
73
|
+
PERSISTENT = "persistent"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Visibility(str, Enum):
|
|
77
|
+
"""Visibility for topics."""
|
|
78
|
+
|
|
79
|
+
PUBLIC = "public"
|
|
80
|
+
RESTRICTED = "restricted"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class MessageType(str, Enum):
|
|
84
|
+
"""Message content types."""
|
|
85
|
+
|
|
86
|
+
TELEMETRY = "telemetry"
|
|
87
|
+
EVENT = "event"
|
|
88
|
+
TEXT = "text"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Qos(str, Enum):
|
|
92
|
+
"""Quality of service options."""
|
|
93
|
+
|
|
94
|
+
BEST_EFFORT = "best_effort"
|
|
95
|
+
GUARANTEED = "guaranteed"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class SubscriberAction(str, Enum):
|
|
99
|
+
"""Subscriber update actions."""
|
|
100
|
+
|
|
101
|
+
SUBSCRIBED = "subscribed"
|
|
102
|
+
UNSUBSCRIBED = "unsubscribed"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class CommandStatus(str, Enum):
|
|
106
|
+
"""Command status values."""
|
|
107
|
+
|
|
108
|
+
ACCEPTED = "accepted"
|
|
109
|
+
REJECTED = "rejected"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contract_version": "1.0",
|
|
3
|
+
"files": {
|
|
4
|
+
"__init__.py": "824987DDE26AA09EB71D6472E38C4A4FACEF4A2A93041A6B538934FE17F09809",
|
|
5
|
+
"enums.py": "1D65B630285628238C3F8FF5B79035959E4E19FD062AF69941B693DEDBFD95D1",
|
|
6
|
+
"schemas.py": "99701050F53DF4396674CE60DF11BFEF6BC6E164E8C99D583E8CA1760F6B4881"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"""Pydantic schemas for the internal API v1 contract."""
|
|
2
|
+
# pylint: disable=import-error
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
import re
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
from typing import Dict
|
|
10
|
+
from typing import List
|
|
11
|
+
from typing import Literal
|
|
12
|
+
from typing import Optional
|
|
13
|
+
from typing import Union
|
|
14
|
+
from uuid import UUID
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
from pydantic import ConfigDict
|
|
18
|
+
from pydantic import Field
|
|
19
|
+
from pydantic import field_validator
|
|
20
|
+
from pydantic import model_validator
|
|
21
|
+
from pydantic_core import PydanticCustomError
|
|
22
|
+
|
|
23
|
+
from .enums import CommandStatus
|
|
24
|
+
from .enums import CommandType
|
|
25
|
+
from .enums import ErrorCode
|
|
26
|
+
from .enums import EventType
|
|
27
|
+
from .enums import IssuerType
|
|
28
|
+
from .enums import MessageType
|
|
29
|
+
from .enums import NodeType
|
|
30
|
+
from .enums import Qos
|
|
31
|
+
from .enums import QueryType
|
|
32
|
+
from .enums import RetentionPolicy
|
|
33
|
+
from .enums import Severity
|
|
34
|
+
from .enums import SubscriberAction
|
|
35
|
+
from .enums import Visibility
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
SUPPORTED_API_VERSION = "1.0"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_api_version(value: str) -> tuple[int, int]:
|
|
42
|
+
"""Parse and validate an API version string.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
value (str): Version string.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
tuple[int, int]: Major and minor versions.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
match = re.fullmatch(r"(\d+)\.(\d+)", value)
|
|
52
|
+
if not match:
|
|
53
|
+
raise PydanticCustomError(
|
|
54
|
+
ErrorCode.API_VERSION_UNSUPPORTED.value,
|
|
55
|
+
"API version unsupported",
|
|
56
|
+
)
|
|
57
|
+
return int(match.group(1)), int(match.group(2))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _supported_version_parts() -> tuple[int, int]:
|
|
61
|
+
"""Return the supported API version parts."""
|
|
62
|
+
|
|
63
|
+
return _parse_api_version(SUPPORTED_API_VERSION)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _reject_numeric_timestamp(value: object) -> object:
|
|
67
|
+
"""Reject numeric timestamps to enforce ISO-8601 strings."""
|
|
68
|
+
|
|
69
|
+
if isinstance(value, (int, float)):
|
|
70
|
+
raise ValueError("timestamp must be ISO-8601")
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ApiEnvelopeBase(BaseModel):
|
|
75
|
+
"""Base class for API envelopes with version enforcement."""
|
|
76
|
+
|
|
77
|
+
model_config = ConfigDict(extra="forbid")
|
|
78
|
+
|
|
79
|
+
api_version: str
|
|
80
|
+
|
|
81
|
+
@model_validator(mode="after")
|
|
82
|
+
def _validate_api_version(self):
|
|
83
|
+
"""Validate API version compatibility."""
|
|
84
|
+
|
|
85
|
+
major, minor = _parse_api_version(self.api_version)
|
|
86
|
+
supported_major, supported_minor = _supported_version_parts()
|
|
87
|
+
if major != supported_major:
|
|
88
|
+
raise PydanticCustomError(
|
|
89
|
+
ErrorCode.API_VERSION_UNSUPPORTED.value,
|
|
90
|
+
"API version unsupported",
|
|
91
|
+
)
|
|
92
|
+
if minor < supported_minor:
|
|
93
|
+
raise PydanticCustomError(
|
|
94
|
+
ErrorCode.API_VERSION_UNSUPPORTED.value,
|
|
95
|
+
"API version unsupported",
|
|
96
|
+
)
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Issuer(BaseModel):
|
|
101
|
+
"""Issuer metadata for commands."""
|
|
102
|
+
|
|
103
|
+
model_config = ConfigDict(extra="forbid")
|
|
104
|
+
|
|
105
|
+
type: IssuerType
|
|
106
|
+
id: str
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Location(BaseModel):
|
|
110
|
+
"""Location metadata for registered nodes."""
|
|
111
|
+
|
|
112
|
+
model_config = ConfigDict(extra="forbid")
|
|
113
|
+
|
|
114
|
+
lat: Optional[float] = None
|
|
115
|
+
lon: Optional[float] = None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class RegisterNodeMetadata(BaseModel):
|
|
119
|
+
"""Metadata for RegisterNode."""
|
|
120
|
+
|
|
121
|
+
model_config = ConfigDict(extra="forbid")
|
|
122
|
+
|
|
123
|
+
name: Optional[str] = Field(default=None, max_length=64)
|
|
124
|
+
description: Optional[str] = Field(default=None, max_length=256)
|
|
125
|
+
capabilities: Optional[List[str]] = None
|
|
126
|
+
location: Optional[Location] = None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class RegisterNodePayload(BaseModel):
|
|
130
|
+
"""Payload for RegisterNode."""
|
|
131
|
+
|
|
132
|
+
model_config = ConfigDict(extra="forbid")
|
|
133
|
+
|
|
134
|
+
node_id: str
|
|
135
|
+
node_type: NodeType
|
|
136
|
+
metadata: Optional[RegisterNodeMetadata] = None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class CreateTopicPayload(BaseModel):
|
|
140
|
+
"""Payload for CreateTopic."""
|
|
141
|
+
|
|
142
|
+
model_config = ConfigDict(extra="forbid")
|
|
143
|
+
|
|
144
|
+
topic_path: str
|
|
145
|
+
retention: RetentionPolicy
|
|
146
|
+
visibility: Visibility
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class SubscribeTopicPayload(BaseModel):
|
|
150
|
+
"""Payload for SubscribeTopic."""
|
|
151
|
+
|
|
152
|
+
model_config = ConfigDict(extra="forbid")
|
|
153
|
+
|
|
154
|
+
subscriber_id: str
|
|
155
|
+
topic_path: str
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TextContent(BaseModel):
|
|
159
|
+
"""Text message content."""
|
|
160
|
+
|
|
161
|
+
model_config = ConfigDict(extra="forbid")
|
|
162
|
+
|
|
163
|
+
message_type: Literal[MessageType.TEXT] = MessageType.TEXT
|
|
164
|
+
text: str = Field(max_length=4096)
|
|
165
|
+
encoding: Literal["utf-8"] = "utf-8"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class Metric(BaseModel):
|
|
169
|
+
"""Telemetry metric."""
|
|
170
|
+
|
|
171
|
+
model_config = ConfigDict(extra="forbid")
|
|
172
|
+
|
|
173
|
+
name: str
|
|
174
|
+
value: Union[float, int, str, bool]
|
|
175
|
+
unit: Optional[str] = None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TelemetryContent(BaseModel):
|
|
179
|
+
"""Telemetry message content."""
|
|
180
|
+
|
|
181
|
+
model_config = ConfigDict(extra="forbid")
|
|
182
|
+
|
|
183
|
+
message_type: Literal[MessageType.TELEMETRY] = MessageType.TELEMETRY
|
|
184
|
+
telemetry_type: Optional[str] = None
|
|
185
|
+
data: Dict[str, object]
|
|
186
|
+
timestamp: Optional[datetime] = None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class EventContent(BaseModel):
|
|
190
|
+
"""Event message content."""
|
|
191
|
+
|
|
192
|
+
model_config = ConfigDict(extra="forbid")
|
|
193
|
+
|
|
194
|
+
message_type: Literal[MessageType.EVENT] = MessageType.EVENT
|
|
195
|
+
event_name: str
|
|
196
|
+
attributes: Dict[str, Union[str, float, int, bool]]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
MessageContent = Annotated[
|
|
200
|
+
Union[TextContent, TelemetryContent, EventContent],
|
|
201
|
+
Field(discriminator="message_type"),
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class PublishMessagePayload(BaseModel):
|
|
206
|
+
"""Payload for PublishMessage."""
|
|
207
|
+
|
|
208
|
+
model_config = ConfigDict(extra="forbid")
|
|
209
|
+
|
|
210
|
+
topic_path: str
|
|
211
|
+
message_type: MessageType
|
|
212
|
+
content: MessageContent
|
|
213
|
+
qos: Qos
|
|
214
|
+
|
|
215
|
+
@model_validator(mode="after")
|
|
216
|
+
def _validate_message_type(self):
|
|
217
|
+
"""Ensure message_type matches content."""
|
|
218
|
+
|
|
219
|
+
if self.content.message_type != self.message_type:
|
|
220
|
+
raise PydanticCustomError(
|
|
221
|
+
"MESSAGE_TYPE_MISMATCH",
|
|
222
|
+
"message_type does not match content",
|
|
223
|
+
)
|
|
224
|
+
return self
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
CommandPayload = Union[
|
|
228
|
+
RegisterNodePayload,
|
|
229
|
+
CreateTopicPayload,
|
|
230
|
+
SubscribeTopicPayload,
|
|
231
|
+
PublishMessagePayload,
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
_COMMAND_PAYLOAD_MAP = {
|
|
236
|
+
CommandType.REGISTER_NODE: RegisterNodePayload,
|
|
237
|
+
CommandType.CREATE_TOPIC: CreateTopicPayload,
|
|
238
|
+
CommandType.SUBSCRIBE_TOPIC: SubscribeTopicPayload,
|
|
239
|
+
CommandType.PUBLISH_MESSAGE: PublishMessagePayload,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class CommandEnvelope(ApiEnvelopeBase):
|
|
244
|
+
"""Envelope for commands."""
|
|
245
|
+
|
|
246
|
+
command_id: UUID
|
|
247
|
+
command_type: CommandType
|
|
248
|
+
issued_at: datetime
|
|
249
|
+
issuer: Issuer
|
|
250
|
+
payload: CommandPayload
|
|
251
|
+
|
|
252
|
+
@field_validator("issued_at", mode="before")
|
|
253
|
+
@classmethod
|
|
254
|
+
def _validate_issued_at(cls, value: object) -> object:
|
|
255
|
+
return _reject_numeric_timestamp(value)
|
|
256
|
+
|
|
257
|
+
@model_validator(mode="after")
|
|
258
|
+
def _validate_payload(self):
|
|
259
|
+
"""Ensure payload type matches command_type."""
|
|
260
|
+
|
|
261
|
+
expected = _COMMAND_PAYLOAD_MAP.get(self.command_type)
|
|
262
|
+
if expected and not isinstance(self.payload, expected):
|
|
263
|
+
raise PydanticCustomError(
|
|
264
|
+
"COMMAND_PAYLOAD_MISMATCH",
|
|
265
|
+
"Payload does not match command_type",
|
|
266
|
+
)
|
|
267
|
+
return self
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class NodeRegisteredPayload(BaseModel):
|
|
271
|
+
"""Payload for NodeRegistered event."""
|
|
272
|
+
|
|
273
|
+
model_config = ConfigDict(extra="forbid")
|
|
274
|
+
|
|
275
|
+
node_id: str
|
|
276
|
+
node_type: NodeType
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class TopicCreatedPayload(BaseModel):
|
|
280
|
+
"""Payload for TopicCreated event."""
|
|
281
|
+
|
|
282
|
+
model_config = ConfigDict(extra="forbid")
|
|
283
|
+
|
|
284
|
+
topic_path: str
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class MessagePublishedPayload(BaseModel):
|
|
288
|
+
"""Payload for MessagePublished event."""
|
|
289
|
+
|
|
290
|
+
model_config = ConfigDict(extra="forbid")
|
|
291
|
+
|
|
292
|
+
topic_path: str
|
|
293
|
+
message_id: str
|
|
294
|
+
originator: str
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class SubscriberUpdatedPayload(BaseModel):
|
|
298
|
+
"""Payload for SubscriberUpdated event."""
|
|
299
|
+
|
|
300
|
+
model_config = ConfigDict(extra="forbid")
|
|
301
|
+
|
|
302
|
+
subscriber_id: str
|
|
303
|
+
topic_path: str
|
|
304
|
+
action: SubscriberAction
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
EventPayload = Union[
|
|
308
|
+
NodeRegisteredPayload,
|
|
309
|
+
TopicCreatedPayload,
|
|
310
|
+
MessagePublishedPayload,
|
|
311
|
+
SubscriberUpdatedPayload,
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
_EVENT_PAYLOAD_MAP = {
|
|
316
|
+
EventType.NODE_REGISTERED: NodeRegisteredPayload,
|
|
317
|
+
EventType.TOPIC_CREATED: TopicCreatedPayload,
|
|
318
|
+
EventType.MESSAGE_PUBLISHED: MessagePublishedPayload,
|
|
319
|
+
EventType.SUBSCRIBER_UPDATED: SubscriberUpdatedPayload,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class EventEnvelope(ApiEnvelopeBase):
|
|
324
|
+
"""Envelope for events."""
|
|
325
|
+
|
|
326
|
+
event_id: UUID
|
|
327
|
+
event_type: EventType
|
|
328
|
+
occurred_at: datetime
|
|
329
|
+
origin: Literal["hub-core"]
|
|
330
|
+
payload: EventPayload
|
|
331
|
+
|
|
332
|
+
@field_validator("occurred_at", mode="before")
|
|
333
|
+
@classmethod
|
|
334
|
+
def _validate_occurred_at(cls, value: object) -> object:
|
|
335
|
+
return _reject_numeric_timestamp(value)
|
|
336
|
+
|
|
337
|
+
@model_validator(mode="after")
|
|
338
|
+
def _validate_payload(self):
|
|
339
|
+
"""Ensure payload type matches event_type."""
|
|
340
|
+
|
|
341
|
+
expected = _EVENT_PAYLOAD_MAP.get(self.event_type)
|
|
342
|
+
if expected and not isinstance(self.payload, expected):
|
|
343
|
+
raise PydanticCustomError(
|
|
344
|
+
"EVENT_PAYLOAD_MISMATCH",
|
|
345
|
+
"Payload does not match event_type",
|
|
346
|
+
)
|
|
347
|
+
return self
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class GetTopicsPayload(BaseModel):
|
|
351
|
+
"""Payload for GetTopics."""
|
|
352
|
+
|
|
353
|
+
model_config = ConfigDict(extra="forbid")
|
|
354
|
+
|
|
355
|
+
prefix: Optional[str] = None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class GetSubscribersPayload(BaseModel):
|
|
359
|
+
"""Payload for GetSubscribers."""
|
|
360
|
+
|
|
361
|
+
model_config = ConfigDict(extra="forbid")
|
|
362
|
+
|
|
363
|
+
topic_path: str
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class GetNodeStatusPayload(BaseModel):
|
|
367
|
+
"""Payload for GetNodeStatus."""
|
|
368
|
+
|
|
369
|
+
model_config = ConfigDict(extra="forbid")
|
|
370
|
+
|
|
371
|
+
node_id: str
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
QueryPayload = Union[GetTopicsPayload, GetSubscribersPayload, GetNodeStatusPayload]
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
_QUERY_PAYLOAD_MAP = {
|
|
378
|
+
QueryType.GET_TOPICS: GetTopicsPayload,
|
|
379
|
+
QueryType.GET_SUBSCRIBERS: GetSubscribersPayload,
|
|
380
|
+
QueryType.GET_NODE_STATUS: GetNodeStatusPayload,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class QueryEnvelope(ApiEnvelopeBase):
|
|
385
|
+
"""Envelope for queries."""
|
|
386
|
+
|
|
387
|
+
query_id: UUID
|
|
388
|
+
query_type: QueryType
|
|
389
|
+
issued_at: datetime
|
|
390
|
+
payload: QueryPayload
|
|
391
|
+
|
|
392
|
+
@field_validator("issued_at", mode="before")
|
|
393
|
+
@classmethod
|
|
394
|
+
def _validate_issued_at(cls, value: object) -> object:
|
|
395
|
+
return _reject_numeric_timestamp(value)
|
|
396
|
+
|
|
397
|
+
@model_validator(mode="after")
|
|
398
|
+
def _validate_payload(self):
|
|
399
|
+
"""Ensure payload type matches query_type."""
|
|
400
|
+
|
|
401
|
+
expected = _QUERY_PAYLOAD_MAP.get(self.query_type)
|
|
402
|
+
if expected and not isinstance(self.payload, expected):
|
|
403
|
+
raise PydanticCustomError(
|
|
404
|
+
"QUERY_PAYLOAD_MISMATCH",
|
|
405
|
+
"Payload does not match query_type",
|
|
406
|
+
)
|
|
407
|
+
return self
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class CommandResult(BaseModel):
|
|
411
|
+
"""Result for command execution."""
|
|
412
|
+
|
|
413
|
+
model_config = ConfigDict(extra="forbid")
|
|
414
|
+
|
|
415
|
+
command_id: UUID
|
|
416
|
+
status: CommandStatus
|
|
417
|
+
reason: Optional[str] = None
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class QueryResult(BaseModel):
|
|
421
|
+
"""Result for query execution."""
|
|
422
|
+
|
|
423
|
+
model_config = ConfigDict(extra="forbid")
|
|
424
|
+
|
|
425
|
+
query_id: UUID
|
|
426
|
+
ok: bool
|
|
427
|
+
result: Optional["QueryResultPayload"] = None
|
|
428
|
+
error: Optional["QueryError"] = None
|
|
429
|
+
|
|
430
|
+
@model_validator(mode="after")
|
|
431
|
+
def _validate_outcome(self):
|
|
432
|
+
"""Ensure ok/error/result coherence."""
|
|
433
|
+
|
|
434
|
+
if self.ok:
|
|
435
|
+
if self.error is not None or self.result is None:
|
|
436
|
+
raise ValueError("ok results must include result and no error")
|
|
437
|
+
else:
|
|
438
|
+
if self.error is None:
|
|
439
|
+
raise ValueError("error results must include error")
|
|
440
|
+
return self
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class QueryCacheHint(BaseModel):
|
|
444
|
+
"""Cache hints for query results."""
|
|
445
|
+
|
|
446
|
+
model_config = ConfigDict(extra="forbid")
|
|
447
|
+
|
|
448
|
+
ttl_seconds: int
|
|
449
|
+
scope: Literal["node", "hub", "network"]
|
|
450
|
+
stale_while_revalidate: bool
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class QueryResultPayload(BaseModel):
|
|
454
|
+
"""Query result payload with optional cache hints."""
|
|
455
|
+
|
|
456
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
457
|
+
|
|
458
|
+
data: Dict[str, object]
|
|
459
|
+
cache: Optional[QueryCacheHint] = Field(default=None, alias="_cache")
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class QueryError(BaseModel):
|
|
463
|
+
"""Query error payload."""
|
|
464
|
+
|
|
465
|
+
model_config = ConfigDict(extra="forbid")
|
|
466
|
+
|
|
467
|
+
code: ErrorCode
|
|
468
|
+
message: str
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
class ErrorDetail(BaseModel):
|
|
472
|
+
"""Error payload for command/query failures."""
|
|
473
|
+
|
|
474
|
+
model_config = ConfigDict(extra="forbid")
|
|
475
|
+
|
|
476
|
+
error_code: ErrorCode
|
|
477
|
+
severity: Severity
|
|
478
|
+
message: str
|