pymcap-cli 0.14.0__tar.gz → 0.16.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.14.0 → pymcap_cli-0.16.0}/PKG-INFO +2 -1
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/pyproject.toml +5 -2
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/_run_processor.py +97 -4
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/_run_processor_multi.py +4 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/compress_cmd.py +19 -3
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/info_cmd.py +80 -20
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/merge_cmd.py +11 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/plot_cmd.py +8 -1
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/roscompress_cmd.py +21 -6
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/split_cmd.py +8 -1
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/input_handler.py +28 -1
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/mcap_processor.py +9 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/mcap_transform.py +32 -22
- pymcap_cli-0.16.0/src/pymcap_cli/core/rosbag2_layout.py +197 -0
- pymcap_cli-0.16.0/src/pymcap_cli/exporters/_summary_hints.py +96 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/driver.py +10 -3
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/plot_exporter.py +191 -60
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/utils.py +15 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/README.md +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/__init__.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cli.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/__init__.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/_rechunk_strategy.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bag2mcap_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bridge/__init__.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bridge/_shared.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bridge/cat.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bridge/inspect.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bridge/record.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/cat_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/convert_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/diag_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/diff_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/doctor_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/du_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/duplicates_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_csv_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_geo_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_images_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_json_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_parquet_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_pcd_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/filter_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/get_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/__init__.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/_helpers.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/duplicates_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/errors_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/info_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/migrate_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/query_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/scan_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/schemas_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/serve_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/sessions_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/status_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/timeline_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/topics_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/tree_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/info_json_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/list_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/msg/__init__.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/msg/def_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/msg/list_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/msg/serve_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/process_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/rechunk_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/records_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/recover_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/recover_inplace_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/rosdecompress_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/tf_export_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/tf_get_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/tftree_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/topic_chunks_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/video_cmd.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/constants.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/__init__.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/input_options.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/input_processor_chain.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/mcap_compare.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/msg_resolver.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/ARCHITECTURE.md +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/__init__.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/always_decode.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/attachment_filter.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/base.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/boundary_split.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/channel_merge.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/chunk_groupers.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/dedup.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/duration_split.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/expression_split.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/latching.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/metadata_filter.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/nth_message.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/size_split.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/time_filter.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/time_offset.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/timestamp_split.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/topic_alias.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/topic_filter.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/topic_rewrite.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/utils.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/qos.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/tf_findings.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/tf_tree.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/debug_wrapper.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/__init__.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/cat_helpers.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/display_utils.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/message_render.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/osc_utils.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/schema_html.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/schema_render.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/sparkline.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/time_ranges.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/doctor.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/encoding/__init__.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/encoding/arrow_schema.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/__init__.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/_common.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/base.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/csv_exporter.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/geo_common.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/geojson_exporter.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/gpx_exporter.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/image_exporter.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/json_exporter.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/kml_exporter.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/parquet_exporter.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/pcd_exporter.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/sdf_exporter.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/urdf_exporter.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/video_exporter.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/video_file_writer.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/http_utils.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/__init__.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/datasette/metadata.yaml +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/datasette/plugins/pymcap_render.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/datasette/templates/index.html +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/datasette/templates/query-index-timeline.html +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/db.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/fingerprint.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0001.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0002_normalise_and_drop_chunks.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0003_intern_channels.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0004_intern_compress_channel_metadata.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0005_content_compression.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0006_channel_statistics.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0007_consolidate_schema.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0008_rewrite_current_file_view.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0009_read_side_covering_indexes.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/__init__.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/scanner.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/summary_fingerprint.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/log_setup.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/py.typed +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/rihs01.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/rosbag_reader/__init__.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/rosbag_reader/_reader.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/rosbag_reader/_types.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/rosbag_reader/py.typed +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/__init__.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/duration.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/info_data.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/info_link.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/info_types.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/qos.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/size.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/to_plain.py +0 -0
- {pymcap_cli-0.14.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/types_manual.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pymcap-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.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
|
|
@@ -37,6 +37,7 @@ Requires-Dist: pymcap-cli[image,draco,bridge,xxhash] ; extra == 'lite'
|
|
|
37
37
|
Requires-Dist: pyarrow>=15.0.0 ; extra == 'parquet'
|
|
38
38
|
Requires-Dist: numpy>=1.24.0 ; extra == 'parquet'
|
|
39
39
|
Requires-Dist: mcap-codec-support[pointcloud] ; extra == 'parquet'
|
|
40
|
+
Requires-Dist: kaleido>=1.0.0 ; extra == 'plot'
|
|
40
41
|
Requires-Dist: plotly>=6.0.0 ; extra == 'plot'
|
|
41
42
|
Requires-Dist: mcap-codec-support[pointcloud] ; extra == 'pointcloud'
|
|
42
43
|
Requires-Dist: datasette>=0.65.2,<1 ; extra == 'serve'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pymcap-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.16.0"
|
|
4
4
|
description = "High-performance Python CLI for MCAP file processing with advanced recovery, filtering, and optimization capabilities"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -52,7 +52,10 @@ bridge = ["robo-ws-bridge"]
|
|
|
52
52
|
draco = ["mcap-codec-support[draco]"]
|
|
53
53
|
image = ["pillow>=10.0"]
|
|
54
54
|
parquet = ["pyarrow>=15.0.0", "numpy>=1.24.0", "mcap-codec-support[pointcloud]"]
|
|
55
|
-
plot = [
|
|
55
|
+
plot = [
|
|
56
|
+
"kaleido>=1.0.0",
|
|
57
|
+
"plotly>=6.0.0",
|
|
58
|
+
]
|
|
56
59
|
pointcloud = ["mcap-codec-support[pointcloud]"]
|
|
57
60
|
video = ["mcap-codec-support[video]"]
|
|
58
61
|
xxhash = ["xxhash>=3.0.0"]
|
|
@@ -19,6 +19,7 @@ from pymcap_cli.core.mcap_processor import (
|
|
|
19
19
|
ProcessingOptions,
|
|
20
20
|
ProcessingStats,
|
|
21
21
|
)
|
|
22
|
+
from pymcap_cli.core.rosbag2_layout import expand_bag_paths
|
|
22
23
|
from pymcap_cli.utils import confirm_output_overwrite, read_info
|
|
23
24
|
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
@@ -64,6 +65,7 @@ def run_processor(
|
|
|
64
65
|
|
|
65
66
|
Raises any exception from McapProcessor.process() to the caller.
|
|
66
67
|
"""
|
|
68
|
+
files = expand_bag_paths(files)
|
|
67
69
|
with contextlib.ExitStack() as stack:
|
|
68
70
|
input_files: list[InputFile] = []
|
|
69
71
|
|
|
@@ -98,6 +100,76 @@ def validate_mcap_output(path: Path) -> bool:
|
|
|
98
100
|
return True
|
|
99
101
|
|
|
100
102
|
|
|
103
|
+
def mcap_message_count(path: Path) -> int | None:
|
|
104
|
+
"""Return the message count from an MCAP summary, or None if it can't be read."""
|
|
105
|
+
try:
|
|
106
|
+
with path.open("rb") as f:
|
|
107
|
+
info = read_info(f)
|
|
108
|
+
except (McapError, InvalidMagicError, OSError, AssertionError) as e:
|
|
109
|
+
logger.debug(f"Could not read message count for {path}: {e}")
|
|
110
|
+
return None
|
|
111
|
+
if info.summary.statistics is None:
|
|
112
|
+
return None
|
|
113
|
+
return info.summary.statistics.message_count
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _outputs_dropped_all_messages(sources: list[str], outputs: list[Path]) -> bool:
|
|
117
|
+
"""True if every output has zero messages while some local source had messages.
|
|
118
|
+
|
|
119
|
+
Guards against deleting sources when a transform silently produced empty output
|
|
120
|
+
(e.g. the source was truncated before being read). Partial drops — dedup, time or
|
|
121
|
+
channel filters — are intentionally not flagged; only total loss is.
|
|
122
|
+
"""
|
|
123
|
+
out_total = 0
|
|
124
|
+
for p in outputs:
|
|
125
|
+
count = mcap_message_count(p)
|
|
126
|
+
if count is None:
|
|
127
|
+
return False # unknown — validation already passed, don't second-guess it
|
|
128
|
+
out_total += count
|
|
129
|
+
if out_total > 0:
|
|
130
|
+
return False
|
|
131
|
+
for src in sources:
|
|
132
|
+
if urlparse(src).scheme in ("http", "https"):
|
|
133
|
+
continue # URLs are never deleted, so their counts don't matter
|
|
134
|
+
count = mcap_message_count(Path(src))
|
|
135
|
+
if count:
|
|
136
|
+
return True
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _outputs_lost_messages(sources: list[str], outputs: list[Path]) -> bool:
|
|
141
|
+
"""True if the outputs hold fewer messages in total than the local sources.
|
|
142
|
+
|
|
143
|
+
For lossless transforms (compress) every source message must appear in the
|
|
144
|
+
output, so any shortfall means data was dropped. Counts that can't be read
|
|
145
|
+
are treated as unknown and never trigger a block.
|
|
146
|
+
"""
|
|
147
|
+
out_total = 0
|
|
148
|
+
for p in outputs:
|
|
149
|
+
count = mcap_message_count(p)
|
|
150
|
+
if count is None:
|
|
151
|
+
return False
|
|
152
|
+
out_total += count
|
|
153
|
+
src_total = 0
|
|
154
|
+
for src in sources:
|
|
155
|
+
if urlparse(src).scheme in ("http", "https"):
|
|
156
|
+
continue
|
|
157
|
+
count = mcap_message_count(Path(src))
|
|
158
|
+
if count is None:
|
|
159
|
+
return False
|
|
160
|
+
src_total += count
|
|
161
|
+
return out_total < src_total
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def processing_had_errors(stats: ProcessingStats) -> bool:
|
|
165
|
+
"""True if the processor swallowed read/validation errors during the run.
|
|
166
|
+
|
|
167
|
+
Such a run produces incomplete output even when it exits cleanly, so it is
|
|
168
|
+
not safe to delete or replace the source from it.
|
|
169
|
+
"""
|
|
170
|
+
return stats.errors_encountered > 0 or stats.validation_errors > 0
|
|
171
|
+
|
|
172
|
+
|
|
101
173
|
def delete_source_files(sources: list[str], outputs: list[Path]) -> None:
|
|
102
174
|
"""Delete each local source file. Skip URLs and any source path that
|
|
103
175
|
resolves to one of ``outputs`` (with a warning).
|
|
@@ -134,14 +206,19 @@ def in_place_temp_path(source: Path) -> Path:
|
|
|
134
206
|
def finalize_replace_source(*, source: Path, tmp_output: Path) -> int:
|
|
135
207
|
"""Validate ``tmp_output`` and atomically replace ``source`` with it.
|
|
136
208
|
|
|
137
|
-
Returns 0 on success and 1 if
|
|
138
|
-
the
|
|
209
|
+
Returns 0 on success and 1 if the temp output failed validation or is empty
|
|
210
|
+
while the source had messages. In those cases the source is preserved and the
|
|
211
|
+
temp file is removed.
|
|
139
212
|
"""
|
|
140
213
|
if not validate_mcap_output(tmp_output):
|
|
141
214
|
logger.error(f"[red]Output failed validation: {tmp_output}[/red]")
|
|
142
215
|
logger.error("Source file preserved — output not safe to replace source.")
|
|
143
216
|
tmp_output.unlink(missing_ok=True)
|
|
144
217
|
return 1
|
|
218
|
+
if _outputs_lost_messages([str(source)], [tmp_output]):
|
|
219
|
+
logger.error("Output has fewer messages than the source — source file preserved.")
|
|
220
|
+
tmp_output.unlink(missing_ok=True)
|
|
221
|
+
return 1
|
|
145
222
|
tmp_output.replace(source)
|
|
146
223
|
logger.info(f"Replaced source: {source}")
|
|
147
224
|
return 0
|
|
@@ -151,17 +228,33 @@ def finalize_delete_source(
|
|
|
151
228
|
*,
|
|
152
229
|
sources: list[str],
|
|
153
230
|
outputs: list[Path],
|
|
231
|
+
require_lossless: bool = False,
|
|
154
232
|
) -> int:
|
|
155
233
|
"""Validate every output and, if all valid, delete the eligible sources.
|
|
156
234
|
|
|
157
|
-
Returns 0 on success (sources deleted or skipped with warning) and 1 if
|
|
158
|
-
any output failed validation
|
|
235
|
+
Returns 0 on success (sources deleted or skipped with warning) and 1 if there
|
|
236
|
+
are no outputs, any output failed validation, or every output is empty while a
|
|
237
|
+
source had messages. No sources are deleted in those cases.
|
|
238
|
+
|
|
239
|
+
When ``require_lossless`` is set (transforms that must preserve every message,
|
|
240
|
+
e.g. ``compress``), any shortfall in total output messages versus the sources
|
|
241
|
+
also preserves them — not just total loss.
|
|
159
242
|
"""
|
|
243
|
+
if not outputs:
|
|
244
|
+
logger.error("No output files were produced — source file(s) preserved.")
|
|
245
|
+
return 1
|
|
160
246
|
invalid = [p for p in outputs if not validate_mcap_output(p)]
|
|
161
247
|
if invalid:
|
|
162
248
|
for p in invalid:
|
|
163
249
|
logger.error(f"[red]Output failed validation: {p}[/red]")
|
|
164
250
|
logger.error("Source file(s) preserved — output not safe to replace source.")
|
|
165
251
|
return 1
|
|
252
|
+
if require_lossless:
|
|
253
|
+
if _outputs_lost_messages(sources, outputs):
|
|
254
|
+
logger.error("Output has fewer messages than the source(s) — source file(s) preserved.")
|
|
255
|
+
return 1
|
|
256
|
+
elif _outputs_dropped_all_messages(sources, outputs):
|
|
257
|
+
logger.error("Output contains no messages but the source did — source file(s) preserved.")
|
|
258
|
+
return 1
|
|
166
259
|
delete_source_files(sources, outputs)
|
|
167
260
|
return 0
|
|
@@ -11,6 +11,7 @@ from pymcap_cli.core.mcap_processor import (
|
|
|
11
11
|
OutputOptions,
|
|
12
12
|
ProcessingOptions,
|
|
13
13
|
)
|
|
14
|
+
from pymcap_cli.core.rosbag2_layout import expand_bag_paths
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def run_processor_multi(
|
|
@@ -28,6 +29,9 @@ def run_processor_multi(
|
|
|
28
29
|
"""
|
|
29
30
|
if input_options is None:
|
|
30
31
|
input_options = InputOptions.from_args()
|
|
32
|
+
files = expand_bag_paths(files)
|
|
33
|
+
# Let the OutputManager refuse to open a segment that would truncate an input.
|
|
34
|
+
output_options.input_paths = tuple(files)
|
|
31
35
|
with contextlib.ExitStack() as stack:
|
|
32
36
|
input_files: list[InputFile] = []
|
|
33
37
|
|
|
@@ -10,6 +10,7 @@ from pymcap_cli.cmd._run_processor import (
|
|
|
10
10
|
finalize_delete_source,
|
|
11
11
|
finalize_replace_source,
|
|
12
12
|
in_place_temp_path,
|
|
13
|
+
processing_had_errors,
|
|
13
14
|
resolve_overwrite_policy,
|
|
14
15
|
run_processor,
|
|
15
16
|
)
|
|
@@ -30,6 +31,7 @@ from pymcap_cli.types.types_manual import (
|
|
|
30
31
|
NoClobberOption,
|
|
31
32
|
OutputPathOption,
|
|
32
33
|
)
|
|
34
|
+
from pymcap_cli.utils import output_overwrites_input
|
|
33
35
|
|
|
34
36
|
logger = logging.getLogger(__name__)
|
|
35
37
|
console = Console()
|
|
@@ -113,6 +115,12 @@ def compress(
|
|
|
113
115
|
if output is None:
|
|
114
116
|
logger.error("Either --output or --in-place is required.")
|
|
115
117
|
return 1
|
|
118
|
+
if output_overwrites_input(file, output):
|
|
119
|
+
logger.error(
|
|
120
|
+
"Output path is the same file as the input. "
|
|
121
|
+
"Use --in-place to compress in place safely."
|
|
122
|
+
)
|
|
123
|
+
return 1
|
|
116
124
|
policy = resolve_overwrite_policy(force=force, no_clobber=no_clobber)
|
|
117
125
|
if policy is None:
|
|
118
126
|
logger.error("--force and --no-clobber cannot be used together.")
|
|
@@ -139,14 +147,22 @@ def compress(
|
|
|
139
147
|
console.print(result.stats)
|
|
140
148
|
except Exception:
|
|
141
149
|
logger.exception("Error during compression")
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
# The output was opened "wb" (truncated) before processing, so a failed run
|
|
151
|
+
# leaves an empty/partial file. Remove it rather than leaving a 0-byte result.
|
|
152
|
+
output.unlink(missing_ok=True)
|
|
144
153
|
return 1
|
|
145
154
|
|
|
146
155
|
if in_place:
|
|
156
|
+
if processing_had_errors(result.stats):
|
|
157
|
+
logger.error("Processing reported errors — source preserved, not replaced in place.")
|
|
158
|
+
output.unlink(missing_ok=True)
|
|
159
|
+
return 1
|
|
147
160
|
return finalize_replace_source(source=Path(file), tmp_output=output)
|
|
148
161
|
|
|
149
162
|
if delete_source:
|
|
150
|
-
|
|
163
|
+
if processing_had_errors(result.stats):
|
|
164
|
+
logger.error("Processing reported errors — source file preserved.")
|
|
165
|
+
return 1
|
|
166
|
+
return finalize_delete_source(sources=[file], outputs=[output], require_lossless=True)
|
|
151
167
|
|
|
152
168
|
return 0
|
|
@@ -10,6 +10,7 @@ from datetime import datetime, timedelta, timezone
|
|
|
10
10
|
from enum import Enum
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Annotated
|
|
13
|
+
from urllib.parse import urlparse
|
|
13
14
|
|
|
14
15
|
from cyclopts import Group as CycloptsGroup
|
|
15
16
|
from cyclopts import Parameter
|
|
@@ -17,12 +18,13 @@ from rich.console import Console, Group, RenderableType
|
|
|
17
18
|
from rich.live import Live
|
|
18
19
|
from rich.table import Table
|
|
19
20
|
from rich.text import Text
|
|
20
|
-
from small_mcap import McapError, rebuild_summary
|
|
21
|
+
from small_mcap import McapError, RebuildInfo, rebuild_summary
|
|
21
22
|
from small_mcap.records import Summary
|
|
22
23
|
|
|
23
24
|
from pymcap_cli.constants import NS_TO_MS, NS_TO_SEC
|
|
24
25
|
from pymcap_cli.core.input_handler import open_input
|
|
25
26
|
from pymcap_cli.core.qos import parse_qos_profiles
|
|
27
|
+
from pymcap_cli.core.rosbag2_layout import find_bag_splits, read_aggregated_bag_info
|
|
26
28
|
from pymcap_cli.display.display_utils import (
|
|
27
29
|
ChannelTableColumn,
|
|
28
30
|
DistributionBar,
|
|
@@ -241,6 +243,41 @@ def _qos_from_summary(summary: Summary) -> dict[int, list[QosProfile]]:
|
|
|
241
243
|
return out
|
|
242
244
|
|
|
243
245
|
|
|
246
|
+
def _is_local_dir(path: str) -> bool:
|
|
247
|
+
return urlparse(path).scheme not in ("http", "https") and Path(path).is_dir()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _load_info(
|
|
251
|
+
file: str,
|
|
252
|
+
*,
|
|
253
|
+
rebuild: bool,
|
|
254
|
+
exact_sizes: bool,
|
|
255
|
+
debug: bool,
|
|
256
|
+
) -> tuple[RebuildInfo, int, str]:
|
|
257
|
+
"""Read info for one ``info`` argument.
|
|
258
|
+
|
|
259
|
+
A multi-split rosbag2 directory is aggregated into one logical RebuildInfo;
|
|
260
|
+
a plain file (or single-split directory) is read directly. Returns
|
|
261
|
+
``(info, size, label)`` where ``label`` names the file or bag for display.
|
|
262
|
+
"""
|
|
263
|
+
if _is_local_dir(file):
|
|
264
|
+
splits = find_bag_splits(Path(file))
|
|
265
|
+
if not splits:
|
|
266
|
+
raise ValueError(f"{file!r} is not an MCAP file or a rosbag2 bag directory")
|
|
267
|
+
if len(splits) > 1:
|
|
268
|
+
info_data, total = read_aggregated_bag_info(
|
|
269
|
+
splits, rebuild=rebuild, exact_sizes=exact_sizes
|
|
270
|
+
)
|
|
271
|
+
return info_data, total, file
|
|
272
|
+
file = str(splits[0])
|
|
273
|
+
|
|
274
|
+
with open_input(file, buffering=0, debug=debug) as (f_buffered, file_size):
|
|
275
|
+
info_data = read_or_rebuild_info(
|
|
276
|
+
f_buffered, file_size, rebuild=rebuild, exact_sizes=exact_sizes
|
|
277
|
+
)
|
|
278
|
+
return info_data, file_size, file
|
|
279
|
+
|
|
280
|
+
|
|
244
281
|
def _build_info_display(
|
|
245
282
|
data: McapInfoOutput,
|
|
246
283
|
has_chunk_info: bool,
|
|
@@ -454,6 +491,14 @@ def info(
|
|
|
454
491
|
group=DISPLAY_GROUP,
|
|
455
492
|
),
|
|
456
493
|
] = False,
|
|
494
|
+
qos: Annotated[
|
|
495
|
+
bool,
|
|
496
|
+
Parameter(
|
|
497
|
+
name=["--qos"],
|
|
498
|
+
negative="--no-qos",
|
|
499
|
+
group=DISPLAY_GROUP,
|
|
500
|
+
),
|
|
501
|
+
] = False,
|
|
457
502
|
watch: Annotated[
|
|
458
503
|
bool,
|
|
459
504
|
Parameter(
|
|
@@ -490,6 +535,8 @@ def info(
|
|
|
490
535
|
----------
|
|
491
536
|
files
|
|
492
537
|
Path(s) to MCAP file(s) to analyze (local files or HTTP/HTTPS URLs).
|
|
538
|
+
A rosbag2 bag directory (``<bag>/<bag>_<N>.mcap`` splits) may be passed
|
|
539
|
+
in place of a file; its splits are aggregated into one combined view.
|
|
493
540
|
rebuild
|
|
494
541
|
Rebuild file metadata by scanning all records (use for corrupt or
|
|
495
542
|
summary-less files).
|
|
@@ -515,6 +562,9 @@ def info(
|
|
|
515
562
|
--rebuild to calculate message intervals.
|
|
516
563
|
tree
|
|
517
564
|
Display channels in a hierarchical tree structure based on topic paths.
|
|
565
|
+
qos
|
|
566
|
+
Show the per-channel QoS column for ROS 2 MCAPs (default: off). Pass
|
|
567
|
+
--qos to show it.
|
|
518
568
|
watch
|
|
519
569
|
Watch the file for changes and display live-updating statistics.
|
|
520
570
|
Requires exactly one local file. Incompatible with --json and --compress.
|
|
@@ -582,20 +632,32 @@ def info(
|
|
|
582
632
|
if len(files) != 1:
|
|
583
633
|
logger.error("--watch requires exactly one file")
|
|
584
634
|
return 1
|
|
635
|
+
watch_target = files[0]
|
|
636
|
+
if _is_local_dir(watch_target):
|
|
637
|
+
splits = find_bag_splits(Path(watch_target))
|
|
638
|
+
if not splits:
|
|
639
|
+
logger.error("%r is not an MCAP file or a rosbag2 bag directory", watch_target)
|
|
640
|
+
return 1
|
|
641
|
+
if len(splits) > 1:
|
|
642
|
+
logger.error(
|
|
643
|
+
"--watch does not support multi-split rosbag2 directories; "
|
|
644
|
+
"pass a single .mcap file"
|
|
645
|
+
)
|
|
646
|
+
return 1
|
|
647
|
+
watch_target = str(splits[0])
|
|
585
648
|
return _watch_file(
|
|
586
|
-
|
|
649
|
+
watch_target, sort.value, reverse, index_duration, median, tree, watch_interval
|
|
587
650
|
)
|
|
588
651
|
|
|
589
652
|
# Link output mode
|
|
590
653
|
if link:
|
|
591
654
|
mode: ScanMode = "exact" if exact_sizes else ("rebuild" if rebuild else "summary")
|
|
592
655
|
for file in files:
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
url = generate_link(data, str(file), file_size, mode)
|
|
656
|
+
info_data, file_size, label = _load_info(
|
|
657
|
+
file, rebuild=rebuild, exact_sizes=exact_sizes, debug=debug
|
|
658
|
+
)
|
|
659
|
+
data = info_to_dict(info_data, label, file_size)
|
|
660
|
+
url = generate_link(data, label, file_size, mode)
|
|
599
661
|
print(url) # noqa: T201
|
|
600
662
|
return 0
|
|
601
663
|
|
|
@@ -603,11 +665,10 @@ def info(
|
|
|
603
665
|
if json_output:
|
|
604
666
|
all_outputs: list[McapInfoOutput] = []
|
|
605
667
|
for file in files:
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
data = info_to_dict(info_data, str(file), file_size)
|
|
668
|
+
info_data, file_size, label = _load_info(
|
|
669
|
+
file, rebuild=rebuild, exact_sizes=exact_sizes, debug=debug
|
|
670
|
+
)
|
|
671
|
+
data = info_to_dict(info_data, label, file_size)
|
|
611
672
|
all_outputs.append(data)
|
|
612
673
|
_output_json(all_outputs, compress)
|
|
613
674
|
return 0
|
|
@@ -617,13 +678,12 @@ def info(
|
|
|
617
678
|
if i > 0:
|
|
618
679
|
console.print("\n" + "=" * 80 + "\n")
|
|
619
680
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
)
|
|
681
|
+
info_data, file_size, label = _load_info(
|
|
682
|
+
file, rebuild=rebuild, exact_sizes=exact_sizes, debug=debug
|
|
683
|
+
)
|
|
624
684
|
|
|
625
685
|
# Get structured data
|
|
626
|
-
data = info_to_dict(info_data,
|
|
686
|
+
data = info_to_dict(info_data, label, file_size)
|
|
627
687
|
has_chunk_info = info_data.chunk_information is not None
|
|
628
688
|
|
|
629
689
|
# Warn if sorting by size fields when channel_sizes is unavailable
|
|
@@ -658,7 +718,7 @@ def info(
|
|
|
658
718
|
)
|
|
659
719
|
console.print()
|
|
660
720
|
|
|
661
|
-
qos_by_channel_id = _qos_from_summary(info_data.summary)
|
|
721
|
+
qos_by_channel_id = _qos_from_summary(info_data.summary) if qos else None
|
|
662
722
|
|
|
663
723
|
# Display all sections
|
|
664
724
|
console.print(
|
|
@@ -676,7 +736,7 @@ def info(
|
|
|
676
736
|
|
|
677
737
|
# Show shareable web inspector link
|
|
678
738
|
mode: ScanMode = "exact" if exact_sizes else ("rebuild" if rebuild else "summary")
|
|
679
|
-
url = generate_link(data,
|
|
739
|
+
url = generate_link(data, label, file_size, mode)
|
|
680
740
|
console.print(f"\n[link={url}][dim]View in web inspector[/dim][/]")
|
|
681
741
|
|
|
682
742
|
return 0
|
|
@@ -8,6 +8,7 @@ from rich.console import Console
|
|
|
8
8
|
|
|
9
9
|
from pymcap_cli.cmd._run_processor import (
|
|
10
10
|
finalize_delete_source,
|
|
11
|
+
processing_had_errors,
|
|
11
12
|
resolve_overwrite_policy,
|
|
12
13
|
run_processor,
|
|
13
14
|
)
|
|
@@ -18,6 +19,7 @@ from pymcap_cli.core.mcap_processor import (
|
|
|
18
19
|
)
|
|
19
20
|
from pymcap_cli.core.processors.base import InputProcessor # noqa: TC001 - runtime annotation below
|
|
20
21
|
from pymcap_cli.core.processors.dedup import DedupIdenticalProcessor
|
|
22
|
+
from pymcap_cli.core.rosbag2_layout import expand_bag_paths
|
|
21
23
|
from pymcap_cli.types.types_manual import (
|
|
22
24
|
ChunkSizeOption,
|
|
23
25
|
CompressionOption,
|
|
@@ -29,6 +31,7 @@ from pymcap_cli.types.types_manual import (
|
|
|
29
31
|
from pymcap_cli.utils import (
|
|
30
32
|
AttachmentsMode,
|
|
31
33
|
MetadataMode,
|
|
34
|
+
output_overwrites_input,
|
|
32
35
|
)
|
|
33
36
|
|
|
34
37
|
logger = logging.getLogger(__name__)
|
|
@@ -109,6 +112,7 @@ def merge(
|
|
|
109
112
|
pymcap-cli merge *.mcap -o all_recordings.mcap --compression lz4
|
|
110
113
|
```
|
|
111
114
|
"""
|
|
115
|
+
files = expand_bag_paths(files)
|
|
112
116
|
if len(files) < 2:
|
|
113
117
|
logger.error("At least 2 input files are required for merging")
|
|
114
118
|
return 1
|
|
@@ -118,6 +122,10 @@ def merge(
|
|
|
118
122
|
logger.error("--force and --no-clobber cannot be used together.")
|
|
119
123
|
return 1
|
|
120
124
|
|
|
125
|
+
if any(output_overwrites_input(f, output) for f in files):
|
|
126
|
+
logger.error("Output path is the same file as an input; choose a different output file.")
|
|
127
|
+
return 1
|
|
128
|
+
|
|
121
129
|
dedup_processor = DedupIdenticalProcessor() if dedup_identical else None
|
|
122
130
|
extra_processors: list[InputProcessor] | None = [dedup_processor] if dedup_processor else None
|
|
123
131
|
|
|
@@ -147,6 +155,9 @@ def merge(
|
|
|
147
155
|
return 1
|
|
148
156
|
|
|
149
157
|
if delete_source:
|
|
158
|
+
if processing_had_errors(result.stats):
|
|
159
|
+
logger.error("Processing reported errors — source file(s) preserved.")
|
|
160
|
+
return 1
|
|
150
161
|
return finalize_delete_source(sources=list(files), outputs=[output])
|
|
151
162
|
|
|
152
163
|
return 0
|
|
@@ -65,7 +65,12 @@ def plot(
|
|
|
65
65
|
|
|
66
66
|
Extracts values along message paths and plots them over time using plotly.
|
|
67
67
|
Supports numeric, boolean, and string values natively. Multiple paths can
|
|
68
|
-
be overlaid.
|
|
68
|
+
be overlaid. Array-valued paths expand into one trace per element index.
|
|
69
|
+
|
|
70
|
+
Output format is chosen from the ``-o`` suffix: ``.html`` (interactive) or
|
|
71
|
+
``.png`` / ``.svg`` / ``.pdf`` / ``.jpg`` / ``.webp`` (static image, no
|
|
72
|
+
browser needed — handy over SSH). With no ``-o`` the figure opens in a
|
|
73
|
+
browser.
|
|
69
74
|
|
|
70
75
|
Paths can be given a custom label: "Label=/topic.field"
|
|
71
76
|
|
|
@@ -75,6 +80,7 @@ def plot(
|
|
|
75
80
|
pymcap-cli plot recording.mcap --xy /odom.pose.position.x /odom.pose.position.y
|
|
76
81
|
pymcap-cli plot recording.mcap /odom.pose.position.x -d 1000
|
|
77
82
|
pymcap-cli plot recording.mcap /odom.pose.position.x -s 10 -e 20 -o plot.html
|
|
83
|
+
pymcap-cli plot recording.mcap "/joints.position[:].@degrees" -o joints.svg
|
|
78
84
|
"""
|
|
79
85
|
if not paths:
|
|
80
86
|
logger.error("At least one message path is required")
|
|
@@ -93,6 +99,7 @@ def plot(
|
|
|
93
99
|
downsample=downsample,
|
|
94
100
|
xy=xy,
|
|
95
101
|
force=force,
|
|
102
|
+
source_name=Path(file).name,
|
|
96
103
|
)
|
|
97
104
|
except (ValueError, ValidationError) as exc:
|
|
98
105
|
logger.error(str(exc)) # noqa: TRY400
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Command to compress image and point cloud topics in MCAP files."""
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import logging
|
|
4
5
|
from collections import deque
|
|
5
6
|
from collections.abc import Iterable, Iterator
|
|
@@ -50,7 +51,7 @@ from pymcap_cli.core.mcap_transform import (
|
|
|
50
51
|
)
|
|
51
52
|
from pymcap_cli.exporters._common import normalize_schema_name
|
|
52
53
|
from pymcap_cli.types.types_manual import ForceOverwriteOption, OutputPathOption
|
|
53
|
-
from pymcap_cli.utils import confirm_output_overwrite
|
|
54
|
+
from pymcap_cli.utils import confirm_output_overwrite, output_overwrites_input
|
|
54
55
|
|
|
55
56
|
logger = logging.getLogger(__name__)
|
|
56
57
|
console = Console()
|
|
@@ -246,6 +247,10 @@ def roscompress(
|
|
|
246
247
|
pointcloud
|
|
247
248
|
Enable point cloud compression. Default: True.
|
|
248
249
|
"""
|
|
250
|
+
if output_overwrites_input(file, output):
|
|
251
|
+
logger.error("Output path is the same file as the input; choose a different output file.")
|
|
252
|
+
return 1
|
|
253
|
+
|
|
249
254
|
confirm_output_overwrite(output, force)
|
|
250
255
|
|
|
251
256
|
if not 1 <= jpeg_quality <= 100:
|
|
@@ -332,11 +337,20 @@ def roscompress(
|
|
|
332
337
|
last_video_times: dict[str, tuple[int, int]] = {}
|
|
333
338
|
pending_messages: dict[str, deque[DecodedMessage]] = {}
|
|
334
339
|
|
|
335
|
-
with (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
+
with contextlib.ExitStack() as stack:
|
|
341
|
+
# Opening the output truncates it; remove the truncated/partial file if the
|
|
342
|
+
# run does not finish cleanly. Registered first so it runs after the stream
|
|
343
|
+
# is closed on exit.
|
|
344
|
+
ok = False
|
|
345
|
+
|
|
346
|
+
def _cleanup_on_failure() -> None:
|
|
347
|
+
if not ok:
|
|
348
|
+
output.unlink(missing_ok=True)
|
|
349
|
+
|
|
350
|
+
stack.callback(_cleanup_on_failure)
|
|
351
|
+
input_stream, input_size = stack.enter_context(open_input(file))
|
|
352
|
+
output_stream = stack.enter_context(output.open("wb"))
|
|
353
|
+
progress = stack.enter_context(create_progress(title="Compressing images"))
|
|
340
354
|
task_id = progress.add_task("Processing messages", total=total_message_count)
|
|
341
355
|
|
|
342
356
|
writer = McapWriter(
|
|
@@ -411,6 +425,7 @@ def roscompress(
|
|
|
411
425
|
counters["converted"] += 1
|
|
412
426
|
|
|
413
427
|
writer.finish()
|
|
428
|
+
ok = compress_ok
|
|
414
429
|
|
|
415
430
|
if not compress_ok:
|
|
416
431
|
return 1
|
|
@@ -8,7 +8,11 @@ from cyclopts import Group, Parameter, validators
|
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
from ros_parser.message_path import MessagePathError
|
|
10
10
|
|
|
11
|
-
from pymcap_cli.cmd._run_processor import
|
|
11
|
+
from pymcap_cli.cmd._run_processor import (
|
|
12
|
+
finalize_delete_source,
|
|
13
|
+
processing_had_errors,
|
|
14
|
+
resolve_overwrite_policy,
|
|
15
|
+
)
|
|
12
16
|
from pymcap_cli.cmd._run_processor_multi import run_processor_multi
|
|
13
17
|
from pymcap_cli.constants import DEFAULT_CHUNK_SIZE, DEFAULT_COMPRESSION
|
|
14
18
|
from pymcap_cli.core.mcap_processor import InputOptions, OutputOptions
|
|
@@ -394,6 +398,9 @@ def split(
|
|
|
394
398
|
)
|
|
395
399
|
|
|
396
400
|
if delete_source:
|
|
401
|
+
if processing_had_errors(result.stats):
|
|
402
|
+
logger.error("Processing reported errors — source file preserved.")
|
|
403
|
+
return 1
|
|
397
404
|
outputs = [
|
|
398
405
|
Path(segment.path) for segment in result.processor.output_manager.segments.values()
|
|
399
406
|
]
|
|
@@ -11,8 +11,35 @@ from pymcap_cli.debug_wrapper import DebugStreamWrapper
|
|
|
11
11
|
from pymcap_cli.http_utils import open_http_stream
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def resolve_mcap_path(path: str) -> str:
|
|
15
|
+
"""Resolve a rosbag2 bag directory to its single ``.mcap`` file.
|
|
16
|
+
|
|
17
|
+
rosbag2 lays a recording out as ``<bagname>/<bagname>_<N>.mcap`` splits.
|
|
18
|
+
For single-file commands this resolves a directory holding exactly one split
|
|
19
|
+
to that file. A multi-split directory (which has no single-file meaning here)
|
|
20
|
+
or a directory with no resolvable MCAP raises ``ValueError`` rather than
|
|
21
|
+
surfacing an opaque ``IsADirectoryError`` downstream. Files and URLs pass
|
|
22
|
+
through unchanged.
|
|
23
|
+
"""
|
|
24
|
+
candidate_dir = Path(path)
|
|
25
|
+
if not candidate_dir.is_dir():
|
|
26
|
+
return path
|
|
27
|
+
|
|
28
|
+
from pymcap_cli.core.rosbag2_layout import find_bag_splits # noqa: PLC0415
|
|
29
|
+
|
|
30
|
+
splits = find_bag_splits(candidate_dir)
|
|
31
|
+
if len(splits) == 1:
|
|
32
|
+
return str(splits[0])
|
|
33
|
+
if not splits:
|
|
34
|
+
raise ValueError(f"{path!r} is not an MCAP file or a rosbag2 bag directory")
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"{path!r} is a multi-split rosbag2 directory ({len(splits)} files); "
|
|
37
|
+
f"this command reads a single file — use 'info' or 'merge'"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
14
41
|
def _open_path_file(url: ParseResult) -> tuple[io.RawIOBase, int]:
|
|
15
|
-
file_path = Path(url.path)
|
|
42
|
+
file_path = Path(resolve_mcap_path(url.path))
|
|
16
43
|
raw_stream = file_path.open("rb", buffering=0)
|
|
17
44
|
size = file_path.stat().st_size
|
|
18
45
|
return raw_stream, size
|