pymcap-cli 0.13.0__tar.gz → 0.14.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.14.0}/PKG-INFO +30 -2
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/README.md +21 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/pyproject.toml +12 -2
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/_run_processor.py +21 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/bag2mcap_cmd.py +1 -0
- pymcap_cli-0.14.0/src/pymcap_cli/cmd/compress_cmd.py +152 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/convert_cmd.py +1 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/index/__init__.py +4 -0
- pymcap_cli-0.14.0/src/pymcap_cli/cmd/index/migrate_cmd.py +73 -0
- pymcap_cli-0.14.0/src/pymcap_cli/cmd/index/serve_cmd.py +195 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/roscompress_cmd.py +3 -3
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/split_cmd.py +5 -2
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/mcap_processor.py +127 -14
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/always_decode.py +3 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/attachment_filter.py +4 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/boundary_split.py +6 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/channel_merge.py +5 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/chunk_groupers.py +4 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/dedup.py +5 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/duration_split.py +5 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/expression_split.py +6 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/latching.py +7 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/metadata_filter.py +4 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/nth_message.py +5 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/size_split.py +5 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/time_filter.py +6 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/time_offset.py +5 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/timestamp_split.py +3 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/topic_alias.py +7 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/topic_filter.py +4 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/topic_rewrite.py +4 -1
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/display/display_utils.py +40 -9
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/http_utils.py +8 -3
- pymcap_cli-0.14.0/src/pymcap_cli/index/datasette/metadata.yaml +724 -0
- pymcap_cli-0.14.0/src/pymcap_cli/index/datasette/plugins/pymcap_render.py +166 -0
- pymcap_cli-0.14.0/src/pymcap_cli/index/datasette/templates/index.html +62 -0
- pymcap_cli-0.14.0/src/pymcap_cli/index/datasette/templates/query-index-timeline.html +26 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/types/types_manual.py +24 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/utils.py +11 -2
- pymcap_cli-0.13.0/src/pymcap_cli/cmd/compress_cmd.py +0 -97
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cli.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/_rechunk_strategy.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/_run_processor_multi.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/bridge/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/bridge/_shared.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/bridge/cat.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/bridge/inspect.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/bridge/record.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/cat_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/diag_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/diff_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/doctor_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/du_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/duplicates_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/export_csv_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/export_geo_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/export_images_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/export_json_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/export_parquet_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/export_pcd_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/filter_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/get_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/index/_helpers.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/index/duplicates_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/index/errors_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/index/info_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/index/query_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/index/scan_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/index/schemas_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/index/sessions_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/index/status_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/index/timeline_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/index/topics_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/index/tree_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/info_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/info_json_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/list_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/merge_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/msg/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/msg/def_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/msg/list_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/msg/serve_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/plot_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/process_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/rechunk_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/records_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/recover_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/recover_inplace_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/rosdecompress_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/tf_export_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/tf_get_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/tftree_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/topic_chunks_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/cmd/video_cmd.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/constants.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/input_handler.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/input_options.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/input_processor_chain.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/mcap_compare.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/mcap_transform.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/msg_resolver.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/ARCHITECTURE.md +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/base.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/processors/utils.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/qos.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/tf_findings.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/core/tf_tree.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/debug_wrapper.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/display/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/display/cat_helpers.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/display/message_render.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/display/osc_utils.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/display/schema_html.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/display/schema_render.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/display/sparkline.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/display/time_ranges.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/doctor.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/encoding/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/encoding/arrow_schema.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/_common.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/base.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/csv_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/driver.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/geo_common.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/geojson_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/gpx_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/image_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/json_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/kml_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/parquet_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/pcd_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/plot_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/sdf_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/urdf_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/video_exporter.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/exporters/video_file_writer.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/index/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/index/db.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/index/fingerprint.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/index/migrations/0001.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/index/migrations/0002_normalise_and_drop_chunks.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/index/migrations/0003_intern_channels.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/index/migrations/0004_intern_compress_channel_metadata.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/index/migrations/0005_content_compression.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/index/migrations/0006_channel_statistics.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/index/migrations/0007_consolidate_schema.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/index/migrations/0008_rewrite_current_file_view.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/index/migrations/0009_read_side_covering_indexes.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/index/migrations/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/index/scanner.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/index/summary_fingerprint.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/log_setup.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/py.typed +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/rihs01.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/rosbag_reader/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/rosbag_reader/_reader.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/rosbag_reader/_types.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/rosbag_reader/py.typed +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/types/__init__.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/types/duration.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/types/info_data.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/types/info_link.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/types/info_types.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/types/qos.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.0}/src/pymcap_cli/types/size.py +0 -0
- {pymcap_cli-0.13.0 → pymcap_cli-0.14.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.14.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'
|
|
@@ -39,6 +39,12 @@ Requires-Dist: numpy>=1.24.0 ; extra == 'parquet'
|
|
|
39
39
|
Requires-Dist: mcap-codec-support[pointcloud] ; extra == 'parquet'
|
|
40
40
|
Requires-Dist: plotly>=6.0.0 ; extra == 'plot'
|
|
41
41
|
Requires-Dist: mcap-codec-support[pointcloud] ; extra == 'pointcloud'
|
|
42
|
+
Requires-Dist: datasette>=0.65.2,<1 ; extra == 'serve'
|
|
43
|
+
Requires-Dist: datasette-block-robots>=1.1,<2 ; extra == 'serve'
|
|
44
|
+
Requires-Dist: datasette-copyable>=0.3.2,<1 ; extra == 'serve'
|
|
45
|
+
Requires-Dist: datasette-dashboards>=0.8.0,<1 ; extra == 'serve'
|
|
46
|
+
Requires-Dist: datasette-export-notebook>=1.0.1,<2 ; extra == 'serve'
|
|
47
|
+
Requires-Dist: datasette-vega>=0.6.2,<1 ; extra == 'serve'
|
|
42
48
|
Requires-Dist: mcap-codec-support[video] ; extra == 'video'
|
|
43
49
|
Requires-Dist: xxhash>=3.0.0 ; extra == 'xxhash'
|
|
44
50
|
Requires-Python: >=3.10
|
|
@@ -53,6 +59,7 @@ Provides-Extra: lite
|
|
|
53
59
|
Provides-Extra: parquet
|
|
54
60
|
Provides-Extra: plot
|
|
55
61
|
Provides-Extra: pointcloud
|
|
62
|
+
Provides-Extra: serve
|
|
56
63
|
Provides-Extra: video
|
|
57
64
|
Provides-Extra: xxhash
|
|
58
65
|
Description-Content-Type: text/markdown
|
|
@@ -415,6 +422,13 @@ Change MCAP file compression.
|
|
|
415
422
|
```bash
|
|
416
423
|
pymcap-cli compress input.mcap -o output.mcap --compression zstd
|
|
417
424
|
pymcap-cli compress input.mcap -o output.mcap --compression lz4
|
|
425
|
+
|
|
426
|
+
# Compress in place: write to a temp file, validate it, then replace the source
|
|
427
|
+
pymcap-cli compress input.mcap --in-place --compression zstd
|
|
428
|
+
|
|
429
|
+
# Trade a little ratio for throughput: --fast (zstd fast mode), or pick a level
|
|
430
|
+
pymcap-cli compress input.mcap -o output.mcap --fast
|
|
431
|
+
pymcap-cli compress input.mcap -o output.mcap --compression-level -5
|
|
418
432
|
```
|
|
419
433
|
|
|
420
434
|
### `du` — Disk Usage Analysis
|
|
@@ -524,8 +538,22 @@ pymcap-cli index tree /data/recordings --max-depth 3
|
|
|
524
538
|
# Query by topic/schema/time and inspect catalog-wide topics
|
|
525
539
|
pymcap-cli index query /data/recordings --topic /camera/front --format json
|
|
526
540
|
pymcap-cli index topics /camera --sort-by messages
|
|
541
|
+
|
|
542
|
+
# Apply pending schema migrations to an existing catalog
|
|
543
|
+
pymcap-cli index migrate
|
|
544
|
+
|
|
545
|
+
# Browse the catalog in a local Datasette web UI (dashboards, charts, cross-links)
|
|
546
|
+
pymcap-cli index serve
|
|
527
547
|
```
|
|
528
548
|
|
|
549
|
+
`index serve` launches [Datasette](https://datasette.io/) against the sidecar DB
|
|
550
|
+
with bundled dashboards, canned queries, and a render plugin. It needs the
|
|
551
|
+
`serve` extra (`pip install 'pymcap-cli[serve]'`; in this workspace, use
|
|
552
|
+
`uv run --package pymcap-cli --extra serve pymcap-cli index serve`). Datasette
|
|
553
|
+
runs from the same environment so the plugin resolves. Use `--db` to point at a
|
|
554
|
+
non-default catalog, `--port` to change the port (default 8001), and
|
|
555
|
+
`--no-browser` to skip auto-open.
|
|
556
|
+
|
|
529
557
|
### `records` — Raw Record Dump
|
|
530
558
|
|
|
531
559
|
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.14.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,7 +46,7 @@ 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]"]
|
|
@@ -56,6 +56,16 @@ plot = ["plotly>=6.0.0"]
|
|
|
56
56
|
pointcloud = ["mcap-codec-support[pointcloud]"]
|
|
57
57
|
video = ["mcap-codec-support[video]"]
|
|
58
58
|
xxhash = ["xxhash>=3.0.0"]
|
|
59
|
+
# `index serve` web UI. datasette<1 pin: datasette-dashboards 500s on the 1.0
|
|
60
|
+
# pre-release. Run from the same env so the bundled pymcap_render plugin imports.
|
|
61
|
+
serve = [
|
|
62
|
+
"datasette>=0.65.2,<1",
|
|
63
|
+
"datasette-block-robots>=1.1,<2",
|
|
64
|
+
"datasette-copyable>=0.3.2,<1",
|
|
65
|
+
"datasette-dashboards>=0.8.0,<1",
|
|
66
|
+
"datasette-export-notebook>=1.0.1,<2",
|
|
67
|
+
"datasette-vega>=0.6.2,<1",
|
|
68
|
+
]
|
|
59
69
|
|
|
60
70
|
|
|
61
71
|
[project.urls]
|
|
@@ -126,6 +126,27 @@ def delete_source_files(sources: list[str], outputs: list[Path]) -> None:
|
|
|
126
126
|
logger.exception(f"Failed to delete '{src}'")
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
def in_place_temp_path(source: Path) -> Path:
|
|
130
|
+
"""Temp output path next to ``source`` so the final rename stays on one filesystem."""
|
|
131
|
+
return source.with_name(source.name + ".tmp")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def finalize_replace_source(*, source: Path, tmp_output: Path) -> int:
|
|
135
|
+
"""Validate ``tmp_output`` and atomically replace ``source`` with it.
|
|
136
|
+
|
|
137
|
+
Returns 0 on success and 1 if validation failed (source is preserved and
|
|
138
|
+
the temp file is removed).
|
|
139
|
+
"""
|
|
140
|
+
if not validate_mcap_output(tmp_output):
|
|
141
|
+
logger.error(f"[red]Output failed validation: {tmp_output}[/red]")
|
|
142
|
+
logger.error("Source file preserved — output not safe to replace source.")
|
|
143
|
+
tmp_output.unlink(missing_ok=True)
|
|
144
|
+
return 1
|
|
145
|
+
tmp_output.replace(source)
|
|
146
|
+
logger.info(f"Replaced source: {source}")
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
|
|
129
150
|
def finalize_delete_source(
|
|
130
151
|
*,
|
|
131
152
|
sources: list[str],
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
resolve_overwrite_policy,
|
|
14
|
+
run_processor,
|
|
15
|
+
)
|
|
16
|
+
from pymcap_cli.constants import DEFAULT_CHUNK_SIZE, DEFAULT_COMPRESSION
|
|
17
|
+
from pymcap_cli.core.mcap_processor import (
|
|
18
|
+
InputOptions,
|
|
19
|
+
OutputOptions,
|
|
20
|
+
OverwriteCollisionPolicy,
|
|
21
|
+
)
|
|
22
|
+
from pymcap_cli.types.types_manual import (
|
|
23
|
+
ChunkSizeOption,
|
|
24
|
+
CompressionLevelOption,
|
|
25
|
+
CompressionOption,
|
|
26
|
+
DeleteSourceOption,
|
|
27
|
+
FastCompressionOption,
|
|
28
|
+
ForceOverwriteOption,
|
|
29
|
+
InPlaceOption,
|
|
30
|
+
NoClobberOption,
|
|
31
|
+
OutputPathOption,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
console = Console()
|
|
36
|
+
|
|
37
|
+
# zstd "fast" mode: a negative level trades ~5% larger output for roughly 2x
|
|
38
|
+
# compression throughput. Used by --fast.
|
|
39
|
+
_FAST_ZSTD_LEVEL = -1
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def compress(
|
|
43
|
+
file: str,
|
|
44
|
+
output: OutputPathOption | None = None,
|
|
45
|
+
*,
|
|
46
|
+
chunk_size: ChunkSizeOption = DEFAULT_CHUNK_SIZE,
|
|
47
|
+
compression: CompressionOption = DEFAULT_COMPRESSION,
|
|
48
|
+
force: ForceOverwriteOption = False,
|
|
49
|
+
no_clobber: NoClobberOption = False,
|
|
50
|
+
delete_source: DeleteSourceOption = False,
|
|
51
|
+
in_place: InPlaceOption = False,
|
|
52
|
+
compression_level: CompressionLevelOption = None,
|
|
53
|
+
fast: FastCompressionOption = False,
|
|
54
|
+
) -> int:
|
|
55
|
+
"""Create a compressed copy of an MCAP file.
|
|
56
|
+
|
|
57
|
+
Copy data in an MCAP file to a new file, compressing the output.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
file
|
|
62
|
+
Path to the MCAP file to compress (local file or HTTP/HTTPS URL).
|
|
63
|
+
output
|
|
64
|
+
Output filename. Required unless --in-place is given.
|
|
65
|
+
chunk_size
|
|
66
|
+
Chunk size of output file in bytes.
|
|
67
|
+
compression
|
|
68
|
+
Compression algorithm for output file.
|
|
69
|
+
force
|
|
70
|
+
Force overwrite of output file without confirmation.
|
|
71
|
+
no_clobber
|
|
72
|
+
Fail instead of prompting if the output file already exists.
|
|
73
|
+
delete_source
|
|
74
|
+
Delete source file(s) after the output is validated (header + summary).
|
|
75
|
+
URL inputs and any source whose path equals the output are skipped.
|
|
76
|
+
in_place
|
|
77
|
+
Compress to a temp file next to the source and, after the output is
|
|
78
|
+
validated (header + summary), atomically replace the source with it.
|
|
79
|
+
Local files only; mutually exclusive with --output and --delete-source.
|
|
80
|
+
compression_level
|
|
81
|
+
zstd level. Omit for the library default (3). Negative levels select the
|
|
82
|
+
fast modes — much higher throughput for slightly larger output. Ignored
|
|
83
|
+
for non-zstd compression. Mutually exclusive with --fast.
|
|
84
|
+
fast
|
|
85
|
+
Shortcut for a fast zstd level (roughly 2x throughput, ~5% larger
|
|
86
|
+
output). Equivalent to ``--compression-level -1``.
|
|
87
|
+
|
|
88
|
+
Examples
|
|
89
|
+
--------
|
|
90
|
+
```
|
|
91
|
+
pymcap-cli compress in.mcap -o out.mcap
|
|
92
|
+
pymcap-cli compress in.mcap --in-place
|
|
93
|
+
pymcap-cli compress in.mcap -o out.mcap --fast
|
|
94
|
+
```
|
|
95
|
+
"""
|
|
96
|
+
if fast and compression_level is not None:
|
|
97
|
+
logger.error("--fast and --compression-level cannot be used together.")
|
|
98
|
+
return 1
|
|
99
|
+
zstd_level = _FAST_ZSTD_LEVEL if fast else compression_level
|
|
100
|
+
if in_place:
|
|
101
|
+
if output is not None:
|
|
102
|
+
logger.error("--in-place and --output cannot be used together.")
|
|
103
|
+
return 1
|
|
104
|
+
if delete_source:
|
|
105
|
+
logger.error("--in-place and --delete-source cannot be used together.")
|
|
106
|
+
return 1
|
|
107
|
+
if urlparse(file).scheme in ("http", "https"):
|
|
108
|
+
logger.error("--in-place requires a local file, not a URL.")
|
|
109
|
+
return 1
|
|
110
|
+
output = in_place_temp_path(Path(file))
|
|
111
|
+
overwrite_policy = OverwriteCollisionPolicy.OVERWRITE
|
|
112
|
+
else:
|
|
113
|
+
if output is None:
|
|
114
|
+
logger.error("Either --output or --in-place is required.")
|
|
115
|
+
return 1
|
|
116
|
+
policy = resolve_overwrite_policy(force=force, no_clobber=no_clobber)
|
|
117
|
+
if policy is None:
|
|
118
|
+
logger.error("--force and --no-clobber cannot be used together.")
|
|
119
|
+
return 1
|
|
120
|
+
overwrite_policy = policy
|
|
121
|
+
|
|
122
|
+
logger.info(f"Compressing '{file}' to '{output}'")
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
result = run_processor(
|
|
126
|
+
files=[file],
|
|
127
|
+
output=output,
|
|
128
|
+
# Do not force always_decode_chunk — the processor now has a
|
|
129
|
+
# chunk-level RECOMPRESS path that avoids per-message parsing.
|
|
130
|
+
input_options=InputOptions.from_args(),
|
|
131
|
+
output_options=OutputOptions(
|
|
132
|
+
compression=compression,
|
|
133
|
+
chunk_size=chunk_size,
|
|
134
|
+
overwrite_policy=overwrite_policy,
|
|
135
|
+
zstd_level=zstd_level,
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
logger.info("[green]✓ Compression completed successfully![/green]")
|
|
139
|
+
console.print(result.stats)
|
|
140
|
+
except Exception:
|
|
141
|
+
logger.exception("Error during compression")
|
|
142
|
+
if in_place:
|
|
143
|
+
output.unlink(missing_ok=True)
|
|
144
|
+
return 1
|
|
145
|
+
|
|
146
|
+
if in_place:
|
|
147
|
+
return finalize_replace_source(source=Path(file), tmp_output=output)
|
|
148
|
+
|
|
149
|
+
if delete_source:
|
|
150
|
+
return finalize_delete_source(sources=[file], outputs=[output])
|
|
151
|
+
|
|
152
|
+
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")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""``pymcap-cli index migrate`` — apply pending schema migrations."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from cyclopts import Parameter
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from pymcap_cli.cmd.index._helpers import _resolve_db, console
|
|
11
|
+
from pymcap_cli.index.db import CURRENT_SCHEMA_VERSION, connect
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _read_user_version(db_path: Path) -> int:
|
|
15
|
+
uri = f"{db_path.resolve().as_uri()}?mode=ro"
|
|
16
|
+
conn = sqlite3.connect(uri, uri=True, timeout=30.0)
|
|
17
|
+
try:
|
|
18
|
+
return int(conn.execute("PRAGMA user_version").fetchone()[0])
|
|
19
|
+
finally:
|
|
20
|
+
conn.close()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def migrate_cmd(
|
|
24
|
+
*,
|
|
25
|
+
db: Annotated[
|
|
26
|
+
Path | None,
|
|
27
|
+
Parameter(name=["--db"], help="Override the sidecar DB path."),
|
|
28
|
+
] = None,
|
|
29
|
+
) -> int:
|
|
30
|
+
"""Apply any pending schema migrations to the index DB."""
|
|
31
|
+
db_path = _resolve_db(db)
|
|
32
|
+
if not db_path.exists():
|
|
33
|
+
console.print(f"[red]Error:[/] no index DB at {db_path}")
|
|
34
|
+
return 1
|
|
35
|
+
|
|
36
|
+
before = _read_user_version(db_path)
|
|
37
|
+
if before == CURRENT_SCHEMA_VERSION:
|
|
38
|
+
console.print(
|
|
39
|
+
f"Index DB at [cyan]{db_path}[/] is already at schema "
|
|
40
|
+
f"[green]v{CURRENT_SCHEMA_VERSION}[/]; nothing to do."
|
|
41
|
+
)
|
|
42
|
+
return 0
|
|
43
|
+
if before > CURRENT_SCHEMA_VERSION:
|
|
44
|
+
console.print(
|
|
45
|
+
f"[yellow]Index DB at {db_path} is at v{before}, newer than this CLI's "
|
|
46
|
+
f"v{CURRENT_SCHEMA_VERSION}; refusing to downgrade.[/]"
|
|
47
|
+
)
|
|
48
|
+
return 1
|
|
49
|
+
|
|
50
|
+
conn = connect(db_path, read_only=False)
|
|
51
|
+
try:
|
|
52
|
+
rows = conn.execute(
|
|
53
|
+
"SELECT version, applied_at, description "
|
|
54
|
+
"FROM schema_migrations "
|
|
55
|
+
"WHERE version > ? "
|
|
56
|
+
"ORDER BY version",
|
|
57
|
+
(before,),
|
|
58
|
+
).fetchall()
|
|
59
|
+
finally:
|
|
60
|
+
conn.close()
|
|
61
|
+
|
|
62
|
+
console.print(
|
|
63
|
+
f"Migrated [cyan]{db_path}[/] from "
|
|
64
|
+
f"[yellow]v{before}[/] to [green]v{CURRENT_SCHEMA_VERSION}[/]."
|
|
65
|
+
)
|
|
66
|
+
if rows:
|
|
67
|
+
table = Table(title=f"Applied migrations ({len(rows)})")
|
|
68
|
+
table.add_column("Version", justify="right", style="cyan")
|
|
69
|
+
table.add_column("Description")
|
|
70
|
+
for version, _applied_at, description in rows:
|
|
71
|
+
table.add_row(f"v{version:04d}", str(description or "-"))
|
|
72
|
+
console.print(table)
|
|
73
|
+
return 0
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""``pymcap-cli index serve`` — browse the sidecar catalog with Datasette.
|
|
2
|
+
|
|
3
|
+
Launches Datasette against the index DB from the *same* interpreter
|
|
4
|
+
(``sys.executable -m datasette``), so the bundled ``pymcap_render`` plugin can
|
|
5
|
+
``import pymcap_cli``. Datasette and its plugins ship in the ``serve`` extra
|
|
6
|
+
(``pip install 'pymcap-cli[serve]'``).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import importlib.resources
|
|
10
|
+
import importlib.util
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
from contextlib import ExitStack
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Annotated
|
|
18
|
+
|
|
19
|
+
from cyclopts import Group, Parameter
|
|
20
|
+
|
|
21
|
+
from pymcap_cli.cmd.index._helpers import _print_db_needs_migration, _resolve_db
|
|
22
|
+
from pymcap_cli.index.db import IndexDbNeedsMigrationError, connect
|
|
23
|
+
from pymcap_cli.log_setup import ERR
|
|
24
|
+
|
|
25
|
+
INDEX_SERVE_OPTIONS_GROUP = Group("Index Serve Options")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _build_datasette_argv(
|
|
29
|
+
db_link: Path,
|
|
30
|
+
metadata: Path,
|
|
31
|
+
plugins_dir: Path,
|
|
32
|
+
template_dir: Path,
|
|
33
|
+
host: str,
|
|
34
|
+
port: int,
|
|
35
|
+
*,
|
|
36
|
+
open_browser: bool,
|
|
37
|
+
) -> list[str]:
|
|
38
|
+
"""Assemble the ``python -m datasette serve …`` command line."""
|
|
39
|
+
argv = [
|
|
40
|
+
sys.executable,
|
|
41
|
+
"-m",
|
|
42
|
+
"datasette",
|
|
43
|
+
"serve",
|
|
44
|
+
str(db_link),
|
|
45
|
+
"--metadata",
|
|
46
|
+
str(metadata),
|
|
47
|
+
"--plugins-dir",
|
|
48
|
+
str(plugins_dir),
|
|
49
|
+
"--template-dir",
|
|
50
|
+
str(template_dir),
|
|
51
|
+
"--setting",
|
|
52
|
+
"sql_time_limit_ms",
|
|
53
|
+
"10000",
|
|
54
|
+
"--setting",
|
|
55
|
+
"allow_download",
|
|
56
|
+
"off",
|
|
57
|
+
"--setting",
|
|
58
|
+
"default_cache_ttl",
|
|
59
|
+
"0",
|
|
60
|
+
"--host",
|
|
61
|
+
host,
|
|
62
|
+
"--port",
|
|
63
|
+
str(port),
|
|
64
|
+
]
|
|
65
|
+
if open_browser:
|
|
66
|
+
argv.append("-o")
|
|
67
|
+
return argv
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _datasette_is_installed() -> bool:
|
|
71
|
+
return importlib.util.find_spec("datasette") is not None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _create_stable_db_link(db_path: Path, db_link: Path) -> None:
|
|
75
|
+
"""Create a stable Datasette DB path, falling back when symlinks are unavailable."""
|
|
76
|
+
resolved = db_path.resolve()
|
|
77
|
+
try:
|
|
78
|
+
db_link.symlink_to(resolved)
|
|
79
|
+
except OSError:
|
|
80
|
+
pass
|
|
81
|
+
else:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
db_link.hardlink_to(resolved)
|
|
86
|
+
except OSError:
|
|
87
|
+
shutil.copy2(resolved, db_link)
|
|
88
|
+
else:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _validate_db_readable(db_path: Path) -> bool:
|
|
93
|
+
try:
|
|
94
|
+
conn = connect(db_path, read_only=True)
|
|
95
|
+
except IndexDbNeedsMigrationError as exc:
|
|
96
|
+
_print_db_needs_migration(exc)
|
|
97
|
+
return False
|
|
98
|
+
except RuntimeError as exc:
|
|
99
|
+
ERR.print(f"[red]Error:[/red] {exc}")
|
|
100
|
+
return False
|
|
101
|
+
else:
|
|
102
|
+
conn.close()
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def index_serve(
|
|
107
|
+
*,
|
|
108
|
+
db: Annotated[
|
|
109
|
+
Path | None,
|
|
110
|
+
Parameter(
|
|
111
|
+
name=["--db"],
|
|
112
|
+
group=INDEX_SERVE_OPTIONS_GROUP,
|
|
113
|
+
help="Override the sidecar DB path.",
|
|
114
|
+
),
|
|
115
|
+
] = None,
|
|
116
|
+
host: Annotated[
|
|
117
|
+
str,
|
|
118
|
+
Parameter(
|
|
119
|
+
name=["--host"],
|
|
120
|
+
group=INDEX_SERVE_OPTIONS_GROUP,
|
|
121
|
+
help="Interface to bind the server to.",
|
|
122
|
+
),
|
|
123
|
+
] = "127.0.0.1",
|
|
124
|
+
port: Annotated[
|
|
125
|
+
int,
|
|
126
|
+
Parameter(
|
|
127
|
+
name=["--port", "-p"],
|
|
128
|
+
group=INDEX_SERVE_OPTIONS_GROUP,
|
|
129
|
+
help="TCP port to listen on.",
|
|
130
|
+
),
|
|
131
|
+
] = 8001,
|
|
132
|
+
no_browser: Annotated[
|
|
133
|
+
bool,
|
|
134
|
+
Parameter(
|
|
135
|
+
name=["--no-browser"],
|
|
136
|
+
group=INDEX_SERVE_OPTIONS_GROUP,
|
|
137
|
+
help="Don't auto-open a browser tab on start.",
|
|
138
|
+
negative="",
|
|
139
|
+
),
|
|
140
|
+
] = False,
|
|
141
|
+
) -> int:
|
|
142
|
+
"""Browse the index catalog in a local Datasette web UI.
|
|
143
|
+
|
|
144
|
+
Serves the sidecar DB read-only with the bundled metadata, dashboards, and
|
|
145
|
+
``pymcap_render`` plugin. Needs the ``serve`` extra
|
|
146
|
+
(``pip install 'pymcap-cli[serve]'``).
|
|
147
|
+
"""
|
|
148
|
+
db_path = _resolve_db(db)
|
|
149
|
+
if not db_path.exists():
|
|
150
|
+
ERR.print(f"[red]Error:[/red] no index DB at {db_path}")
|
|
151
|
+
return 1
|
|
152
|
+
|
|
153
|
+
if not _datasette_is_installed():
|
|
154
|
+
ERR.print(
|
|
155
|
+
"[red]Error:[/red] Datasette is not installed. Install the serve extra: "
|
|
156
|
+
"`uv run --package pymcap-cli --extra serve pymcap-cli index serve` "
|
|
157
|
+
"or `pip install 'pymcap-cli[serve]'`."
|
|
158
|
+
)
|
|
159
|
+
return 1
|
|
160
|
+
|
|
161
|
+
if not _validate_db_readable(db_path):
|
|
162
|
+
return 1
|
|
163
|
+
|
|
164
|
+
with ExitStack() as stack:
|
|
165
|
+
# Datasette names a DB by its filename stem; serve through a stable
|
|
166
|
+
# `index.sqlite` path so the metadata/template `/index/...` links
|
|
167
|
+
# resolve regardless of the real --db filename.
|
|
168
|
+
tmp = Path(stack.enter_context(tempfile.TemporaryDirectory()))
|
|
169
|
+
db_link = tmp / "index.sqlite"
|
|
170
|
+
_create_stable_db_link(db_path, db_link)
|
|
171
|
+
|
|
172
|
+
assets = stack.enter_context(
|
|
173
|
+
importlib.resources.as_file(
|
|
174
|
+
importlib.resources.files("pymcap_cli.index").joinpath("datasette")
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
argv = _build_datasette_argv(
|
|
178
|
+
db_link,
|
|
179
|
+
assets / "metadata.yaml",
|
|
180
|
+
assets / "plugins",
|
|
181
|
+
assets / "templates",
|
|
182
|
+
host,
|
|
183
|
+
port,
|
|
184
|
+
open_browser=not no_browser,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
url = f"http://{host}:{port}/"
|
|
188
|
+
ERR.print(f"Serving index [cyan]{db_path}[/] on [link={url}]{url}[/link]")
|
|
189
|
+
ERR.print("Press Ctrl-C to stop.")
|
|
190
|
+
try:
|
|
191
|
+
# argv is sys.executable + fixed args + resolved paths, run without a shell.
|
|
192
|
+
return subprocess.run(argv, check=False).returncode # noqa: S603
|
|
193
|
+
except KeyboardInterrupt:
|
|
194
|
+
ERR.print("Stopping.")
|
|
195
|
+
return 0
|
|
@@ -23,9 +23,9 @@ from mcap_codec_support.video import (
|
|
|
23
23
|
FOXGLOVE_COMPRESSED_VIDEO,
|
|
24
24
|
IMAGE_SCHEMAS,
|
|
25
25
|
RAW_SCHEMAS,
|
|
26
|
+
AnyVideoBackend,
|
|
26
27
|
EncoderConfig,
|
|
27
28
|
EncoderMode,
|
|
28
|
-
VideoCompressionBackend,
|
|
29
29
|
VideoEncoderError,
|
|
30
30
|
calculate_downscale_dimensions,
|
|
31
31
|
create_video_compression_backend,
|
|
@@ -540,7 +540,7 @@ def _handle_pointcloud(
|
|
|
540
540
|
|
|
541
541
|
def _handle_raw_to_jpeg(
|
|
542
542
|
msg: DecodedMessage,
|
|
543
|
-
backend:
|
|
543
|
+
backend: AnyVideoBackend,
|
|
544
544
|
decode_future: Future[Any] | None,
|
|
545
545
|
encoders: dict[str, Any],
|
|
546
546
|
jpeg_quality: int,
|
|
@@ -610,7 +610,7 @@ def _handle_raw_to_jpeg(
|
|
|
610
610
|
|
|
611
611
|
def _run_compress_loop(
|
|
612
612
|
messages: Iterator[tuple[DecodedMessage, Future[Any] | None]],
|
|
613
|
-
backend:
|
|
613
|
+
backend: AnyVideoBackend,
|
|
614
614
|
do_video: bool,
|
|
615
615
|
do_jpeg: bool,
|
|
616
616
|
jpeg_quality: int,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Annotated
|
|
5
|
+
from typing import TYPE_CHECKING, Annotated
|
|
6
6
|
|
|
7
7
|
from cyclopts import Group, Parameter, validators
|
|
8
8
|
from rich.console import Console
|
|
@@ -27,6 +27,9 @@ from pymcap_cli.types.types_manual import (
|
|
|
27
27
|
)
|
|
28
28
|
from pymcap_cli.utils import bytes_to_human, parse_time_arg
|
|
29
29
|
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from pymcap_cli.core.processors.base import OutputRouter
|
|
32
|
+
|
|
30
33
|
logger = logging.getLogger(__name__)
|
|
31
34
|
console = Console()
|
|
32
35
|
|
|
@@ -266,7 +269,7 @@ def split(
|
|
|
266
269
|
return 1
|
|
267
270
|
|
|
268
271
|
# Build split processors
|
|
269
|
-
processors = []
|
|
272
|
+
processors: list[OutputRouter] = []
|
|
270
273
|
if duration:
|
|
271
274
|
try:
|
|
272
275
|
duration_ns = parse_duration_ns(duration)
|