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.
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/PKG-INFO +18 -7
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/README.md +5 -2
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/pyproject.toml +23 -12
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cli.py +110 -19
- pymcap_cli-0.9.0/src/pymcap_cli/cmd/_run_processor.py +146 -0
- 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
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/bag2mcap_cmd.py +7 -7
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/cat_cmd.py +41 -35
- pymcap_cli-0.9.0/src/pymcap_cli/cmd/compress_cmd.py +98 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/convert_cmd.py +11 -13
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/diag_cmd.py +11 -9
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/diff_cmd.py +205 -68
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/du_cmd.py +13 -6
- pymcap_cli-0.9.0/src/pymcap_cli/cmd/duplicates_cmd.py +765 -0
- pymcap_cli-0.9.0/src/pymcap_cli/cmd/export_csv_cmd.py +40 -0
- pymcap_cli-0.9.0/src/pymcap_cli/cmd/export_geo_cmd.py +114 -0
- pymcap_cli-0.9.0/src/pymcap_cli/cmd/export_images_cmd.py +43 -0
- pymcap_cli-0.9.0/src/pymcap_cli/cmd/export_json_cmd.py +40 -0
- pymcap_cli-0.9.0/src/pymcap_cli/cmd/export_parquet_cmd.py +100 -0
- pymcap_cli-0.9.0/src/pymcap_cli/cmd/export_pcd_cmd.py +38 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/filter_cmd.py +32 -8
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/info_cmd.py +10 -8
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/merge_cmd.py +32 -8
- pymcap_cli-0.9.0/src/pymcap_cli/cmd/plot_cmd.py +119 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/process_cmd.py +33 -9
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/rechunk_cmd.py +42 -19
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/records_cmd.py +6 -6
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/recover_cmd.py +41 -12
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/recover_inplace_cmd.py +12 -14
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/roscompress_cmd.py +333 -362
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/rosdecompress_cmd.py +43 -37
- pymcap_cli-0.9.0/src/pymcap_cli/cmd/split_cmd.py +267 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/tftree_cmd.py +5 -4
- pymcap_cli-0.9.0/src/pymcap_cli/cmd/video_cmd.py +117 -0
- pymcap_cli-0.9.0/src/pymcap_cli/core/mcap_compare.py +1105 -0
- pymcap_cli-0.9.0/src/pymcap_cli/core/mcap_processor.py +1372 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/mcap_transform.py +40 -21
- pymcap_cli-0.9.0/src/pymcap_cli/core/processors/__init__.py +1 -0
- pymcap_cli-0.9.0/src/pymcap_cli/core/processors/always_decode.py +16 -0
- pymcap_cli-0.9.0/src/pymcap_cli/core/processors/attachment_filter.py +19 -0
- pymcap_cli-0.9.0/src/pymcap_cli/core/processors/base.py +80 -0
- pymcap_cli-0.9.0/src/pymcap_cli/core/processors/duration_split.py +73 -0
- pymcap_cli-0.9.0/src/pymcap_cli/core/processors/expression_split.py +153 -0
- pymcap_cli-0.9.0/src/pymcap_cli/core/processors/metadata_filter.py +19 -0
- pymcap_cli-0.9.0/src/pymcap_cli/core/processors/time_filter.py +48 -0
- pymcap_cli-0.9.0/src/pymcap_cli/core/processors/timestamp_split.py +70 -0
- pymcap_cli-0.9.0/src/pymcap_cli/core/processors/topic_filter.py +36 -0
- pymcap_cli-0.9.0/src/pymcap_cli/core/processors/utils.py +22 -0
- pymcap_cli-0.9.0/src/pymcap_cli/encoding/arrow_schema.py +149 -0
- pymcap_cli-0.9.0/src/pymcap_cli/exporters/__init__.py +27 -0
- pymcap_cli-0.9.0/src/pymcap_cli/exporters/_common.py +174 -0
- pymcap_cli-0.9.0/src/pymcap_cli/exporters/base.py +134 -0
- pymcap_cli-0.9.0/src/pymcap_cli/exporters/csv_exporter.py +94 -0
- pymcap_cli-0.9.0/src/pymcap_cli/exporters/driver.py +149 -0
- pymcap_cli-0.9.0/src/pymcap_cli/exporters/geo_common.py +257 -0
- pymcap_cli-0.9.0/src/pymcap_cli/exporters/geojson_exporter.py +157 -0
- pymcap_cli-0.9.0/src/pymcap_cli/exporters/gpx_exporter.py +141 -0
- pymcap_cli-0.9.0/src/pymcap_cli/exporters/image_exporter.py +240 -0
- pymcap_cli-0.9.0/src/pymcap_cli/exporters/json_exporter.py +86 -0
- pymcap_cli-0.9.0/src/pymcap_cli/exporters/kml_exporter.py +157 -0
- pymcap_cli-0.9.0/src/pymcap_cli/exporters/parquet_exporter.py +335 -0
- pymcap_cli-0.9.0/src/pymcap_cli/exporters/pcd_exporter.py +161 -0
- pymcap_cli-0.9.0/src/pymcap_cli/exporters/plot_exporter.py +355 -0
- pymcap_cli-0.9.0/src/pymcap_cli/exporters/video_exporter.py +149 -0
- pymcap_cli-0.9.0/src/pymcap_cli/log_setup.py +59 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/rihs01.py +11 -9
- pymcap_cli-0.9.0/src/pymcap_cli/types/duration.py +24 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/types/info_data.py +1 -1
- pymcap_cli-0.9.0/src/pymcap_cli/types/to_plain.py +47 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/types/types_manual.py +16 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/utils.py +24 -5
- pymcap_cli-0.7.0/src/pymcap_cli/cmd/compress_cmd.py +0 -70
- pymcap_cli-0.7.0/src/pymcap_cli/cmd/plot_cmd.py +0 -421
- pymcap_cli-0.7.0/src/pymcap_cli/cmd/video_cmd.py +0 -557
- pymcap_cli-0.7.0/src/pymcap_cli/core/mcap_processor.py +0 -904
- pymcap_cli-0.7.0/src/pymcap_cli/core/processors.py +0 -154
- pymcap_cli-0.7.0/src/pymcap_cli/encoding/decompress.py +0 -213
- pymcap_cli-0.7.0/src/pymcap_cli/encoding/encoder_common.py +0 -275
- pymcap_cli-0.7.0/src/pymcap_cli/encoding/pointcloud.py +0 -212
- pymcap_cli-0.7.0/src/pymcap_cli/encoding/video_factory.py +0 -47
- pymcap_cli-0.7.0/src/pymcap_cli/encoding/video_ffmpeg.py +0 -793
- pymcap_cli-0.7.0/src/pymcap_cli/encoding/video_protocols.py +0 -40
- pymcap_cli-0.7.0/src/pymcap_cli/encoding/video_pyav.py +0 -396
- pymcap_cli-0.7.0/src/pymcap_cli/types/info_types.py +0 -514
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/__init__.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/__init__.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/info_json_cmd.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/list_cmd.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/topic_chunks_cmd.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/__init__.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/input_handler.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/msg_resolver.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/debug_wrapper.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/display/__init__.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/display/display_utils.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/display/osc_utils.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/display/sparkline.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/encoding/__init__.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/http_utils.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/py.typed +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/rosbag_reader/__init__.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/rosbag_reader/_reader.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/rosbag_reader/_types.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/rosbag_reader/py.typed +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/types/__init__.py +0 -0
- {pymcap_cli-0.7.0 → pymcap_cli-0.9.0}/src/pymcap_cli/types/info_link.py +0 -0
- {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.
|
|
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:
|
|
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.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
|
-
|
|
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 }
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Main CLI entry point for pymcap-cli using Cyclopts."""
|
|
2
2
|
|
|
3
|
-
import
|
|
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(
|
|
41
|
-
"Error
|
|
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(
|
|
62
|
-
"Error
|
|
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(
|
|
83
|
-
"Error
|
|
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
|
-
|
|
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(
|
|
104
|
-
"Error
|
|
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
|
|
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 (
|
|
@@ -249,7 +249,7 @@ def bag2mcap(
|
|
|
249
249
|
"""
|
|
250
250
|
input_path = Path(file)
|
|
251
251
|
if not input_path.exists():
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
282
|
-
|
|
281
|
+
except Exception:
|
|
282
|
+
logger.exception("Error during conversion")
|
|
283
283
|
return 1
|
|
284
284
|
|
|
285
285
|
return 0
|