pymcap-cli 0.13.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.13.0 → pymcap_cli-0.16.0}/PKG-INFO +31 -2
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/README.md +21 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/pyproject.toml +16 -3
- pymcap_cli-0.16.0/src/pymcap_cli/cmd/_run_processor.py +260 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/_run_processor_multi.py +4 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bag2mcap_cmd.py +1 -0
- pymcap_cli-0.16.0/src/pymcap_cli/cmd/compress_cmd.py +168 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/convert_cmd.py +1 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/__init__.py +4 -0
- pymcap_cli-0.16.0/src/pymcap_cli/cmd/index/migrate_cmd.py +73 -0
- pymcap_cli-0.16.0/src/pymcap_cli/cmd/index/serve_cmd.py +195 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/info_cmd.py +80 -20
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/merge_cmd.py +11 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/plot_cmd.py +8 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/roscompress_cmd.py +24 -9
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/split_cmd.py +13 -3
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/input_handler.py +28 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/mcap_processor.py +136 -14
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/mcap_transform.py +32 -22
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/always_decode.py +3 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/attachment_filter.py +4 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/boundary_split.py +6 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/channel_merge.py +5 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/chunk_groupers.py +4 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/dedup.py +5 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/duration_split.py +5 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/expression_split.py +6 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/latching.py +7 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/metadata_filter.py +4 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/nth_message.py +5 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/size_split.py +5 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/time_filter.py +6 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/time_offset.py +5 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/timestamp_split.py +3 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/topic_alias.py +7 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/topic_filter.py +4 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/topic_rewrite.py +4 -1
- pymcap_cli-0.16.0/src/pymcap_cli/core/rosbag2_layout.py +197 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/display_utils.py +40 -9
- pymcap_cli-0.16.0/src/pymcap_cli/exporters/_summary_hints.py +96 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/driver.py +10 -3
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/plot_exporter.py +191 -60
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/http_utils.py +8 -3
- pymcap_cli-0.16.0/src/pymcap_cli/index/datasette/metadata.yaml +724 -0
- pymcap_cli-0.16.0/src/pymcap_cli/index/datasette/plugins/pymcap_render.py +166 -0
- pymcap_cli-0.16.0/src/pymcap_cli/index/datasette/templates/index.html +62 -0
- pymcap_cli-0.16.0/src/pymcap_cli/index/datasette/templates/query-index-timeline.html +26 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/types_manual.py +24 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/utils.py +26 -2
- pymcap_cli-0.13.0/src/pymcap_cli/cmd/_run_processor.py +0 -146
- pymcap_cli-0.13.0/src/pymcap_cli/cmd/compress_cmd.py +0 -97
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cli.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/_rechunk_strategy.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bridge/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bridge/_shared.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bridge/cat.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bridge/inspect.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bridge/record.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/cat_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/diag_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/diff_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/doctor_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/du_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/duplicates_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_csv_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_geo_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_images_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_json_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_parquet_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_pcd_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/filter_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/get_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/_helpers.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/duplicates_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/errors_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/info_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/query_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/scan_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/schemas_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/sessions_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/status_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/timeline_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/topics_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/tree_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/info_json_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/list_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/msg/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/msg/def_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/msg/list_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/msg/serve_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/process_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/rechunk_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/records_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/recover_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/recover_inplace_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/rosdecompress_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/tf_export_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/tf_get_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/tftree_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/topic_chunks_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/video_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/constants.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/input_options.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/input_processor_chain.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/mcap_compare.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/msg_resolver.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/ARCHITECTURE.md +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/base.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/utils.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/qos.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/tf_findings.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/tf_tree.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/debug_wrapper.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/cat_helpers.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/message_render.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/osc_utils.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/schema_html.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/schema_render.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/sparkline.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/time_ranges.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/doctor.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/encoding/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/encoding/arrow_schema.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/_common.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/base.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/csv_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/geo_common.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/geojson_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/gpx_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/image_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/json_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/kml_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/parquet_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/pcd_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/sdf_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/urdf_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/video_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/video_file_writer.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/db.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/fingerprint.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0001.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0002_normalise_and_drop_chunks.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0003_intern_channels.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0004_intern_compress_channel_metadata.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0005_content_compression.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0006_channel_statistics.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0007_consolidate_schema.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0008_rewrite_current_file_view.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0009_read_side_covering_indexes.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/scanner.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/summary_fingerprint.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/log_setup.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/py.typed +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/rihs01.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/rosbag_reader/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/rosbag_reader/_reader.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/rosbag_reader/_types.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/rosbag_reader/py.typed +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/duration.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/info_data.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/info_link.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/info_types.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/qos.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/size.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/to_plain.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
|
|
@@ -29,7 +29,7 @@ Requires-Dist: ros-parser
|
|
|
29
29
|
Requires-Dist: platformdirs>=4.0.0
|
|
30
30
|
Requires-Dist: pyyaml>=6.0
|
|
31
31
|
Requires-Dist: typing-extensions>=4.15.0
|
|
32
|
-
Requires-Dist: pymcap-cli[video,pointcloud,plot,parquet,image,draco,bridge,xxhash] ; extra == 'all'
|
|
32
|
+
Requires-Dist: pymcap-cli[video,pointcloud,plot,parquet,image,draco,bridge,xxhash,serve] ; extra == 'all'
|
|
33
33
|
Requires-Dist: robo-ws-bridge ; extra == 'bridge'
|
|
34
34
|
Requires-Dist: mcap-codec-support[draco] ; extra == 'draco'
|
|
35
35
|
Requires-Dist: pillow>=10.0 ; extra == 'image'
|
|
@@ -37,8 +37,15 @@ 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'
|
|
43
|
+
Requires-Dist: datasette>=0.65.2,<1 ; extra == 'serve'
|
|
44
|
+
Requires-Dist: datasette-block-robots>=1.1,<2 ; extra == 'serve'
|
|
45
|
+
Requires-Dist: datasette-copyable>=0.3.2,<1 ; extra == 'serve'
|
|
46
|
+
Requires-Dist: datasette-dashboards>=0.8.0,<1 ; extra == 'serve'
|
|
47
|
+
Requires-Dist: datasette-export-notebook>=1.0.1,<2 ; extra == 'serve'
|
|
48
|
+
Requires-Dist: datasette-vega>=0.6.2,<1 ; extra == 'serve'
|
|
42
49
|
Requires-Dist: mcap-codec-support[video] ; extra == 'video'
|
|
43
50
|
Requires-Dist: xxhash>=3.0.0 ; extra == 'xxhash'
|
|
44
51
|
Requires-Python: >=3.10
|
|
@@ -53,6 +60,7 @@ Provides-Extra: lite
|
|
|
53
60
|
Provides-Extra: parquet
|
|
54
61
|
Provides-Extra: plot
|
|
55
62
|
Provides-Extra: pointcloud
|
|
63
|
+
Provides-Extra: serve
|
|
56
64
|
Provides-Extra: video
|
|
57
65
|
Provides-Extra: xxhash
|
|
58
66
|
Description-Content-Type: text/markdown
|
|
@@ -415,6 +423,13 @@ Change MCAP file compression.
|
|
|
415
423
|
```bash
|
|
416
424
|
pymcap-cli compress input.mcap -o output.mcap --compression zstd
|
|
417
425
|
pymcap-cli compress input.mcap -o output.mcap --compression lz4
|
|
426
|
+
|
|
427
|
+
# Compress in place: write to a temp file, validate it, then replace the source
|
|
428
|
+
pymcap-cli compress input.mcap --in-place --compression zstd
|
|
429
|
+
|
|
430
|
+
# Trade a little ratio for throughput: --fast (zstd fast mode), or pick a level
|
|
431
|
+
pymcap-cli compress input.mcap -o output.mcap --fast
|
|
432
|
+
pymcap-cli compress input.mcap -o output.mcap --compression-level -5
|
|
418
433
|
```
|
|
419
434
|
|
|
420
435
|
### `du` — Disk Usage Analysis
|
|
@@ -524,8 +539,22 @@ pymcap-cli index tree /data/recordings --max-depth 3
|
|
|
524
539
|
# Query by topic/schema/time and inspect catalog-wide topics
|
|
525
540
|
pymcap-cli index query /data/recordings --topic /camera/front --format json
|
|
526
541
|
pymcap-cli index topics /camera --sort-by messages
|
|
542
|
+
|
|
543
|
+
# Apply pending schema migrations to an existing catalog
|
|
544
|
+
pymcap-cli index migrate
|
|
545
|
+
|
|
546
|
+
# Browse the catalog in a local Datasette web UI (dashboards, charts, cross-links)
|
|
547
|
+
pymcap-cli index serve
|
|
527
548
|
```
|
|
528
549
|
|
|
550
|
+
`index serve` launches [Datasette](https://datasette.io/) against the sidecar DB
|
|
551
|
+
with bundled dashboards, canned queries, and a render plugin. It needs the
|
|
552
|
+
`serve` extra (`pip install 'pymcap-cli[serve]'`; in this workspace, use
|
|
553
|
+
`uv run --package pymcap-cli --extra serve pymcap-cli index serve`). Datasette
|
|
554
|
+
runs from the same environment so the plugin resolves. Use `--db` to point at a
|
|
555
|
+
non-default catalog, `--port` to change the port (default 8001), and
|
|
556
|
+
`--no-browser` to skip auto-open.
|
|
557
|
+
|
|
529
558
|
### `records` — Raw Record Dump
|
|
530
559
|
|
|
531
560
|
Print every MCAP record in file order using its `repr`. Useful for inspecting
|
|
@@ -356,6 +356,13 @@ Change MCAP file compression.
|
|
|
356
356
|
```bash
|
|
357
357
|
pymcap-cli compress input.mcap -o output.mcap --compression zstd
|
|
358
358
|
pymcap-cli compress input.mcap -o output.mcap --compression lz4
|
|
359
|
+
|
|
360
|
+
# Compress in place: write to a temp file, validate it, then replace the source
|
|
361
|
+
pymcap-cli compress input.mcap --in-place --compression zstd
|
|
362
|
+
|
|
363
|
+
# Trade a little ratio for throughput: --fast (zstd fast mode), or pick a level
|
|
364
|
+
pymcap-cli compress input.mcap -o output.mcap --fast
|
|
365
|
+
pymcap-cli compress input.mcap -o output.mcap --compression-level -5
|
|
359
366
|
```
|
|
360
367
|
|
|
361
368
|
### `du` — Disk Usage Analysis
|
|
@@ -465,8 +472,22 @@ pymcap-cli index tree /data/recordings --max-depth 3
|
|
|
465
472
|
# Query by topic/schema/time and inspect catalog-wide topics
|
|
466
473
|
pymcap-cli index query /data/recordings --topic /camera/front --format json
|
|
467
474
|
pymcap-cli index topics /camera --sort-by messages
|
|
475
|
+
|
|
476
|
+
# Apply pending schema migrations to an existing catalog
|
|
477
|
+
pymcap-cli index migrate
|
|
478
|
+
|
|
479
|
+
# Browse the catalog in a local Datasette web UI (dashboards, charts, cross-links)
|
|
480
|
+
pymcap-cli index serve
|
|
468
481
|
```
|
|
469
482
|
|
|
483
|
+
`index serve` launches [Datasette](https://datasette.io/) against the sidecar DB
|
|
484
|
+
with bundled dashboards, canned queries, and a render plugin. It needs the
|
|
485
|
+
`serve` extra (`pip install 'pymcap-cli[serve]'`; in this workspace, use
|
|
486
|
+
`uv run --package pymcap-cli --extra serve pymcap-cli index serve`). Datasette
|
|
487
|
+
runs from the same environment so the plugin resolves. Use `--db` to point at a
|
|
488
|
+
non-default catalog, `--port` to change the port (default 8001), and
|
|
489
|
+
`--no-browser` to skip auto-open.
|
|
490
|
+
|
|
470
491
|
### `records` — Raw Record Dump
|
|
471
492
|
|
|
472
493
|
Print every MCAP record in file order using its `repr`. Useful for inspecting
|
|
@@ -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"
|
|
@@ -46,16 +46,29 @@ dependencies = [
|
|
|
46
46
|
]
|
|
47
47
|
|
|
48
48
|
[project.optional-dependencies]
|
|
49
|
-
all = ["pymcap-cli[video,pointcloud,plot,parquet,image,draco,bridge,xxhash]"]
|
|
49
|
+
all = ["pymcap-cli[video,pointcloud,plot,parquet,image,draco,bridge,xxhash,serve]"]
|
|
50
50
|
lite = ["pymcap-cli[image,draco,bridge,xxhash]"]
|
|
51
51
|
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"]
|
|
62
|
+
# `index serve` web UI. datasette<1 pin: datasette-dashboards 500s on the 1.0
|
|
63
|
+
# pre-release. Run from the same env so the bundled pymcap_render plugin imports.
|
|
64
|
+
serve = [
|
|
65
|
+
"datasette>=0.65.2,<1",
|
|
66
|
+
"datasette-block-robots>=1.1,<2",
|
|
67
|
+
"datasette-copyable>=0.3.2,<1",
|
|
68
|
+
"datasette-dashboards>=0.8.0,<1",
|
|
69
|
+
"datasette-export-notebook>=1.0.1,<2",
|
|
70
|
+
"datasette-vega>=0.6.2,<1",
|
|
71
|
+
]
|
|
59
72
|
|
|
60
73
|
|
|
61
74
|
[project.urls]
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Shared processor pipeline for transform commands."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import BinaryIO
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
from small_mcap import InvalidMagicError, McapError
|
|
11
|
+
|
|
12
|
+
from pymcap_cli.core.input_handler import open_input
|
|
13
|
+
from pymcap_cli.core.mcap_processor import (
|
|
14
|
+
InputFile,
|
|
15
|
+
InputOptions,
|
|
16
|
+
McapProcessor,
|
|
17
|
+
OutputOptions,
|
|
18
|
+
OverwriteCollisionPolicy,
|
|
19
|
+
ProcessingOptions,
|
|
20
|
+
ProcessingStats,
|
|
21
|
+
)
|
|
22
|
+
from pymcap_cli.core.rosbag2_layout import expand_bag_paths
|
|
23
|
+
from pymcap_cli.utils import confirm_output_overwrite, read_info
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(slots=True)
|
|
29
|
+
class ProcessorResult:
|
|
30
|
+
"""Result of a processor run."""
|
|
31
|
+
|
|
32
|
+
stats: ProcessingStats
|
|
33
|
+
processor: McapProcessor
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_overwrite_policy(*, force: bool, no_clobber: bool) -> OverwriteCollisionPolicy | None:
|
|
37
|
+
"""Map CLI overwrite flags to the processor overwrite policy."""
|
|
38
|
+
if force and no_clobber:
|
|
39
|
+
return None
|
|
40
|
+
if force:
|
|
41
|
+
return OverwriteCollisionPolicy.OVERWRITE
|
|
42
|
+
if no_clobber:
|
|
43
|
+
return OverwriteCollisionPolicy.ERROR
|
|
44
|
+
return OverwriteCollisionPolicy.ASK
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _open_output_stream(output: Path, overwrite_policy: OverwriteCollisionPolicy) -> BinaryIO:
|
|
48
|
+
"""Open a single-output destination with the configured overwrite policy."""
|
|
49
|
+
if overwrite_policy == OverwriteCollisionPolicy.ASK:
|
|
50
|
+
confirm_output_overwrite(output, force=False)
|
|
51
|
+
elif overwrite_policy == OverwriteCollisionPolicy.ERROR and output.exists():
|
|
52
|
+
raise FileExistsError(f"Output file '{output}' already exists.")
|
|
53
|
+
|
|
54
|
+
return output.open("wb")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def run_processor(
|
|
58
|
+
*,
|
|
59
|
+
files: list[str],
|
|
60
|
+
output: Path,
|
|
61
|
+
input_options: InputOptions,
|
|
62
|
+
output_options: OutputOptions,
|
|
63
|
+
) -> ProcessorResult:
|
|
64
|
+
"""Open files, build ProcessingOptions, run McapProcessor, return results.
|
|
65
|
+
|
|
66
|
+
Raises any exception from McapProcessor.process() to the caller.
|
|
67
|
+
"""
|
|
68
|
+
files = expand_bag_paths(files)
|
|
69
|
+
with contextlib.ExitStack() as stack:
|
|
70
|
+
input_files: list[InputFile] = []
|
|
71
|
+
|
|
72
|
+
for f in files:
|
|
73
|
+
stream, size = stack.enter_context(open_input(f))
|
|
74
|
+
input_files.append(InputFile(stream=stream, size=size, options=input_options))
|
|
75
|
+
|
|
76
|
+
output_stream = stack.enter_context(
|
|
77
|
+
_open_output_stream(output, output_options.overwrite_policy)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
processing_options = ProcessingOptions(
|
|
81
|
+
inputs=input_files,
|
|
82
|
+
input_options=InputOptions.from_args(),
|
|
83
|
+
output_options=output_options,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
processor = McapProcessor(processing_options)
|
|
87
|
+
stats = processor.process(output_stream)
|
|
88
|
+
|
|
89
|
+
return ProcessorResult(stats=stats, processor=processor)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def validate_mcap_output(path: Path) -> bool:
|
|
93
|
+
"""Return True iff the MCAP at ``path`` has a readable header and summary."""
|
|
94
|
+
try:
|
|
95
|
+
with path.open("rb") as f:
|
|
96
|
+
read_info(f)
|
|
97
|
+
except (McapError, InvalidMagicError, OSError, AssertionError) as e:
|
|
98
|
+
logger.debug(f"Output validation failed for {path}: {e}")
|
|
99
|
+
return False
|
|
100
|
+
return True
|
|
101
|
+
|
|
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
|
+
|
|
173
|
+
def delete_source_files(sources: list[str], outputs: list[Path]) -> None:
|
|
174
|
+
"""Delete each local source file. Skip URLs and any source path that
|
|
175
|
+
resolves to one of ``outputs`` (with a warning).
|
|
176
|
+
"""
|
|
177
|
+
output_resolved = {p.resolve() for p in outputs}
|
|
178
|
+
for src in sources:
|
|
179
|
+
scheme = urlparse(src).scheme
|
|
180
|
+
if scheme in ("http", "https"):
|
|
181
|
+
logger.warning(f"Skipping delete: '{src}' is a remote URL")
|
|
182
|
+
continue
|
|
183
|
+
path = Path(src)
|
|
184
|
+
try:
|
|
185
|
+
resolved = path.resolve()
|
|
186
|
+
except OSError as e:
|
|
187
|
+
logger.warning(f"Skipping delete '{src}': {e}")
|
|
188
|
+
continue
|
|
189
|
+
if resolved in output_resolved:
|
|
190
|
+
logger.warning(f"Skipping delete: source '{src}' is also an output")
|
|
191
|
+
continue
|
|
192
|
+
try:
|
|
193
|
+
path.unlink()
|
|
194
|
+
logger.info(f"Deleted source: {src}")
|
|
195
|
+
except FileNotFoundError:
|
|
196
|
+
logger.debug(f"Source already gone: {src}")
|
|
197
|
+
except OSError:
|
|
198
|
+
logger.exception(f"Failed to delete '{src}'")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def in_place_temp_path(source: Path) -> Path:
|
|
202
|
+
"""Temp output path next to ``source`` so the final rename stays on one filesystem."""
|
|
203
|
+
return source.with_name(source.name + ".tmp")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def finalize_replace_source(*, source: Path, tmp_output: Path) -> int:
|
|
207
|
+
"""Validate ``tmp_output`` and atomically replace ``source`` with it.
|
|
208
|
+
|
|
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.
|
|
212
|
+
"""
|
|
213
|
+
if not validate_mcap_output(tmp_output):
|
|
214
|
+
logger.error(f"[red]Output failed validation: {tmp_output}[/red]")
|
|
215
|
+
logger.error("Source file preserved — output not safe to replace source.")
|
|
216
|
+
tmp_output.unlink(missing_ok=True)
|
|
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
|
|
222
|
+
tmp_output.replace(source)
|
|
223
|
+
logger.info(f"Replaced source: {source}")
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def finalize_delete_source(
|
|
228
|
+
*,
|
|
229
|
+
sources: list[str],
|
|
230
|
+
outputs: list[Path],
|
|
231
|
+
require_lossless: bool = False,
|
|
232
|
+
) -> int:
|
|
233
|
+
"""Validate every output and, if all valid, delete the eligible sources.
|
|
234
|
+
|
|
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.
|
|
242
|
+
"""
|
|
243
|
+
if not outputs:
|
|
244
|
+
logger.error("No output files were produced — source file(s) preserved.")
|
|
245
|
+
return 1
|
|
246
|
+
invalid = [p for p in outputs if not validate_mcap_output(p)]
|
|
247
|
+
if invalid:
|
|
248
|
+
for p in invalid:
|
|
249
|
+
logger.error(f"[red]Output failed validation: {p}[/red]")
|
|
250
|
+
logger.error("Source file(s) preserved — output not safe to replace source.")
|
|
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
|
|
259
|
+
delete_source_files(sources, outputs)
|
|
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
|
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Compress command for pymcap-cli."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from pymcap_cli.cmd._run_processor import (
|
|
10
|
+
finalize_delete_source,
|
|
11
|
+
finalize_replace_source,
|
|
12
|
+
in_place_temp_path,
|
|
13
|
+
processing_had_errors,
|
|
14
|
+
resolve_overwrite_policy,
|
|
15
|
+
run_processor,
|
|
16
|
+
)
|
|
17
|
+
from pymcap_cli.constants import DEFAULT_CHUNK_SIZE, DEFAULT_COMPRESSION
|
|
18
|
+
from pymcap_cli.core.mcap_processor import (
|
|
19
|
+
InputOptions,
|
|
20
|
+
OutputOptions,
|
|
21
|
+
OverwriteCollisionPolicy,
|
|
22
|
+
)
|
|
23
|
+
from pymcap_cli.types.types_manual import (
|
|
24
|
+
ChunkSizeOption,
|
|
25
|
+
CompressionLevelOption,
|
|
26
|
+
CompressionOption,
|
|
27
|
+
DeleteSourceOption,
|
|
28
|
+
FastCompressionOption,
|
|
29
|
+
ForceOverwriteOption,
|
|
30
|
+
InPlaceOption,
|
|
31
|
+
NoClobberOption,
|
|
32
|
+
OutputPathOption,
|
|
33
|
+
)
|
|
34
|
+
from pymcap_cli.utils import output_overwrites_input
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
console = Console()
|
|
38
|
+
|
|
39
|
+
# zstd "fast" mode: a negative level trades ~5% larger output for roughly 2x
|
|
40
|
+
# compression throughput. Used by --fast.
|
|
41
|
+
_FAST_ZSTD_LEVEL = -1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def compress(
|
|
45
|
+
file: str,
|
|
46
|
+
output: OutputPathOption | None = None,
|
|
47
|
+
*,
|
|
48
|
+
chunk_size: ChunkSizeOption = DEFAULT_CHUNK_SIZE,
|
|
49
|
+
compression: CompressionOption = DEFAULT_COMPRESSION,
|
|
50
|
+
force: ForceOverwriteOption = False,
|
|
51
|
+
no_clobber: NoClobberOption = False,
|
|
52
|
+
delete_source: DeleteSourceOption = False,
|
|
53
|
+
in_place: InPlaceOption = False,
|
|
54
|
+
compression_level: CompressionLevelOption = None,
|
|
55
|
+
fast: FastCompressionOption = False,
|
|
56
|
+
) -> int:
|
|
57
|
+
"""Create a compressed copy of an MCAP file.
|
|
58
|
+
|
|
59
|
+
Copy data in an MCAP file to a new file, compressing the output.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
file
|
|
64
|
+
Path to the MCAP file to compress (local file or HTTP/HTTPS URL).
|
|
65
|
+
output
|
|
66
|
+
Output filename. Required unless --in-place is given.
|
|
67
|
+
chunk_size
|
|
68
|
+
Chunk size of output file in bytes.
|
|
69
|
+
compression
|
|
70
|
+
Compression algorithm for output file.
|
|
71
|
+
force
|
|
72
|
+
Force overwrite of output file without confirmation.
|
|
73
|
+
no_clobber
|
|
74
|
+
Fail instead of prompting if the output file already exists.
|
|
75
|
+
delete_source
|
|
76
|
+
Delete source file(s) after the output is validated (header + summary).
|
|
77
|
+
URL inputs and any source whose path equals the output are skipped.
|
|
78
|
+
in_place
|
|
79
|
+
Compress to a temp file next to the source and, after the output is
|
|
80
|
+
validated (header + summary), atomically replace the source with it.
|
|
81
|
+
Local files only; mutually exclusive with --output and --delete-source.
|
|
82
|
+
compression_level
|
|
83
|
+
zstd level. Omit for the library default (3). Negative levels select the
|
|
84
|
+
fast modes — much higher throughput for slightly larger output. Ignored
|
|
85
|
+
for non-zstd compression. Mutually exclusive with --fast.
|
|
86
|
+
fast
|
|
87
|
+
Shortcut for a fast zstd level (roughly 2x throughput, ~5% larger
|
|
88
|
+
output). Equivalent to ``--compression-level -1``.
|
|
89
|
+
|
|
90
|
+
Examples
|
|
91
|
+
--------
|
|
92
|
+
```
|
|
93
|
+
pymcap-cli compress in.mcap -o out.mcap
|
|
94
|
+
pymcap-cli compress in.mcap --in-place
|
|
95
|
+
pymcap-cli compress in.mcap -o out.mcap --fast
|
|
96
|
+
```
|
|
97
|
+
"""
|
|
98
|
+
if fast and compression_level is not None:
|
|
99
|
+
logger.error("--fast and --compression-level cannot be used together.")
|
|
100
|
+
return 1
|
|
101
|
+
zstd_level = _FAST_ZSTD_LEVEL if fast else compression_level
|
|
102
|
+
if in_place:
|
|
103
|
+
if output is not None:
|
|
104
|
+
logger.error("--in-place and --output cannot be used together.")
|
|
105
|
+
return 1
|
|
106
|
+
if delete_source:
|
|
107
|
+
logger.error("--in-place and --delete-source cannot be used together.")
|
|
108
|
+
return 1
|
|
109
|
+
if urlparse(file).scheme in ("http", "https"):
|
|
110
|
+
logger.error("--in-place requires a local file, not a URL.")
|
|
111
|
+
return 1
|
|
112
|
+
output = in_place_temp_path(Path(file))
|
|
113
|
+
overwrite_policy = OverwriteCollisionPolicy.OVERWRITE
|
|
114
|
+
else:
|
|
115
|
+
if output is None:
|
|
116
|
+
logger.error("Either --output or --in-place is required.")
|
|
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
|
|
124
|
+
policy = resolve_overwrite_policy(force=force, no_clobber=no_clobber)
|
|
125
|
+
if policy is None:
|
|
126
|
+
logger.error("--force and --no-clobber cannot be used together.")
|
|
127
|
+
return 1
|
|
128
|
+
overwrite_policy = policy
|
|
129
|
+
|
|
130
|
+
logger.info(f"Compressing '{file}' to '{output}'")
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
result = run_processor(
|
|
134
|
+
files=[file],
|
|
135
|
+
output=output,
|
|
136
|
+
# Do not force always_decode_chunk — the processor now has a
|
|
137
|
+
# chunk-level RECOMPRESS path that avoids per-message parsing.
|
|
138
|
+
input_options=InputOptions.from_args(),
|
|
139
|
+
output_options=OutputOptions(
|
|
140
|
+
compression=compression,
|
|
141
|
+
chunk_size=chunk_size,
|
|
142
|
+
overwrite_policy=overwrite_policy,
|
|
143
|
+
zstd_level=zstd_level,
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
logger.info("[green]✓ Compression completed successfully![/green]")
|
|
147
|
+
console.print(result.stats)
|
|
148
|
+
except Exception:
|
|
149
|
+
logger.exception("Error during compression")
|
|
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)
|
|
153
|
+
return 1
|
|
154
|
+
|
|
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
|
|
160
|
+
return finalize_replace_source(source=Path(file), tmp_output=output)
|
|
161
|
+
|
|
162
|
+
if delete_source:
|
|
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)
|
|
167
|
+
|
|
168
|
+
return 0
|
|
@@ -12,9 +12,11 @@ from cyclopts import App
|
|
|
12
12
|
from pymcap_cli.cmd.index.duplicates_cmd import duplicates_cmd
|
|
13
13
|
from pymcap_cli.cmd.index.errors_cmd import errors_cmd
|
|
14
14
|
from pymcap_cli.cmd.index.info_cmd import info_cmd
|
|
15
|
+
from pymcap_cli.cmd.index.migrate_cmd import migrate_cmd
|
|
15
16
|
from pymcap_cli.cmd.index.query_cmd import query_cmd
|
|
16
17
|
from pymcap_cli.cmd.index.scan_cmd import scan_cmd
|
|
17
18
|
from pymcap_cli.cmd.index.schemas_cmd import schemas_cmd
|
|
19
|
+
from pymcap_cli.cmd.index.serve_cmd import index_serve
|
|
18
20
|
from pymcap_cli.cmd.index.sessions_cmd import sessions_cmd
|
|
19
21
|
from pymcap_cli.cmd.index.status_cmd import status_cmd
|
|
20
22
|
from pymcap_cli.cmd.index.timeline_cmd import timeline_cmd
|
|
@@ -39,3 +41,5 @@ index_app.command(sessions_cmd, name="sessions")
|
|
|
39
41
|
index_app.command(errors_cmd, name="errors")
|
|
40
42
|
index_app.command(timeline_cmd, name="timeline")
|
|
41
43
|
index_app.command(info_cmd, name="info")
|
|
44
|
+
index_app.command(migrate_cmd, name="migrate")
|
|
45
|
+
index_app.command(index_serve, name="serve")
|