pymcap-cli 0.6.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.
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/PKG-INFO +18 -7
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/README.md +5 -2
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/pyproject.toml +23 -12
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cli.py +72 -0
- pymcap_cli-0.8.0/src/pymcap_cli/cmd/_run_processor.py +81 -0
- pymcap_cli-0.6.0/src/pymcap_cli/cmd/_run_processor.py → pymcap_cli-0.8.0/src/pymcap_cli/cmd/_run_processor_multi.py +11 -10
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/bag2mcap_cmd.py +2 -2
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/cat_cmd.py +23 -6
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/compress_cmd.py +18 -5
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/convert_cmd.py +2 -2
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/diag_cmd.py +1 -1
- pymcap_cli-0.8.0/src/pymcap_cli/cmd/diff_cmd.py +689 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/du_cmd.py +13 -6
- pymcap_cli-0.8.0/src/pymcap_cli/cmd/export_csv_cmd.py +40 -0
- pymcap_cli-0.8.0/src/pymcap_cli/cmd/export_geo_cmd.py +113 -0
- pymcap_cli-0.8.0/src/pymcap_cli/cmd/export_images_cmd.py +43 -0
- pymcap_cli-0.8.0/src/pymcap_cli/cmd/export_json_cmd.py +40 -0
- pymcap_cli-0.8.0/src/pymcap_cli/cmd/export_parquet_cmd.py +99 -0
- pymcap_cli-0.8.0/src/pymcap_cli/cmd/export_pcd_cmd.py +38 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/filter_cmd.py +14 -4
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/info_cmd.py +8 -2
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/merge_cmd.py +14 -4
- pymcap_cli-0.8.0/src/pymcap_cli/cmd/plot_cmd.py +120 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/process_cmd.py +14 -4
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/rechunk_cmd.py +21 -6
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/records_cmd.py +1 -1
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/recover_cmd.py +20 -12
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/recover_inplace_cmd.py +3 -3
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/roscompress_cmd.py +322 -343
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/rosdecompress_cmd.py +41 -35
- pymcap_cli-0.8.0/src/pymcap_cli/cmd/split_cmd.py +255 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/tftree_cmd.py +1 -2
- pymcap_cli-0.8.0/src/pymcap_cli/cmd/video_cmd.py +116 -0
- pymcap_cli-0.8.0/src/pymcap_cli/core/mcap_processor.py +1372 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/core/mcap_transform.py +1 -2
- pymcap_cli-0.8.0/src/pymcap_cli/core/processors/__init__.py +1 -0
- pymcap_cli-0.8.0/src/pymcap_cli/core/processors/always_decode.py +16 -0
- pymcap_cli-0.8.0/src/pymcap_cli/core/processors/attachment_filter.py +19 -0
- pymcap_cli-0.8.0/src/pymcap_cli/core/processors/base.py +80 -0
- pymcap_cli-0.8.0/src/pymcap_cli/core/processors/duration_split.py +73 -0
- pymcap_cli-0.8.0/src/pymcap_cli/core/processors/expression_split.py +153 -0
- pymcap_cli-0.8.0/src/pymcap_cli/core/processors/metadata_filter.py +19 -0
- pymcap_cli-0.8.0/src/pymcap_cli/core/processors/time_filter.py +48 -0
- pymcap_cli-0.8.0/src/pymcap_cli/core/processors/timestamp_split.py +70 -0
- pymcap_cli-0.8.0/src/pymcap_cli/core/processors/topic_filter.py +36 -0
- pymcap_cli-0.8.0/src/pymcap_cli/core/processors/utils.py +22 -0
- pymcap_cli-0.8.0/src/pymcap_cli/encoding/arrow_schema.py +149 -0
- pymcap_cli-0.8.0/src/pymcap_cli/exporters/__init__.py +27 -0
- pymcap_cli-0.8.0/src/pymcap_cli/exporters/_common.py +173 -0
- pymcap_cli-0.8.0/src/pymcap_cli/exporters/base.py +135 -0
- pymcap_cli-0.8.0/src/pymcap_cli/exporters/csv_exporter.py +94 -0
- pymcap_cli-0.8.0/src/pymcap_cli/exporters/driver.py +152 -0
- pymcap_cli-0.8.0/src/pymcap_cli/exporters/geo_common.py +257 -0
- pymcap_cli-0.8.0/src/pymcap_cli/exporters/geojson_exporter.py +157 -0
- pymcap_cli-0.8.0/src/pymcap_cli/exporters/gpx_exporter.py +140 -0
- pymcap_cli-0.8.0/src/pymcap_cli/exporters/image_exporter.py +240 -0
- pymcap_cli-0.8.0/src/pymcap_cli/exporters/json_exporter.py +86 -0
- pymcap_cli-0.8.0/src/pymcap_cli/exporters/kml_exporter.py +156 -0
- pymcap_cli-0.8.0/src/pymcap_cli/exporters/parquet_exporter.py +334 -0
- pymcap_cli-0.8.0/src/pymcap_cli/exporters/pcd_exporter.py +161 -0
- pymcap_cli-0.8.0/src/pymcap_cli/exporters/plot_exporter.py +365 -0
- pymcap_cli-0.8.0/src/pymcap_cli/exporters/video_exporter.py +155 -0
- pymcap_cli-0.8.0/src/pymcap_cli/rihs01.py +146 -0
- pymcap_cli-0.8.0/src/pymcap_cli/types/duration.py +24 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/types/info_data.py +1 -1
- pymcap_cli-0.8.0/src/pymcap_cli/types/to_plain.py +47 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/types/types_manual.py +8 -0
- pymcap_cli-0.6.0/src/pymcap_cli/cmd/plot_cmd.py +0 -421
- pymcap_cli-0.6.0/src/pymcap_cli/cmd/video_cmd.py +0 -557
- pymcap_cli-0.6.0/src/pymcap_cli/core/mcap_processor.py +0 -904
- pymcap_cli-0.6.0/src/pymcap_cli/core/processors.py +0 -152
- pymcap_cli-0.6.0/src/pymcap_cli/encoding/decompress.py +0 -213
- pymcap_cli-0.6.0/src/pymcap_cli/encoding/encoder_common.py +0 -275
- pymcap_cli-0.6.0/src/pymcap_cli/encoding/pointcloud.py +0 -212
- pymcap_cli-0.6.0/src/pymcap_cli/encoding/video_factory.py +0 -47
- pymcap_cli-0.6.0/src/pymcap_cli/encoding/video_ffmpeg.py +0 -793
- pymcap_cli-0.6.0/src/pymcap_cli/encoding/video_protocols.py +0 -40
- pymcap_cli-0.6.0/src/pymcap_cli/encoding/video_pyav.py +0 -372
- pymcap_cli-0.6.0/src/pymcap_cli/types/info_types.py +0 -514
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/__init__.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/__init__.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/info_json_cmd.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/list_cmd.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/cmd/topic_chunks_cmd.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/core/__init__.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/core/input_handler.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/core/msg_resolver.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/debug_wrapper.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/display/__init__.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/display/display_utils.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/display/osc_utils.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/display/sparkline.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/encoding/__init__.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/http_utils.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/py.typed +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/rosbag_reader/__init__.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/rosbag_reader/_reader.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/rosbag_reader/_types.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/rosbag_reader/py.typed +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/types/__init__.py +0 -0
- {pymcap_cli-0.6.0 → pymcap_cli-0.8.0}/src/pymcap_cli/types/info_link.py +0 -0
- {pymcap_cli-0.6.0/src/pymcap_cli → pymcap_cli-0.8.0/src/pymcap_cli/types}/info_types.py +0 -0
- {pymcap_cli-0.6.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.
|
|
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:
|
|
34
|
-
Requires-Dist:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 = ["
|
|
47
|
-
pointcloud = ["
|
|
48
|
-
plot = [
|
|
49
|
-
|
|
50
|
-
]
|
|
51
|
-
all = [
|
|
52
|
-
|
|
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 }
|
|
@@ -10,7 +10,11 @@ from pymcap_cli.cmd import (
|
|
|
10
10
|
compress_cmd,
|
|
11
11
|
convert_cmd,
|
|
12
12
|
diag_cmd,
|
|
13
|
+
diff_cmd,
|
|
13
14
|
du_cmd,
|
|
15
|
+
export_csv_cmd,
|
|
16
|
+
export_geo_cmd,
|
|
17
|
+
export_json_cmd,
|
|
14
18
|
filter_cmd,
|
|
15
19
|
info_cmd,
|
|
16
20
|
info_json_cmd,
|
|
@@ -21,6 +25,7 @@ from pymcap_cli.cmd import (
|
|
|
21
25
|
records_cmd,
|
|
22
26
|
recover_cmd,
|
|
23
27
|
recover_inplace_cmd,
|
|
28
|
+
split_cmd,
|
|
24
29
|
tftree_cmd,
|
|
25
30
|
topic_chunks_cmd,
|
|
26
31
|
)
|
|
@@ -88,6 +93,65 @@ except ImportError:
|
|
|
88
93
|
return 1
|
|
89
94
|
|
|
90
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
|
+
|
|
91
155
|
try:
|
|
92
156
|
from pymcap_cli.cmd.rosdecompress_cmd import rosdecompress
|
|
93
157
|
except ImportError:
|
|
@@ -122,6 +186,7 @@ transform_group = Group("Transform", sort_key=1)
|
|
|
122
186
|
app.command(name="cat", group=inspect_group)(cat_cmd.cat)
|
|
123
187
|
app.command(name="diag", group=inspect_group)(diag_cmd.diag)
|
|
124
188
|
app.command(name="du", group=inspect_group)(du_cmd.du)
|
|
189
|
+
app.command(name="diff", group=inspect_group)(diff_cmd.diff_cmd)
|
|
125
190
|
app.command(name="info", group=inspect_group)(info_cmd.info)
|
|
126
191
|
app.command(name="info-json", group=inspect_group)(info_json_cmd.info_json)
|
|
127
192
|
list_cmd.list_app.group = (inspect_group,)
|
|
@@ -138,9 +203,16 @@ app.command(name="filter", group=transform_group)(filter_cmd.filter_cmd)
|
|
|
138
203
|
app.command(name="merge", group=transform_group)(merge_cmd.merge)
|
|
139
204
|
app.command(name="process", group=transform_group)(process_cmd.process)
|
|
140
205
|
app.command(name="rechunk", group=transform_group)(rechunk_cmd.rechunk)
|
|
206
|
+
app.command(name="split", group=transform_group)(split_cmd.split)
|
|
141
207
|
app.command(name="recover", group=transform_group)(recover_cmd.recover)
|
|
142
208
|
app.command(name="recover-inplace", group=transform_group)(recover_inplace_cmd.recover_inplace)
|
|
143
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)
|
|
144
216
|
app.command(name="roscompress", group=transform_group)(roscompress)
|
|
145
217
|
app.command(name="rosdecompress", group=transform_group)(rosdecompress)
|
|
146
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
|
|
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
|
|
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(
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
|
21
|
-
from small_mcap
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
22
|
-
from small_mcap
|
|
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
|
|
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
|