rosotacom-dev 2.2__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.
- rosotacom/__init__.py +32 -0
- rosotacom/__main__.py +8 -0
- rosotacom/anonymize.py +360 -0
- rosotacom/bag_ground_truth.py +395 -0
- rosotacom/benched_set.py +492 -0
- rosotacom/benchmark.py +1842 -0
- rosotacom/bundle_check.py +517 -0
- rosotacom/cli.py +8637 -0
- rosotacom/cli_benchmark.py +8211 -0
- rosotacom/deployment.py +196 -0
- rosotacom/ffmpeg_packet.py +123 -0
- rosotacom/forensics.py +1172 -0
- rosotacom/geomap.py +848 -0
- rosotacom/network_profiles.py +563 -0
- rosotacom/network_shaper.py +127 -0
- rosotacom/plots.py +609 -0
- rosotacom/py.typed +0 -0
- rosotacom/resources/benched-set.yaml +317 -0
- rosotacom/resources/examples/.gitignore +1 -0
- rosotacom/resources/examples/README.md +163 -0
- rosotacom/resources/examples/__init__.py +1 -0
- rosotacom/resources/examples/bags/8_drop_reference/metadata.yaml +17 -0
- rosotacom/resources/examples/deployment.example.yaml +14 -0
- rosotacom/resources/examples/profiles.yaml +85 -0
- rosotacom/resources/examples/ros2docker.json +33 -0
- rosotacom/resources/examples/rosotacom.yaml +12 -0
- rosotacom/resources/examples/scenarios/2_native_chatter/scenario-definition.yaml +10 -0
- rosotacom/resources/examples/scripts/1_heartbeat/run_machine_a.sh +9 -0
- rosotacom/resources/examples/scripts/1_heartbeat/run_machine_b.sh +9 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_a/external.ros2docker.json +21 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_a/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_a/run_external.py +15 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_a/wait_for_echo_topic.sh +10 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_b/external.ros2docker.json +19 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_b/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/2_native_chatter/machine_b/run_external.py +15 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/external.ros2docker.json +12 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/external_session.yaml +9 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/foxglove.json +89 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_a/run_external.py +15 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/costmap_1second/costmap_1second_0.mcap +0 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/costmap_1second/metadata.yaml +44 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/external.ros2docker.json +13 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/3_comp_occ_grid/machine_b/run_external.py +15 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/external.ros2docker.json +12 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/external_session.yaml +9 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/foxglove.json +89 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_a/run_external.py +15 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/costmap_1second/costmap_1second_0.mcap +0 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/costmap_1second/metadata.yaml +44 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/external.ros2docker.json +13 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/4_comp_occ_grid_zen/machine_b/run_external.py +15 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_a/external.ros2docker.json +20 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_a/external_session.yaml +22 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_a/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_a/run_external.py +15 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_b/external.ros2docker.json +20 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_b/external_session.yaml +22 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_b/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/5_sized_payload/machine_b/run_external.py +15 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/external.ros2docker.json +18 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/external_session.yaml +22 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_a/run_external.py +15 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/external.ros2docker.json +18 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/external_session.yaml +22 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/run_communication.sh +9 -0
- rosotacom/resources/examples/scripts/6_sized_payload_zen/machine_b/run_external.py +15 -0
- rosotacom/resources/examples/sessions/10_restamp/session-definition.yaml +39 -0
- rosotacom/resources/examples/sessions/11_trickle/session-definition.yaml +42 -0
- rosotacom/resources/examples/sessions/12_content_integrity/session-definition.yaml +33 -0
- rosotacom/resources/examples/sessions/13_link_latency/session-definition.yaml +42 -0
- rosotacom/resources/examples/sessions/14_remote_assist_anonymized/session-definition.yaml +211 -0
- rosotacom/resources/examples/sessions/15_remote_assist_anonymized_costmap/session-definition.yaml +59 -0
- rosotacom/resources/examples/sessions/16_remote_assist_anonymized_camera/session-definition.yaml +61 -0
- rosotacom/resources/examples/sessions/17_synthetic_camera_quality/session-definition.yaml +57 -0
- rosotacom/resources/examples/sessions/1_heartbeat/session-definition.yaml +26 -0
- rosotacom/resources/examples/sessions/1_heartbeat_status/session-definition.yaml +37 -0
- rosotacom/resources/examples/sessions/2_native_chatter/session-definition.yaml +34 -0
- rosotacom/resources/examples/sessions/3_comp_occ_grid/session-definition.yaml +43 -0
- rosotacom/resources/examples/sessions/4_comp_occ_grid_zen/session-definition.yaml +43 -0
- rosotacom/resources/examples/sessions/5_sized_payload/session-definition.yaml +39 -0
- rosotacom/resources/examples/sessions/6_sized_payload_zen/session-definition.yaml +50 -0
- rosotacom/resources/examples/sessions/7_latched_static/session-definition.yaml +49 -0
- rosotacom/resources/examples/sessions/8_drop/session-definition.yaml +37 -0
- rosotacom/resources/examples/sessions/9_throttle/session-definition.yaml +37 -0
- rosotacom/resources/examples/sessions/bench_1_1_capacity/session-definition.yaml +42 -0
- rosotacom/resources/examples/sessions/bench_1_2_load_sweep/session-definition.yaml +51 -0
- rosotacom/resources/examples/sessions/bench_1_3_ramp/session-definition.yaml +41 -0
- rosotacom/resources/examples/sessions/bench_1_4_recovery/session-definition.yaml +47 -0
- rosotacom/resources/examples/sessions/bench_1_5_camera/session-definition.yaml +45 -0
- rosotacom/resources/ros2docker.json.example +33 -0
- rosotacom/resources/ws/.vscode/c_cpp_properties.json +20 -0
- rosotacom/resources/ws/.vscode/settings.json +8 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_easy_mode.xml.template +32 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_initial_peer_list.xml.template +21 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_v2.xml.template +17 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_v3.xml.template +48 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_v4.xml.template +95 -0
- rosotacom/resources/ws/ota_configs/archive/fastdds_v5.xml.template +64 -0
- rosotacom/resources/ws/ota_configs/create_zenoh_json5.py +56 -0
- rosotacom/resources/ws/ota_configs/cyclonedds_minimal.xml.template +9 -0
- rosotacom/resources/ws/ota_configs/cyclonedds_tuned.xml.template +33 -0
- rosotacom/resources/ws/ota_configs/fastdds_unicast.xml.template +71 -0
- rosotacom/resources/ws/ota_configs/get_ota_xml.py +214 -0
- rosotacom/resources/ws/ota_configs/utils.py +74 -0
- rosotacom/resources/ws/ota_configs/zenoh.json5.template +32 -0
- rosotacom/resources/ws/ros2src/.gitkeep +0 -0
- rosotacom/resources/ws/ros2src/com_cpp/CMakeLists.txt +67 -0
- rosotacom/resources/ws/ros2src/com_cpp/COLCON_IGNORE +0 -0
- rosotacom/resources/ws/ros2src/com_cpp/LICENSE.txt +25 -0
- rosotacom/resources/ws/ros2src/com_cpp/package.xml +37 -0
- rosotacom/resources/ws/ros2src/com_cpp/src/hz_monitor.cpp +192 -0
- rosotacom/resources/ws/ros2src/com_cpp/src/universal_compressor.cpp +283 -0
- rosotacom/resources/ws/ros2src/com_cpp/src/universal_decompressor.cpp +306 -0
- rosotacom/resources/ws/ros2src/com_msgs/CMakeLists.txt +22 -0
- rosotacom/resources/ws/ros2src/com_msgs/LICENSE.txt +25 -0
- rosotacom/resources/ws/ros2src/com_msgs/msg/CompressedData.msg +3 -0
- rosotacom/resources/ws/ros2src/com_msgs/msg/EchoHeartbeat.msg +10 -0
- rosotacom/resources/ws/ros2src/com_msgs/msg/OtaStamped.msg +4 -0
- rosotacom/resources/ws/ros2src/com_msgs/msg/SizedPayload.msg +4 -0
- rosotacom/resources/ws/ros2src/com_msgs/package.xml +25 -0
- rosotacom/resources/ws/ros2src/com_py/LICENSE.txt +25 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/__init__.py +0 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/base_bridge_relay.py +106 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/bridge_in.py +76 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/bridge_out.py +70 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/heartbeat_echo.py +221 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/hz_monitor.py +178 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/image_bandwidth_reducer.py +218 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/image_pixel_capper.py +370 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/lanelet_saver.py +80 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/latch_relay.py +205 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/costmap_remapper.py +73 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/marker_remapper.py +93 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/speed_test.py +107 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/tf_remapper.py +156 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/traffic_monitor.py +91 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/legacy/wifi_network_analysis_1.py +213 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/link_bytes.py +177 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/link_trace.py +186 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/local_global_frame_bridge.py +465 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/localization_map_split_reduce_saver.py +258 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/msg_transform.py +277 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/pair_management.py +87 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/pub_sub_pair.py +145 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/qos.py +254 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/relay_in.py +78 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/relay_out.py +75 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/restamp.py +191 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/sized_publisher.py +244 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/stage_latency.py +64 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/stage_latency_core.py +51 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/status_overview.py +476 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/status_overview_core.py +989 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/tf_map_to_cart_saver.py +99 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/topic_monitor.py +327 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/topic_resolution.py +171 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/trickle.py +183 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/universal_compressor.py +653 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/universal_decompressor.py +372 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/universal_ota_unwrapper.py +191 -0
- rosotacom/resources/ws/ros2src/com_py/com_py/universal_ota_wrapper.py +154 -0
- rosotacom/resources/ws/ros2src/com_py/package.xml +34 -0
- rosotacom/resources/ws/ros2src/com_py/resource/com_py +0 -0
- rosotacom/resources/ws/ros2src/com_py/setup.cfg +6 -0
- rosotacom/resources/ws/ros2src/com_py/setup.py +55 -0
- rosotacom/resources/ws/ros2src/com_py/test/test_copyright.py +25 -0
- rosotacom/resources/ws/ros2src/com_py/test/test_flake8.py +25 -0
- rosotacom/resources/ws/ros2src/com_py/test/test_pep257.py +23 -0
- rosotacom/resources/ws/session/content/base/session_plugin_base.yaml +633 -0
- rosotacom/resources/ws/session/content/network/up_connectivity.py +364 -0
- rosotacom/resources/ws/session/content/network/wifi_network_analysis_2.py +215 -0
- rosotacom/resources/ws/session/creation/anonymize_bag.py +156 -0
- rosotacom/resources/ws/session/creation/catmux_log_setup.sh +65 -0
- rosotacom/resources/ws/session/creation/create_session_yaml.py +145 -0
- rosotacom/resources/ws/session/creation/generate_session_files.py +3217 -0
- rosotacom/resources/ws/session/creation/run_session.py +447 -0
- rosotacom/resources/ws/session/creation/run_session_in_external_terminal.py +54 -0
- rosotacom/resources/ws/session/creation/strip_ansi.py +42 -0
- rosotacom/status_eval.py +475 -0
- rosotacom/stream_stats.py +650 -0
- rosotacom/trace_profiles.py +561 -0
- rosotacom/transit.py +249 -0
- rosotacom/video_quality.py +906 -0
- rosotacom_dev-2.2.dist-info/METADATA +874 -0
- rosotacom_dev-2.2.dist-info/RECORD +195 -0
- rosotacom_dev-2.2.dist-info/WHEEL +5 -0
- rosotacom_dev-2.2.dist-info/entry_points.txt +4 -0
- rosotacom_dev-2.2.dist-info/licenses/LICENSE.txt +25 -0
- rosotacom_dev-2.2.dist-info/top_level.txt +1 -0
rosotacom/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""rosotacom package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
_DISTRIBUTION_NAME = "rosotacom"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _package_version() -> str:
|
|
12
|
+
try:
|
|
13
|
+
return version(_DISTRIBUTION_NAME)
|
|
14
|
+
except PackageNotFoundError:
|
|
15
|
+
return "0+unknown"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__version__ = _package_version()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def __getattr__(name: str) -> Any:
|
|
22
|
+
"""Lazily expose the historical root-module CLI attributes."""
|
|
23
|
+
|
|
24
|
+
from . import cli
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
return getattr(cli, name)
|
|
28
|
+
except AttributeError as exc:
|
|
29
|
+
raise AttributeError(f"module 'rosotacom' has no attribute {name!r}") from exc
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = ["__version__"]
|
rosotacom/__main__.py
ADDED
rosotacom/anonymize.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import tempfile
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class HandoffTopic:
|
|
14
|
+
source_peer: str
|
|
15
|
+
target_peer: str
|
|
16
|
+
direction: str
|
|
17
|
+
source_topic: str
|
|
18
|
+
source_type: str | None
|
|
19
|
+
handoff_topic: str
|
|
20
|
+
handoff_type: str | None
|
|
21
|
+
generic_topic: str
|
|
22
|
+
expect: dict[str, Any] | None
|
|
23
|
+
qos: dict[str, Any] | None
|
|
24
|
+
zen_qos: dict[str, Any] | None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class PlaybackTopic:
|
|
29
|
+
source_peer: str
|
|
30
|
+
bag_topic: str
|
|
31
|
+
publish_topic: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def metadata_path_for_bag(path: Path) -> Path:
|
|
35
|
+
return path / "metadata.yaml" if path.is_dir() else path.parent / "metadata.yaml"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_bag_metadata(path: Path) -> dict[str, Any]:
|
|
39
|
+
metadata_path = metadata_path_for_bag(path)
|
|
40
|
+
if not metadata_path.exists():
|
|
41
|
+
raise FileNotFoundError(f"metadata.yaml not found for bag at {path}")
|
|
42
|
+
loaded = yaml.safe_load(metadata_path.read_text(encoding="utf-8")) or {}
|
|
43
|
+
if not isinstance(loaded, dict):
|
|
44
|
+
raise RuntimeError(f"Bag metadata must contain a mapping: {metadata_path}")
|
|
45
|
+
return loaded
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def bag_storage_id(metadata: dict[str, Any]) -> str:
|
|
49
|
+
return str((metadata.get("rosbag2_bagfile_information") or {}).get("storage_identifier") or "mcap")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def bag_topics_info(metadata: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
53
|
+
info = metadata.get("rosbag2_bagfile_information") or {}
|
|
54
|
+
topics: dict[str, dict[str, Any]] = {}
|
|
55
|
+
for entry in info.get("topics_with_message_count") or []:
|
|
56
|
+
meta = entry.get("topic_metadata") or {}
|
|
57
|
+
name = meta.get("name")
|
|
58
|
+
if not name:
|
|
59
|
+
continue
|
|
60
|
+
topics[str(name)] = {
|
|
61
|
+
"type": meta.get("type"),
|
|
62
|
+
"serialization_format": meta.get("serialization_format", "cdr"),
|
|
63
|
+
"offered_qos_profiles": meta.get("offered_qos_profiles", []),
|
|
64
|
+
"message_count": int(entry.get("message_count") or 0),
|
|
65
|
+
}
|
|
66
|
+
return topics
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _topic_entry_topic(entry: Any) -> str | None:
|
|
70
|
+
if isinstance(entry, str):
|
|
71
|
+
return entry
|
|
72
|
+
if isinstance(entry, dict) and isinstance(entry.get("topic"), str):
|
|
73
|
+
return str(entry["topic"])
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _topic_entry_type(entry: Any) -> str | None:
|
|
78
|
+
if isinstance(entry, dict) and entry.get("type") is not None:
|
|
79
|
+
return str(entry["type"])
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _topic_entry_mapping(entry: Any) -> dict[str, Any]:
|
|
84
|
+
if isinstance(entry, dict):
|
|
85
|
+
return dict(entry)
|
|
86
|
+
if isinstance(entry, str):
|
|
87
|
+
return {"topic": entry}
|
|
88
|
+
return {}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _peer_keys(session_cfg: dict[str, Any]) -> list[str]:
|
|
92
|
+
peers = session_cfg.get("peers")
|
|
93
|
+
if not isinstance(peers, dict) or not peers:
|
|
94
|
+
raise RuntimeError("session configuration must define peers before anonymization planning")
|
|
95
|
+
keys = list(peers.keys())
|
|
96
|
+
if len(keys) != 2:
|
|
97
|
+
raise RuntimeError(f"anonymization planning currently supports exactly 2 peers, got {keys}")
|
|
98
|
+
return keys
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _planning_config(session_cfg: dict[str, Any]) -> dict[str, Any]:
|
|
102
|
+
cfg = copy.deepcopy(session_cfg)
|
|
103
|
+
shared = cfg.get("shared") or {}
|
|
104
|
+
if not isinstance(shared, dict):
|
|
105
|
+
shared = {}
|
|
106
|
+
shared["use_status_overview"] = True
|
|
107
|
+
cfg["shared"] = shared
|
|
108
|
+
return cfg
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _dummy_peer_addresses(peer_keys: list[str]) -> dict[str, str]:
|
|
112
|
+
return {peer: f"127.99.0.{idx + 1}" for idx, peer in enumerate(peer_keys)}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _load_pipeline_spec(path: Path) -> dict[str, Any]:
|
|
116
|
+
loaded = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
117
|
+
if not isinstance(loaded, dict):
|
|
118
|
+
raise RuntimeError(f"pipeline spec must contain a mapping: {path}")
|
|
119
|
+
return loaded
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _handoff_stage(topic_spec: dict[str, Any]) -> dict[str, Any]:
|
|
123
|
+
stages = topic_spec.get("stages") or []
|
|
124
|
+
if not isinstance(stages, list):
|
|
125
|
+
raise RuntimeError(f"pipeline topic stages must be a list for {topic_spec.get('base')!r}")
|
|
126
|
+
for stage_name in ("processed", "native"):
|
|
127
|
+
for stage in stages:
|
|
128
|
+
if isinstance(stage, dict) and stage.get("stage") == stage_name:
|
|
129
|
+
return stage
|
|
130
|
+
raise RuntimeError(f"no outbound handoff stage found for {topic_spec.get('base')!r}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _outbound_specs_by_base(spec: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
|
|
134
|
+
by_base: dict[str, list[dict[str, Any]]] = {}
|
|
135
|
+
for topic_spec in spec.get("topics") or []:
|
|
136
|
+
if not isinstance(topic_spec, dict) or topic_spec.get("direction") != "outbound":
|
|
137
|
+
continue
|
|
138
|
+
base = topic_spec.get("base")
|
|
139
|
+
if not isinstance(base, str):
|
|
140
|
+
continue
|
|
141
|
+
by_base.setdefault(base, []).append(topic_spec)
|
|
142
|
+
return by_base
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def plan_handoff_topics(session_cfg: dict[str, Any], session_gen: Any) -> list[HandoffTopic]:
|
|
146
|
+
peer_keys = _peer_keys(session_cfg)
|
|
147
|
+
topics_cfg = session_cfg.get("topics") or {}
|
|
148
|
+
if not isinstance(topics_cfg, dict):
|
|
149
|
+
raise RuntimeError("session configuration topics must be a mapping")
|
|
150
|
+
|
|
151
|
+
planned: list[HandoffTopic] = []
|
|
152
|
+
with tempfile.TemporaryDirectory(prefix="rosotacom-anonymize-plan-") as tmp:
|
|
153
|
+
tmp_path = Path(tmp)
|
|
154
|
+
session_gen.func(
|
|
155
|
+
session_config_obj=_planning_config(session_cfg),
|
|
156
|
+
output_dir=str(tmp_path),
|
|
157
|
+
peer_addresses=_dummy_peer_addresses(peer_keys),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
for source_peer in peer_keys:
|
|
161
|
+
target_peer = peer_keys[0] if source_peer == peer_keys[1] else peer_keys[1]
|
|
162
|
+
direction = f"{source_peer}_to_{target_peer}"
|
|
163
|
+
entries = topics_cfg.get(direction) or []
|
|
164
|
+
if not isinstance(entries, list) or not entries:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
spec_path = tmp_path / source_peer / "pipeline_spec.yaml"
|
|
168
|
+
if not spec_path.exists():
|
|
169
|
+
raise RuntimeError(f"pipeline spec missing for peer {source_peer}: {spec_path}")
|
|
170
|
+
by_base = _outbound_specs_by_base(_load_pipeline_spec(spec_path))
|
|
171
|
+
|
|
172
|
+
for entry in entries:
|
|
173
|
+
source_topic = _topic_entry_topic(entry)
|
|
174
|
+
if not source_topic:
|
|
175
|
+
continue
|
|
176
|
+
candidates = by_base.get(source_topic) or []
|
|
177
|
+
if not candidates:
|
|
178
|
+
raise RuntimeError(
|
|
179
|
+
f"generated pipeline has no outbound topic for {direction} source topic {source_topic!r}"
|
|
180
|
+
)
|
|
181
|
+
topic_spec = candidates.pop(0)
|
|
182
|
+
handoff = _handoff_stage(topic_spec)
|
|
183
|
+
generic_topic = f"/topic{len(planned) + 1}"
|
|
184
|
+
entry_map = _topic_entry_mapping(entry)
|
|
185
|
+
expect = entry_map.get("expect")
|
|
186
|
+
qos = entry_map.get("qos")
|
|
187
|
+
zen_qos = entry_map.get("zen_qos")
|
|
188
|
+
planned.append(
|
|
189
|
+
HandoffTopic(
|
|
190
|
+
source_peer=source_peer,
|
|
191
|
+
target_peer=target_peer,
|
|
192
|
+
direction=direction,
|
|
193
|
+
source_topic=source_topic,
|
|
194
|
+
source_type=_topic_entry_type(entry),
|
|
195
|
+
handoff_topic=str(handoff["topic"]),
|
|
196
|
+
handoff_type=(str(handoff["type"]) if handoff.get("type") is not None else None),
|
|
197
|
+
generic_topic=generic_topic,
|
|
198
|
+
expect=copy.deepcopy(expect) if isinstance(expect, dict) else None,
|
|
199
|
+
qos=copy.deepcopy(qos) if isinstance(qos, dict) else None,
|
|
200
|
+
zen_qos=copy.deepcopy(zen_qos) if isinstance(zen_qos, dict) else None,
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if not planned:
|
|
205
|
+
raise RuntimeError("session configuration does not define any anonymizable outbound topics")
|
|
206
|
+
return planned
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def topics_map(plan: list[HandoffTopic]) -> dict[str, str]:
|
|
210
|
+
return {item.handoff_topic: item.generic_topic for item in plan}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def source_topics_by_peer(plan: list[HandoffTopic]) -> dict[str, list[str]]:
|
|
214
|
+
by_peer: dict[str, list[str]] = {}
|
|
215
|
+
for item in plan:
|
|
216
|
+
by_peer.setdefault(item.source_peer, []).append(item.generic_topic)
|
|
217
|
+
return by_peer
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def playback_topics_by_peer(
|
|
221
|
+
replay_session_cfg: dict[str, Any],
|
|
222
|
+
plan: list[HandoffTopic],
|
|
223
|
+
session_gen: Any,
|
|
224
|
+
) -> dict[str, list[PlaybackTopic]]:
|
|
225
|
+
replay_plan = plan_handoff_topics(replay_session_cfg, session_gen)
|
|
226
|
+
replay_publish_topics = {(item.source_peer, item.source_topic): item.handoff_topic for item in replay_plan}
|
|
227
|
+
|
|
228
|
+
by_peer: dict[str, list[PlaybackTopic]] = {}
|
|
229
|
+
for item in plan:
|
|
230
|
+
publish_topic = replay_publish_topics.get((item.source_peer, item.generic_topic))
|
|
231
|
+
if publish_topic is None:
|
|
232
|
+
raise RuntimeError(
|
|
233
|
+
"generated replay session has no outbound handoff topic for "
|
|
234
|
+
f"{item.source_peer} bag topic {item.generic_topic!r}"
|
|
235
|
+
)
|
|
236
|
+
by_peer.setdefault(item.source_peer, []).append(
|
|
237
|
+
PlaybackTopic(
|
|
238
|
+
source_peer=item.source_peer,
|
|
239
|
+
bag_topic=item.generic_topic,
|
|
240
|
+
publish_topic=publish_topic,
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
return by_peer
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def missing_handoff_topics(plan: list[HandoffTopic], info_by_topic: dict[str, dict[str, Any]]) -> list[HandoffTopic]:
|
|
247
|
+
return [item for item in plan if item.handoff_topic not in info_by_topic]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _replay_expect(expect: Any) -> dict[str, Any] | None:
|
|
251
|
+
if not isinstance(expect, dict):
|
|
252
|
+
return None
|
|
253
|
+
replay = copy.deepcopy(expect)
|
|
254
|
+
replay.pop("latency_ms", None)
|
|
255
|
+
return replay or None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def build_replay_session_config(session_cfg: dict[str, Any], plan: list[HandoffTopic]) -> dict[str, Any]:
|
|
259
|
+
cfg = copy.deepcopy(session_cfg)
|
|
260
|
+
plan_by_direction: dict[str, list[HandoffTopic]] = {}
|
|
261
|
+
for item in plan:
|
|
262
|
+
plan_by_direction.setdefault(item.direction, []).append(item)
|
|
263
|
+
|
|
264
|
+
replay_topics: dict[str, list[dict[str, Any]]] = {}
|
|
265
|
+
topics_cfg = session_cfg.get("topics") or {}
|
|
266
|
+
if not isinstance(topics_cfg, dict):
|
|
267
|
+
topics_cfg = {}
|
|
268
|
+
|
|
269
|
+
for direction, entries in topics_cfg.items():
|
|
270
|
+
if not isinstance(entries, list):
|
|
271
|
+
continue
|
|
272
|
+
direction_plan = list(plan_by_direction.get(direction) or [])
|
|
273
|
+
rewritten: list[dict[str, Any]] = []
|
|
274
|
+
for entry in entries:
|
|
275
|
+
if not direction_plan:
|
|
276
|
+
continue
|
|
277
|
+
item = direction_plan.pop(0)
|
|
278
|
+
new_entry = _topic_entry_mapping(entry)
|
|
279
|
+
new_entry["topic"] = item.generic_topic
|
|
280
|
+
if item.handoff_type:
|
|
281
|
+
new_entry["type"] = item.handoff_type
|
|
282
|
+
new_entry.pop("processing", None)
|
|
283
|
+
replay_expect = _replay_expect(new_entry.get("expect"))
|
|
284
|
+
if replay_expect is None:
|
|
285
|
+
new_entry.pop("expect", None)
|
|
286
|
+
else:
|
|
287
|
+
new_entry["expect"] = replay_expect
|
|
288
|
+
rewritten.append(new_entry)
|
|
289
|
+
replay_topics[direction] = rewritten
|
|
290
|
+
|
|
291
|
+
cfg["topics"] = replay_topics
|
|
292
|
+
return cfg
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _first_qos_profile(raw: Any) -> dict[str, Any]:
|
|
296
|
+
offered = raw
|
|
297
|
+
if isinstance(offered, str):
|
|
298
|
+
try:
|
|
299
|
+
offered = yaml.safe_load(offered)
|
|
300
|
+
except yaml.YAMLError:
|
|
301
|
+
return {}
|
|
302
|
+
if isinstance(offered, list) and offered and isinstance(offered[0], dict):
|
|
303
|
+
return dict(offered[0])
|
|
304
|
+
if isinstance(offered, dict):
|
|
305
|
+
return dict(offered)
|
|
306
|
+
return {}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def playback_qos_overrides(
|
|
310
|
+
plan: list[HandoffTopic],
|
|
311
|
+
info_by_topic: dict[str, dict[str, Any]],
|
|
312
|
+
) -> dict[str, dict[str, Any]]:
|
|
313
|
+
allowed = {
|
|
314
|
+
"history",
|
|
315
|
+
"depth",
|
|
316
|
+
"reliability",
|
|
317
|
+
"durability",
|
|
318
|
+
"deadline",
|
|
319
|
+
"lifespan",
|
|
320
|
+
"liveliness",
|
|
321
|
+
"liveliness_lease_duration",
|
|
322
|
+
}
|
|
323
|
+
overrides: dict[str, dict[str, Any]] = {}
|
|
324
|
+
for item in plan:
|
|
325
|
+
info = info_by_topic.get(item.handoff_topic) or {}
|
|
326
|
+
qos = _first_qos_profile(info.get("offered_qos_profiles"))
|
|
327
|
+
clean = {key: value for key, value in qos.items() if key in allowed and value is not None}
|
|
328
|
+
if clean:
|
|
329
|
+
overrides[item.generic_topic] = clean
|
|
330
|
+
return overrides
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def anonymization_manifest(
|
|
334
|
+
plan: list[HandoffTopic],
|
|
335
|
+
info_by_topic: dict[str, dict[str, Any]],
|
|
336
|
+
) -> dict[str, Any]:
|
|
337
|
+
topics: list[dict[str, Any]] = []
|
|
338
|
+
for item in plan:
|
|
339
|
+
info = info_by_topic.get(item.handoff_topic) or {}
|
|
340
|
+
topics.append(
|
|
341
|
+
{
|
|
342
|
+
"source_peer": item.source_peer,
|
|
343
|
+
"target_peer": item.target_peer,
|
|
344
|
+
"direction": item.direction,
|
|
345
|
+
"source_topic": item.source_topic,
|
|
346
|
+
"source_type": item.source_type,
|
|
347
|
+
"handoff_topic": item.handoff_topic,
|
|
348
|
+
"handoff_type": item.handoff_type,
|
|
349
|
+
"generic_topic": item.generic_topic,
|
|
350
|
+
"message_count": info.get("message_count"),
|
|
351
|
+
"serialization_format": info.get("serialization_format"),
|
|
352
|
+
"qos_source": "bag.offered_qos_profiles" if info.get("offered_qos_profiles") else "none",
|
|
353
|
+
"offered_qos_profiles": info.get("offered_qos_profiles") or [],
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
return {
|
|
357
|
+
"schema_version": 1,
|
|
358
|
+
"mode": "processed_handoff_replay",
|
|
359
|
+
"topics": topics,
|
|
360
|
+
}
|