mimosa-tool 1.0.0__cp314-cp314-win_amd64.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.
mimosa/cli.py ADDED
@@ -0,0 +1,619 @@
1
+ import argparse
2
+ import json
3
+ import logging
4
+ import os
5
+ import sys
6
+ from typing import Any, Dict
7
+
8
+ from mimosa.pipeline import run_pipeline
9
+
10
+
11
+ def setup_logging(verbose: bool):
12
+ """Setup logging configuration."""
13
+ level = logging.DEBUG if verbose else logging.INFO
14
+ logging.basicConfig(level=level, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
15
+ if verbose:
16
+ logging.getLogger("numba").setLevel(logging.WARNING)
17
+
18
+
19
+ def create_arg_parser() -> argparse.ArgumentParser:
20
+ """Create and configure argument parser with subcommands."""
21
+ parser = argparse.ArgumentParser(
22
+ description=("MIMOSA: Compare motifs using three distinct approaches - profile, motif, and tomtom-like"),
23
+ formatter_class=argparse.RawDescriptionHelpFormatter,
24
+ epilog="""
25
+ Examples:
26
+ # Profile-based comparison
27
+ mimosa profile scores_1.fasta scores_2.fasta \\
28
+ --metric corr --permutations 1000 --distortion 0.5
29
+
30
+ # Motif-based comparison with PWM models
31
+ mimosa motif model1.meme model2.pfm --model1-type pwm --model2-type pwm \\
32
+ --fasta sequences.fa --metric co --permutations 1000 \\
33
+ --distortion 0.3
34
+
35
+ # Motif-based comparison with BAMM models
36
+ mimosa motif model1.hbcp model2.ihbcp --model1-type bamm --model2-type bamm \\
37
+ --fasta sequences.fa --promoters promoters.fa --search-range 15 \\
38
+ --min-kernel-size 5 --max-kernel-size 15 --jobs 4 --seed 42
39
+
40
+ # Motali comparison with SiteGA models
41
+ mimosa motali model1.mat model2.meme --model1-type sitega --model2-type pwm \\
42
+ --fasta sequences.fa --promoters promoters.fa \\
43
+ --tmp-dir . --num-sequences 5000 --seq-length 150
44
+
45
+ # TomTom-like comparison with PWM models
46
+ mimosa tomtom-like model1.meme model2.pfm --model1-type pwm --model2-type pwm \\
47
+ --metric pcc --permutations 1000 --permute-rows \\
48
+ --jobs 8 --seed 123
49
+
50
+ # TomTom-like comparison with BAMM models using PFM mode
51
+ mimosa tomtom-like model1.hbcp model2.ihbcp --model1-type bamm --model2-type bamm \\
52
+ --pfm-mode --num-sequences 10000 --seq-length 120 --metric ed \\
53
+ --permutations 500 --permute-rows
54
+ """,
55
+ )
56
+
57
+ # Create subparsers for the three main modes
58
+ subparsers = parser.add_subparsers(dest="mode", help="Operation mode", required=True)
59
+
60
+ # Score-based comparison subcommand
61
+ profile_parser = subparsers.add_parser(
62
+ "profile", help="Compare motifs based on pre-calculated score profiles (uses DataComparator engine)."
63
+ )
64
+ profile_parser.add_argument("profile1", help="Path to the first profile file containing pre-calculated scores.")
65
+ profile_parser.add_argument("profile2", help="Path to the second profile file containing pre-calculated scores.")
66
+
67
+ profile_group = profile_parser.add_argument_group("Profile Comparator Options")
68
+ profile_group.add_argument(
69
+ "--metric",
70
+ choices=["cj", "co", "corr"],
71
+ default="cj",
72
+ help=(
73
+ "Similarity metric for comparing frequency profiles. "
74
+ "Choices: cj (Continuous Jaccard), co (Continuous Overlap), "
75
+ "corr (Pearson Correlation). (default: %(default)s)"
76
+ ),
77
+ )
78
+ profile_group.add_argument(
79
+ "--permutations",
80
+ type=int,
81
+ default=0,
82
+ help="Number of permutations to perform for p-value calculation. (default: %(default)s)",
83
+ )
84
+ profile_group.add_argument(
85
+ "--distortion",
86
+ type=float,
87
+ default=0.4,
88
+ help=(
89
+ "Distortion level (0.0-1.0) applied to kernels during surrogate data generation. "
90
+ "Higher values increase variance in the null model. Used for cj and co options. "
91
+ "(default: %(default)s)"
92
+ ),
93
+ )
94
+ profile_group.add_argument(
95
+ "--search-range",
96
+ type=int,
97
+ default=10,
98
+ help="Maximum offset (shift) range to explore when aligning profiles. (default: %(default)s)",
99
+ )
100
+ profile_group.add_argument(
101
+ "--min-kernel-size",
102
+ type=int,
103
+ default=3,
104
+ help=(
105
+ "Minimum kernel size for convolution during surrogate generation. "
106
+ "Used for cj and co options. (default: %(default)s)"
107
+ ),
108
+ )
109
+ profile_group.add_argument(
110
+ "--max-kernel-size",
111
+ type=int,
112
+ default=11,
113
+ help=(
114
+ "Maximum kernel size for convolution during surrogate generation. "
115
+ "Used for cj and co options. (default: %(default)s)"
116
+ ),
117
+ )
118
+
119
+ profile_technical_group = profile_parser.add_argument_group("Technical Options")
120
+ profile_technical_group.add_argument(
121
+ "-v",
122
+ "--verbose",
123
+ action="store_true",
124
+ help="Enable verbose logging to standard output for detailed execution tracking.",
125
+ )
126
+ profile_technical_group.add_argument(
127
+ "--seed",
128
+ type=int,
129
+ help=(
130
+ "Set a global random seed for reproducible results in stochastic operations "
131
+ "(e.g., permutations, surrogate generation)."
132
+ ),
133
+ )
134
+ profile_technical_group.add_argument(
135
+ "--jobs",
136
+ type=int,
137
+ default=-1,
138
+ help="Number of parallel jobs to run. Set to -1 to use all available CPU cores. (default: %(default)s)",
139
+ )
140
+
141
+ # Motif scan-based comparison subcommand
142
+ motif_parser = subparsers.add_parser(
143
+ "motif", help="Compare motifs by calculating scores derived from scanning sequences with models."
144
+ )
145
+ motif_parser.add_argument("model1", help="Path to the first motif model file.")
146
+ motif_parser.add_argument("model2", help="Path to the second motif model file.")
147
+
148
+ # Input/Output Options for sequence parser
149
+ motif_io_group = motif_parser.add_argument_group("Input/Output Options")
150
+ motif_io_group.add_argument(
151
+ "--model1-type",
152
+ choices=["pwm", "bamm", "sitega"],
153
+ required=True,
154
+ help="Format of the first model. Choices: pwm, bamm, sitega.",
155
+ )
156
+ motif_io_group.add_argument(
157
+ "--model2-type",
158
+ choices=["pwm", "bamm", "sitega"],
159
+ required=True,
160
+ help="Format of the second model. Choices: pwm, bamm, sitega.",
161
+ )
162
+ motif_io_group.add_argument(
163
+ "--fasta",
164
+ help=(
165
+ "Path to a FASTA file containing target sequences for comparison. "
166
+ "If omitted, random sequences are generated."
167
+ ),
168
+ )
169
+ motif_io_group.add_argument(
170
+ "--promoters",
171
+ help=(
172
+ "Path to a FASTA file containing promoter sequences, required for "
173
+ "calculating threshold tables in Motali comparisons."
174
+ ),
175
+ )
176
+ motif_io_group.add_argument(
177
+ "--num-sequences",
178
+ type=int,
179
+ default=1000,
180
+ help="Number of random sequences to generate if --fasta is not provided. (default: %(default)s)",
181
+ )
182
+ motif_io_group.add_argument(
183
+ "--seq-length",
184
+ type=int,
185
+ default=200,
186
+ help="Length of each random sequence to generate if --fasta is not provided. (default: %(default)s)",
187
+ )
188
+
189
+ # Motif / Data Comparator options
190
+ motif_group = motif_parser.add_argument_group("Motif Comparator Options")
191
+ motif_group.add_argument(
192
+ "--metric",
193
+ choices=["cj", "co", "corr"],
194
+ default="cj",
195
+ help=(
196
+ "Similarity metric for comparing frequency profiles. "
197
+ "Choices: cj (Continuous Jaccard), co (Continuous Overlap), "
198
+ "corr (Pearson Correlation). (default: %(default)s)"
199
+ ),
200
+ )
201
+ motif_group.add_argument(
202
+ "--permutations",
203
+ type=int,
204
+ default=0,
205
+ help="Number of permutations to perform for p-value calculation. (default: %(default)s)",
206
+ )
207
+ motif_group.add_argument(
208
+ "--distortion",
209
+ type=float,
210
+ default=0.4,
211
+ help=(
212
+ "Distortion level (0.0-1.0) applied to kernels during surrogate data generation. "
213
+ "Higher values increase variance in the null model. Used for cj and co options. "
214
+ "(default: %(default)s)"
215
+ ),
216
+ )
217
+ motif_group.add_argument(
218
+ "--search-range",
219
+ type=int,
220
+ default=10,
221
+ help="Maximum offset (shift) range to explore when aligning profiles. (default: %(default)s)",
222
+ )
223
+ motif_group.add_argument(
224
+ "--min-kernel-size",
225
+ type=int,
226
+ default=3,
227
+ help=(
228
+ "Minimum kernel size for convolution during surrogate generation. "
229
+ "Used for cj and co options. (default: %(default)s)"
230
+ ),
231
+ )
232
+ motif_group.add_argument(
233
+ "--max-kernel-size",
234
+ type=int,
235
+ default=11,
236
+ help=(
237
+ "Maximum kernel size for convolution during surrogate generation. "
238
+ "Used for cj and co options. (default: %(default)s)"
239
+ ),
240
+ )
241
+
242
+ motif_technical_group = motif_parser.add_argument_group("Technical Options")
243
+ motif_technical_group.add_argument(
244
+ "-v",
245
+ "--verbose",
246
+ action="store_true",
247
+ help="Enable verbose logging to standard output for detailed execution tracking.",
248
+ )
249
+ motif_technical_group.add_argument(
250
+ "--seed",
251
+ type=int,
252
+ help=(
253
+ "Set a global random seed for reproducible results in stochastic operations "
254
+ "(e.g., permutations, surrogate generation)."
255
+ ),
256
+ )
257
+ motif_technical_group.add_argument(
258
+ "--jobs",
259
+ type=int,
260
+ default=-1,
261
+ help="Number of parallel jobs to run. Set to -1 to use all available CPU cores. (default: %(default)s)",
262
+ )
263
+
264
+ # Motali
265
+ motali_parser = subparsers.add_parser(
266
+ "motali", help="Compare motifs by calculating PRC AUC derived from scanning sequences with models."
267
+ )
268
+
269
+ # Motali-specific options
270
+ motali_group = motali_parser.add_argument_group("Motali Options")
271
+
272
+ motali_group.add_argument("model1", help="Path to the first motif model file.")
273
+ motali_group.add_argument("model2", help="Path to the second motif model file.")
274
+
275
+ # Input/Output Options for sequence parser
276
+ motali_io_group = motali_parser.add_argument_group("Input/Output Options")
277
+ motali_io_group.add_argument(
278
+ "--model1-type",
279
+ choices=["pwm", "sitega"],
280
+ required=True,
281
+ help="Format of the first model. Choices: pwm, bamm, sitega.",
282
+ )
283
+ motali_io_group.add_argument(
284
+ "--model2-type",
285
+ choices=["pwm", "sitega"],
286
+ required=True,
287
+ help="Format of the second model. Choices: pwm, bamm, sitega.",
288
+ )
289
+ motali_io_group.add_argument(
290
+ "--fasta",
291
+ help=(
292
+ "Path to a FASTA file containing target sequences for comparison. "
293
+ "If omitted, random sequences are generated."
294
+ ),
295
+ )
296
+ motali_io_group.add_argument(
297
+ "--promoters",
298
+ help=(
299
+ "Path to a FASTA file containing promoter sequences, required for "
300
+ "calculating threshold tables in Motali comparisons."
301
+ ),
302
+ )
303
+ motali_io_group.add_argument(
304
+ "--num-sequences",
305
+ type=int,
306
+ default=10000,
307
+ help="Number of random sequences to generate if --fasta is not provided. (default: %(default)s)",
308
+ )
309
+ motali_io_group.add_argument(
310
+ "--seq-length",
311
+ type=int,
312
+ default=200,
313
+ help="Length of each random sequence to generate if --fasta is not provided. (default: %(default)s)",
314
+ )
315
+
316
+ motali_io_group.add_argument(
317
+ "--tmp-dir",
318
+ default=".",
319
+ help=(
320
+ "Directory path for storing temporary intermediate files generated "
321
+ "during Motali execution. (default: %(default)s)"
322
+ ),
323
+ )
324
+
325
+ motali_technical_group = motali_parser.add_argument_group("Technical Options")
326
+ motali_technical_group.add_argument(
327
+ "-v",
328
+ "--verbose",
329
+ action="store_true",
330
+ help="Enable verbose logging to standard output for detailed execution tracking.",
331
+ )
332
+
333
+ # TomTom-like comparison subcommand
334
+ tomtom_parser = subparsers.add_parser(
335
+ "tomtom-like", help="Compare motifs by direct matrix comparison (uses TomTomComparator engine)."
336
+ )
337
+ tomtom_parser.add_argument("model1", help="Path to the first motif model file.")
338
+ tomtom_parser.add_argument("model2", help="Path to the second motif model file.")
339
+
340
+ # Input/Output Options for tomtom parser
341
+ tomtom_io_group = tomtom_parser.add_argument_group("Input/Output Options")
342
+ tomtom_io_group.add_argument(
343
+ "--model1-type",
344
+ choices=["pwm", "bamm", "sitega"],
345
+ required=True,
346
+ help="Format of the first model. Choices: pwm, bamm, sitega.",
347
+ )
348
+ tomtom_io_group.add_argument(
349
+ "--model2-type",
350
+ choices=["pwm", "bamm", "sitega"],
351
+ required=True,
352
+ help="Format of the second model. Choices: pwm, bamm, sitega.",
353
+ )
354
+
355
+ # TomTom-specific options
356
+ tomtom_options_group = tomtom_parser.add_argument_group("TomTom Options")
357
+ tomtom_options_group.add_argument(
358
+ "--metric",
359
+ choices=["pcc", "ed", "cosine"],
360
+ default="pcc",
361
+ help=(
362
+ "Metric for column-wise motif comparison. "
363
+ "Choices: pcc (Pearson Correlation Coefficient), ed (Euclidean Distance), "
364
+ "cosine (Cosine Similarity). (default: %(default)s)"
365
+ ),
366
+ )
367
+ tomtom_options_group.add_argument(
368
+ "--permutations",
369
+ type=int,
370
+ default=0,
371
+ help="Number of Monte Carlo permutations for p-value estimation. (default: %(default)s)",
372
+ )
373
+ tomtom_options_group.add_argument(
374
+ "--permute-rows",
375
+ action="store_true",
376
+ help=(
377
+ "If set, shuffles values within columns during permutation, destroying "
378
+ "nucleotide dependencies. Default behavior shuffles only columns (positions)."
379
+ ),
380
+ )
381
+ tomtom_options_group.add_argument(
382
+ "--pfm-mode",
383
+ action="store_true",
384
+ help=(
385
+ "If set, a Position Frequency Matrix (PFM) is derived for the model motifs "
386
+ "by scanning sequences and constructing the PFM based on the top 5% of "
387
+ "predicted binding sites"
388
+ ),
389
+ )
390
+
391
+ tomtom_options_group.add_argument(
392
+ "--num-sequences",
393
+ type=int,
394
+ default=20000,
395
+ help="Number of random sequences to generate if --pfm-mode is used. (default: %(default)s)",
396
+ )
397
+ tomtom_options_group.add_argument(
398
+ "--seq-length",
399
+ type=int,
400
+ default=100,
401
+ help="Length of each random sequence to generate if --pfm-mode is used. (default: %(default)s)",
402
+ )
403
+
404
+ tomtom_technical_group = tomtom_parser.add_argument_group("Technical Options")
405
+ tomtom_technical_group.add_argument(
406
+ "-v",
407
+ "--verbose",
408
+ action="store_true",
409
+ help="Enable verbose logging to standard output for detailed execution tracking.",
410
+ )
411
+ tomtom_technical_group.add_argument(
412
+ "--seed",
413
+ type=int,
414
+ help=(
415
+ "Set a global random seed for reproducible results in stochastic operations "
416
+ "(e.g., permutations, surrogate generation)."
417
+ ),
418
+ )
419
+ tomtom_technical_group.add_argument(
420
+ "--jobs",
421
+ type=int,
422
+ default=-1,
423
+ help="Number of parallel jobs to run. Set to -1 to use all available CPU cores. (default: %(default)s)",
424
+ )
425
+
426
+ return parser
427
+
428
+
429
+ def validate_inputs(args) -> None:
430
+ """Validate input files and parameters."""
431
+ logger = logging.getLogger(__name__)
432
+ # Validate mode-specific inputs
433
+ if args.mode == "profile":
434
+ if not os.path.exists(args.profile1):
435
+ logger.error(f"Profile file not found: {args.profile1}")
436
+ sys.exit(1)
437
+ if not os.path.exists(args.profile2):
438
+ logger.error(f"Profile file not found: {args.profile2}")
439
+ sys.exit(1)
440
+
441
+ elif args.mode in ["motif", "motali"]:
442
+ suffix_1 = ""
443
+ suffix_2 = ""
444
+ if args.model1_type == "bamm":
445
+ suffix_1 = ".ihbcp"
446
+ if args.model2_type == "bamm":
447
+ suffix_2 = ".ihbcp"
448
+
449
+ if not os.path.exists(args.model1 + suffix_1):
450
+ logger.error(f"Model file not found: {args.model1}")
451
+ sys.exit(1)
452
+ if not os.path.exists(args.model2 + suffix_2):
453
+ logger.error(f"Model file not found: {args.model2}")
454
+ sys.exit(1)
455
+ if args.fasta and not os.path.exists(args.fasta):
456
+ logger.error(f"FASTA file not found: {args.fasta}")
457
+ sys.exit(1)
458
+ if args.promoters and not os.path.exists(args.promoters):
459
+ logger.error(f"Promoter threshold file not found: {args.promoters}")
460
+ sys.exit(1)
461
+
462
+ elif args.mode == "tomtom-like":
463
+ suffix_1 = ""
464
+ suffix_2 = ""
465
+ if args.model1_type == "bamm":
466
+ suffix_1 = ".ihbcp"
467
+ if args.model2_type == "bamm":
468
+ suffix_2 = ".ihbcp"
469
+
470
+ if not os.path.exists(args.model1 + suffix_1):
471
+ logger.error(f"Model file not found: {args.model1}")
472
+ sys.exit(1)
473
+ if not os.path.exists(args.model2 + suffix_2):
474
+ logger.error(f"Model file not found: {args.model2}")
475
+ sys.exit(1)
476
+
477
+
478
+ def map_args_to_pipeline_kwargs(args) -> Dict[str, Any]:
479
+ """Map CLI arguments to pipeline keyword arguments."""
480
+ kwargs = {}
481
+
482
+ if args.mode == "tomtom-like":
483
+ kwargs.update(
484
+ {
485
+ "metric": getattr(args, "metric", "pcc"),
486
+ "n_permutations": getattr(args, "permutations", 1000),
487
+ "permute_rows": getattr(args, "permute_rows", False),
488
+ "n_jobs": getattr(args, "jobs", -1),
489
+ "seed": getattr(args, "seed", None),
490
+ "pfm_mode": getattr(args, "pfm_mode", False),
491
+ "comparator": "tomtom",
492
+ }
493
+ )
494
+ elif args.mode == "motali":
495
+ kwargs.update({"fasta_path": getattr(args, "fasta", None), "tmp_directory": getattr(args, "tmp_dir", ".")})
496
+ elif args.mode == "motif":
497
+ kwargs.update(
498
+ {
499
+ "metric": getattr(args, "metric", "cj"),
500
+ "n_permutations": getattr(args, "permutations", 1000),
501
+ "distortion_level": getattr(args, "distortion", 0.4),
502
+ "n_jobs": getattr(args, "jobs", -1),
503
+ "permute_rows": getattr(args, "permute_rows", False),
504
+ "pfm_mode": getattr(args, "pfm_mode", False),
505
+ "seed": getattr(args, "seed", None),
506
+ "search_range": getattr(args, "search_range", 10),
507
+ "min_kernel_size": getattr(args, "min_kernel_size", 3),
508
+ "max_kernel_size": getattr(args, "max_kernel_size", 11),
509
+ }
510
+ )
511
+ elif args.mode == "profile":
512
+ kwargs.update(
513
+ {
514
+ "metric": getattr(args, "metric", "cj"),
515
+ "n_permutations": getattr(args, "permutations", 1000),
516
+ "distortion_level": getattr(args, "distortion", 0.4),
517
+ "n_jobs": getattr(args, "jobs", -1),
518
+ "seed": getattr(args, "seed", None),
519
+ "search_range": getattr(args, "search_range", 10),
520
+ "min_kernel_size": getattr(args, "min_kernel_size", 3),
521
+ "max_kernel_size": getattr(args, "max_kernel_size", 11),
522
+ }
523
+ )
524
+
525
+ return kwargs
526
+
527
+
528
+ def main_cli():
529
+ """Main CLI entry point."""
530
+ # Parse arguments
531
+ parser = create_arg_parser()
532
+
533
+ if len(sys.argv) == 1:
534
+ parser.print_help(sys.stderr)
535
+ sys.exit(1)
536
+
537
+ args = parser.parse_args()
538
+
539
+ # Setup logging
540
+ setup_logging(args.verbose)
541
+
542
+ # Validate inputs
543
+ validate_inputs(args)
544
+
545
+ # Prepare pipeline arguments based on mode
546
+ if args.mode == "profile":
547
+ # Profile-based comparison
548
+ model1_path = args.profile1
549
+ model2_path = args.profile2
550
+ seq_source1 = None
551
+ seq_source2 = None
552
+
553
+ elif args.mode in ["motif", "motali"]:
554
+ # Sequence-based comparison
555
+ model1_path = args.model1
556
+ model2_path = args.model2
557
+ seq_source1 = args.fasta
558
+ seq_source2 = args.promoters
559
+
560
+ elif args.mode == "tomtom-like":
561
+ model1_path = args.model1
562
+ model2_path = args.model2
563
+ seq_source1 = None
564
+ seq_source2 = None
565
+
566
+ else:
567
+ logger = logging.getLogger(__name__)
568
+ logger.error(f"Unknown mode: {args.mode}")
569
+ sys.exit(1)
570
+
571
+ # Map CLI arguments to pipeline kwargs
572
+ pipeline_kwargs = map_args_to_pipeline_kwargs(args)
573
+
574
+ if args.verbose:
575
+ logger = logging.getLogger(__name__)
576
+ logger.info("=" * 60)
577
+ logger.info(f"UniMotifComparator Pipeline - {args.mode.capitalize()} Mode")
578
+ logger.info("=" * 60)
579
+ logger.info(f"Comparison method: {args.mode}")
580
+ logger.info(f"Model 1: {model1_path}")
581
+ logger.info(f"Model 2: {model2_path}")
582
+ if args.mode in ["motif", "motali", "tomtom-like"]:
583
+ logger.info(f"Model 1 type: {getattr(args, 'model1_type', 'N/A')}")
584
+ logger.info(f"Model 2 type: {getattr(args, 'model2_type', 'N/A')}")
585
+ if args.mode in ["motif", "motali"]:
586
+ logger.info(f"Sequences: {args.fasta or 'Generated internally'}")
587
+ logger.info("=" * 60)
588
+
589
+ try:
590
+ # Run the pipeline
591
+ comparison_type = args.mode
592
+ result = run_pipeline(
593
+ model1_path=model1_path,
594
+ model2_path=model2_path,
595
+ model1_type=getattr(args, "model1_type", ""),
596
+ model2_type=getattr(args, "model2_type", ""),
597
+ comparison_type=comparison_type,
598
+ seq_source1=seq_source1,
599
+ seq_source2=seq_source2,
600
+ num_sequences=getattr(args, "num_sequences", 1000),
601
+ seq_length=getattr(args, "seq_length", 200),
602
+ **pipeline_kwargs,
603
+ )
604
+
605
+ logger = logging.getLogger(__name__)
606
+ json_string = json.dumps(result)
607
+ print(json_string)
608
+
609
+ except Exception as e:
610
+ print(f"ERROR: Pipeline execution failed: {e}")
611
+ if args.verbose:
612
+ import traceback
613
+
614
+ traceback.print_exc()
615
+ sys.exit(1)
616
+
617
+
618
+ if __name__ == "__main__":
619
+ main_cli()