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