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.
Files changed (174) hide show
  1. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/PKG-INFO +31 -2
  2. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/README.md +21 -0
  3. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/pyproject.toml +16 -3
  4. pymcap_cli-0.16.0/src/pymcap_cli/cmd/_run_processor.py +260 -0
  5. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/_run_processor_multi.py +4 -0
  6. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bag2mcap_cmd.py +1 -0
  7. pymcap_cli-0.16.0/src/pymcap_cli/cmd/compress_cmd.py +168 -0
  8. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/convert_cmd.py +1 -0
  9. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/__init__.py +4 -0
  10. pymcap_cli-0.16.0/src/pymcap_cli/cmd/index/migrate_cmd.py +73 -0
  11. pymcap_cli-0.16.0/src/pymcap_cli/cmd/index/serve_cmd.py +195 -0
  12. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/info_cmd.py +80 -20
  13. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/merge_cmd.py +11 -0
  14. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/plot_cmd.py +8 -1
  15. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/roscompress_cmd.py +24 -9
  16. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/split_cmd.py +13 -3
  17. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/input_handler.py +28 -1
  18. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/mcap_processor.py +136 -14
  19. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/mcap_transform.py +32 -22
  20. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/always_decode.py +3 -1
  21. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/attachment_filter.py +4 -1
  22. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/boundary_split.py +6 -1
  23. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/channel_merge.py +5 -1
  24. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/chunk_groupers.py +4 -1
  25. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/dedup.py +5 -1
  26. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/duration_split.py +5 -1
  27. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/expression_split.py +6 -1
  28. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/latching.py +7 -1
  29. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/metadata_filter.py +4 -1
  30. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/nth_message.py +5 -1
  31. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/size_split.py +5 -0
  32. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/time_filter.py +6 -1
  33. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/time_offset.py +5 -1
  34. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/timestamp_split.py +3 -0
  35. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/topic_alias.py +7 -1
  36. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/topic_filter.py +4 -1
  37. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/topic_rewrite.py +4 -1
  38. pymcap_cli-0.16.0/src/pymcap_cli/core/rosbag2_layout.py +197 -0
  39. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/display_utils.py +40 -9
  40. pymcap_cli-0.16.0/src/pymcap_cli/exporters/_summary_hints.py +96 -0
  41. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/driver.py +10 -3
  42. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/plot_exporter.py +191 -60
  43. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/http_utils.py +8 -3
  44. pymcap_cli-0.16.0/src/pymcap_cli/index/datasette/metadata.yaml +724 -0
  45. pymcap_cli-0.16.0/src/pymcap_cli/index/datasette/plugins/pymcap_render.py +166 -0
  46. pymcap_cli-0.16.0/src/pymcap_cli/index/datasette/templates/index.html +62 -0
  47. pymcap_cli-0.16.0/src/pymcap_cli/index/datasette/templates/query-index-timeline.html +26 -0
  48. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/types_manual.py +24 -0
  49. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/utils.py +26 -2
  50. pymcap_cli-0.13.0/src/pymcap_cli/cmd/_run_processor.py +0 -146
  51. pymcap_cli-0.13.0/src/pymcap_cli/cmd/compress_cmd.py +0 -97
  52. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/__init__.py +0 -0
  53. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cli.py +0 -0
  54. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/__init__.py +0 -0
  55. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/_rechunk_strategy.py +0 -0
  56. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bridge/__init__.py +0 -0
  57. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bridge/_shared.py +0 -0
  58. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bridge/cat.py +0 -0
  59. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bridge/inspect.py +0 -0
  60. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/bridge/record.py +0 -0
  61. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/cat_cmd.py +0 -0
  62. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/diag_cmd.py +0 -0
  63. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/diff_cmd.py +0 -0
  64. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/doctor_cmd.py +0 -0
  65. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/du_cmd.py +0 -0
  66. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/duplicates_cmd.py +0 -0
  67. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_csv_cmd.py +0 -0
  68. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_geo_cmd.py +0 -0
  69. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_images_cmd.py +0 -0
  70. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_json_cmd.py +0 -0
  71. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_parquet_cmd.py +0 -0
  72. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/export_pcd_cmd.py +0 -0
  73. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/filter_cmd.py +0 -0
  74. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/get_cmd.py +0 -0
  75. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/_helpers.py +0 -0
  76. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/duplicates_cmd.py +0 -0
  77. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/errors_cmd.py +0 -0
  78. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/info_cmd.py +0 -0
  79. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/query_cmd.py +0 -0
  80. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/scan_cmd.py +0 -0
  81. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/schemas_cmd.py +0 -0
  82. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/sessions_cmd.py +0 -0
  83. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/status_cmd.py +0 -0
  84. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/timeline_cmd.py +0 -0
  85. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/topics_cmd.py +0 -0
  86. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/index/tree_cmd.py +0 -0
  87. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/info_json_cmd.py +0 -0
  88. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/list_cmd.py +0 -0
  89. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/msg/__init__.py +0 -0
  90. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/msg/def_cmd.py +0 -0
  91. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/msg/list_cmd.py +0 -0
  92. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/msg/serve_cmd.py +0 -0
  93. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/process_cmd.py +0 -0
  94. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/rechunk_cmd.py +0 -0
  95. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/records_cmd.py +0 -0
  96. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/recover_cmd.py +0 -0
  97. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/recover_inplace_cmd.py +0 -0
  98. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/rosdecompress_cmd.py +0 -0
  99. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/tf_export_cmd.py +0 -0
  100. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/tf_get_cmd.py +0 -0
  101. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/tftree_cmd.py +0 -0
  102. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/topic_chunks_cmd.py +0 -0
  103. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/cmd/video_cmd.py +0 -0
  104. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/constants.py +0 -0
  105. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/__init__.py +0 -0
  106. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/input_options.py +0 -0
  107. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/input_processor_chain.py +0 -0
  108. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/mcap_compare.py +0 -0
  109. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/msg_resolver.py +0 -0
  110. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/ARCHITECTURE.md +0 -0
  111. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/__init__.py +0 -0
  112. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/base.py +0 -0
  113. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/processors/utils.py +0 -0
  114. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/qos.py +0 -0
  115. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/tf_findings.py +0 -0
  116. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/core/tf_tree.py +0 -0
  117. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/debug_wrapper.py +0 -0
  118. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/__init__.py +0 -0
  119. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/cat_helpers.py +0 -0
  120. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/message_render.py +0 -0
  121. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/osc_utils.py +0 -0
  122. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/schema_html.py +0 -0
  123. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/schema_render.py +0 -0
  124. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/sparkline.py +0 -0
  125. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/display/time_ranges.py +0 -0
  126. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/doctor.py +0 -0
  127. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/encoding/__init__.py +0 -0
  128. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/encoding/arrow_schema.py +0 -0
  129. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/__init__.py +0 -0
  130. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/_common.py +0 -0
  131. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/base.py +0 -0
  132. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/csv_exporter.py +0 -0
  133. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/geo_common.py +0 -0
  134. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/geojson_exporter.py +0 -0
  135. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/gpx_exporter.py +0 -0
  136. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/image_exporter.py +0 -0
  137. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/json_exporter.py +0 -0
  138. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/kml_exporter.py +0 -0
  139. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/parquet_exporter.py +0 -0
  140. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/pcd_exporter.py +0 -0
  141. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/sdf_exporter.py +0 -0
  142. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/urdf_exporter.py +0 -0
  143. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/video_exporter.py +0 -0
  144. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/exporters/video_file_writer.py +0 -0
  145. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/__init__.py +0 -0
  146. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/db.py +0 -0
  147. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/fingerprint.py +0 -0
  148. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0001.py +0 -0
  149. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0002_normalise_and_drop_chunks.py +0 -0
  150. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0003_intern_channels.py +0 -0
  151. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0004_intern_compress_channel_metadata.py +0 -0
  152. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0005_content_compression.py +0 -0
  153. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0006_channel_statistics.py +0 -0
  154. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0007_consolidate_schema.py +0 -0
  155. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0008_rewrite_current_file_view.py +0 -0
  156. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/0009_read_side_covering_indexes.py +0 -0
  157. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/migrations/__init__.py +0 -0
  158. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/scanner.py +0 -0
  159. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/index/summary_fingerprint.py +0 -0
  160. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/log_setup.py +0 -0
  161. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/py.typed +0 -0
  162. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/rihs01.py +0 -0
  163. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/rosbag_reader/__init__.py +0 -0
  164. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/rosbag_reader/_reader.py +0 -0
  165. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/rosbag_reader/_types.py +0 -0
  166. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/rosbag_reader/py.typed +0 -0
  167. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/__init__.py +0 -0
  168. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/duration.py +0 -0
  169. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/info_data.py +0 -0
  170. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/info_link.py +0 -0
  171. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/info_types.py +0 -0
  172. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/qos.py +0 -0
  173. {pymcap_cli-0.13.0 → pymcap_cli-0.16.0}/src/pymcap_cli/types/size.py +0 -0
  174. {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.13.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.13.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 = ["plotly>=6.0.0"]
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
 
@@ -37,6 +37,7 @@ class Bag2McapOptions:
37
37
  compression: WriterCompression = DEFAULT_COMPRESSION
38
38
  enable_crcs: bool = True
39
39
  use_chunking: bool = True
40
+ zstd_level: int | None = None
40
41
 
41
42
 
42
43
  @dataclass
@@ -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
@@ -43,6 +43,7 @@ class ConvertOptions:
43
43
  compression: WriterCompression = DEFAULT_COMPRESSION
44
44
  enable_crcs: bool = True
45
45
  use_chunking: bool = True
46
+ zstd_level: int | None = None
46
47
 
47
48
 
48
49
  @dataclass
@@ -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")