uns-kit 0.0.3__py3-none-any.whl → 0.0.5__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.
- uns_kit/__init__.py +9 -0
- uns_kit/cli.py +19 -0
- uns_kit/client.py +230 -39
- uns_kit/config.py +66 -8
- uns_kit/packet.py +130 -18
- uns_kit/proxy.py +64 -0
- uns_kit/proxy_process.py +102 -0
- uns_kit/status_monitor.py +88 -0
- uns_kit/templates/default/README.md +39 -0
- uns_kit/templates/default/pyproject.toml +24 -0
- uns_kit/templates/default/src/data_example.py +110 -0
- uns_kit/templates/default/src/load_test.py +103 -0
- uns_kit/topic_builder.py +10 -0
- uns_kit/uns_mqtt_proxy.py +237 -0
- uns_kit/version.py +9 -0
- {uns_kit-0.0.3.dist-info → uns_kit-0.0.5.dist-info}/METADATA +38 -13
- uns_kit-0.0.5.dist-info/RECORD +19 -0
- uns_kit-0.0.3.dist-info/RECORD +0 -10
- {uns_kit-0.0.3.dist-info → uns_kit-0.0.5.dist-info}/WHEEL +0 -0
- {uns_kit-0.0.3.dist-info → uns_kit-0.0.5.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import math
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from uns_kit import UnsConfig, UnsMqttClient, TopicBuilder
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_config() -> UnsConfig:
|
|
10
|
+
cfg_path = Path("config.json")
|
|
11
|
+
if cfg_path.exists():
|
|
12
|
+
return UnsConfig.load(cfg_path)
|
|
13
|
+
return UnsConfig(host="localhost")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def simulate_sensor_value(step: int) -> float:
|
|
17
|
+
base_value = 42.0
|
|
18
|
+
fast_cycle = math.sin(step / 5.0) * 3.0
|
|
19
|
+
slow_cycle = math.sin(step / 25.0) * 6.0
|
|
20
|
+
ripple = math.sin(step / 2.0 + math.pi / 4.0) * 0.5
|
|
21
|
+
value = base_value + fast_cycle + slow_cycle + ripple
|
|
22
|
+
return round(value, 2)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def prompt_bool(prompt: str, default_yes: bool = True) -> bool:
|
|
26
|
+
suffix = " (Y/n) " if default_yes else " (y/N) "
|
|
27
|
+
answer = input(prompt + suffix).strip().lower()
|
|
28
|
+
if not answer:
|
|
29
|
+
return default_yes
|
|
30
|
+
return answer in ("y", "yes")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def prompt_int(prompt: str, default: int, suffix: str = "") -> int:
|
|
34
|
+
answer = input(f"{prompt} (default {default}{suffix}) ").strip()
|
|
35
|
+
try:
|
|
36
|
+
return int(answer) if answer else default
|
|
37
|
+
except ValueError:
|
|
38
|
+
return default
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def prompt_topic(prompt: str, default: str) -> str:
|
|
42
|
+
answer = input(f"{prompt} (default {default}) ").strip()
|
|
43
|
+
return answer or default
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def log_info(message: str) -> None:
|
|
47
|
+
timestamp = datetime.now().isoformat(timespec="seconds")
|
|
48
|
+
print(f"{timestamp} [INFO] {message}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def main() -> None:
|
|
52
|
+
cfg = load_config()
|
|
53
|
+
host = f"{cfg.host}:{cfg.port}" if cfg.port else cfg.host
|
|
54
|
+
|
|
55
|
+
if not prompt_bool(f"Would you like to continue with load-test on {host}?"):
|
|
56
|
+
print("Load test aborted.")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
count = prompt_int("How many iterations should be run?", 100, "")
|
|
60
|
+
delay_ms = prompt_int("What should be the delay between intervals in milliseconds?", 0, " ms")
|
|
61
|
+
topic = prompt_topic("Topic to publish to", "raw/data")
|
|
62
|
+
retain = prompt_bool("Retain each published message so late subscribers see the last value?", True)
|
|
63
|
+
delay_s = delay_ms / 1000.0
|
|
64
|
+
|
|
65
|
+
log_info(f"Starting load test with {count} messages and {delay_ms} ms delay...")
|
|
66
|
+
|
|
67
|
+
# Use a dedicated identity for load testing to avoid clashing with app processes.
|
|
68
|
+
tb = TopicBuilder(package_name="uns-loadtest", package_version="0.0.0", process_name="load-tester")
|
|
69
|
+
client = UnsMqttClient(
|
|
70
|
+
cfg.host,
|
|
71
|
+
port=cfg.port,
|
|
72
|
+
username=cfg.username or None,
|
|
73
|
+
password=cfg.password or None,
|
|
74
|
+
tls=cfg.tls,
|
|
75
|
+
client_id="uns-loadtest-client",
|
|
76
|
+
topic_builder=tb,
|
|
77
|
+
instance_name="py-load-test",
|
|
78
|
+
reconnect_interval=1,
|
|
79
|
+
)
|
|
80
|
+
await client.connect()
|
|
81
|
+
|
|
82
|
+
start = asyncio.get_event_loop().time()
|
|
83
|
+
for i in range(count):
|
|
84
|
+
now = asyncio.get_event_loop().time()
|
|
85
|
+
sensor_value = simulate_sensor_value(i)
|
|
86
|
+
raw_payload = f"{i},{int(now * 1000)},{sensor_value}"
|
|
87
|
+
await client.publish_raw(topic, raw_payload, retain=retain)
|
|
88
|
+
if delay_s:
|
|
89
|
+
await asyncio.sleep(delay_s)
|
|
90
|
+
|
|
91
|
+
log_info("Sleeping for 50ms.")
|
|
92
|
+
await asyncio.sleep(0.05)
|
|
93
|
+
end = asyncio.get_event_loop().time()
|
|
94
|
+
duration = end - start
|
|
95
|
+
rate = count / duration if duration > 0 else 0
|
|
96
|
+
log_info(f"Load test completed in {duration:.2f} seconds.")
|
|
97
|
+
log_info(f"Message rate: {rate:.2f} msg/s.")
|
|
98
|
+
|
|
99
|
+
await client.close()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
asyncio.run(main())
|
uns_kit/topic_builder.py
CHANGED
|
@@ -33,6 +33,16 @@ class TopicBuilder:
|
|
|
33
33
|
def handover_topic(self) -> str:
|
|
34
34
|
return f"{self._base}handover"
|
|
35
35
|
|
|
36
|
+
def wildcard_active_topic(self) -> str:
|
|
37
|
+
parts = self._base.strip("/").split("/")
|
|
38
|
+
if len(parts) < 2:
|
|
39
|
+
raise ValueError("processStatusTopic must follow 'uns-infra/<package>/<version>/<process>/'")
|
|
40
|
+
return "/".join(parts[:2]) + "/+/+/active"
|
|
41
|
+
|
|
42
|
+
def instance_status_topic(self, instance_name: str) -> str:
|
|
43
|
+
sanitized = self.sanitize_topic_part(instance_name)
|
|
44
|
+
return f"{self._base}{sanitized}/"
|
|
45
|
+
|
|
36
46
|
@staticmethod
|
|
37
47
|
def extract_base_topic(full_topic: str) -> str:
|
|
38
48
|
parts = full_topic.split("/")
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from .client import UnsMqttClient
|
|
9
|
+
from .packet import UnsPacket, isoformat
|
|
10
|
+
from .proxy import UnsProxy
|
|
11
|
+
from .topic_builder import TopicBuilder
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MessageMode(str, Enum):
|
|
15
|
+
RAW = "raw"
|
|
16
|
+
DELTA = "delta"
|
|
17
|
+
BOTH = "both"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class LastValueEntry:
|
|
22
|
+
value: Any
|
|
23
|
+
uom: Optional[str]
|
|
24
|
+
timestamp: datetime
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UnsMqttProxy(UnsProxy):
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
host: str,
|
|
31
|
+
*,
|
|
32
|
+
process_name: str,
|
|
33
|
+
instance_name: str,
|
|
34
|
+
package_name: str = "uns-kit",
|
|
35
|
+
package_version: str = "0.0.1",
|
|
36
|
+
port: Optional[int] = None,
|
|
37
|
+
username: Optional[str] = None,
|
|
38
|
+
password: Optional[str] = None,
|
|
39
|
+
tls: bool = False,
|
|
40
|
+
client_id: Optional[str] = None,
|
|
41
|
+
keepalive: int = 60,
|
|
42
|
+
clean_session: bool = True,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.topic_builder = TopicBuilder(package_name, package_version, process_name)
|
|
45
|
+
self.instance_status_topic = self.topic_builder.instance_status_topic(instance_name)
|
|
46
|
+
self.client = UnsMqttClient(
|
|
47
|
+
host,
|
|
48
|
+
port=port,
|
|
49
|
+
username=username,
|
|
50
|
+
password=password,
|
|
51
|
+
tls=tls,
|
|
52
|
+
client_id=client_id,
|
|
53
|
+
keepalive=keepalive,
|
|
54
|
+
clean_session=clean_session,
|
|
55
|
+
topic_builder=self.topic_builder,
|
|
56
|
+
instance_name=instance_name,
|
|
57
|
+
publisher_active=True,
|
|
58
|
+
subscriber_active=True,
|
|
59
|
+
)
|
|
60
|
+
super().__init__(self.client, self.instance_status_topic, instance_name)
|
|
61
|
+
self._last_values: Dict[str, LastValueEntry] = {}
|
|
62
|
+
self._sequence_ids: Dict[str, int] = {}
|
|
63
|
+
|
|
64
|
+
async def connect(self) -> None:
|
|
65
|
+
await self.client.connect()
|
|
66
|
+
await self.start()
|
|
67
|
+
|
|
68
|
+
async def close(self) -> None:
|
|
69
|
+
await self.stop()
|
|
70
|
+
await self.client.close()
|
|
71
|
+
|
|
72
|
+
async def publish_message(self, topic: str, payload: str | bytes) -> None:
|
|
73
|
+
await self.client.publish_raw(topic, payload)
|
|
74
|
+
|
|
75
|
+
async def publish_packet(self, topic: str, packet: Dict[str, Any]) -> None:
|
|
76
|
+
await self.client.publish_packet(topic, packet)
|
|
77
|
+
|
|
78
|
+
async def publish_mqtt_message(self, mqtt_message: Dict[str, Any], mode: MessageMode = MessageMode.RAW) -> None:
|
|
79
|
+
attrs = mqtt_message.get("attributes")
|
|
80
|
+
if attrs is None:
|
|
81
|
+
raise ValueError("mqtt_message must include attributes")
|
|
82
|
+
if not isinstance(attrs, list):
|
|
83
|
+
attrs = [attrs]
|
|
84
|
+
|
|
85
|
+
base_topic = mqtt_message.get("topic", "")
|
|
86
|
+
asset = mqtt_message.get("asset")
|
|
87
|
+
asset_description = mqtt_message.get("assetDescription")
|
|
88
|
+
object_type = mqtt_message.get("objectType")
|
|
89
|
+
object_type_description = mqtt_message.get("objectTypeDescription")
|
|
90
|
+
object_id = mqtt_message.get("objectId")
|
|
91
|
+
|
|
92
|
+
for attr in attrs:
|
|
93
|
+
attribute = attr.get("attribute")
|
|
94
|
+
if attribute is None:
|
|
95
|
+
raise ValueError("attribute is required")
|
|
96
|
+
description = attr.get("description")
|
|
97
|
+
tags = attr.get("tags")
|
|
98
|
+
attribute_needs_persistence = attr.get("attributeNeedsPersistence")
|
|
99
|
+
|
|
100
|
+
message = attr.get("message")
|
|
101
|
+
if message is None:
|
|
102
|
+
if "data" in attr:
|
|
103
|
+
message = {
|
|
104
|
+
"data": attr["data"],
|
|
105
|
+
**({"createdAt": attr["createdAt"]} if attr.get("createdAt") else {}),
|
|
106
|
+
**({"expiresAt": attr["expiresAt"]} if attr.get("expiresAt") else {}),
|
|
107
|
+
}
|
|
108
|
+
elif "table" in attr:
|
|
109
|
+
message = {
|
|
110
|
+
"table": attr["table"],
|
|
111
|
+
**({"createdAt": attr["createdAt"]} if attr.get("createdAt") else {}),
|
|
112
|
+
**({"expiresAt": attr["expiresAt"]} if attr.get("expiresAt") else {}),
|
|
113
|
+
}
|
|
114
|
+
else:
|
|
115
|
+
raise ValueError("Attribute entry must include exactly one of data/table/message")
|
|
116
|
+
|
|
117
|
+
packet = UnsPacket.from_message(message)
|
|
118
|
+
|
|
119
|
+
msg = {
|
|
120
|
+
"topic": base_topic,
|
|
121
|
+
"asset": asset,
|
|
122
|
+
"assetDescription": asset_description,
|
|
123
|
+
"objectType": object_type,
|
|
124
|
+
"objectTypeDescription": object_type_description,
|
|
125
|
+
"objectId": object_id,
|
|
126
|
+
"attribute": attribute,
|
|
127
|
+
"description": description or attribute,
|
|
128
|
+
"tags": tags,
|
|
129
|
+
"attributeNeedsPersistence": attribute_needs_persistence,
|
|
130
|
+
"packet": packet,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if mode == MessageMode.RAW:
|
|
134
|
+
await self._process_and_publish(msg, value_is_cumulative=False)
|
|
135
|
+
elif mode == MessageMode.DELTA:
|
|
136
|
+
delta_msg = dict(msg)
|
|
137
|
+
delta_msg["attribute"] = f"{attribute}-delta"
|
|
138
|
+
delta_msg["description"] = f"{msg['description']} (delta)"
|
|
139
|
+
await self._process_and_publish(delta_msg, value_is_cumulative=True)
|
|
140
|
+
elif mode == MessageMode.BOTH:
|
|
141
|
+
await self._process_and_publish(msg, value_is_cumulative=False)
|
|
142
|
+
delta_msg = dict(msg)
|
|
143
|
+
delta_msg["attribute"] = f"{attribute}-delta"
|
|
144
|
+
delta_msg["description"] = f"{msg['description']} (delta)"
|
|
145
|
+
await self._process_and_publish(delta_msg, value_is_cumulative=True)
|
|
146
|
+
|
|
147
|
+
def _resolve_object_identity(self, msg: Dict[str, Any]) -> None:
|
|
148
|
+
topic = msg.get("topic", "")
|
|
149
|
+
provided_type = msg.get("objectType")
|
|
150
|
+
provided_id = msg.get("objectId")
|
|
151
|
+
provided_asset = msg.get("asset")
|
|
152
|
+
|
|
153
|
+
parts = [p for p in topic.split("/") if p]
|
|
154
|
+
parsed_type = parts[-2] if len(parts) >= 2 else None
|
|
155
|
+
parsed_id = parts[-1] if len(parts) >= 1 else None
|
|
156
|
+
parsed_asset = parts[-3] if len(parts) >= 3 else None
|
|
157
|
+
|
|
158
|
+
msg["objectType"] = provided_type or parsed_type
|
|
159
|
+
msg["objectId"] = provided_id or parsed_id or "main"
|
|
160
|
+
msg["asset"] = provided_asset or parsed_asset
|
|
161
|
+
|
|
162
|
+
def _normalize_topic(self, topic: str) -> str:
|
|
163
|
+
return topic if topic.endswith("/") else f"{topic}/"
|
|
164
|
+
|
|
165
|
+
async def _process_and_publish(self, msg: Dict[str, Any], *, value_is_cumulative: bool) -> None:
|
|
166
|
+
self._resolve_object_identity(msg)
|
|
167
|
+
base_topic = self._normalize_topic(msg.get("topic", ""))
|
|
168
|
+
|
|
169
|
+
packet = msg["packet"]
|
|
170
|
+
message = packet.get("message", {})
|
|
171
|
+
data = message.get("data")
|
|
172
|
+
table = message.get("table")
|
|
173
|
+
|
|
174
|
+
attribute_type = "Data" if data is not None else "Table" if table is not None else None
|
|
175
|
+
data_group = ""
|
|
176
|
+
if isinstance(data, dict):
|
|
177
|
+
data_group = data.get("dataGroup") or ""
|
|
178
|
+
if isinstance(table, dict):
|
|
179
|
+
data_group = table.get("dataGroup") or ""
|
|
180
|
+
|
|
181
|
+
await self.register_unique_topic(
|
|
182
|
+
{
|
|
183
|
+
"timestamp": isoformat(datetime.now(timezone.utc)),
|
|
184
|
+
"topic": base_topic,
|
|
185
|
+
"asset": msg.get("asset"),
|
|
186
|
+
"assetDescription": msg.get("assetDescription"),
|
|
187
|
+
"objectType": msg.get("objectType"),
|
|
188
|
+
"objectTypeDescription": msg.get("objectTypeDescription"),
|
|
189
|
+
"objectId": msg.get("objectId"),
|
|
190
|
+
"attribute": msg.get("attribute"),
|
|
191
|
+
"attributeType": attribute_type,
|
|
192
|
+
"description": msg.get("description"),
|
|
193
|
+
"tags": msg.get("tags"),
|
|
194
|
+
"attributeNeedsPersistence": msg.get("attributeNeedsPersistence"),
|
|
195
|
+
"dataGroup": data_group,
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
publish_topic = (
|
|
200
|
+
f"{base_topic}"
|
|
201
|
+
f"{msg.get('asset') + '/' if msg.get('asset') else ''}"
|
|
202
|
+
f"{msg.get('objectType') + '/' if msg.get('objectType') else ''}"
|
|
203
|
+
f"{msg.get('objectId') + '/' if msg.get('objectId') else ''}"
|
|
204
|
+
f"{msg.get('attribute')}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
seq_id = self._sequence_ids.get(base_topic, 0)
|
|
208
|
+
self._sequence_ids[base_topic] = seq_id + 1
|
|
209
|
+
packet["sequenceId"] = seq_id
|
|
210
|
+
|
|
211
|
+
if isinstance(data, dict):
|
|
212
|
+
time_value = data.get("time")
|
|
213
|
+
if not time_value:
|
|
214
|
+
time_value = UnsPacket.data(value=0)["message"]["data"]["time"]
|
|
215
|
+
data["time"] = time_value
|
|
216
|
+
current_time = datetime.fromisoformat(time_value.replace("Z", "+00:00"))
|
|
217
|
+
new_value = data.get("value")
|
|
218
|
+
new_uom = data.get("uom")
|
|
219
|
+
last = self._last_values.get(publish_topic)
|
|
220
|
+
if last:
|
|
221
|
+
interval_ms = int((current_time - last.timestamp).total_seconds() * 1000)
|
|
222
|
+
packet["interval"] = interval_ms
|
|
223
|
+
if value_is_cumulative and isinstance(new_value, (int, float)) and isinstance(last.value, (int, float)):
|
|
224
|
+
delta = new_value - last.value
|
|
225
|
+
data["value"] = delta
|
|
226
|
+
data["time"] = isoformat(current_time)
|
|
227
|
+
self._last_values[publish_topic] = LastValueEntry(new_value, new_uom, current_time)
|
|
228
|
+
await self.client.publish_raw(publish_topic, UnsPacket.to_json(packet))
|
|
229
|
+
else:
|
|
230
|
+
self._last_values[publish_topic] = LastValueEntry(new_value, new_uom, current_time)
|
|
231
|
+
# For delta mode with no previous value, skip to avoid bogus delta; otherwise publish.
|
|
232
|
+
if not value_is_cumulative:
|
|
233
|
+
await self.client.publish_raw(publish_topic, UnsPacket.to_json(packet))
|
|
234
|
+
elif isinstance(table, dict):
|
|
235
|
+
await self.client.publish_raw(publish_topic, UnsPacket.to_json(packet))
|
|
236
|
+
else:
|
|
237
|
+
raise ValueError("packet.message must include data or table")
|
uns_kit/version.py
ADDED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uns-kit
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: Lightweight Python UNS MQTT client (pub/sub + infra topics)
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Aljoša Vister
|
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
16
|
Requires-Dist: asyncio-mqtt (>=0.16.1,<0.17.0)
|
|
17
17
|
Requires-Dist: click (>=8.1.7,<9.0.0)
|
|
18
|
+
Requires-Dist: paho-mqtt (<2)
|
|
18
19
|
Description-Content-Type: text/markdown
|
|
19
20
|
|
|
20
21
|
# uns-kit (Python)
|
|
@@ -22,8 +23,8 @@ Description-Content-Type: text/markdown
|
|
|
22
23
|
Lightweight UNS MQTT client for Python. Provides:
|
|
23
24
|
- Topic builder compatible with UNS infra topics (`uns-infra/<package>/<version>/<process>/`).
|
|
24
25
|
- Async publish/subscribe via MQTT v5 (using `asyncio-mqtt`).
|
|
25
|
-
-
|
|
26
|
-
- Minimal UNS packet builder/parser (data/table).
|
|
26
|
+
- Process + instance status topics (active/heap/uptime/alive + stats).
|
|
27
|
+
- Minimal UNS packet builder/parser (data/table) aligned with TS core.
|
|
27
28
|
|
|
28
29
|
## Install (editable)
|
|
29
30
|
```bash
|
|
@@ -42,20 +43,21 @@ poetry run uns-kit-py write-config --path config.json
|
|
|
42
43
|
## Quick start
|
|
43
44
|
```python
|
|
44
45
|
import asyncio
|
|
45
|
-
from uns_kit import
|
|
46
|
+
from uns_kit import UnsConfig, UnsPacket, UnsProxyProcess
|
|
46
47
|
|
|
47
48
|
async def main():
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
await
|
|
49
|
+
process = UnsProxyProcess("mqtt-broker", config=UnsConfig(host="mqtt-broker"))
|
|
50
|
+
await process.start()
|
|
51
|
+
mqtt = await process.create_mqtt_proxy("py")
|
|
51
52
|
|
|
52
53
|
# Subscribe
|
|
53
|
-
async with client.messages("uns-infra/#") as messages:
|
|
54
|
-
await
|
|
54
|
+
async with mqtt.client.messages("uns-infra/#") as messages:
|
|
55
|
+
await mqtt.publish_packet("raw/data/", UnsPacket.data(value=1, uom="count"))
|
|
55
56
|
msg = await messages.__anext__()
|
|
56
57
|
print(msg.topic, msg.payload.decode())
|
|
57
58
|
|
|
58
|
-
await
|
|
59
|
+
await mqtt.close()
|
|
60
|
+
await process.stop()
|
|
59
61
|
|
|
60
62
|
asyncio.run(main())
|
|
61
63
|
```
|
|
@@ -69,9 +71,32 @@ async for msg in client.resilient_messages("uns-infra/#"):
|
|
|
69
71
|
### Examples
|
|
70
72
|
- `examples/publish.py` — publish 5 data packets.
|
|
71
73
|
- `examples/subscribe.py` — resilient subscription with auto-reconnect.
|
|
74
|
+
- `examples/load_test.py` — interactive publish burst.
|
|
75
|
+
|
|
76
|
+
### Create a new project
|
|
77
|
+
```bash
|
|
78
|
+
uns-kit-py create my-uns-py-app
|
|
79
|
+
cd my-uns-py-app
|
|
80
|
+
poetry install
|
|
81
|
+
poetry run python src/main.py
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Create a sandbox app in this repo
|
|
85
|
+
From the monorepo root:
|
|
86
|
+
```bash
|
|
87
|
+
pnpm run py:sandbox
|
|
88
|
+
```
|
|
89
|
+
This creates `sandbox-app-py/` using the default Python template.
|
|
72
90
|
|
|
73
91
|
## Notes
|
|
74
|
-
- Default QoS is 0
|
|
75
|
-
-
|
|
76
|
-
- Packet shape mirrors the TypeScript core: `{"version":1,"message":{"data":{...}},"sequenceId":0}`.
|
|
92
|
+
- Default QoS is 0.
|
|
93
|
+
- Instance status topics are published every 10 seconds; stats every 60 seconds.
|
|
94
|
+
- Packet shape mirrors the TypeScript core: `{"version":"1.3.0","message":{"data":{...}},"sequenceId":0}`.
|
|
95
|
+
|
|
96
|
+
## TODO (parity with TS core)
|
|
97
|
+
- Handover manager (cross-version active detection + handover_* messages).
|
|
98
|
+
- Publish throttling/queue.
|
|
99
|
+
- Status parity (publisher/subscriber active flags everywhere, richer metrics).
|
|
100
|
+
- API endpoints registry (to mirror @uns-kit/api produced endpoints).
|
|
101
|
+
- Optional: dictionary/measurement helpers + CLI wrapper.
|
|
77
102
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
uns_kit/__init__.py,sha256=oLhYBOJkoYXBJGN7VDtJpMXAb1v5qFVphun5QgFwthw,564
|
|
2
|
+
uns_kit/cli.py,sha256=USq7bHl6sL75s--Y_nV2j7v1JScZWehZp92GyqYgjyw,5552
|
|
3
|
+
uns_kit/client.py,sha256=GjlGOTPuaxeLlgkfcJWpxMId6nLeZNsZTNBfOYAe8YY,14892
|
|
4
|
+
uns_kit/config.py,sha256=K9FIvFB_zk59I_9XW5PA7sDRJOMl-5-3VCHbpyLgxyM,2899
|
|
5
|
+
uns_kit/packet.py,sha256=M-JJ0--FVe-iFPGnBUFynx_0iKB9F3NFwvETJ7yKibc,5874
|
|
6
|
+
uns_kit/proxy.py,sha256=H1Afs71rdb7DTHYp_8qeoM8uN5vOvAzYK50u9JszXQQ,2338
|
|
7
|
+
uns_kit/proxy_process.py,sha256=If8HXd_YAf1xYjzy8BA73Jo0DRh0-3WpHHIoVcIbS-Q,3906
|
|
8
|
+
uns_kit/status_monitor.py,sha256=8XXBaUHh8SW7gX4d3EtChnEkgQ7rnQ14tEHb0jDB25I,3022
|
|
9
|
+
uns_kit/templates/default/README.md,sha256=V8HXk3MxL9Aq21lm-zt8WOK7n3pDBI4CMrvtRYOPPK4,1213
|
|
10
|
+
uns_kit/templates/default/pyproject.toml,sha256=JT2gYvmofwBuS7FI7U0i_5U3TzyiXrwU6w-bz-cTvKU,540
|
|
11
|
+
uns_kit/templates/default/src/data_example.py,sha256=8D1CyGVsbeKzZV79l8zaX-hqz_gxrA_ps2TA-GwzLQI,4381
|
|
12
|
+
uns_kit/templates/default/src/load_test.py,sha256=cmQ3FzakBisLJU5TTNVpHpfdnIoA32c2pN7dnEwlfgo,3359
|
|
13
|
+
uns_kit/topic_builder.py,sha256=UYC2SS9ptHopeyQ3ud1HEg1IHr5RJ7X2DEkfZC1CM5Y,1938
|
|
14
|
+
uns_kit/uns_mqtt_proxy.py,sha256=QzG0E42r7n33Z4Ri70-DxTlVoM-wj0_wAtnbMyaet08,9831
|
|
15
|
+
uns_kit/version.py,sha256=vmk-z_o25XOVgX5lSUrECllqau8NL7B_pkKsSmwhmX0,247
|
|
16
|
+
uns_kit-0.0.5.dist-info/METADATA,sha256=VZ2JjJ_7twYxRtrSEURUXfPKsdSiqhGKuVIH08Zrg-M,3306
|
|
17
|
+
uns_kit-0.0.5.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
|
|
18
|
+
uns_kit-0.0.5.dist-info/entry_points.txt,sha256=sLvTioiJQfGczUD-ODVx2xwwtHcGKjlIOn8t_Lt87Pg,47
|
|
19
|
+
uns_kit-0.0.5.dist-info/RECORD,,
|
uns_kit-0.0.3.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
uns_kit/__init__.py,sha256=-4R72u-uzg2ib2Z1SFQiSX5KSNYY6oMqVvGoVPI3eQs,290
|
|
2
|
-
uns_kit/cli.py,sha256=BVqLPNegmbEasBr_k2Qt4S_A3fQuP8tn5d5Z_bO5t5w,4800
|
|
3
|
-
uns_kit/client.py,sha256=bFXAKmuR06mSlO8tsAoeB7jv9RQgw1a_AIbXQNpNFTw,5815
|
|
4
|
-
uns_kit/config.py,sha256=mRMiuxGV6CEIBDVZUJQqYIeGxKzfBhlXCcGNXnVtBj4,869
|
|
5
|
-
uns_kit/packet.py,sha256=qd6BrCv_JE8fxh-iaJBAVhUZvY28lhiDr9F1At8CXjw,1675
|
|
6
|
-
uns_kit/topic_builder.py,sha256=JGYdYynRWWSwzXaCaRLjap0-du13eSc6qe1V0GrD_ak,1492
|
|
7
|
-
uns_kit-0.0.3.dist-info/METADATA,sha256=w6WCkwhbS7KmMuqskMiFKgblVFJsgKu4-G_Rbw6WPBk,2604
|
|
8
|
-
uns_kit-0.0.3.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
|
|
9
|
-
uns_kit-0.0.3.dist-info/entry_points.txt,sha256=sLvTioiJQfGczUD-ODVx2xwwtHcGKjlIOn8t_Lt87Pg,47
|
|
10
|
-
uns_kit-0.0.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|