pymcap-cli 0.7.0__tar.gz → 0.9.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 (107) hide show
  1. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/PKG-INFO +18 -7
  2. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/README.md +5 -2
  3. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/pyproject.toml +23 -12
  4. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cli.py +110 -19
  5. pymcap_cli-0.9.0/src/pymcap_cli/cmd/_run_processor.py +146 -0
  6. pymcap_cli-0.7.0/src/pymcap_cli/cmd/_run_processor.py → pymcap_cli-0.9.0/src/pymcap_cli/cmd/_run_processor_multi.py +11 -10
  7. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/bag2mcap_cmd.py +7 -7
  8. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/cat_cmd.py +41 -35
  9. pymcap_cli-0.9.0/src/pymcap_cli/cmd/compress_cmd.py +98 -0
  10. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/convert_cmd.py +11 -13
  11. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/diag_cmd.py +11 -9
  12. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/diff_cmd.py +205 -68
  13. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/du_cmd.py +13 -6
  14. pymcap_cli-0.9.0/src/pymcap_cli/cmd/duplicates_cmd.py +765 -0
  15. pymcap_cli-0.9.0/src/pymcap_cli/cmd/export_csv_cmd.py +40 -0
  16. pymcap_cli-0.9.0/src/pymcap_cli/cmd/export_geo_cmd.py +114 -0
  17. pymcap_cli-0.9.0/src/pymcap_cli/cmd/export_images_cmd.py +43 -0
  18. pymcap_cli-0.9.0/src/pymcap_cli/cmd/export_json_cmd.py +40 -0
  19. pymcap_cli-0.9.0/src/pymcap_cli/cmd/export_parquet_cmd.py +100 -0
  20. pymcap_cli-0.9.0/src/pymcap_cli/cmd/export_pcd_cmd.py +38 -0
  21. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/filter_cmd.py +32 -8
  22. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/info_cmd.py +10 -8
  23. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/merge_cmd.py +32 -8
  24. pymcap_cli-0.9.0/src/pymcap_cli/cmd/plot_cmd.py +119 -0
  25. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/process_cmd.py +33 -9
  26. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/rechunk_cmd.py +42 -19
  27. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/records_cmd.py +6 -6
  28. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/recover_cmd.py +41 -12
  29. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/recover_inplace_cmd.py +12 -14
  30. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/roscompress_cmd.py +333 -362
  31. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/rosdecompress_cmd.py +43 -37
  32. pymcap_cli-0.9.0/src/pymcap_cli/cmd/split_cmd.py +267 -0
  33. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/tftree_cmd.py +5 -4
  34. pymcap_cli-0.9.0/src/pymcap_cli/cmd/video_cmd.py +117 -0
  35. pymcap_cli-0.9.0/src/pymcap_cli/core/mcap_compare.py +1105 -0
  36. pymcap_cli-0.9.0/src/pymcap_cli/core/mcap_processor.py +1372 -0
  37. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/mcap_transform.py +40 -21
  38. pymcap_cli-0.9.0/src/pymcap_cli/core/processors/__init__.py +1 -0
  39. pymcap_cli-0.9.0/src/pymcap_cli/core/processors/always_decode.py +16 -0
  40. pymcap_cli-0.9.0/src/pymcap_cli/core/processors/attachment_filter.py +19 -0
  41. pymcap_cli-0.9.0/src/pymcap_cli/core/processors/base.py +80 -0
  42. pymcap_cli-0.9.0/src/pymcap_cli/core/processors/duration_split.py +73 -0
  43. pymcap_cli-0.9.0/src/pymcap_cli/core/processors/expression_split.py +153 -0
  44. pymcap_cli-0.9.0/src/pymcap_cli/core/processors/metadata_filter.py +19 -0
  45. pymcap_cli-0.9.0/src/pymcap_cli/core/processors/time_filter.py +48 -0
  46. pymcap_cli-0.9.0/src/pymcap_cli/core/processors/timestamp_split.py +70 -0
  47. pymcap_cli-0.9.0/src/pymcap_cli/core/processors/topic_filter.py +36 -0
  48. pymcap_cli-0.9.0/src/pymcap_cli/core/processors/utils.py +22 -0
  49. pymcap_cli-0.9.0/src/pymcap_cli/encoding/arrow_schema.py +149 -0
  50. pymcap_cli-0.9.0/src/pymcap_cli/exporters/__init__.py +27 -0
  51. pymcap_cli-0.9.0/src/pymcap_cli/exporters/_common.py +174 -0
  52. pymcap_cli-0.9.0/src/pymcap_cli/exporters/base.py +134 -0
  53. pymcap_cli-0.9.0/src/pymcap_cli/exporters/csv_exporter.py +94 -0
  54. pymcap_cli-0.9.0/src/pymcap_cli/exporters/driver.py +149 -0
  55. pymcap_cli-0.9.0/src/pymcap_cli/exporters/geo_common.py +257 -0
  56. pymcap_cli-0.9.0/src/pymcap_cli/exporters/geojson_exporter.py +157 -0
  57. pymcap_cli-0.9.0/src/pymcap_cli/exporters/gpx_exporter.py +141 -0
  58. pymcap_cli-0.9.0/src/pymcap_cli/exporters/image_exporter.py +240 -0
  59. pymcap_cli-0.9.0/src/pymcap_cli/exporters/json_exporter.py +86 -0
  60. pymcap_cli-0.9.0/src/pymcap_cli/exporters/kml_exporter.py +157 -0
  61. pymcap_cli-0.9.0/src/pymcap_cli/exporters/parquet_exporter.py +335 -0
  62. pymcap_cli-0.9.0/src/pymcap_cli/exporters/pcd_exporter.py +161 -0
  63. pymcap_cli-0.9.0/src/pymcap_cli/exporters/plot_exporter.py +355 -0
  64. pymcap_cli-0.9.0/src/pymcap_cli/exporters/video_exporter.py +149 -0
  65. pymcap_cli-0.9.0/src/pymcap_cli/log_setup.py +59 -0
  66. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/rihs01.py +11 -9
  67. pymcap_cli-0.9.0/src/pymcap_cli/types/duration.py +24 -0
  68. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/types/info_data.py +1 -1
  69. pymcap_cli-0.9.0/src/pymcap_cli/types/to_plain.py +47 -0
  70. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/types/types_manual.py +16 -0
  71. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/utils.py +24 -5
  72. pymcap_cli-0.7.0/src/pymcap_cli/cmd/compress_cmd.py +0 -70
  73. pymcap_cli-0.7.0/src/pymcap_cli/cmd/plot_cmd.py +0 -421
  74. pymcap_cli-0.7.0/src/pymcap_cli/cmd/video_cmd.py +0 -557
  75. pymcap_cli-0.7.0/src/pymcap_cli/core/mcap_processor.py +0 -904
  76. pymcap_cli-0.7.0/src/pymcap_cli/core/processors.py +0 -154
  77. pymcap_cli-0.7.0/src/pymcap_cli/encoding/decompress.py +0 -213
  78. pymcap_cli-0.7.0/src/pymcap_cli/encoding/encoder_common.py +0 -275
  79. pymcap_cli-0.7.0/src/pymcap_cli/encoding/pointcloud.py +0 -212
  80. pymcap_cli-0.7.0/src/pymcap_cli/encoding/video_factory.py +0 -47
  81. pymcap_cli-0.7.0/src/pymcap_cli/encoding/video_ffmpeg.py +0 -793
  82. pymcap_cli-0.7.0/src/pymcap_cli/encoding/video_protocols.py +0 -40
  83. pymcap_cli-0.7.0/src/pymcap_cli/encoding/video_pyav.py +0 -396
  84. pymcap_cli-0.7.0/src/pymcap_cli/types/info_types.py +0 -514
  85. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/__init__.py +0 -0
  86. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/__init__.py +0 -0
  87. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/info_json_cmd.py +0 -0
  88. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/list_cmd.py +0 -0
  89. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/topic_chunks_cmd.py +0 -0
  90. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/__init__.py +0 -0
  91. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/input_handler.py +0 -0
  92. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/msg_resolver.py +0 -0
  93. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/debug_wrapper.py +0 -0
  94. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/display/__init__.py +0 -0
  95. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/display/display_utils.py +0 -0
  96. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/display/osc_utils.py +0 -0
  97. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/display/sparkline.py +0 -0
  98. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/encoding/__init__.py +0 -0
  99. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/http_utils.py +0 -0
  100. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/py.typed +0 -0
  101. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/rosbag_reader/__init__.py +0 -0
  102. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/rosbag_reader/_reader.py +0 -0
  103. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/rosbag_reader/_types.py +0 -0
  104. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/rosbag_reader/py.typed +0 -0
  105. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/types/__init__.py +0 -0
  106. {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/types/info_link.py +0 -0
  107. {pymcap_cli-0.7.0/src/pymcap_cli → pymcap_cli-0.9.0/src/pymcap_cli/types}/info_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pymcap-cli
3
- Version: 0.7.0
3
+ Version: 0.9.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
@@ -22,22 +22,30 @@ Classifier: Topic :: Utilities
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Dist: rich>=14.1.0
24
24
  Requires-Dist: small-mcap[compression]
25
+ Requires-Dist: mcap-codec-support[ros2]
25
26
  Requires-Dist: mcap-ros2-support-fast
26
27
  Requires-Dist: cyclopts>=4
27
28
  Requires-Dist: ros-parser
28
29
  Requires-Dist: platformdirs>=4.0.0
29
30
  Requires-Dist: pyyaml>=6.0
30
31
  Requires-Dist: typing-extensions>=4.15.0
31
- Requires-Dist: pymcap-cli[video,pointcloud,plot] ; extra == 'all'
32
+ Requires-Dist: pymcap-cli[video,pointcloud,plot,parquet,image,draco] ; extra == 'all'
33
+ Requires-Dist: mcap-codec-support[draco] ; extra == 'draco'
34
+ Requires-Dist: mcap-codec-support[image] ; extra == 'image'
35
+ Requires-Dist: pyarrow>=15.0.0 ; extra == 'parquet'
36
+ Requires-Dist: numpy>=1.24.0 ; extra == 'parquet'
37
+ Requires-Dist: mcap-codec-support[pointcloud] ; extra == 'parquet'
32
38
  Requires-Dist: plotly>=6.0.0 ; extra == 'plot'
33
- Requires-Dist: pureini ; extra == 'pointcloud'
34
- Requires-Dist: av>=12.0.0 ; extra == 'video'
35
- Requires-Dist: numpy>=1.24.0 ; extra == 'video'
39
+ Requires-Dist: mcap-codec-support[pointcloud] ; extra == 'pointcloud'
40
+ Requires-Dist: mcap-codec-support[video] ; extra == 'video'
36
41
  Requires-Python: >=3.10
37
42
  Project-URL: Homepage, https://github.com/mrkbac/robotic-tools
38
43
  Project-URL: Issues, https://github.com/mrkbac/robotic-tools/issues
39
44
  Project-URL: Repository, https://github.com/mrkbac/robotic-tools
40
45
  Provides-Extra: all
46
+ Provides-Extra: draco
47
+ Provides-Extra: image
48
+ Provides-Extra: parquet
41
49
  Provides-Extra: plot
42
50
  Provides-Extra: pointcloud
43
51
  Provides-Extra: video
@@ -352,7 +360,7 @@ pymcap-cli video data.mcap --topic /lidar/image --output lidar.mp4 --codec h265
352
360
 
353
361
  ### `roscompress` — ROS Image Compression
354
362
 
355
- Compress ROS MCAP files by converting CompressedImage/Image topics to CompressedVideo format. Requires the `video` extra.
363
+ Compress ROS MCAP files by converting CompressedImage/Image topics to CompressedVideo format and PointCloud2 topics to Cloudini or Draco compressed point clouds.
356
364
 
357
365
  ```bash
358
366
  # Basic compression
@@ -360,11 +368,14 @@ pymcap-cli roscompress data.mcap -o compressed.mcap
360
368
 
361
369
  # Specify quality and codec
362
370
  pymcap-cli roscompress data.mcap -o compressed.mcap --quality 28 --codec h265
371
+
372
+ # Draco point cloud compression using the Foxglove compressed point cloud schema
373
+ pymcap-cli roscompress data.mcap -o compressed.mcap --pc-format draco --pc-schema foxglove
363
374
  ```
364
375
 
365
376
  ### `rosdecompress` — ROS Decompression
366
377
 
367
- Decompress CompressedVideo and CompressedPointCloud2 topics back to standard ROS formats. Requires the `video` extra.
378
+ Decompress CompressedVideo, CompressedPointCloud2, and Foxglove CompressedPointCloud topics back to standard ROS formats.
368
379
 
369
380
  ```bash
370
381
  # Decompress to CompressedImage (JPEG)
@@ -307,7 +307,7 @@ pymcap-cli video data.mcap --topic /lidar/image --output lidar.mp4 --codec h265
307
307
 
308
308
  ### `roscompress` — ROS Image Compression
309
309
 
310
- Compress ROS MCAP files by converting CompressedImage/Image topics to CompressedVideo format. Requires the `video` extra.
310
+ Compress ROS MCAP files by converting CompressedImage/Image topics to CompressedVideo format and PointCloud2 topics to Cloudini or Draco compressed point clouds.
311
311
 
312
312
  ```bash
313
313
  # Basic compression
@@ -315,11 +315,14 @@ pymcap-cli roscompress data.mcap -o compressed.mcap
315
315
 
316
316
  # Specify quality and codec
317
317
  pymcap-cli roscompress data.mcap -o compressed.mcap --quality 28 --codec h265
318
+
319
+ # Draco point cloud compression using the Foxglove compressed point cloud schema
320
+ pymcap-cli roscompress data.mcap -o compressed.mcap --pc-format draco --pc-schema foxglove
318
321
  ```
319
322
 
320
323
  ### `rosdecompress` — ROS Decompression
321
324
 
322
- Decompress CompressedVideo and CompressedPointCloud2 topics back to standard ROS formats. Requires the `video` extra.
325
+ Decompress CompressedVideo, CompressedPointCloud2, and Foxglove CompressedPointCloud topics back to standard ROS formats.
323
326
 
324
327
  ```bash
325
328
  # Decompress to CompressedImage (JPEG)
@@ -1,14 +1,21 @@
1
1
  [project]
2
2
  name = "pymcap-cli"
3
- version = "0.7.0"
3
+ version = "0.9.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"
7
- license = {text = "GPL-3.0"}
8
- authors = [
9
- {name = "Marko Bausch"}
7
+ license = { text = "GPL-3.0" }
8
+ authors = [{ name = "Marko Bausch" }]
9
+ keywords = [
10
+ "mcap",
11
+ "cli",
12
+ "robotics",
13
+ "ros",
14
+ "ros2",
15
+ "recovery",
16
+ "filtering",
17
+ "compression",
10
18
  ]
11
- keywords = ["mcap", "cli", "robotics", "ros", "ros2", "recovery", "filtering", "compression"]
12
19
  classifiers = [
13
20
  "Development Status :: 4 - Beta",
14
21
  "Environment :: Console",
@@ -29,6 +36,7 @@ classifiers = [
29
36
  dependencies = [
30
37
  "rich>=14.1.0",
31
38
  "small-mcap[compression]",
39
+ "mcap-codec-support[ros2]",
32
40
  "mcap-ros2-support-fast",
33
41
  "cyclopts>=4",
34
42
  "ros-parser",
@@ -43,13 +51,14 @@ Repository = "https://github.com/mrkbac/robotic-tools"
43
51
  Issues = "https://github.com/mrkbac/robotic-tools/issues"
44
52
 
45
53
  [project.optional-dependencies]
46
- video = ["av>=12.0.0", "numpy>=1.24.0"]
47
- pointcloud = ["pureini"]
48
- plot = [
49
- "plotly>=6.0.0",
50
- ]
51
- all = [
52
- "pymcap-cli[video,pointcloud,plot]",
54
+ video = ["mcap-codec-support[video]"]
55
+ pointcloud = ["mcap-codec-support[pointcloud]"]
56
+ plot = ["plotly>=6.0.0"]
57
+ parquet = ["pyarrow>=15.0.0", "numpy>=1.24.0", "mcap-codec-support[pointcloud]"]
58
+ image = ["mcap-codec-support[image]"]
59
+ all = ["pymcap-cli[video,pointcloud,plot,parquet,image,draco]"]
60
+ draco = [
61
+ "mcap-codec-support[draco]",
53
62
  ]
54
63
 
55
64
  [build-system]
@@ -65,3 +74,5 @@ small-mcap = { workspace = true }
65
74
  mcap-ros2-support-fast = { workspace = true }
66
75
  ros-parser = { workspace = true }
67
76
  pureini = { workspace = true }
77
+ pointcloud2 = { workspace = true }
78
+ mcap-codec-support = { workspace = true }
@@ -1,8 +1,8 @@
1
1
  """Main CLI entry point for pymcap-cli using Cyclopts."""
2
2
 
3
- import sys
3
+ from typing import Annotated
4
4
 
5
- from cyclopts import App, Group
5
+ from cyclopts import App, Group, Parameter
6
6
 
7
7
  from pymcap_cli.cmd import (
8
8
  bag2mcap_cmd,
@@ -12,6 +12,10 @@ from pymcap_cli.cmd import (
12
12
  diag_cmd,
13
13
  diff_cmd,
14
14
  du_cmd,
15
+ duplicates_cmd,
16
+ export_csv_cmd,
17
+ export_geo_cmd,
18
+ export_json_cmd,
15
19
  filter_cmd,
16
20
  info_cmd,
17
21
  info_json_cmd,
@@ -22,9 +26,11 @@ from pymcap_cli.cmd import (
22
26
  records_cmd,
23
27
  recover_cmd,
24
28
  recover_inplace_cmd,
29
+ split_cmd,
25
30
  tftree_cmd,
26
31
  topic_chunks_cmd,
27
32
  )
33
+ from pymcap_cli.log_setup import ERR, setup_logging
28
34
 
29
35
  try:
30
36
  from pymcap_cli.cmd.video_cmd import video
@@ -37,12 +43,11 @@ except ImportError:
37
43
 
38
44
  uv add --group video pymcap-cli
39
45
  """
40
- print( # noqa: T201
41
- "Error:\n"
46
+ ERR.print(
47
+ "[red]Error:[/red]\n"
42
48
  "Video command is unavailable because the 'av' and/or 'numpy' are not installed.\n"
43
49
  "To enable video functionality, please install pymcap-cli with the 'video' extra:\n\n"
44
- " uv add --group video pymcap-cli\n",
45
- file=sys.stderr,
50
+ " uv add --group video pymcap-cli\n"
46
51
  )
47
52
  return 1
48
53
 
@@ -58,12 +63,11 @@ except ImportError:
58
63
 
59
64
  uv add --group plot pymcap-cli
60
65
  """
61
- print( # noqa: T201
62
- "Error:\n"
66
+ ERR.print(
67
+ "[red]Error:[/red]\n"
63
68
  "Plot command is unavailable because 'plotly' is not installed.\n"
64
69
  "To enable plot functionality, please install pymcap-cli with the 'plot' extra:\n\n"
65
- " uv add --group plot pymcap-cli\n",
66
- file=sys.stderr,
70
+ " uv add --group plot pymcap-cli\n"
67
71
  )
68
72
  return 1
69
73
 
@@ -79,12 +83,67 @@ except ImportError:
79
83
 
80
84
  uv add --group video pymcap-cli
81
85
  """
82
- print( # noqa: T201
83
- "Error:\n"
86
+ ERR.print(
87
+ "[red]Error:[/red]\n"
84
88
  "ROS compress command is unavailable because the 'av' package is not installed.\n"
85
89
  "To enable this functionality, please install pymcap-cli with the 'video' extra:\n\n"
86
- " uv add --group video pymcap-cli\n",
87
- file=sys.stderr,
90
+ " uv add --group video pymcap-cli\n"
91
+ )
92
+ return 1
93
+
94
+
95
+ try:
96
+ from pymcap_cli.cmd.export_parquet_cmd import export_parquet
97
+ except ImportError:
98
+
99
+ def export_parquet() -> int:
100
+ """Export command is unavailable because 'pyarrow' is not installed.
101
+
102
+ To enable Parquet export, install pymcap-cli with the 'parquet' extra:
103
+
104
+ uv add 'pymcap-cli[parquet]'
105
+ """
106
+ ERR.print(
107
+ "[red]Error:[/red]\n"
108
+ "Export to Parquet is unavailable because 'pyarrow' is not installed.\n"
109
+ "Install with:\n\n"
110
+ " uv add 'pymcap-cli[parquet]'\n"
111
+ )
112
+ return 1
113
+
114
+
115
+ try:
116
+ from pymcap_cli.cmd.export_pcd_cmd import export_pcd
117
+ except ImportError:
118
+
119
+ def export_pcd() -> int:
120
+ """PCD export is unavailable because numpy / pointcloud2 are not installed.
121
+
122
+ Install with:
123
+
124
+ uv add 'pymcap-cli[pointcloud]'
125
+ """
126
+ ERR.print(
127
+ "[red]Error:[/red]\nPCD export requires numpy + pointcloud2.\n"
128
+ "Install with:\n\n uv add 'pymcap-cli[pointcloud]'\n"
129
+ )
130
+ return 1
131
+
132
+
133
+ try:
134
+ from pymcap_cli.cmd.export_images_cmd import export_images
135
+ except ImportError:
136
+
137
+ def export_images() -> int:
138
+ """Image export is unavailable because required image deps are missing.
139
+
140
+ Install with:
141
+
142
+ uv add 'pymcap-cli[image]'
143
+ """
144
+ ERR.print(
145
+ "[red]Error:[/red]\nImage export requires the 'image' extra (imagecodecs).\n"
146
+ "Install with:\n\n uv add 'pymcap-cli[image]'\n"
88
147
  )
89
148
  return 1
90
149
 
@@ -100,12 +159,11 @@ except ImportError:
100
159
 
101
160
  uv add --group video pymcap-cli
102
161
  """
103
- print( # noqa: T201
104
- "Error:\n"
162
+ ERR.print(
163
+ "[red]Error:[/red]\n"
105
164
  "ROS decompress command is unavailable because the 'av' package is not installed.\n"
106
165
  "To enable this functionality, please install pymcap-cli with the 'video' extra:\n\n"
107
- " uv add --group video pymcap-cli\n",
108
- file=sys.stderr,
166
+ " uv add --group video pymcap-cli\n"
109
167
  )
110
168
  return 1
111
169
 
@@ -114,6 +172,7 @@ app = App(
114
172
  name="pymcap-cli",
115
173
  help="CLI tool for slicing and dicing MCAP files.",
116
174
  help_format="rich",
175
+ default_parameter=Parameter(negative_iterable=""),
117
176
  )
118
177
 
119
178
  inspect_group = Group("Inspect", sort_key=0)
@@ -124,6 +183,7 @@ app.command(name="cat", group=inspect_group)(cat_cmd.cat)
124
183
  app.command(name="diag", group=inspect_group)(diag_cmd.diag)
125
184
  app.command(name="du", group=inspect_group)(du_cmd.du)
126
185
  app.command(name="diff", group=inspect_group)(diff_cmd.diff_cmd)
186
+ app.command(name="duplicates", group=inspect_group)(duplicates_cmd.duplicates)
127
187
  app.command(name="info", group=inspect_group)(info_cmd.info)
128
188
  app.command(name="info-json", group=inspect_group)(info_json_cmd.info_json)
129
189
  list_cmd.list_app.group = (inspect_group,)
@@ -140,16 +200,47 @@ app.command(name="filter", group=transform_group)(filter_cmd.filter_cmd)
140
200
  app.command(name="merge", group=transform_group)(merge_cmd.merge)
141
201
  app.command(name="process", group=transform_group)(process_cmd.process)
142
202
  app.command(name="rechunk", group=transform_group)(rechunk_cmd.rechunk)
203
+ app.command(name="split", group=transform_group)(split_cmd.split)
143
204
  app.command(name="recover", group=transform_group)(recover_cmd.recover)
144
205
  app.command(name="recover-inplace", group=transform_group)(recover_inplace_cmd.recover_inplace)
145
206
  app.command(name="plot", group=inspect_group)(plot)
207
+ app.command(name="export-csv", group=transform_group)(export_csv_cmd.export_csv)
208
+ app.command(name="export-geo", group=transform_group)(export_geo_cmd.export_geo)
209
+ app.command(name="export-json", group=transform_group)(export_json_cmd.export_json)
210
+ app.command(name="export-pcd", group=transform_group)(export_pcd)
211
+ app.command(name="export-images", group=transform_group)(export_images)
212
+ app.command(name="export-parquet", group=transform_group)(export_parquet)
146
213
  app.command(name="roscompress", group=transform_group)(roscompress)
147
214
  app.command(name="rosdecompress", group=transform_group)(rosdecompress)
148
215
  app.command(name="video", group=transform_group)(video)
149
216
 
150
217
 
218
+ @app.meta.default
219
+ def launcher(
220
+ *tokens: Annotated[str, Parameter(allow_leading_hyphen=True)],
221
+ verbose: Annotated[
222
+ int,
223
+ Parameter(
224
+ name=["--verbose", "-v"],
225
+ count=True,
226
+ help="Increase log verbosity. -v: DEBUG.",
227
+ ),
228
+ ] = 0,
229
+ quiet: Annotated[
230
+ int,
231
+ Parameter(
232
+ name=["--quiet"],
233
+ count=True,
234
+ help="Decrease log verbosity. Once: WARNING; twice: ERROR.",
235
+ ),
236
+ ] = 0,
237
+ ) -> int | None:
238
+ setup_logging(verbose=verbose, quiet=quiet)
239
+ return app(tokens)
240
+
241
+
151
242
  def main() -> None:
152
- app()
243
+ app.meta()
153
244
 
154
245
 
155
246
  if __name__ == "__main__":
@@ -0,0 +1,146 @@
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.utils import confirm_output_overwrite, read_info
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ @dataclass(slots=True)
28
+ class ProcessorResult:
29
+ """Result of a processor run."""
30
+
31
+ stats: ProcessingStats
32
+ processor: McapProcessor
33
+
34
+
35
+ def resolve_overwrite_policy(*, force: bool, no_clobber: bool) -> OverwriteCollisionPolicy | None:
36
+ """Map CLI overwrite flags to the processor overwrite policy."""
37
+ if force and no_clobber:
38
+ return None
39
+ if force:
40
+ return OverwriteCollisionPolicy.OVERWRITE
41
+ if no_clobber:
42
+ return OverwriteCollisionPolicy.ERROR
43
+ return OverwriteCollisionPolicy.ASK
44
+
45
+
46
+ def _open_output_stream(output: Path, overwrite_policy: OverwriteCollisionPolicy) -> BinaryIO:
47
+ """Open a single-output destination with the configured overwrite policy."""
48
+ if overwrite_policy == OverwriteCollisionPolicy.ASK:
49
+ confirm_output_overwrite(output, force=False)
50
+ elif overwrite_policy == OverwriteCollisionPolicy.ERROR and output.exists():
51
+ raise FileExistsError(f"Output file '{output}' already exists.")
52
+
53
+ return output.open("wb")
54
+
55
+
56
+ def run_processor(
57
+ *,
58
+ files: list[str],
59
+ output: Path,
60
+ input_options: InputOptions,
61
+ output_options: OutputOptions,
62
+ ) -> ProcessorResult:
63
+ """Open files, build ProcessingOptions, run McapProcessor, return results.
64
+
65
+ Raises any exception from McapProcessor.process() to the caller.
66
+ """
67
+ with contextlib.ExitStack() as stack:
68
+ input_files: list[InputFile] = []
69
+
70
+ for f in files:
71
+ stream, size = stack.enter_context(open_input(f))
72
+ input_files.append(InputFile(stream=stream, size=size, options=input_options))
73
+
74
+ output_stream = stack.enter_context(
75
+ _open_output_stream(output, output_options.overwrite_policy)
76
+ )
77
+
78
+ processing_options = ProcessingOptions(
79
+ inputs=input_files,
80
+ input_options=InputOptions.from_args(),
81
+ output_options=output_options,
82
+ )
83
+
84
+ processor = McapProcessor(processing_options)
85
+ stats = processor.process(output_stream)
86
+
87
+ return ProcessorResult(stats=stats, processor=processor)
88
+
89
+
90
+ def validate_mcap_output(path: Path) -> bool:
91
+ """Return True iff the MCAP at ``path`` has a readable header and summary."""
92
+ try:
93
+ with path.open("rb") as f:
94
+ read_info(f)
95
+ except (McapError, InvalidMagicError, OSError, AssertionError) as e:
96
+ logger.debug(f"Output validation failed for {path}: {e}")
97
+ return False
98
+ return True
99
+
100
+
101
+ def delete_source_files(sources: list[str], outputs: list[Path]) -> None:
102
+ """Delete each local source file. Skip URLs and any source path that
103
+ resolves to one of ``outputs`` (with a warning).
104
+ """
105
+ output_resolved = {p.resolve() for p in outputs}
106
+ for src in sources:
107
+ scheme = urlparse(src).scheme
108
+ if scheme in ("http", "https"):
109
+ logger.warning(f"Skipping delete: '{src}' is a remote URL")
110
+ continue
111
+ path = Path(src)
112
+ try:
113
+ resolved = path.resolve()
114
+ except OSError as e:
115
+ logger.warning(f"Skipping delete '{src}': {e}")
116
+ continue
117
+ if resolved in output_resolved:
118
+ logger.warning(f"Skipping delete: source '{src}' is also an output")
119
+ continue
120
+ try:
121
+ path.unlink()
122
+ logger.info(f"Deleted source: {src}")
123
+ except FileNotFoundError:
124
+ logger.debug(f"Source already gone: {src}")
125
+ except OSError:
126
+ logger.exception(f"Failed to delete '{src}'")
127
+
128
+
129
+ def finalize_delete_source(
130
+ *,
131
+ sources: list[str],
132
+ outputs: list[Path],
133
+ ) -> int:
134
+ """Validate every output and, if all valid, delete the eligible sources.
135
+
136
+ Returns 0 on success (sources deleted or skipped with warning) and 1 if
137
+ any output failed validation (no sources are deleted in that case).
138
+ """
139
+ invalid = [p for p in outputs if not validate_mcap_output(p)]
140
+ if invalid:
141
+ for p in invalid:
142
+ logger.error(f"[red]Output failed validation: {p}[/red]")
143
+ logger.error("Source file(s) preserved — output not safe to replace source.")
144
+ return 1
145
+ delete_source_files(sources, outputs)
146
+ return 0
@@ -1,8 +1,7 @@
1
- """Shared processor pipeline for transform commands."""
1
+ """Shared processor pipeline for multi-output transform commands (splitting)."""
2
2
 
3
3
  import contextlib
4
4
  from dataclasses import dataclass
5
- from pathlib import Path
6
5
 
7
6
  from pymcap_cli.core.input_handler import open_input
8
7
  from pymcap_cli.core.mcap_processor import (
@@ -23,14 +22,15 @@ class ProcessorResult:
23
22
  processor: McapProcessor
24
23
 
25
24
 
26
- def run_processor(
25
+ def run_processor_multi(
27
26
  *,
28
27
  files: list[str],
29
- output: Path,
30
- input_options: InputOptions,
31
28
  output_options: OutputOptions,
32
29
  ) -> ProcessorResult:
33
- """Open files, build ProcessingOptions, run McapProcessor, return results.
30
+ """Open input files, build ProcessingOptions, run McapProcessor in multi-output mode.
31
+
32
+ Unlike run_processor(), this does not open an output stream. The OutputManager
33
+ creates and manages output files based on split routing.
34
34
 
35
35
  Raises any exception from McapProcessor.process() to the caller.
36
36
  """
@@ -39,9 +39,9 @@ def run_processor(
39
39
 
40
40
  for f in files:
41
41
  stream, size = stack.enter_context(open_input(f))
42
- input_files.append(InputFile(stream=stream, size=size, options=input_options))
43
-
44
- output_stream = stack.enter_context(output.open("wb"))
42
+ input_files.append(
43
+ InputFile(stream=stream, size=size, options=InputOptions.from_args())
44
+ )
45
45
 
46
46
  processing_options = ProcessingOptions(
47
47
  inputs=input_files,
@@ -50,6 +50,7 @@ def run_processor(
50
50
  )
51
51
 
52
52
  processor = McapProcessor(processing_options)
53
- stats = processor.process(output_stream)
53
+ # Multi-output mode: pass None, OutputManager creates files
54
+ stats = processor.process(output_stream=None)
54
55
 
55
56
  return ProcessorResult(stats=stats, processor=processor)
@@ -17,8 +17,8 @@ from rich.progress import (
17
17
  TextColumn,
18
18
  TimeRemainingColumn,
19
19
  )
20
- from small_mcap.writer import CompressionType, McapWriter
21
- from small_mcap.writer import CompressionType as WriterCompressionType
20
+ from small_mcap import CompressionType, McapWriter
21
+ from small_mcap import CompressionType as WriterCompressionType
22
22
 
23
23
  from pymcap_cli.display.osc_utils import OSCProgressColumn
24
24
  from pymcap_cli.types.types_manual import (
@@ -249,7 +249,7 @@ def bag2mcap(
249
249
  """
250
250
  input_path = Path(file)
251
251
  if not input_path.exists():
252
- console.print(f"[red]Error: Input file '{file}' does not exist[/red]")
252
+ logger.error(f"Input file '{file}' does not exist")
253
253
  return 1
254
254
 
255
255
  confirm_output_overwrite(output, force)
@@ -266,20 +266,20 @@ def bag2mcap(
266
266
  compression=writer_compression,
267
267
  )
268
268
 
269
- console.print(f"[blue]Converting '{file}' to '{output}'[/blue]")
269
+ logger.info(f"Converting '{file}' to '{output}'")
270
270
 
271
271
  with output.open("wb") as output_stream:
272
272
  try:
273
273
  stats = convert_bag_to_mcap(input_path, output_stream, options)
274
274
 
275
- console.print("[green]Conversion completed successfully[/green]")
275
+ logger.info("[green]Conversion completed successfully[/green]")
276
276
  console.print(
277
277
  f"Converted {stats.topic_count} topics, "
278
278
  f"{stats.message_count:,} messages, "
279
279
  f"{stats.schema_count} schemas"
280
280
  )
281
- except Exception as e: # noqa: BLE001
282
- console.print(f"[red]Error during conversion: {e}[/red]")
281
+ except Exception:
282
+ logger.exception("Error during conversion")
283
283
  return 1
284
284
 
285
285
  return 0