pymcap-cli 0.7.0__tar.gz → 0.8.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 (103) hide show
  1. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/PKG-INFO +18 -7
  2. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/README.md +5 -2
  3. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/pyproject.toml +23 -12
  4. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cli.py +70 -0
  5. pymcap_cli-0.8.0/src/pymcap_cli/cmd/_run_processor.py +81 -0
  6. pymcap_cli-0.7.0/src/pymcap_cli/cmd/_run_processor.py → pymcap_cli-0.8.0/src/pymcap_cli/cmd/_run_processor_multi.py +11 -10
  7. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/bag2mcap_cmd.py +2 -2
  8. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/cat_cmd.py +23 -6
  9. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/compress_cmd.py +18 -5
  10. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/convert_cmd.py +2 -2
  11. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/diag_cmd.py +1 -1
  12. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/du_cmd.py +13 -6
  13. pymcap_cli-0.8.0/src/pymcap_cli/cmd/export_csv_cmd.py +40 -0
  14. pymcap_cli-0.8.0/src/pymcap_cli/cmd/export_geo_cmd.py +113 -0
  15. pymcap_cli-0.8.0/src/pymcap_cli/cmd/export_images_cmd.py +43 -0
  16. pymcap_cli-0.8.0/src/pymcap_cli/cmd/export_json_cmd.py +40 -0
  17. pymcap_cli-0.8.0/src/pymcap_cli/cmd/export_parquet_cmd.py +99 -0
  18. pymcap_cli-0.8.0/src/pymcap_cli/cmd/export_pcd_cmd.py +38 -0
  19. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/filter_cmd.py +14 -4
  20. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/merge_cmd.py +14 -4
  21. pymcap_cli-0.8.0/src/pymcap_cli/cmd/plot_cmd.py +120 -0
  22. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/process_cmd.py +14 -4
  23. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/rechunk_cmd.py +21 -6
  24. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/records_cmd.py +1 -1
  25. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/recover_cmd.py +15 -5
  26. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/recover_inplace_cmd.py +3 -3
  27. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/roscompress_cmd.py +322 -343
  28. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/rosdecompress_cmd.py +41 -35
  29. pymcap_cli-0.8.0/src/pymcap_cli/cmd/split_cmd.py +255 -0
  30. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/tftree_cmd.py +1 -2
  31. pymcap_cli-0.8.0/src/pymcap_cli/cmd/video_cmd.py +116 -0
  32. pymcap_cli-0.8.0/src/pymcap_cli/core/mcap_processor.py +1372 -0
  33. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/core/mcap_transform.py +1 -2
  34. pymcap_cli-0.8.0/src/pymcap_cli/core/processors/__init__.py +1 -0
  35. pymcap_cli-0.8.0/src/pymcap_cli/core/processors/always_decode.py +16 -0
  36. pymcap_cli-0.8.0/src/pymcap_cli/core/processors/attachment_filter.py +19 -0
  37. pymcap_cli-0.8.0/src/pymcap_cli/core/processors/base.py +80 -0
  38. pymcap_cli-0.8.0/src/pymcap_cli/core/processors/duration_split.py +73 -0
  39. pymcap_cli-0.8.0/src/pymcap_cli/core/processors/expression_split.py +153 -0
  40. pymcap_cli-0.8.0/src/pymcap_cli/core/processors/metadata_filter.py +19 -0
  41. pymcap_cli-0.8.0/src/pymcap_cli/core/processors/time_filter.py +48 -0
  42. pymcap_cli-0.8.0/src/pymcap_cli/core/processors/timestamp_split.py +70 -0
  43. pymcap_cli-0.8.0/src/pymcap_cli/core/processors/topic_filter.py +36 -0
  44. pymcap_cli-0.8.0/src/pymcap_cli/core/processors/utils.py +22 -0
  45. pymcap_cli-0.8.0/src/pymcap_cli/encoding/arrow_schema.py +149 -0
  46. pymcap_cli-0.8.0/src/pymcap_cli/exporters/__init__.py +27 -0
  47. pymcap_cli-0.8.0/src/pymcap_cli/exporters/_common.py +173 -0
  48. pymcap_cli-0.8.0/src/pymcap_cli/exporters/base.py +135 -0
  49. pymcap_cli-0.8.0/src/pymcap_cli/exporters/csv_exporter.py +94 -0
  50. pymcap_cli-0.8.0/src/pymcap_cli/exporters/driver.py +152 -0
  51. pymcap_cli-0.8.0/src/pymcap_cli/exporters/geo_common.py +257 -0
  52. pymcap_cli-0.8.0/src/pymcap_cli/exporters/geojson_exporter.py +157 -0
  53. pymcap_cli-0.8.0/src/pymcap_cli/exporters/gpx_exporter.py +140 -0
  54. pymcap_cli-0.8.0/src/pymcap_cli/exporters/image_exporter.py +240 -0
  55. pymcap_cli-0.8.0/src/pymcap_cli/exporters/json_exporter.py +86 -0
  56. pymcap_cli-0.8.0/src/pymcap_cli/exporters/kml_exporter.py +156 -0
  57. pymcap_cli-0.8.0/src/pymcap_cli/exporters/parquet_exporter.py +334 -0
  58. pymcap_cli-0.8.0/src/pymcap_cli/exporters/pcd_exporter.py +161 -0
  59. pymcap_cli-0.8.0/src/pymcap_cli/exporters/plot_exporter.py +365 -0
  60. pymcap_cli-0.8.0/src/pymcap_cli/exporters/video_exporter.py +155 -0
  61. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/rihs01.py +11 -9
  62. pymcap_cli-0.8.0/src/pymcap_cli/types/duration.py +24 -0
  63. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/types/info_data.py +1 -1
  64. pymcap_cli-0.8.0/src/pymcap_cli/types/to_plain.py +47 -0
  65. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/types/types_manual.py +8 -0
  66. pymcap_cli-0.7.0/src/pymcap_cli/cmd/plot_cmd.py +0 -421
  67. pymcap_cli-0.7.0/src/pymcap_cli/cmd/video_cmd.py +0 -557
  68. pymcap_cli-0.7.0/src/pymcap_cli/core/mcap_processor.py +0 -904
  69. pymcap_cli-0.7.0/src/pymcap_cli/core/processors.py +0 -154
  70. pymcap_cli-0.7.0/src/pymcap_cli/encoding/decompress.py +0 -213
  71. pymcap_cli-0.7.0/src/pymcap_cli/encoding/encoder_common.py +0 -275
  72. pymcap_cli-0.7.0/src/pymcap_cli/encoding/pointcloud.py +0 -212
  73. pymcap_cli-0.7.0/src/pymcap_cli/encoding/video_factory.py +0 -47
  74. pymcap_cli-0.7.0/src/pymcap_cli/encoding/video_ffmpeg.py +0 -793
  75. pymcap_cli-0.7.0/src/pymcap_cli/encoding/video_protocols.py +0 -40
  76. pymcap_cli-0.7.0/src/pymcap_cli/encoding/video_pyav.py +0 -396
  77. pymcap_cli-0.7.0/src/pymcap_cli/types/info_types.py +0 -514
  78. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/__init__.py +0 -0
  79. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/__init__.py +0 -0
  80. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/diff_cmd.py +0 -0
  81. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/info_cmd.py +0 -0
  82. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/info_json_cmd.py +0 -0
  83. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/list_cmd.py +0 -0
  84. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/topic_chunks_cmd.py +0 -0
  85. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/core/__init__.py +0 -0
  86. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/core/input_handler.py +0 -0
  87. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/core/msg_resolver.py +0 -0
  88. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/debug_wrapper.py +0 -0
  89. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/display/__init__.py +0 -0
  90. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/display/display_utils.py +0 -0
  91. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/display/osc_utils.py +0 -0
  92. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/display/sparkline.py +0 -0
  93. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/encoding/__init__.py +0 -0
  94. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/http_utils.py +0 -0
  95. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/py.typed +0 -0
  96. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/rosbag_reader/__init__.py +0 -0
  97. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/rosbag_reader/_reader.py +0 -0
  98. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/rosbag_reader/_types.py +0 -0
  99. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/rosbag_reader/py.typed +0 -0
  100. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/types/__init__.py +0 -0
  101. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/types/info_link.py +0 -0
  102. {pymcap_cli-0.7.0/src/pymcap_cli → pymcap_cli-0.8.0/src/pymcap_cli/types}/info_types.py +0 -0
  103. {pymcap_cli-0.7.0 → pymcap_cli-0.8.0}/src/pymcap_cli/utils.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.8.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.8.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 }
@@ -12,6 +12,9 @@ from pymcap_cli.cmd import (
12
12
  diag_cmd,
13
13
  diff_cmd,
14
14
  du_cmd,
15
+ export_csv_cmd,
16
+ export_geo_cmd,
17
+ export_json_cmd,
15
18
  filter_cmd,
16
19
  info_cmd,
17
20
  info_json_cmd,
@@ -22,6 +25,7 @@ from pymcap_cli.cmd import (
22
25
  records_cmd,
23
26
  recover_cmd,
24
27
  recover_inplace_cmd,
28
+ split_cmd,
25
29
  tftree_cmd,
26
30
  topic_chunks_cmd,
27
31
  )
@@ -89,6 +93,65 @@ except ImportError:
89
93
  return 1
90
94
 
91
95
 
96
+ try:
97
+ from pymcap_cli.cmd.export_parquet_cmd import export_parquet
98
+ except ImportError:
99
+
100
+ def export_parquet() -> int:
101
+ """Export command is unavailable because 'pyarrow' is not installed.
102
+
103
+ To enable Parquet export, install pymcap-cli with the 'parquet' extra:
104
+
105
+ uv add 'pymcap-cli[parquet]'
106
+ """
107
+ print( # noqa: T201
108
+ "Error:\n"
109
+ "Export to Parquet is unavailable because 'pyarrow' is not installed.\n"
110
+ "Install with:\n\n"
111
+ " uv add 'pymcap-cli[parquet]'\n",
112
+ file=sys.stderr,
113
+ )
114
+ return 1
115
+
116
+
117
+ try:
118
+ from pymcap_cli.cmd.export_pcd_cmd import export_pcd
119
+ except ImportError:
120
+
121
+ def export_pcd() -> int:
122
+ """PCD export is unavailable because numpy / pointcloud2 are not installed.
123
+
124
+ Install with:
125
+
126
+ uv add 'pymcap-cli[pointcloud]'
127
+ """
128
+ print( # noqa: T201
129
+ "Error:\nPCD export requires numpy + pointcloud2.\n"
130
+ "Install with:\n\n uv add 'pymcap-cli[pointcloud]'\n",
131
+ file=sys.stderr,
132
+ )
133
+ return 1
134
+
135
+
136
+ try:
137
+ from pymcap_cli.cmd.export_images_cmd import export_images
138
+ except ImportError:
139
+
140
+ def export_images() -> int:
141
+ """Image export is unavailable because required image deps are missing.
142
+
143
+ Install with:
144
+
145
+ uv add 'pymcap-cli[image]'
146
+ """
147
+ print( # noqa: T201
148
+ "Error:\nImage export requires the 'image' extra (imagecodecs).\n"
149
+ "Install with:\n\n uv add 'pymcap-cli[image]'\n",
150
+ file=sys.stderr,
151
+ )
152
+ return 1
153
+
154
+
92
155
  try:
93
156
  from pymcap_cli.cmd.rosdecompress_cmd import rosdecompress
94
157
  except ImportError:
@@ -140,9 +203,16 @@ app.command(name="filter", group=transform_group)(filter_cmd.filter_cmd)
140
203
  app.command(name="merge", group=transform_group)(merge_cmd.merge)
141
204
  app.command(name="process", group=transform_group)(process_cmd.process)
142
205
  app.command(name="rechunk", group=transform_group)(rechunk_cmd.rechunk)
206
+ app.command(name="split", group=transform_group)(split_cmd.split)
143
207
  app.command(name="recover", group=transform_group)(recover_cmd.recover)
144
208
  app.command(name="recover-inplace", group=transform_group)(recover_inplace_cmd.recover_inplace)
145
209
  app.command(name="plot", group=inspect_group)(plot)
210
+ app.command(name="export-csv", group=transform_group)(export_csv_cmd.export_csv)
211
+ app.command(name="export-geo", group=transform_group)(export_geo_cmd.export_geo)
212
+ app.command(name="export-json", group=transform_group)(export_json_cmd.export_json)
213
+ app.command(name="export-pcd", group=transform_group)(export_pcd)
214
+ app.command(name="export-images", group=transform_group)(export_images)
215
+ app.command(name="export-parquet", group=transform_group)(export_parquet)
146
216
  app.command(name="roscompress", group=transform_group)(roscompress)
147
217
  app.command(name="rosdecompress", group=transform_group)(rosdecompress)
148
218
  app.command(name="video", group=transform_group)(video)
@@ -0,0 +1,81 @@
1
+ """Shared processor pipeline for transform commands."""
2
+
3
+ import contextlib
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import BinaryIO
7
+
8
+ from pymcap_cli.core.input_handler import open_input
9
+ from pymcap_cli.core.mcap_processor import (
10
+ InputFile,
11
+ InputOptions,
12
+ McapProcessor,
13
+ OutputOptions,
14
+ OverwriteCollisionPolicy,
15
+ ProcessingOptions,
16
+ ProcessingStats,
17
+ )
18
+ from pymcap_cli.utils import confirm_output_overwrite
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class ProcessorResult:
23
+ """Result of a processor run."""
24
+
25
+ stats: ProcessingStats
26
+ processor: McapProcessor
27
+
28
+
29
+ def resolve_overwrite_policy(*, force: bool, no_clobber: bool) -> OverwriteCollisionPolicy | None:
30
+ """Map CLI overwrite flags to the processor overwrite policy."""
31
+ if force and no_clobber:
32
+ return None
33
+ if force:
34
+ return OverwriteCollisionPolicy.OVERWRITE
35
+ if no_clobber:
36
+ return OverwriteCollisionPolicy.ERROR
37
+ return OverwriteCollisionPolicy.ASK
38
+
39
+
40
+ def _open_output_stream(output: Path, overwrite_policy: OverwriteCollisionPolicy) -> BinaryIO:
41
+ """Open a single-output destination with the configured overwrite policy."""
42
+ if overwrite_policy == OverwriteCollisionPolicy.ASK:
43
+ confirm_output_overwrite(output, force=False)
44
+ elif overwrite_policy == OverwriteCollisionPolicy.ERROR and output.exists():
45
+ raise FileExistsError(f"Output file '{output}' already exists.")
46
+
47
+ return output.open("wb")
48
+
49
+
50
+ def run_processor(
51
+ *,
52
+ files: list[str],
53
+ output: Path,
54
+ input_options: InputOptions,
55
+ output_options: OutputOptions,
56
+ ) -> ProcessorResult:
57
+ """Open files, build ProcessingOptions, run McapProcessor, return results.
58
+
59
+ Raises any exception from McapProcessor.process() to the caller.
60
+ """
61
+ with contextlib.ExitStack() as stack:
62
+ input_files: list[InputFile] = []
63
+
64
+ for f in files:
65
+ stream, size = stack.enter_context(open_input(f))
66
+ input_files.append(InputFile(stream=stream, size=size, options=input_options))
67
+
68
+ output_stream = stack.enter_context(
69
+ _open_output_stream(output, output_options.overwrite_policy)
70
+ )
71
+
72
+ processing_options = ProcessingOptions(
73
+ inputs=input_files,
74
+ input_options=InputOptions.from_args(),
75
+ output_options=output_options,
76
+ )
77
+
78
+ processor = McapProcessor(processing_options)
79
+ stats = processor.process(output_stream)
80
+
81
+ return ProcessorResult(stats=stats, processor=processor)
@@ -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 (
@@ -10,7 +10,7 @@ from pathlib import Path
10
10
  from typing import IO, TYPE_CHECKING, Annotated, Any
11
11
 
12
12
  if TYPE_CHECKING:
13
- from small_mcap.reader import DecodedMessage
13
+ from small_mcap import DecodedMessage
14
14
 
15
15
  from cyclopts import Group, Parameter
16
16
  from mcap_ros2_support_fast.decoder import DecoderFactory
@@ -21,9 +21,7 @@ from rich.panel import Panel
21
21
  from rich.text import Text
22
22
  from ros_parser import parse_schema_to_definitions
23
23
  from ros_parser.message_path import MessagePathError, ValidationError, parse_message_path
24
- from small_mcap import JSONDecoderFactory
25
- from small_mcap.reader import read_message_decoded
26
- from small_mcap.records import Channel
24
+ from small_mcap import Channel, JSONDecoderFactory, read_message_decoded
27
25
 
28
26
  from pymcap_cli.core.input_handler import open_input
29
27
  from pymcap_cli.utils import MAX_INT64, ProgressTrackingIO, file_progress, parse_timestamp_args
@@ -35,11 +33,16 @@ FILTERING_GROUP = Group("Filtering")
35
33
  OUTPUT_GROUP = Group("Output")
36
34
 
37
35
  _TTY_BYTES_TRUNCATE = 32
36
+ # Threshold (bytes) below which `smart` mode inlines the full payload as an int
37
+ # list. Anything larger becomes a `<N bytes>` placeholder so large binary
38
+ # payloads (Image, PointCloud2) don't drown out the rest of the message.
39
+ _SMART_BYTES_INLINE_LIMIT = 64
38
40
 
39
41
 
40
42
  class BytesMode(str, Enum):
41
43
  """How to serialize bytes fields in JSON output."""
42
44
 
45
+ SMART = "smart"
43
46
  INTS = "ints"
44
47
  BASE64 = "base64"
45
48
  SKIP = "skip"
@@ -48,7 +51,7 @@ class BytesMode(str, Enum):
48
51
  def message_to_dict(
49
52
  obj: Any,
50
53
  *,
51
- bytes_mode: BytesMode = BytesMode.INTS,
54
+ bytes_mode: BytesMode = BytesMode.SMART,
52
55
  truncate_bytes: int = 0,
53
56
  ) -> Any:
54
57
  """Recursively convert a message object to a JSON-serializable dict.
@@ -74,6 +77,12 @@ def message_to_dict(
74
77
  return f"<{total} bytes>"
75
78
  if bytes_mode == BytesMode.BASE64:
76
79
  return base64.b64encode(bytes(obj)).decode("ascii")
80
+ if bytes_mode == BytesMode.SMART:
81
+ # Small payloads inline as ints for easy inspection; large ones
82
+ # collapse to a placeholder so output stays grep/jq-friendly.
83
+ if total <= _SMART_BYTES_INLINE_LIMIT:
84
+ return list(obj)
85
+ return f"<{total} bytes>"
77
86
  if truncate_bytes and total > truncate_bytes:
78
87
  return [*list(obj[:truncate_bytes]), f"... ({total} bytes total)"]
79
88
  return list(obj)
@@ -152,8 +161,16 @@ def cat(
152
161
  Parameter(
153
162
  name=["--bytes"],
154
163
  group=OUTPUT_GROUP,
164
+ help=(
165
+ "How to render `bytes` fields in JSON output. `smart` (default) "
166
+ f"inlines payloads ≤{_SMART_BYTES_INLINE_LIMIT} bytes as int lists "
167
+ "and collapses larger ones to `<N bytes>` so `cat` stays readable "
168
+ "on messages with Image/PointCloud2 payloads. Use `ints` for the "
169
+ "full int list, `base64` for a compact serialisable string, or "
170
+ "`skip` to always drop the payload."
171
+ ),
155
172
  ),
156
- ] = BytesMode.INTS,
173
+ ] = BytesMode.SMART,
157
174
  ) -> int:
158
175
  """Stream MCAP messages to stdout.
159
176
 
@@ -2,17 +2,20 @@
2
2
 
3
3
  from rich.console import Console
4
4
 
5
- from pymcap_cli.cmd._run_processor import run_processor
6
- from pymcap_cli.core.mcap_processor import InputOptions, OutputOptions
5
+ from pymcap_cli.cmd._run_processor import resolve_overwrite_policy, run_processor
6
+ from pymcap_cli.core.mcap_processor import (
7
+ InputOptions,
8
+ OutputOptions,
9
+ )
7
10
  from pymcap_cli.types.types_manual import (
8
11
  DEFAULT_CHUNK_SIZE,
9
12
  DEFAULT_COMPRESSION,
10
13
  ChunkSizeOption,
11
14
  CompressionOption,
12
15
  ForceOverwriteOption,
16
+ NoClobberOption,
13
17
  OutputPathOption,
14
18
  )
15
- from pymcap_cli.utils import confirm_output_overwrite
16
19
 
17
20
  console = Console()
18
21
 
@@ -24,6 +27,7 @@ def compress(
24
27
  chunk_size: ChunkSizeOption = DEFAULT_CHUNK_SIZE,
25
28
  compression: CompressionOption = DEFAULT_COMPRESSION,
26
29
  force: ForceOverwriteOption = False,
30
+ no_clobber: NoClobberOption = False,
27
31
  ) -> int:
28
32
  """Create a compressed copy of an MCAP file.
29
33
 
@@ -41,6 +45,8 @@ def compress(
41
45
  Compression algorithm for output file.
42
46
  force
43
47
  Force overwrite of output file without confirmation.
48
+ no_clobber
49
+ Fail instead of prompting if the output file already exists.
44
50
 
45
51
  Examples
46
52
  --------
@@ -48,17 +54,24 @@ def compress(
48
54
  pymcap-cli compress in.mcap -o out.mcap
49
55
  ```
50
56
  """
51
- confirm_output_overwrite(output, force)
57
+ overwrite_policy = resolve_overwrite_policy(force=force, no_clobber=no_clobber)
58
+ if overwrite_policy is None:
59
+ console.print("[red]Error: --force and --no-clobber cannot be used together.[/red]")
60
+ return 1
61
+
52
62
  console.print(f"[blue]Compressing '{file}' to '{output}'[/blue]")
53
63
 
54
64
  try:
55
65
  result = run_processor(
56
66
  files=[file],
57
67
  output=output,
58
- input_options=InputOptions.from_args(always_decode_chunk=True),
68
+ # Do not force always_decode_chunk — the processor now has a
69
+ # chunk-level RECOMPRESS path that avoids per-message parsing.
70
+ input_options=InputOptions.from_args(),
59
71
  output_options=OutputOptions(
60
72
  compression=compression.value,
61
73
  chunk_size=chunk_size,
74
+ overwrite_policy=overwrite_policy,
62
75
  ),
63
76
  )
64
77
  console.print("[green]✓ Compression completed successfully![/green]")
@@ -18,8 +18,8 @@ from rich.progress import (
18
18
  TextColumn,
19
19
  TimeRemainingColumn,
20
20
  )
21
- from small_mcap.writer import CompressionType, McapWriter
22
- from small_mcap.writer import CompressionType as WriterCompressionType
21
+ from small_mcap import CompressionType, McapWriter
22
+ from small_mcap import CompressionType as WriterCompressionType
23
23
 
24
24
  from pymcap_cli.core.msg_resolver import ROS2Distro, get_message_definition
25
25
  from pymcap_cli.display.osc_utils import OSCProgressColumn
@@ -14,7 +14,7 @@ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeEl
14
14
  from rich.table import Table
15
15
  from rich.text import Text
16
16
  from rich.tree import Tree
17
- from small_mcap.reader import include_topics, read_message_decoded
17
+ from small_mcap import include_topics, read_message_decoded
18
18
 
19
19
  from pymcap_cli.core.input_handler import open_input
20
20
  from pymcap_cli.display.sparkline import sparkline
@@ -4,6 +4,7 @@ from typing import Annotated
4
4
 
5
5
  from cyclopts import Parameter
6
6
  from rich.console import Console
7
+ from small_mcap import read_info_approximate
7
8
 
8
9
  from pymcap_cli.core.input_handler import open_input
9
10
  from pymcap_cli.display.display_utils import ChannelTableColumn, display_channels_table
@@ -25,20 +26,26 @@ def du(
25
26
  ) -> int:
26
27
  """Report space usage within an MCAP file.
27
28
 
28
- This command reports space usage within an mcap file. Space usage for messages is
29
- calculated using the uncompressed size.
30
-
31
- Note: This command will scan and uncompress the entire file.
29
+ Space usage for messages is calculated using the uncompressed size.
32
30
 
33
31
  Parameters
34
32
  ----------
35
33
  file
36
34
  Path to the MCAP file to analyze (local file or HTTP/HTTPS URL).
37
35
  exact_sizes
38
- Use exact sizes for message data (may be slower).
36
+ Decompress every chunk for exact per-message sizes (slow).
39
37
  """
40
38
  with open_input(file) as (f, file_size):
41
- info = read_or_rebuild_info(f, file_size, rebuild=True, exact_sizes=exact_sizes)
39
+ if exact_sizes:
40
+ info = read_or_rebuild_info(f, file_size, rebuild=True, exact_sizes=True)
41
+ else:
42
+ info = read_info_approximate(f)
43
+ if info is None:
44
+ console.print(
45
+ "[yellow]No summary section found; falling back to full scan.[/yellow]"
46
+ )
47
+ f.seek(0)
48
+ info = read_or_rebuild_info(f, file_size, rebuild=True, exact_sizes=False)
42
49
 
43
50
  data = info_to_dict(info, str(file), file_size)
44
51
 
@@ -0,0 +1,40 @@
1
+ """Export an MCAP file to a directory of CSV files (one per topic)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ from cyclopts import Parameter
8
+
9
+ from pymcap_cli.exporters import run_export
10
+ from pymcap_cli.exporters.csv_exporter import CsvExporter
11
+ from pymcap_cli.types.types_manual import ( # noqa: TC001 — runtime for cyclopts
12
+ ForceOverwriteOption,
13
+ OutputPathOption,
14
+ )
15
+
16
+
17
+ def export_csv(
18
+ file: str,
19
+ output: OutputPathOption,
20
+ *,
21
+ force: ForceOverwriteOption = False,
22
+ topic: Annotated[list[str] | None, Parameter(name=["--topic", "-t"])] = None,
23
+ include_blobs: Annotated[bool, Parameter(name=["--include-blobs"])] = False,
24
+ num_workers: Annotated[int, Parameter(name=["--num-workers"])] = 8,
25
+ ) -> int:
26
+ """Export an MCAP file to a directory of CSV files (one per topic).
27
+
28
+ Nested message fields are flattened with dot notation
29
+ (``pose.position.x``); arrays are kept as JSON strings to preserve row
30
+ counts. Schemas with raw media payloads (``sensor_msgs/Image``,
31
+ ``CompressedImage`` …) are skipped unless ``--include-blobs`` is passed.
32
+ """
33
+ return run_export(
34
+ file=file,
35
+ output=output,
36
+ exporter=CsvExporter(include_blobs=include_blobs),
37
+ topics=topic,
38
+ force=force,
39
+ num_workers=num_workers,
40
+ )