pymcap-cli 0.8.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.8.0 → pymcap_cli-0.9.0}/PKG-INFO +1 -1
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/pyproject.toml +1 -1
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cli.py +52 -31
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/_run_processor.py +66 -1
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/bag2mcap_cmd.py +5 -5
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/cat_cmd.py +18 -29
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/compress_cmd.py +21 -6
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/convert_cmd.py +9 -11
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/diag_cmd.py +10 -8
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/diff_cmd.py +205 -68
- pymcap_cli-0.9.0/src/pymcap_cli/cmd/duplicates_cmd.py +765 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/export_geo_cmd.py +3 -2
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/export_parquet_cmd.py +5 -4
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/filter_cmd.py +20 -6
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/info_cmd.py +10 -8
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/merge_cmd.py +20 -6
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/plot_cmd.py +7 -8
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/process_cmd.py +21 -7
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/rechunk_cmd.py +23 -15
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/records_cmd.py +5 -5
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/recover_cmd.py +28 -9
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/recover_inplace_cmd.py +9 -11
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/roscompress_cmd.py +34 -42
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/rosdecompress_cmd.py +2 -2
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/split_cmd.py +30 -18
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/tftree_cmd.py +4 -2
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/video_cmd.py +4 -3
- pymcap_cli-0.9.0/src/pymcap_cli/core/mcap_compare.py +1105 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/mcap_transform.py +40 -20
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/exporters/_common.py +5 -4
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/exporters/base.py +7 -8
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/exporters/driver.py +21 -24
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/exporters/gpx_exporter.py +4 -3
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/exporters/kml_exporter.py +4 -3
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/exporters/parquet_exporter.py +11 -10
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/exporters/plot_exporter.py +20 -30
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/exporters/video_exporter.py +4 -10
- pymcap_cli-0.9.0/src/pymcap_cli/log_setup.py +59 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/types/types_manual.py +8 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/utils.py +24 -5
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/README.md +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/__init__.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/__init__.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/_run_processor_multi.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/du_cmd.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/export_csv_cmd.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/export_images_cmd.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/export_json_cmd.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/export_pcd_cmd.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/info_json_cmd.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/list_cmd.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/cmd/topic_chunks_cmd.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/__init__.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/input_handler.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/mcap_processor.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/msg_resolver.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/processors/__init__.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/processors/always_decode.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/processors/attachment_filter.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/processors/base.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/processors/duration_split.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/processors/expression_split.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/processors/metadata_filter.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/processors/time_filter.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/processors/timestamp_split.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/processors/topic_filter.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/core/processors/utils.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/debug_wrapper.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/display/__init__.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/display/display_utils.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/display/osc_utils.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/display/sparkline.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/encoding/__init__.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/encoding/arrow_schema.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/exporters/__init__.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/exporters/csv_exporter.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/exporters/geo_common.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/exporters/geojson_exporter.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/exporters/image_exporter.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/exporters/json_exporter.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/exporters/pcd_exporter.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/http_utils.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/py.typed +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/rihs01.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/rosbag_reader/__init__.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/rosbag_reader/_reader.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/rosbag_reader/_types.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/rosbag_reader/py.typed +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/types/__init__.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/types/duration.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/types/info_data.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/types/info_link.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/types/info_types.py +0 -0
- {pymcap_cli-0.8.0 → pymcap_cli-0.9.0}/src/pymcap_cli/types/to_plain.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pymcap-cli
|
|
3
|
-
Version: 0.
|
|
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
|
|
@@ -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,7 @@ from pymcap_cli.cmd import (
|
|
|
12
12
|
diag_cmd,
|
|
13
13
|
diff_cmd,
|
|
14
14
|
du_cmd,
|
|
15
|
+
duplicates_cmd,
|
|
15
16
|
export_csv_cmd,
|
|
16
17
|
export_geo_cmd,
|
|
17
18
|
export_json_cmd,
|
|
@@ -29,6 +30,7 @@ from pymcap_cli.cmd import (
|
|
|
29
30
|
tftree_cmd,
|
|
30
31
|
topic_chunks_cmd,
|
|
31
32
|
)
|
|
33
|
+
from pymcap_cli.log_setup import ERR, setup_logging
|
|
32
34
|
|
|
33
35
|
try:
|
|
34
36
|
from pymcap_cli.cmd.video_cmd import video
|
|
@@ -41,12 +43,11 @@ except ImportError:
|
|
|
41
43
|
|
|
42
44
|
uv add --group video pymcap-cli
|
|
43
45
|
"""
|
|
44
|
-
print(
|
|
45
|
-
"Error
|
|
46
|
+
ERR.print(
|
|
47
|
+
"[red]Error:[/red]\n"
|
|
46
48
|
"Video command is unavailable because the 'av' and/or 'numpy' are not installed.\n"
|
|
47
49
|
"To enable video functionality, please install pymcap-cli with the 'video' extra:\n\n"
|
|
48
|
-
" uv add --group video pymcap-cli\n"
|
|
49
|
-
file=sys.stderr,
|
|
50
|
+
" uv add --group video pymcap-cli\n"
|
|
50
51
|
)
|
|
51
52
|
return 1
|
|
52
53
|
|
|
@@ -62,12 +63,11 @@ except ImportError:
|
|
|
62
63
|
|
|
63
64
|
uv add --group plot pymcap-cli
|
|
64
65
|
"""
|
|
65
|
-
print(
|
|
66
|
-
"Error
|
|
66
|
+
ERR.print(
|
|
67
|
+
"[red]Error:[/red]\n"
|
|
67
68
|
"Plot command is unavailable because 'plotly' is not installed.\n"
|
|
68
69
|
"To enable plot functionality, please install pymcap-cli with the 'plot' extra:\n\n"
|
|
69
|
-
" uv add --group plot pymcap-cli\n"
|
|
70
|
-
file=sys.stderr,
|
|
70
|
+
" uv add --group plot pymcap-cli\n"
|
|
71
71
|
)
|
|
72
72
|
return 1
|
|
73
73
|
|
|
@@ -83,12 +83,11 @@ except ImportError:
|
|
|
83
83
|
|
|
84
84
|
uv add --group video pymcap-cli
|
|
85
85
|
"""
|
|
86
|
-
print(
|
|
87
|
-
"Error
|
|
86
|
+
ERR.print(
|
|
87
|
+
"[red]Error:[/red]\n"
|
|
88
88
|
"ROS compress command is unavailable because the 'av' package is not installed.\n"
|
|
89
89
|
"To enable this functionality, please install pymcap-cli with the 'video' extra:\n\n"
|
|
90
|
-
" uv add --group video pymcap-cli\n"
|
|
91
|
-
file=sys.stderr,
|
|
90
|
+
" uv add --group video pymcap-cli\n"
|
|
92
91
|
)
|
|
93
92
|
return 1
|
|
94
93
|
|
|
@@ -104,12 +103,11 @@ except ImportError:
|
|
|
104
103
|
|
|
105
104
|
uv add 'pymcap-cli[parquet]'
|
|
106
105
|
"""
|
|
107
|
-
print(
|
|
108
|
-
"Error
|
|
106
|
+
ERR.print(
|
|
107
|
+
"[red]Error:[/red]\n"
|
|
109
108
|
"Export to Parquet is unavailable because 'pyarrow' is not installed.\n"
|
|
110
109
|
"Install with:\n\n"
|
|
111
|
-
" uv add 'pymcap-cli[parquet]'\n"
|
|
112
|
-
file=sys.stderr,
|
|
110
|
+
" uv add 'pymcap-cli[parquet]'\n"
|
|
113
111
|
)
|
|
114
112
|
return 1
|
|
115
113
|
|
|
@@ -125,10 +123,9 @@ except ImportError:
|
|
|
125
123
|
|
|
126
124
|
uv add 'pymcap-cli[pointcloud]'
|
|
127
125
|
"""
|
|
128
|
-
print(
|
|
129
|
-
"Error
|
|
130
|
-
"Install with:\n\n uv add 'pymcap-cli[pointcloud]'\n"
|
|
131
|
-
file=sys.stderr,
|
|
126
|
+
ERR.print(
|
|
127
|
+
"[red]Error:[/red]\nPCD export requires numpy + pointcloud2.\n"
|
|
128
|
+
"Install with:\n\n uv add 'pymcap-cli[pointcloud]'\n"
|
|
132
129
|
)
|
|
133
130
|
return 1
|
|
134
131
|
|
|
@@ -144,10 +141,9 @@ except ImportError:
|
|
|
144
141
|
|
|
145
142
|
uv add 'pymcap-cli[image]'
|
|
146
143
|
"""
|
|
147
|
-
print(
|
|
148
|
-
"Error
|
|
149
|
-
"Install with:\n\n uv add 'pymcap-cli[image]'\n"
|
|
150
|
-
file=sys.stderr,
|
|
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"
|
|
151
147
|
)
|
|
152
148
|
return 1
|
|
153
149
|
|
|
@@ -163,12 +159,11 @@ except ImportError:
|
|
|
163
159
|
|
|
164
160
|
uv add --group video pymcap-cli
|
|
165
161
|
"""
|
|
166
|
-
print(
|
|
167
|
-
"Error
|
|
162
|
+
ERR.print(
|
|
163
|
+
"[red]Error:[/red]\n"
|
|
168
164
|
"ROS decompress command is unavailable because the 'av' package is not installed.\n"
|
|
169
165
|
"To enable this functionality, please install pymcap-cli with the 'video' extra:\n\n"
|
|
170
|
-
" uv add --group video pymcap-cli\n"
|
|
171
|
-
file=sys.stderr,
|
|
166
|
+
" uv add --group video pymcap-cli\n"
|
|
172
167
|
)
|
|
173
168
|
return 1
|
|
174
169
|
|
|
@@ -177,6 +172,7 @@ app = App(
|
|
|
177
172
|
name="pymcap-cli",
|
|
178
173
|
help="CLI tool for slicing and dicing MCAP files.",
|
|
179
174
|
help_format="rich",
|
|
175
|
+
default_parameter=Parameter(negative_iterable=""),
|
|
180
176
|
)
|
|
181
177
|
|
|
182
178
|
inspect_group = Group("Inspect", sort_key=0)
|
|
@@ -187,6 +183,7 @@ app.command(name="cat", group=inspect_group)(cat_cmd.cat)
|
|
|
187
183
|
app.command(name="diag", group=inspect_group)(diag_cmd.diag)
|
|
188
184
|
app.command(name="du", group=inspect_group)(du_cmd.du)
|
|
189
185
|
app.command(name="diff", group=inspect_group)(diff_cmd.diff_cmd)
|
|
186
|
+
app.command(name="duplicates", group=inspect_group)(duplicates_cmd.duplicates)
|
|
190
187
|
app.command(name="info", group=inspect_group)(info_cmd.info)
|
|
191
188
|
app.command(name="info-json", group=inspect_group)(info_json_cmd.info_json)
|
|
192
189
|
list_cmd.list_app.group = (inspect_group,)
|
|
@@ -218,8 +215,32 @@ app.command(name="rosdecompress", group=transform_group)(rosdecompress)
|
|
|
218
215
|
app.command(name="video", group=transform_group)(video)
|
|
219
216
|
|
|
220
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
|
+
|
|
221
242
|
def main() -> None:
|
|
222
|
-
app()
|
|
243
|
+
app.meta()
|
|
223
244
|
|
|
224
245
|
|
|
225
246
|
if __name__ == "__main__":
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
"""Shared processor pipeline for transform commands."""
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
|
+
import logging
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import BinaryIO
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
from small_mcap import InvalidMagicError, McapError
|
|
7
11
|
|
|
8
12
|
from pymcap_cli.core.input_handler import open_input
|
|
9
13
|
from pymcap_cli.core.mcap_processor import (
|
|
@@ -15,7 +19,9 @@ from pymcap_cli.core.mcap_processor import (
|
|
|
15
19
|
ProcessingOptions,
|
|
16
20
|
ProcessingStats,
|
|
17
21
|
)
|
|
18
|
-
from pymcap_cli.utils import confirm_output_overwrite
|
|
22
|
+
from pymcap_cli.utils import confirm_output_overwrite, read_info
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
19
25
|
|
|
20
26
|
|
|
21
27
|
@dataclass(slots=True)
|
|
@@ -79,3 +85,62 @@ def run_processor(
|
|
|
79
85
|
stats = processor.process(output_stream)
|
|
80
86
|
|
|
81
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
|
|
@@ -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
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
4
|
import json
|
|
5
|
+
import logging
|
|
5
6
|
import re
|
|
6
7
|
import sys
|
|
7
8
|
from contextlib import ExitStack
|
|
@@ -26,7 +27,7 @@ from small_mcap import Channel, JSONDecoderFactory, read_message_decoded
|
|
|
26
27
|
from pymcap_cli.core.input_handler import open_input
|
|
27
28
|
from pymcap_cli.utils import MAX_INT64, ProgressTrackingIO, file_progress, parse_timestamp_args
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
30
31
|
console_out = Console()
|
|
31
32
|
|
|
32
33
|
FILTERING_GROUP = Group("Filtering")
|
|
@@ -220,8 +221,8 @@ def cat(
|
|
|
220
221
|
if query:
|
|
221
222
|
try:
|
|
222
223
|
parsed_query = parse_message_path(query)
|
|
223
|
-
except Exception
|
|
224
|
-
|
|
224
|
+
except Exception:
|
|
225
|
+
logger.exception("Invalid query syntax")
|
|
225
226
|
return 1
|
|
226
227
|
|
|
227
228
|
# Determine output mode
|
|
@@ -256,18 +257,12 @@ def cat(
|
|
|
256
257
|
root_msgdef = all_definitions.get(f"{parts[0]}/{parts[-1]}")
|
|
257
258
|
|
|
258
259
|
if root_msgdef is None:
|
|
259
|
-
|
|
260
|
-
f"[yellow]Warning: Could not find message definition "
|
|
261
|
-
f"for schema '{msg_schema_name}'[/yellow]"
|
|
262
|
-
)
|
|
260
|
+
logger.warning(f"Could not find message definition for schema '{msg_schema_name}'")
|
|
263
261
|
else:
|
|
264
262
|
parsed_query.validate(root_msgdef, all_definitions) # type: ignore[union-attr]
|
|
265
|
-
except ValidationError
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
console_err.print(
|
|
269
|
-
f"\n[yellow]Query:[/yellow] {query}\n[yellow]Schema:[/yellow] {msg_schema_name}"
|
|
270
|
-
)
|
|
263
|
+
except ValidationError:
|
|
264
|
+
logger.exception(f"Query validation error for topic '{topic}'")
|
|
265
|
+
logger.exception(f"Query: {query} Schema: {msg_schema_name}")
|
|
271
266
|
return 1
|
|
272
267
|
return None
|
|
273
268
|
|
|
@@ -288,7 +283,7 @@ def cat(
|
|
|
288
283
|
with open_input(file) as (input_stream, file_size), ExitStack() as stack:
|
|
289
284
|
stream: IO[bytes] = input_stream
|
|
290
285
|
if writing_to_file and file_size:
|
|
291
|
-
progress = file_progress("[bold blue]Reading MCAP..."
|
|
286
|
+
progress = file_progress("[bold blue]Reading MCAP...")
|
|
292
287
|
progress.start()
|
|
293
288
|
stack.callback(progress.stop)
|
|
294
289
|
task = progress.add_task("Processing", total=file_size)
|
|
@@ -312,9 +307,9 @@ def cat(
|
|
|
312
307
|
validated_topics.add(msg.channel.topic)
|
|
313
308
|
|
|
314
309
|
if msg.schema is None:
|
|
315
|
-
|
|
316
|
-
f"
|
|
317
|
-
|
|
310
|
+
logger.warning(
|
|
311
|
+
f"Cannot validate query for topic '{msg.channel.topic}' "
|
|
312
|
+
"(no schema available)"
|
|
318
313
|
)
|
|
319
314
|
else:
|
|
320
315
|
err = _validate_query(msg.schema.name, msg.schema.data, msg.channel.topic)
|
|
@@ -328,9 +323,7 @@ def cat(
|
|
|
328
323
|
if data is None:
|
|
329
324
|
continue
|
|
330
325
|
except MessagePathError as e:
|
|
331
|
-
|
|
332
|
-
f"[yellow]Filter error on {msg.channel.topic}: {e}[/yellow]",
|
|
333
|
-
)
|
|
326
|
+
logger.warning(f"Filter error on {msg.channel.topic}: {e}")
|
|
334
327
|
continue
|
|
335
328
|
else:
|
|
336
329
|
data = msg.decoded_message
|
|
@@ -369,22 +362,18 @@ def cat(
|
|
|
369
362
|
print(line, file=sys.stdout) # noqa: T201
|
|
370
363
|
|
|
371
364
|
if writing_to_file:
|
|
372
|
-
|
|
373
|
-
f"Wrote [bold]{message_count:,}[/bold] messages to [cyan]{output}[/cyan]"
|
|
374
|
-
)
|
|
365
|
+
logger.info(f"Wrote {message_count:,} messages to {output}")
|
|
375
366
|
|
|
376
367
|
if parsed_query and not validated_topics:
|
|
377
|
-
|
|
378
|
-
f"[red]Error: Topic '{parsed_query.topic}' not found in MCAP file[/red]"
|
|
379
|
-
)
|
|
368
|
+
logger.error(f"Topic '{parsed_query.topic}' not found in MCAP file")
|
|
380
369
|
return 1
|
|
381
370
|
|
|
382
371
|
except KeyboardInterrupt:
|
|
383
|
-
|
|
372
|
+
logger.warning("Interrupted by user")
|
|
384
373
|
return 0
|
|
385
374
|
|
|
386
|
-
except Exception
|
|
387
|
-
|
|
375
|
+
except Exception:
|
|
376
|
+
logger.exception("Error reading MCAP")
|
|
388
377
|
return 1
|
|
389
378
|
|
|
390
379
|
return 0
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
"""Compress command for pymcap-cli."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
4
|
+
|
|
3
5
|
from rich.console import Console
|
|
4
6
|
|
|
5
|
-
from pymcap_cli.cmd._run_processor import
|
|
7
|
+
from pymcap_cli.cmd._run_processor import (
|
|
8
|
+
finalize_delete_source,
|
|
9
|
+
resolve_overwrite_policy,
|
|
10
|
+
run_processor,
|
|
11
|
+
)
|
|
6
12
|
from pymcap_cli.core.mcap_processor import (
|
|
7
13
|
InputOptions,
|
|
8
14
|
OutputOptions,
|
|
@@ -12,11 +18,13 @@ from pymcap_cli.types.types_manual import (
|
|
|
12
18
|
DEFAULT_COMPRESSION,
|
|
13
19
|
ChunkSizeOption,
|
|
14
20
|
CompressionOption,
|
|
21
|
+
DeleteSourceOption,
|
|
15
22
|
ForceOverwriteOption,
|
|
16
23
|
NoClobberOption,
|
|
17
24
|
OutputPathOption,
|
|
18
25
|
)
|
|
19
26
|
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
20
28
|
console = Console()
|
|
21
29
|
|
|
22
30
|
|
|
@@ -28,6 +36,7 @@ def compress(
|
|
|
28
36
|
compression: CompressionOption = DEFAULT_COMPRESSION,
|
|
29
37
|
force: ForceOverwriteOption = False,
|
|
30
38
|
no_clobber: NoClobberOption = False,
|
|
39
|
+
delete_source: DeleteSourceOption = False,
|
|
31
40
|
) -> int:
|
|
32
41
|
"""Create a compressed copy of an MCAP file.
|
|
33
42
|
|
|
@@ -47,6 +56,9 @@ def compress(
|
|
|
47
56
|
Force overwrite of output file without confirmation.
|
|
48
57
|
no_clobber
|
|
49
58
|
Fail instead of prompting if the output file already exists.
|
|
59
|
+
delete_source
|
|
60
|
+
Delete source file(s) after the output is validated (header + summary).
|
|
61
|
+
URL inputs and any source whose path equals the output are skipped.
|
|
50
62
|
|
|
51
63
|
Examples
|
|
52
64
|
--------
|
|
@@ -56,10 +68,10 @@ def compress(
|
|
|
56
68
|
"""
|
|
57
69
|
overwrite_policy = resolve_overwrite_policy(force=force, no_clobber=no_clobber)
|
|
58
70
|
if overwrite_policy is None:
|
|
59
|
-
|
|
71
|
+
logger.error("--force and --no-clobber cannot be used together.")
|
|
60
72
|
return 1
|
|
61
73
|
|
|
62
|
-
|
|
74
|
+
logger.info(f"Compressing '{file}' to '{output}'")
|
|
63
75
|
|
|
64
76
|
try:
|
|
65
77
|
result = run_processor(
|
|
@@ -74,10 +86,13 @@ def compress(
|
|
|
74
86
|
overwrite_policy=overwrite_policy,
|
|
75
87
|
),
|
|
76
88
|
)
|
|
77
|
-
|
|
89
|
+
logger.info("[green]✓ Compression completed successfully![/green]")
|
|
78
90
|
console.print(result.stats)
|
|
79
|
-
except Exception
|
|
80
|
-
|
|
91
|
+
except Exception:
|
|
92
|
+
logger.exception("Error during compression")
|
|
81
93
|
return 1
|
|
82
94
|
|
|
95
|
+
if delete_source:
|
|
96
|
+
return finalize_delete_source(sources=[file], outputs=[output])
|
|
97
|
+
|
|
83
98
|
return 0
|
|
@@ -522,7 +522,7 @@ def convert(
|
|
|
522
522
|
# Validate input file
|
|
523
523
|
input_path = Path(file)
|
|
524
524
|
if not input_path.exists():
|
|
525
|
-
|
|
525
|
+
logger.error(f"Input file '{file}' does not exist")
|
|
526
526
|
return 1
|
|
527
527
|
|
|
528
528
|
# Confirm overwrite if needed
|
|
@@ -546,17 +546,17 @@ def convert(
|
|
|
546
546
|
use_chunking=True,
|
|
547
547
|
)
|
|
548
548
|
|
|
549
|
-
|
|
550
|
-
|
|
549
|
+
logger.info(f"Converting '{file}' to '{output}'")
|
|
550
|
+
logger.info(f"ROS2 distro: {distro.value}")
|
|
551
551
|
if extra_path:
|
|
552
|
-
|
|
552
|
+
logger.info(f"Extra paths: {', '.join(str(p) for p in extra_path)}")
|
|
553
553
|
|
|
554
554
|
# Perform conversion
|
|
555
555
|
with output.open("wb") as output_stream:
|
|
556
556
|
try:
|
|
557
557
|
stats = convert_db3_to_mcap(input_path, output_stream, options)
|
|
558
558
|
|
|
559
|
-
|
|
559
|
+
logger.info("[green]✓ Conversion completed successfully![/green]")
|
|
560
560
|
console.print(
|
|
561
561
|
f"Converted {stats.topic_count} topics, "
|
|
562
562
|
f"{stats.message_count:,} messages, "
|
|
@@ -565,18 +565,16 @@ def convert(
|
|
|
565
565
|
|
|
566
566
|
# Report skipped topics if any
|
|
567
567
|
if stats.skipped_topics:
|
|
568
|
-
|
|
569
|
-
"
|
|
570
|
-
f"⚠ Skipped {len(stats.skipped_topics)} topics "
|
|
568
|
+
logger.warning(
|
|
569
|
+
f"Skipped {len(stats.skipped_topics)} topics "
|
|
571
570
|
f"({stats.skipped_message_count:,} messages) due to missing message definitions"
|
|
572
|
-
"[/]"
|
|
573
571
|
)
|
|
574
572
|
console.print("[dim]Skipped topics:[/dim]")
|
|
575
573
|
for topic in sorted(stats.skipped_topics):
|
|
576
574
|
console.print(f" [dim]- {topic}[/dim]")
|
|
577
575
|
|
|
578
|
-
except Exception
|
|
579
|
-
|
|
576
|
+
except Exception:
|
|
577
|
+
logger.exception("Error during conversion")
|
|
580
578
|
return 1
|
|
581
579
|
|
|
582
580
|
return 0
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Diag command - inspect ROS2 diagnostics from MCAP files."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
import re
|
|
5
6
|
import sys
|
|
6
7
|
from dataclasses import dataclass, field
|
|
@@ -18,9 +19,10 @@ from small_mcap import include_topics, read_message_decoded
|
|
|
18
19
|
|
|
19
20
|
from pymcap_cli.core.input_handler import open_input
|
|
20
21
|
from pymcap_cli.display.sparkline import sparkline
|
|
22
|
+
from pymcap_cli.log_setup import ERR
|
|
21
23
|
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
22
25
|
console = Console()
|
|
23
|
-
console_err = Console(stderr=True)
|
|
24
26
|
|
|
25
27
|
LEVEL_NAMES = {0: "OK", 1: "WARN", 2: "ERROR", 3: "STALE"}
|
|
26
28
|
LEVEL_STYLES = {0: "green", 1: "yellow", 2: "red", 3: "dim"}
|
|
@@ -64,7 +66,7 @@ def _collect_diagnostics(file: str, topics: list[str]) -> dict[str, DiagEntry]:
|
|
|
64
66
|
TextColumn("[cyan]{task.fields[msgs]} msgs"),
|
|
65
67
|
TextColumn("[dim]{task.fields[components]} components"),
|
|
66
68
|
TimeElapsedColumn(),
|
|
67
|
-
console=
|
|
69
|
+
console=ERR,
|
|
68
70
|
transient=True,
|
|
69
71
|
) as progress,
|
|
70
72
|
):
|
|
@@ -186,7 +188,7 @@ def _compile_pattern(pattern: str, flag_name: str) -> re.Pattern[str]:
|
|
|
186
188
|
try:
|
|
187
189
|
return re.compile(pattern, re.IGNORECASE)
|
|
188
190
|
except re.error as e:
|
|
189
|
-
|
|
191
|
+
logger.exception(f"Invalid regex for {flag_name}")
|
|
190
192
|
raise SystemExit(1) from e
|
|
191
193
|
|
|
192
194
|
|
|
@@ -559,16 +561,16 @@ def diag(
|
|
|
559
561
|
|
|
560
562
|
try:
|
|
561
563
|
entries = _collect_diagnostics(file, resolved_topics)
|
|
562
|
-
except (OSError, ValueError, RuntimeError)
|
|
563
|
-
|
|
564
|
+
except (OSError, ValueError, RuntimeError):
|
|
565
|
+
logger.exception("Error reading MCAP file")
|
|
564
566
|
return 1
|
|
565
567
|
except KeyboardInterrupt:
|
|
566
|
-
|
|
568
|
+
logger.warning("Interrupted by user")
|
|
567
569
|
return 0
|
|
568
570
|
|
|
569
571
|
if not entries:
|
|
570
572
|
topic_str = ", ".join(resolved_topics)
|
|
571
|
-
|
|
573
|
+
logger.warning(f"No diagnostics found on topic(s) '{topic_str}'")
|
|
572
574
|
return 0
|
|
573
575
|
|
|
574
576
|
level_totals: dict[int, int] = {0: 0, 1: 0, 2: 0, 3: 0}
|
|
@@ -592,7 +594,7 @@ def diag(
|
|
|
592
594
|
if inspect_re:
|
|
593
595
|
matched = [e for e in filtered if inspect_re.search(e.name)]
|
|
594
596
|
if not matched:
|
|
595
|
-
|
|
597
|
+
logger.warning(f"No components matching '{inspect}'")
|
|
596
598
|
return 0
|
|
597
599
|
|
|
598
600
|
for renderable in _build_inspect_view(matched):
|