lheutils 0.0.2__py3-none-any.whl

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.
@@ -0,0 +1,371 @@
1
+ #!/usr/bin/env python3
2
+ # ruff: noqa: SIM103
3
+ """
4
+ CLI tool to filter LHE files based on various criteria.
5
+
6
+ This tool filters Les Houches Event (LHE) files based on process ID,
7
+ particle PDG IDs (incoming/outgoing), and event numbers.
8
+ """
9
+
10
+ import argparse
11
+ import sys
12
+ from collections.abc import Iterable
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+ import pylhe
17
+
18
+ from lheutils.cli.util import create_base_parser
19
+
20
+
21
+ def matches_process_filter(
22
+ event: pylhe.LHEEvent,
23
+ process_ids: Optional[set[int]],
24
+ exclude_process_ids: Optional[set[int]],
25
+ ) -> bool:
26
+ """Check if event matches process ID filters."""
27
+ if process_ids is not None and event.eventinfo.pid not in process_ids:
28
+ return False
29
+ if exclude_process_ids is not None and event.eventinfo.pid in exclude_process_ids:
30
+ return False
31
+ return True
32
+
33
+
34
+ def matches_particle_filter(
35
+ event: pylhe.LHEEvent,
36
+ incoming_pdgids: Optional[set[int]],
37
+ exclude_incoming_pdgids: Optional[set[int]],
38
+ outgoing_pdgids: Optional[set[int]],
39
+ exclude_outgoing_pdgids: Optional[set[int]],
40
+ ) -> bool:
41
+ """Check if event matches particle PDG ID filters."""
42
+ # Get incoming particles (status -1)
43
+ incoming_particles = [p for p in event.particles if p.status == -1]
44
+ incoming_ids = {p.id for p in incoming_particles}
45
+
46
+ # Get outgoing particles (status 1)
47
+ outgoing_particles = [p for p in event.particles if p.status == 1]
48
+ outgoing_ids = {p.id for p in outgoing_particles}
49
+
50
+ # Check incoming particle filters
51
+ if incoming_pdgids is not None and not incoming_ids.intersection(incoming_pdgids):
52
+ return False
53
+
54
+ if exclude_incoming_pdgids is not None and incoming_ids.intersection(
55
+ exclude_incoming_pdgids
56
+ ):
57
+ return False
58
+
59
+ # Check outgoing particle filters
60
+ if outgoing_pdgids is not None and not outgoing_ids.intersection(outgoing_pdgids):
61
+ return False
62
+
63
+ if exclude_outgoing_pdgids is not None and outgoing_ids.intersection(
64
+ exclude_outgoing_pdgids
65
+ ):
66
+ return False
67
+ return True
68
+
69
+
70
+ def matches_event_filter(
71
+ event_index: int,
72
+ include_event_ranges: Optional[list[tuple[int, int]]],
73
+ exclude_event_ranges: Optional[list[tuple[int, int]]],
74
+ ) -> bool:
75
+ """Check if event matches event number filters."""
76
+ # event_index is 0-based, but user input is 1-based
77
+ event_number = event_index + 1
78
+
79
+ # Check include ranges
80
+ if include_event_ranges is not None:
81
+ matches_include = False
82
+ for start, end in include_event_ranges:
83
+ if end == -1: # Open upper bound
84
+ if event_number >= start:
85
+ matches_include = True
86
+ break
87
+ elif start <= event_number <= end:
88
+ matches_include = True
89
+ break
90
+ if not matches_include:
91
+ return False
92
+
93
+ # Check exclude ranges
94
+ if exclude_event_ranges is not None:
95
+ for start, end in exclude_event_ranges:
96
+ if end == -1: # Open upper bound
97
+ if event_number >= start:
98
+ return False
99
+ elif start <= event_number <= end:
100
+ return False
101
+
102
+ return True
103
+
104
+
105
+ def filter_lhe_file(
106
+ input_file: str,
107
+ rwgt: bool,
108
+ weights: bool,
109
+ output_file: Optional[str] = None,
110
+ process_ids: Optional[set[int]] = None,
111
+ exclude_process_ids: Optional[set[int]] = None,
112
+ incoming_pdgids: Optional[set[int]] = None,
113
+ exclude_incoming_pdgids: Optional[set[int]] = None,
114
+ outgoing_pdgids: Optional[set[int]] = None,
115
+ exclude_outgoing_pdgids: Optional[set[int]] = None,
116
+ include_event_ranges: Optional[list[tuple[int, int]]] = None,
117
+ exclude_event_ranges: Optional[list[tuple[int, int]]] = None,
118
+ ) -> None:
119
+ """Filter an LHE file based on the given criteria."""
120
+ try:
121
+ # Read the input LHE file
122
+ if input_file == "-":
123
+ lhefile = pylhe.LHEFile.frombuffer(sys.stdin)
124
+ else:
125
+ lhefile = pylhe.LHEFile.fromfile(input_file)
126
+
127
+ # Filter events
128
+ def _generator() -> Iterable[pylhe.LHEEvent]:
129
+ for event_index, event in enumerate(lhefile.events):
130
+ # Apply all filters
131
+ if (
132
+ matches_process_filter(event, process_ids, exclude_process_ids)
133
+ and matches_particle_filter(
134
+ event,
135
+ incoming_pdgids,
136
+ exclude_incoming_pdgids,
137
+ outgoing_pdgids,
138
+ exclude_outgoing_pdgids,
139
+ )
140
+ and matches_event_filter(
141
+ event_index, include_event_ranges, exclude_event_ranges
142
+ )
143
+ ):
144
+ yield event
145
+
146
+ # Create filtered LHE file
147
+ filtered_lhefile = pylhe.LHEFile(init=lhefile.init, events=_generator())
148
+
149
+ # Output the result
150
+ if output_file:
151
+ filtered_lhefile.tofile(output_file, rwgt=rwgt, weights=weights)
152
+ else:
153
+ # Write to stdout
154
+ filtered_lhefile.write(sys.stdout, rwgt=rwgt, weights=weights)
155
+
156
+ except FileNotFoundError:
157
+ if input_file == "-":
158
+ print("Error: Unable to read from stdin", file=sys.stderr)
159
+ else:
160
+ print(f"Error: File '{input_file}' not found", file=sys.stderr)
161
+ sys.exit(1)
162
+ except Exception as e:
163
+ source = "stdin" if input_file == "-" else f"file '{input_file}'"
164
+ print(f"Error processing {source}: {e}", file=sys.stderr)
165
+ sys.exit(1)
166
+
167
+
168
+ def parse_int_list(value: str) -> set[int]:
169
+ """Parse comma-separated list of integers."""
170
+ try:
171
+ return {int(x.strip()) for x in value.split(",")}
172
+ except ValueError as e:
173
+ err = f"Invalid integer list: {value}"
174
+ raise argparse.ArgumentTypeError(err) from e
175
+
176
+
177
+ def parse_range_list(value: str) -> list[tuple[int, int]]:
178
+ """Parse comma-separated list of integers and ranges.
179
+
180
+ Returns list of (start, end) tuples where -1 indicates open bound.
181
+
182
+ Supports:
183
+ - Individual numbers: 5 -> [(5, 5)]
184
+ - Ranges: 5-10 -> [(5, 10)] (inclusive)
185
+ - Lower bound: 5- -> [(5, -1)] (from 5 to end)
186
+ - Upper bound: -10 -> [(1, 10)] (from start to 10)
187
+ - Mixed: 1,5-10,15-,20,-25 -> [(1, 1), (5, 10), (15, -1), (20, 20), (1, 25)]
188
+ """
189
+ result = []
190
+
191
+ try:
192
+ for vitem in value.split(","):
193
+ item = vitem.strip()
194
+
195
+ if "-" not in item:
196
+ # Single number
197
+ num = int(item)
198
+ result.append((num, num))
199
+ elif item.startswith("-"):
200
+ # Upper bound: -N
201
+ upper = int(item[1:])
202
+ result.append((1, upper))
203
+ elif item.endswith("-"):
204
+ # Lower bound: N- (open range)
205
+ lower = int(item[:-1])
206
+ result.append((lower, -1)) # -1 indicates open upper bound
207
+ else:
208
+ # Range: N-M
209
+ parts = item.split("-")
210
+ if len(parts) == 2:
211
+ lower, upper = int(parts[0]), int(parts[1])
212
+ if lower > upper:
213
+ err = f"Invalid range: {item} (start > end)"
214
+ raise ValueError(err)
215
+ result.append((lower, upper))
216
+ else:
217
+ err = f"Invalid range format: {item}"
218
+ raise ValueError(err)
219
+
220
+ except ValueError as e:
221
+ err = f"Invalid range specification: {value} ({e})"
222
+ raise argparse.ArgumentTypeError(err) from e
223
+
224
+ return result
225
+
226
+
227
+ def main() -> None:
228
+ """Main CLI function."""
229
+ parser = create_base_parser(
230
+ description="Filter LHE files based on process ID, particle PDG IDs, and event numbers",
231
+ formatter_class=argparse.RawDescriptionHelpFormatter,
232
+ epilog="""
233
+ Examples:
234
+ lhefilter input.lhe -o filtered.lhe --process-p 81,82
235
+ lhefilter input.lhe --PROCESS 91 --incoming 21 --outgoing 11,-11
236
+ lhefilter input.lhe --events 1,5,10 --outgoing 13,-13
237
+ lhefilter input.lhe --EVENTS 7 --incoming 2,-2
238
+ lhefilter input.lhe.gz --out 6,-6 | gzip > filtered.lhe.gz
239
+ lhefilter input.lhe --events 10-20 --outgoing 11,-11
240
+ lhefilter input.lhe --events 50- --EVENTS 55-60
241
+ cat input.lhe | lhefilter --outgoing 11,-11
242
+ zcat input.lhe.gz | lhefilter --process-p 81 --outgoing 13,-13
243
+
244
+ Process ID filters:
245
+ --process-p ID[,ID...] Include only events with these process IDs
246
+ --PROCESS/-P ID[,ID...] Exclude events with these process IDs
247
+
248
+ Particle PDG ID filters:
249
+ --incoming/-in ID[,ID...] Include events containing these incoming particles
250
+ --INCOMING/-IN ID[,ID...] Exclude events containing these incoming particles
251
+ --outgoing/-out ID[,ID...] Include events containing these outgoing particles
252
+ --OUTGOING/-OUT ID[,ID...] Exclude events containing these outgoing particles
253
+
254
+ Event filters:
255
+ --events RANGE[,RANGE...] Include events in these ranges (1-indexed)
256
+ Supports: N (single), N-M (range), N- (from N to end), -M (up to M)
257
+ --EVENTS RANGE[,RANGE...] Exclude events in these ranges (1-indexed)
258
+ Supports: N (single), N-M (range), N- (from N to end), -M (up to M)
259
+
260
+ Note: Multiple filters are combined with AND logic.
261
+ PDG ID 0 can be used as wildcard for any particle.
262
+ Open ranges (N-) use -1 internally to represent no upper bound.
263
+ """,
264
+ )
265
+
266
+ parser.add_argument(
267
+ "input", nargs="?", default="-", help="Input LHE file (default: stdin)"
268
+ )
269
+ parser.add_argument("-o", "--output", help="Output file (default: write to stdout)")
270
+
271
+ # Process ID filters
272
+ parser.add_argument(
273
+ "--process-p",
274
+ type=parse_int_list,
275
+ metavar="ID[,ID...]",
276
+ help="Include only events with these process IDs",
277
+ )
278
+ parser.add_argument(
279
+ "--PROCESS",
280
+ "-P",
281
+ type=parse_int_list,
282
+ metavar="ID[,ID...]",
283
+ help="Exclude events with these process IDs",
284
+ )
285
+
286
+ # Incoming particle filters
287
+ parser.add_argument(
288
+ "--incoming",
289
+ "-in",
290
+ type=parse_int_list,
291
+ metavar="PDGID[,PDGID...]",
292
+ help="Include events containing these incoming particles",
293
+ )
294
+ parser.add_argument(
295
+ "--INCOMING",
296
+ "-IN",
297
+ type=parse_int_list,
298
+ metavar="PDGID[,PDGID...]",
299
+ help="Exclude events containing these incoming particles",
300
+ )
301
+
302
+ # Outgoing particle filters
303
+ parser.add_argument(
304
+ "--outgoing",
305
+ "-out",
306
+ type=parse_int_list,
307
+ metavar="PDGID[,PDGID...]",
308
+ help="Include events containing these outgoing particles",
309
+ )
310
+ parser.add_argument(
311
+ "--OUTGOING",
312
+ "-OUT",
313
+ type=parse_int_list,
314
+ metavar="PDGID[,PDGID...]",
315
+ help="Exclude events containing these outgoing particles",
316
+ )
317
+
318
+ # Event range filters
319
+ parser.add_argument(
320
+ "--events",
321
+ type=parse_range_list,
322
+ metavar="RANGE[,RANGE...]",
323
+ help="Include events in these ranges (1-indexed). Supports: N (single), N-M (range), N- (from N), -M (up to M)",
324
+ )
325
+ parser.add_argument(
326
+ "--EVENTS",
327
+ type=parse_range_list,
328
+ metavar="RANGE[,RANGE...]",
329
+ help="Exclude events in these ranges (1-indexed). Supports: N (single), N-M (range), N- (from N), -M (up to M)",
330
+ )
331
+
332
+ parser.add_argument(
333
+ "--rwgt",
334
+ action="store_true",
335
+ help="Use rwgt section if present in the input file",
336
+ )
337
+
338
+ parser.add_argument(
339
+ "--no-weights",
340
+ action="store_true",
341
+ help="Do not preserve event weights in output file",
342
+ )
343
+
344
+ args = parser.parse_args()
345
+
346
+ # Validate input file (skip validation for stdin)
347
+ if args.input != "-":
348
+ input_path = Path(args.input)
349
+ if not input_path.exists():
350
+ print(f"Error: Input file '{args.input}' does not exist", file=sys.stderr)
351
+ sys.exit(1)
352
+
353
+ # Call the filtering function
354
+ filter_lhe_file(
355
+ rwgt=args.rwgt,
356
+ weights=not args.no_weights,
357
+ input_file=args.input,
358
+ output_file=args.output,
359
+ process_ids=args.process_p,
360
+ exclude_process_ids=args.PROCESS,
361
+ incoming_pdgids=args.incoming,
362
+ exclude_incoming_pdgids=args.INCOMING,
363
+ outgoing_pdgids=args.outgoing,
364
+ exclude_outgoing_pdgids=args.OUTGOING,
365
+ include_event_ranges=args.events,
366
+ exclude_event_ranges=args.EVENTS,
367
+ )
368
+
369
+
370
+ if __name__ == "__main__":
371
+ main()
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CLI tool to display information about LHE files.
4
+
5
+ This tool analyzes Les Houches Event (LHE) files and displays relevant information
6
+ including number of events, process information, particle combinations, etc.
7
+ """
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+ import warnings
13
+ from collections import Counter
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import TextIO, Union
17
+
18
+ import yaml # type: ignore[import-untyped]
19
+
20
+ import pylhe
21
+ from pylhe.cli.util import dataclass_with_properties_to_dict
22
+
23
+ from lheutils.cli.util import create_base_parser
24
+
25
+
26
+ @dataclass
27
+ class LHEChannel:
28
+ """Information about a single channel in an LHE file."""
29
+
30
+ incoming_pdgid: list[int]
31
+ outgoing_pdgid: list[int]
32
+ num_events: int
33
+
34
+
35
+ @dataclass
36
+ class LHEProcess:
37
+ """Information about a single process in an LHE file."""
38
+
39
+ procId: int
40
+ xSection: float
41
+ error: float
42
+ channels: list[LHEChannel]
43
+
44
+
45
+ @dataclass
46
+ class LHEInfo:
47
+ """Information about a single LHE file."""
48
+
49
+ filepath: str
50
+ beamA: int
51
+ energyA: float
52
+ beamB: int
53
+ energyB: float
54
+ weight_groups: dict[str, int]
55
+ num_events: int
56
+ negative_weighted_events: int
57
+ process_info: list[LHEProcess]
58
+
59
+ @property
60
+ def negative_weighted_events_ratio(self) -> float:
61
+ """Ratio of negative weighted events to total events."""
62
+ return (
63
+ self.negative_weighted_events / self.num_events
64
+ if self.num_events > 0
65
+ else 0.0
66
+ )
67
+
68
+ def __str__(self) -> str:
69
+ lines = []
70
+ lines.append("-" * 60)
71
+ lines.append(f"File: {self.filepath}")
72
+
73
+ # Beam information
74
+ lines.append(f"Beam A: {self.beamA} @ {self.energyA} GeV")
75
+ lines.append(f"Beam B: {self.beamB} @ {self.energyB} GeV")
76
+ # Weight groups
77
+ if self.weight_groups:
78
+ lines.append(" Weight Groups:")
79
+ for name, count in self.weight_groups.items():
80
+ lines.append(f" {name}: {count} weights")
81
+ # Number of events
82
+ lines.append(
83
+ f"Number of events: {self.num_events} (negative: {self.negative_weighted_events_ratio:.2%})"
84
+ )
85
+
86
+ # Process information
87
+ processes = self.process_info
88
+ if processes:
89
+ for proc in processes:
90
+ lines.append(
91
+ f"Process {proc.procId} cross-section: ({proc.xSection:.3e} +- {proc.error:.3e}) pb"
92
+ )
93
+
94
+ channels = proc.channels
95
+ if channels:
96
+ # Sort channels by num_events in descending order
97
+ sorted_channels = sorted(
98
+ channels, key=lambda ch: ch.num_events, reverse=True
99
+ )
100
+ for channel in sorted_channels:
101
+ percentage = 100 * channel.num_events / self.num_events
102
+ lines.append(
103
+ f" {channel.incoming_pdgid} -> {channel.outgoing_pdgid}: {channel.num_events:,} events ({percentage:.1f}%)"
104
+ )
105
+
106
+ return "\n".join(lines)
107
+
108
+
109
+ def get_lheinfo(filepath_or_fileobj: Union[str, TextIO]) -> LHEInfo:
110
+ # Read LHE file
111
+ if isinstance(filepath_or_fileobj, str):
112
+ lhefile = pylhe.LHEFile.fromfile(filepath_or_fileobj)
113
+ file_display_name = filepath_or_fileobj
114
+ else:
115
+ lhefile = pylhe.LHEFile.frombuffer(filepath_or_fileobj)
116
+ file_display_name = "<stdin>"
117
+ init_info = lhefile.init.initInfo
118
+
119
+ initial_final_combinations: Counter[
120
+ tuple[int, tuple[int, ...], tuple[int, ...]]
121
+ ] = Counter()
122
+
123
+ num_events = 0
124
+ num_negative_weighted_events = 0
125
+ for event in lhefile.events:
126
+ num_events += 1
127
+ if event.eventinfo.weight < 0:
128
+ num_negative_weighted_events += 1
129
+ initial = []
130
+ final = []
131
+
132
+ for particle in event.particles:
133
+ if particle.status == -1: # Incoming particles
134
+ initial.append(particle.id)
135
+ elif particle.status == 1: # Outgoing particles
136
+ final.append(particle.id)
137
+
138
+ # Count initial particles
139
+ initial_tuple = tuple(sorted(initial))
140
+
141
+ # Count final particles
142
+ final_tuple = tuple(sorted(final))
143
+
144
+ # Count initial -> final combinations
145
+ combination = (event.eventinfo.pid, initial_tuple, final_tuple)
146
+ initial_final_combinations[combination] += 1
147
+
148
+ return LHEInfo(
149
+ filepath=file_display_name,
150
+ beamA=init_info.beamA,
151
+ energyA=init_info.energyA,
152
+ beamB=init_info.beamB,
153
+ energyB=init_info.energyB,
154
+ weight_groups={
155
+ name: len(wg.weights) for name, wg in lhefile.init.weightgroup.items()
156
+ },
157
+ num_events=num_events,
158
+ negative_weighted_events=num_negative_weighted_events,
159
+ process_info=[
160
+ LHEProcess(
161
+ procId=proc.procId,
162
+ xSection=proc.xSection,
163
+ error=proc.error,
164
+ channels=[
165
+ LHEChannel(
166
+ incoming_pdgid=list(incoming_pdgid),
167
+ outgoing_pdgid=list(outgoing_pdgid),
168
+ num_events=count,
169
+ )
170
+ for (
171
+ pid,
172
+ incoming_pdgid,
173
+ outgoing_pdgid,
174
+ ), count in initial_final_combinations.items()
175
+ if pid == proc.procId
176
+ ],
177
+ )
178
+ for proc in lhefile.init.procInfo
179
+ ],
180
+ )
181
+
182
+
183
+ @dataclass
184
+ class LHESummary:
185
+ """Summary information from multiple LHE files."""
186
+
187
+ files: list[LHEInfo]
188
+
189
+ @property
190
+ def total_events(self) -> int:
191
+ total_events = 0
192
+ for lheinfo in self.files:
193
+ total_events += lheinfo.num_events
194
+ return total_events
195
+
196
+ @property
197
+ def total_negative_weighted_events(self) -> int:
198
+ total_negative_weighted_events = 0
199
+ for lheinfo in self.files:
200
+ total_negative_weighted_events += lheinfo.negative_weighted_events
201
+ return total_negative_weighted_events
202
+
203
+ @property
204
+ def negative_weighted_events_ratio(self) -> float:
205
+ """Ratio of negative weighted events to total events."""
206
+ return (
207
+ self.total_negative_weighted_events / self.total_events
208
+ if self.total_events > 0
209
+ else 0.0
210
+ )
211
+
212
+ def __str__(self) -> str:
213
+ lines = []
214
+ for lheinfo in self.files:
215
+ lines.append(str(lheinfo))
216
+ lines.append("=" * 60)
217
+ lines.append(
218
+ f"Total number of events: {self.total_events} (negative: {self.negative_weighted_events_ratio:.2%})"
219
+ )
220
+ lines.append("=" * 60)
221
+ return "\n".join(lines)
222
+
223
+
224
+ def get_lhesummary(
225
+ filepaths_or_fileobjs: list[Union[str, TextIO]],
226
+ ) -> LHESummary:
227
+ lheinfos = []
228
+ # Analyze all files
229
+ for filepath_or_fileobj in filepaths_or_fileobjs:
230
+ lheinfo = get_lheinfo(filepath_or_fileobj)
231
+ lheinfos.append(lheinfo)
232
+
233
+ return LHESummary(files=lheinfos)
234
+
235
+
236
+ def main() -> None:
237
+ """Main CLI function."""
238
+ parser = create_base_parser(
239
+ description="Display information about LHE files",
240
+ formatter_class=argparse.RawDescriptionHelpFormatter,
241
+ epilog="""
242
+ Examples:
243
+ lheinfo file.lhe # Analyze single file (plain format)
244
+ cat file.lhe | lheinfo # Read from stdin
245
+ lheinfo *.lhe # Analyze multiple files
246
+ lheinfo file1.lhe --format=json # Output results in JSON format
247
+ lheinfo file1.lhe --format=yaml # Output results in YAML format
248
+ """,
249
+ )
250
+
251
+ parser.add_argument(
252
+ "files",
253
+ nargs="*",
254
+ help="LHE file(s) to analyze (or read from stdin if not provided)",
255
+ )
256
+
257
+ parser.add_argument(
258
+ "--format",
259
+ choices=["plain", "json", "yaml"],
260
+ default="plain",
261
+ help="Output format (default: plain)",
262
+ )
263
+
264
+ args = parser.parse_args()
265
+
266
+ # Check if reading from stdin
267
+ use_stdin = not args.files and not sys.stdin.isatty()
268
+
269
+ file_inputs: list[Union[str, TextIO]] = []
270
+ if use_stdin:
271
+ # Read from stdin
272
+ file_inputs += [sys.stdin]
273
+ else:
274
+ # Expand file paths
275
+ for pattern in args.files:
276
+ path = Path(pattern)
277
+ if path.exists():
278
+ if path.is_file():
279
+ file_inputs.append(str(path))
280
+ else:
281
+ warnings.warn(f"{pattern} is not a file", UserWarning, stacklevel=2)
282
+ if not file_inputs:
283
+ print("Error: No valid files found and no stdin data", file=sys.stderr)
284
+ sys.exit(1)
285
+
286
+ summary = get_lhesummary(file_inputs)
287
+ if args.format == "json":
288
+ print(
289
+ json.dumps(
290
+ dataclass_with_properties_to_dict(summary),
291
+ indent=2,
292
+ )
293
+ )
294
+ elif args.format == "yaml":
295
+ print(
296
+ yaml.dump(
297
+ dataclass_with_properties_to_dict(summary),
298
+ sort_keys=False,
299
+ )
300
+ )
301
+ else:
302
+ print(str(summary))
303
+
304
+
305
+ if __name__ == "__main__":
306
+ main()