pymcap-cli 0.10.0__tar.gz → 0.12.0__tar.gz
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.
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/PKG-INFO +1 -1
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/pyproject.toml +1 -1
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cli.py +34 -26
- pymcap_cli-0.12.0/src/pymcap_cli/cmd/_rechunk_strategy.py +42 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/diff_cmd.py +95 -23
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/duplicates_cmd.py +202 -92
- pymcap_cli-0.12.0/src/pymcap_cli/cmd/index_cmd.py +1066 -0
- pymcap_cli-0.12.0/src/pymcap_cli/cmd/process_cmd.py +733 -0
- pymcap_cli-0.12.0/src/pymcap_cli/cmd/rechunk_cmd.py +248 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/split_cmd.py +5 -12
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/tf_export_cmd.py +4 -2
- pymcap_cli-0.12.0/src/pymcap_cli/cmd/tf_get_cmd.py +182 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/tftree_cmd.py +46 -27
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/mcap_compare.py +455 -3
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/mcap_processor.py +116 -123
- pymcap_cli-0.12.0/src/pymcap_cli/core/processors/ARCHITECTURE.md +180 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/base.py +27 -1
- pymcap_cli-0.12.0/src/pymcap_cli/core/processors/chunk_groupers.py +61 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/dedup.py +4 -2
- pymcap_cli-0.12.0/src/pymcap_cli/core/tf_findings.py +254 -0
- pymcap_cli-0.12.0/src/pymcap_cli/core/tf_tree.py +551 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/video_file_writer.py +19 -12
- pymcap_cli-0.12.0/src/pymcap_cli/index/db.py +78 -0
- pymcap_cli-0.12.0/src/pymcap_cli/index/fingerprint.py +50 -0
- pymcap_cli-0.12.0/src/pymcap_cli/index/migrations/0001.py +155 -0
- pymcap_cli-0.12.0/src/pymcap_cli/index/migrations/__init__.py +67 -0
- pymcap_cli-0.12.0/src/pymcap_cli/index/scanner.py +687 -0
- pymcap_cli-0.12.0/src/pymcap_cli/index/summary_fingerprint.py +132 -0
- pymcap_cli-0.12.0/src/pymcap_cli/types/__init__.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/types/duration.py +14 -0
- pymcap_cli-0.10.0/src/pymcap_cli/cmd/process_cmd.py +0 -237
- pymcap_cli-0.10.0/src/pymcap_cli/cmd/rechunk_cmd.py +0 -185
- pymcap_cli-0.10.0/src/pymcap_cli/core/tf_tree.py +0 -154
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/README.md +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/__init__.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/__init__.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/_run_processor.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/_run_processor_multi.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/bag2mcap_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/bridge/__init__.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/bridge/_shared.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/bridge/cat.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/bridge/inspect.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/bridge/record.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/cat_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/compress_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/convert_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/diag_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/doctor_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/du_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/export_csv_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/export_geo_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/export_images_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/export_json_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/export_parquet_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/export_pcd_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/filter_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/get_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/info_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/info_json_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/list_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/merge_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/plot_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/records_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/recover_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/recover_inplace_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/roscompress_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/rosdecompress_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/topic_chunks_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/cmd/video_cmd.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/constants.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/__init__.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/input_handler.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/input_options.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/input_processor_chain.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/mcap_transform.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/msg_resolver.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/__init__.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/always_decode.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/attachment_filter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/boundary_split.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/channel_merge.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/duration_split.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/expression_split.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/latching.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/metadata_filter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/nth_message.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/size_split.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/time_filter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/time_offset.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/timestamp_split.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/topic_alias.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/topic_filter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/topic_rewrite.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/core/processors/utils.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/debug_wrapper.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/display/__init__.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/display/cat_helpers.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/display/display_utils.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/display/message_render.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/display/osc_utils.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/display/sparkline.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/display/time_ranges.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/doctor.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/encoding/__init__.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/encoding/arrow_schema.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/__init__.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/_common.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/base.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/csv_exporter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/driver.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/geo_common.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/geojson_exporter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/gpx_exporter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/image_exporter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/json_exporter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/kml_exporter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/parquet_exporter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/pcd_exporter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/plot_exporter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/sdf_exporter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/urdf_exporter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/exporters/video_exporter.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/http_utils.py +0 -0
- {pymcap_cli-0.10.0/src/pymcap_cli/types → pymcap_cli-0.12.0/src/pymcap_cli/index}/__init__.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/log_setup.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/py.typed +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/rihs01.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/rosbag_reader/__init__.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/rosbag_reader/_reader.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/rosbag_reader/_types.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/rosbag_reader/py.typed +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/types/info_data.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/types/info_link.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/types/info_types.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/types/size.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/types/to_plain.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/types/types_manual.py +0 -0
- {pymcap_cli-0.10.0 → pymcap_cli-0.12.0}/src/pymcap_cli/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pymcap-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: High-performance Python CLI for MCAP file processing with advanced recovery, filtering, and optimization capabilities
|
|
5
5
|
Keywords: mcap,cli,robotics,ros,ros2,recovery,filtering,compression
|
|
6
6
|
Author: Marko Bausch
|
|
@@ -33,6 +33,7 @@ from pymcap_cli.cmd import (
|
|
|
33
33
|
recover_inplace_cmd,
|
|
34
34
|
split_cmd,
|
|
35
35
|
tf_export_cmd,
|
|
36
|
+
tf_get_cmd,
|
|
36
37
|
tftree_cmd,
|
|
37
38
|
topic_chunks_cmd,
|
|
38
39
|
)
|
|
@@ -120,6 +121,22 @@ def _load_optional_app(
|
|
|
120
121
|
return cast("App", getattr(module, app_name))
|
|
121
122
|
|
|
122
123
|
|
|
124
|
+
# Modules that go missing together when a given extra is absent. Used by the
|
|
125
|
+
# optional-command loader to suppress ImportError for the *expected* shape of
|
|
126
|
+
# "extra not installed."
|
|
127
|
+
_VIDEO_MODULES: tuple[str, ...] = ("av", "mcap_codec_support", "numpy")
|
|
128
|
+
_POINTCLOUD_MODULES: tuple[str, ...] = (
|
|
129
|
+
"DracoPy",
|
|
130
|
+
"mcap_codec_support",
|
|
131
|
+
"numpy",
|
|
132
|
+
"pointcloud2",
|
|
133
|
+
"pureini",
|
|
134
|
+
)
|
|
135
|
+
_VIDEO_AND_POINTCLOUD_MODULES: tuple[str, ...] = tuple(
|
|
136
|
+
sorted({*_VIDEO_MODULES, *_POINTCLOUD_MODULES})
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
123
140
|
bridge_app = _load_optional_app(
|
|
124
141
|
"pymcap_cli.cmd.bridge",
|
|
125
142
|
"bridge_app",
|
|
@@ -130,7 +147,7 @@ bridge_app = _load_optional_app(
|
|
|
130
147
|
video = _load_optional_command(
|
|
131
148
|
"pymcap_cli.cmd.video_cmd",
|
|
132
149
|
"video",
|
|
133
|
-
expected_missing_modules=
|
|
150
|
+
expected_missing_modules=_VIDEO_MODULES,
|
|
134
151
|
message="Video command requires the 'video' extra.",
|
|
135
152
|
install_command="uv add 'pymcap-cli[video]'",
|
|
136
153
|
)
|
|
@@ -144,35 +161,21 @@ plot = _load_optional_command(
|
|
|
144
161
|
roscompress = _load_optional_command(
|
|
145
162
|
"pymcap_cli.cmd.roscompress_cmd",
|
|
146
163
|
"roscompress",
|
|
147
|
-
expected_missing_modules=
|
|
148
|
-
"av",
|
|
149
|
-
"DracoPy",
|
|
150
|
-
"mcap_codec_support",
|
|
151
|
-
"numpy",
|
|
152
|
-
"pointcloud2",
|
|
153
|
-
"pureini",
|
|
154
|
-
),
|
|
164
|
+
expected_missing_modules=_VIDEO_AND_POINTCLOUD_MODULES,
|
|
155
165
|
message="ROS compression requires the 'video' and 'pointcloud' extras.",
|
|
156
166
|
install_command="uv add 'pymcap-cli[video,pointcloud]'",
|
|
157
167
|
)
|
|
158
168
|
export_parquet = _load_optional_command(
|
|
159
169
|
"pymcap_cli.cmd.export_parquet_cmd",
|
|
160
170
|
"export_parquet",
|
|
161
|
-
expected_missing_modules=(
|
|
162
|
-
"DracoPy",
|
|
163
|
-
"mcap_codec_support",
|
|
164
|
-
"numpy",
|
|
165
|
-
"pointcloud2",
|
|
166
|
-
"pureini",
|
|
167
|
-
"pyarrow",
|
|
168
|
-
),
|
|
171
|
+
expected_missing_modules=(*_POINTCLOUD_MODULES, "pyarrow"),
|
|
169
172
|
message="Parquet export requires the 'parquet' extra.",
|
|
170
173
|
install_command="uv add 'pymcap-cli[parquet]'",
|
|
171
174
|
)
|
|
172
175
|
export_pcd = _load_optional_command(
|
|
173
176
|
"pymcap_cli.cmd.export_pcd_cmd",
|
|
174
177
|
"export_pcd",
|
|
175
|
-
expected_missing_modules=
|
|
178
|
+
expected_missing_modules=_POINTCLOUD_MODULES,
|
|
176
179
|
message="PCD export requires the 'pointcloud' extra.",
|
|
177
180
|
install_command="uv add 'pymcap-cli[pointcloud]'",
|
|
178
181
|
)
|
|
@@ -186,23 +189,25 @@ export_images = _load_optional_command(
|
|
|
186
189
|
rosdecompress = _load_optional_command(
|
|
187
190
|
"pymcap_cli.cmd.rosdecompress_cmd",
|
|
188
191
|
"rosdecompress",
|
|
189
|
-
expected_missing_modules=
|
|
190
|
-
"av",
|
|
191
|
-
"DracoPy",
|
|
192
|
-
"mcap_codec_support",
|
|
193
|
-
"numpy",
|
|
194
|
-
"pointcloud2",
|
|
195
|
-
"pureini",
|
|
196
|
-
),
|
|
192
|
+
expected_missing_modules=_VIDEO_AND_POINTCLOUD_MODULES,
|
|
197
193
|
message="ROS decompression requires the 'video' and 'pointcloud' extras.",
|
|
198
194
|
install_command="uv add 'pymcap-cli[video,pointcloud]'",
|
|
199
195
|
)
|
|
196
|
+
index_app = _load_optional_app(
|
|
197
|
+
"pymcap_cli.cmd.index_cmd",
|
|
198
|
+
"index_app",
|
|
199
|
+
expected_missing_modules=("xxhash",),
|
|
200
|
+
message="Index command requires the 'xxhash' extra "
|
|
201
|
+
"(fingerprint stability requires a single committed hash function).",
|
|
202
|
+
install_command="uv add 'pymcap-cli[xxhash]'",
|
|
203
|
+
)
|
|
200
204
|
|
|
201
205
|
|
|
202
206
|
app = App(
|
|
203
207
|
name="pymcap-cli",
|
|
204
208
|
help="CLI tool for slicing and dicing MCAP files.",
|
|
205
209
|
help_format="rich",
|
|
210
|
+
help_on_error=True,
|
|
206
211
|
default_parameter=Parameter(negative_iterable=""),
|
|
207
212
|
)
|
|
208
213
|
|
|
@@ -220,11 +225,14 @@ app.command(name="diff", group=inspect_group)(diff_cmd.diff_cmd)
|
|
|
220
225
|
app.command(name="duplicates", group=inspect_group)(duplicates_cmd.duplicates)
|
|
221
226
|
app.command(name="info", group=inspect_group)(info_cmd.info)
|
|
222
227
|
app.command(name="info-json", group=inspect_group)(info_json_cmd.info_json)
|
|
228
|
+
index_app.group = (inspect_group,)
|
|
229
|
+
app.command(index_app, name="index")
|
|
223
230
|
get_cmd.get_app.group = (inspect_group,)
|
|
224
231
|
app.command(get_cmd.get_app, name="get")
|
|
225
232
|
list_cmd.list_app.group = (inspect_group,)
|
|
226
233
|
app.command(list_cmd.list_app, name="list")
|
|
227
234
|
app.command(name="records", group=inspect_group)(records_cmd.records)
|
|
235
|
+
app.command(name="tf-get", group=inspect_group)(tf_get_cmd.tf_get)
|
|
228
236
|
app.command(name="tftree", group=inspect_group)(tftree_cmd.tftree)
|
|
229
237
|
app.command(name="topic-chunks", group=inspect_group)(topic_chunks_cmd.topic_chunks)
|
|
230
238
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""CLI-level rechunking strategy enum.
|
|
2
|
+
|
|
3
|
+
Lives in the cmd layer because it is purely a user-facing selector that the
|
|
4
|
+
``rechunk`` and ``process`` commands translate into concrete
|
|
5
|
+
``OutputProcessor`` instances. The core processor itself is strategy-agnostic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from pymcap_cli.core.processors.chunk_groupers import (
|
|
14
|
+
PatternGrouper,
|
|
15
|
+
PerChannelGrouper,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from re import Pattern
|
|
20
|
+
|
|
21
|
+
from pymcap_cli.core.processors.base import OutputProcessor
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RechunkStrategy(str, Enum):
|
|
25
|
+
"""User-facing rechunking strategy."""
|
|
26
|
+
|
|
27
|
+
NONE = "none" # No rechunking — fast-copy optimization when possible
|
|
28
|
+
PATTERN = "pattern" # Group by topic / schema regex
|
|
29
|
+
ALL = "all" # Each channel in its own chunk group
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_output_processors(
|
|
33
|
+
strategy: RechunkStrategy,
|
|
34
|
+
topic_patterns: list[Pattern[str]] | None = None,
|
|
35
|
+
schema_patterns: list[Pattern[str]] | None = None,
|
|
36
|
+
) -> list[OutputProcessor]:
|
|
37
|
+
"""Translate a CLI rechunking strategy into concrete OutputProcessors."""
|
|
38
|
+
if strategy == RechunkStrategy.NONE:
|
|
39
|
+
return []
|
|
40
|
+
if strategy == RechunkStrategy.ALL:
|
|
41
|
+
return [PerChannelGrouper()]
|
|
42
|
+
return [PatternGrouper(topic_patterns or [], schema_patterns or [])]
|
|
@@ -20,11 +20,14 @@ from pymcap_cli.core.mcap_compare import (
|
|
|
20
20
|
IndexedCompareKind,
|
|
21
21
|
IndexedComparison,
|
|
22
22
|
MessageIndexIdentityReadResult,
|
|
23
|
+
PayloadLookup,
|
|
23
24
|
collect_message_timestamps,
|
|
24
25
|
compare_indexed_identities,
|
|
25
26
|
message_index_identity_from_info,
|
|
27
|
+
payload_lookup_from_info,
|
|
26
28
|
read_compare_file,
|
|
27
29
|
split_timestamps_into_segments,
|
|
30
|
+
verify_comparison_payloads,
|
|
28
31
|
)
|
|
29
32
|
from pymcap_cli.display.time_ranges import (
|
|
30
33
|
format_count,
|
|
@@ -236,8 +239,13 @@ def _extract_summary(path: str, label: str, info: RebuildInfo, file_size: int) -
|
|
|
236
239
|
|
|
237
240
|
|
|
238
241
|
def _process_file(
|
|
239
|
-
path: str, label: str
|
|
240
|
-
) -> tuple[
|
|
242
|
+
path: str, label: str, *, compare_payloads: bool
|
|
243
|
+
) -> tuple[
|
|
244
|
+
FileSummary,
|
|
245
|
+
dict[int, set[int]],
|
|
246
|
+
MessageIndexIdentityReadResult,
|
|
247
|
+
PayloadLookup | None,
|
|
248
|
+
]:
|
|
241
249
|
result = read_compare_file(path, rebuild_missing=True)
|
|
242
250
|
indexed = MessageIndexIdentityReadResult(
|
|
243
251
|
path=path,
|
|
@@ -245,10 +253,16 @@ def _process_file(
|
|
|
245
253
|
identity=message_index_identity_from_info(result.info),
|
|
246
254
|
read_mode=result.read_mode,
|
|
247
255
|
)
|
|
256
|
+
payload_lookup = (
|
|
257
|
+
payload_lookup_from_info(path, result.size_bytes, result.info, result.read_mode)
|
|
258
|
+
if compare_payloads
|
|
259
|
+
else None
|
|
260
|
+
)
|
|
248
261
|
return (
|
|
249
262
|
_extract_summary(path, label, result.info, result.size_bytes),
|
|
250
263
|
collect_message_timestamps(result.info),
|
|
251
264
|
indexed,
|
|
265
|
+
payload_lookup,
|
|
252
266
|
)
|
|
253
267
|
|
|
254
268
|
|
|
@@ -568,6 +582,8 @@ def _verdict_text(
|
|
|
568
582
|
return "[yellow]edge overlap[/yellow]"
|
|
569
583
|
if comparison.kind is IndexedCompareKind.PARTIAL_OVERLAP:
|
|
570
584
|
return "[yellow]partial overlap[/yellow]"
|
|
585
|
+
if comparison.kind is IndexedCompareKind.PAYLOAD_MISMATCH:
|
|
586
|
+
return "[red]payload mismatch[/red] [dim](timestamps/indexes match)[/dim]"
|
|
571
587
|
return "[red]no indexed message overlap[/red]"
|
|
572
588
|
|
|
573
589
|
|
|
@@ -593,6 +609,13 @@ def _build_smart_diff_tree(
|
|
|
593
609
|
f"left-only {format_count(comparison.left_extra_messages, 'msg')}, "
|
|
594
610
|
f"right-only {format_count(comparison.right_extra_messages, 'msg')})[/dim]"
|
|
595
611
|
)
|
|
612
|
+
if comparison.payload_mismatch is not None:
|
|
613
|
+
mismatch = comparison.payload_mismatch
|
|
614
|
+
node.add(
|
|
615
|
+
f"[red]first mismatch[/red] [cyan]{escape(mismatch.topic)}[/cyan] "
|
|
616
|
+
f"[dim]@ {format_ts_short(mismatch.log_time)}: "
|
|
617
|
+
f"{escape(mismatch.reason)}[/dim]"
|
|
618
|
+
)
|
|
596
619
|
|
|
597
620
|
for topic in comparison.topics[:max_topic_rows]:
|
|
598
621
|
overlap_window = format_optional_time_window(
|
|
@@ -623,21 +646,34 @@ def _compare_to_first(
|
|
|
623
646
|
labels: list[str],
|
|
624
647
|
*,
|
|
625
648
|
max_range_preview: int,
|
|
649
|
+
payload_lookups: list[PayloadLookup | None] | None = None,
|
|
626
650
|
) -> list[LabeledIndexedComparison]:
|
|
627
651
|
first = indexed_results[0]
|
|
628
652
|
first_label = labels[0]
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
653
|
+
comparisons: list[LabeledIndexedComparison] = []
|
|
654
|
+
for index, indexed_result in enumerate(indexed_results[1:], start=1):
|
|
655
|
+
comparison = compare_indexed_identities(
|
|
656
|
+
first,
|
|
657
|
+
indexed_result,
|
|
658
|
+
max_range_preview=max_range_preview,
|
|
659
|
+
)
|
|
660
|
+
if payload_lookups is not None:
|
|
661
|
+
left_lookup = payload_lookups[0]
|
|
662
|
+
right_lookup = payload_lookups[index]
|
|
663
|
+
if left_lookup is not None and right_lookup is not None:
|
|
664
|
+
comparison = verify_comparison_payloads(
|
|
665
|
+
comparison,
|
|
666
|
+
left_lookup,
|
|
667
|
+
right_lookup,
|
|
668
|
+
)
|
|
669
|
+
comparisons.append(
|
|
670
|
+
LabeledIndexedComparison(
|
|
671
|
+
left_label=first_label,
|
|
672
|
+
right_label=labels[index],
|
|
673
|
+
comparison=comparison,
|
|
674
|
+
)
|
|
638
675
|
)
|
|
639
|
-
|
|
640
|
-
]
|
|
676
|
+
return comparisons
|
|
641
677
|
|
|
642
678
|
|
|
643
679
|
def diff_cmd(
|
|
@@ -663,6 +699,16 @@ def diff_cmd(
|
|
|
663
699
|
help="Maximum number of timestamp ranges to show per channel",
|
|
664
700
|
),
|
|
665
701
|
] = 3,
|
|
702
|
+
compare_payloads: Annotated[
|
|
703
|
+
bool,
|
|
704
|
+
Parameter(
|
|
705
|
+
name=["--compare-payloads"],
|
|
706
|
+
help=(
|
|
707
|
+
"After indexes match, compare raw message payload bytes and fail "
|
|
708
|
+
"fast on the first payload/header mismatch."
|
|
709
|
+
),
|
|
710
|
+
),
|
|
711
|
+
] = False,
|
|
666
712
|
) -> int:
|
|
667
713
|
"""Compare MCAP files using summary and message index timestamps.
|
|
668
714
|
|
|
@@ -678,6 +724,8 @@ def diff_cmd(
|
|
|
678
724
|
Hide channels where all message timestamps match exactly
|
|
679
725
|
max_ranges
|
|
680
726
|
Maximum timestamp ranges to display per channel (default: 3)
|
|
727
|
+
compare_payloads
|
|
728
|
+
Verify raw message payloads after the fast index comparison passes.
|
|
681
729
|
|
|
682
730
|
Examples
|
|
683
731
|
--------
|
|
@@ -700,12 +748,17 @@ def diff_cmd(
|
|
|
700
748
|
all_timestamps: dict[str, dict[int, set[int]]] = {}
|
|
701
749
|
all_summaries: dict[str, FileSummary] = {}
|
|
702
750
|
indexed_results: list[MessageIndexIdentityReadResult] = []
|
|
751
|
+
payload_lookups: list[PayloadLookup | None] = []
|
|
703
752
|
seen_labels: dict[str, int] = {}
|
|
704
753
|
|
|
705
754
|
for path in files:
|
|
706
755
|
label = _unique_label(path, seen_labels)
|
|
707
756
|
try:
|
|
708
|
-
fs, ts, indexed = _process_file(
|
|
757
|
+
fs, ts, indexed, payload_lookup = _process_file(
|
|
758
|
+
path,
|
|
759
|
+
label,
|
|
760
|
+
compare_payloads=compare_payloads,
|
|
761
|
+
)
|
|
709
762
|
except Exception:
|
|
710
763
|
logger.exception(f"Error reading {path}")
|
|
711
764
|
return 1
|
|
@@ -713,14 +766,20 @@ def diff_cmd(
|
|
|
713
766
|
all_timestamps[label] = ts
|
|
714
767
|
all_summaries[label] = fs
|
|
715
768
|
indexed_results.append(indexed)
|
|
769
|
+
payload_lookups.append(payload_lookup)
|
|
716
770
|
|
|
717
771
|
labels = [fs.label for fs in summaries]
|
|
718
772
|
first_label = labels[0]
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
773
|
+
try:
|
|
774
|
+
smart_comparisons = _compare_to_first(
|
|
775
|
+
indexed_results,
|
|
776
|
+
labels,
|
|
777
|
+
max_range_preview=max_ranges,
|
|
778
|
+
payload_lookups=payload_lookups if compare_payloads else None,
|
|
779
|
+
)
|
|
780
|
+
except Exception:
|
|
781
|
+
logger.exception("Error comparing message payloads")
|
|
782
|
+
return 1
|
|
724
783
|
|
|
725
784
|
channel_diffs = _compare_channels(all_timestamps, all_summaries)
|
|
726
785
|
schema_diffs = _compare_schemas(all_summaries)
|
|
@@ -732,6 +791,9 @@ def diff_cmd(
|
|
|
732
791
|
has_diffs = total_added > 0 or total_removed > 0
|
|
733
792
|
has_schema_diffs = any(not d.is_identical for d in schema_diffs.values())
|
|
734
793
|
has_mismatches = bool(channel_schema_mismatches)
|
|
794
|
+
has_payload_mismatches = any(
|
|
795
|
+
item.comparison.kind is IndexedCompareKind.PAYLOAD_MISMATCH for item in smart_comparisons
|
|
796
|
+
)
|
|
735
797
|
|
|
736
798
|
console.print()
|
|
737
799
|
console.print(_build_smart_diff_tree(smart_comparisons))
|
|
@@ -768,11 +830,19 @@ def diff_cmd(
|
|
|
768
830
|
console.print(mismatch_table)
|
|
769
831
|
|
|
770
832
|
console.print()
|
|
771
|
-
all_good =
|
|
833
|
+
all_good = (
|
|
834
|
+
not has_diffs and not has_schema_diffs and not has_mismatches and not has_payload_mismatches
|
|
835
|
+
)
|
|
772
836
|
if all_good:
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
837
|
+
if compare_payloads:
|
|
838
|
+
console.print(
|
|
839
|
+
f"[green]✓ All {total_common:,} messages have identical timestamps, "
|
|
840
|
+
"schemas and payloads[/]"
|
|
841
|
+
)
|
|
842
|
+
else:
|
|
843
|
+
console.print(
|
|
844
|
+
f"[green]✓ All {total_common:,} messages have identical timestamps and schemas[/]"
|
|
845
|
+
)
|
|
776
846
|
else:
|
|
777
847
|
if not has_diffs:
|
|
778
848
|
console.print(f"[green]✓ All {total_common:,} messages have identical timestamps[/]")
|
|
@@ -790,5 +860,7 @@ def diff_cmd(
|
|
|
790
860
|
f"[red]⚠ {len(channel_schema_mismatches)} "
|
|
791
861
|
f"channel(s) use different schemas across files[/]"
|
|
792
862
|
)
|
|
863
|
+
if has_payload_mismatches:
|
|
864
|
+
console.print("[red]⚠ message payloads differ across files[/]")
|
|
793
865
|
|
|
794
866
|
return 0
|