rpp-protocol 0.1.5__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.
rpp/cli.py ADDED
@@ -0,0 +1,608 @@
1
+ """
2
+ RPP Command Line Interface
3
+
4
+ Emulator-proof CLI for RPP operations.
5
+ Works via stdin/stdout with no ANSI codes or cursor control.
6
+ Compatible with: SSH, PuTTY, serial terminals, air-gapped systems.
7
+
8
+ Commands:
9
+ rpp encode --theta T --phi P --shell S --harmonic H
10
+ rpp decode --address 0xADDRESS
11
+ rpp resolve --address 0xADDRESS [--operation read|write]
12
+
13
+ Flags:
14
+ --fancy, -f: Enable ANSI colors for modern terminals
15
+ --visual, -V: Show ASCII diagrams and visual feedback
16
+
17
+ Exit codes:
18
+ 0: Success
19
+ 1: Invalid input
20
+ 2: Resolution denied
21
+ 3: Internal error
22
+ """
23
+
24
+ import sys
25
+ import json
26
+ import argparse
27
+ import io
28
+ from typing import List, Optional, TextIO
29
+
30
+ # Ensure UTF-8 output on Windows
31
+ if sys.platform == "win32":
32
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
33
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
34
+
35
+ from rpp.address import (
36
+ from_raw,
37
+ from_components,
38
+ parse_address,
39
+ MAX_SHELL,
40
+ MAX_THETA,
41
+ MAX_PHI,
42
+ MAX_HARMONIC,
43
+ )
44
+ from rpp.resolver import resolve
45
+ from rpp import visual
46
+ from rpp.i18n import t, get_supported_languages, DEFAULT_LANG
47
+
48
+
49
+ # Exit codes
50
+ EXIT_SUCCESS = 0
51
+ EXIT_INVALID_INPUT = 1
52
+ EXIT_DENIED = 2
53
+ EXIT_ERROR = 3
54
+
55
+
56
+ def output(text: str, file: TextIO = sys.stdout) -> None:
57
+ """Write output line. No ANSI, no color, just plain text."""
58
+ print(text, file=file, flush=True)
59
+
60
+
61
+ def output_json(data: dict, file: TextIO = sys.stdout) -> None:
62
+ """Write JSON output. Compact, single-line for simple parsing."""
63
+ print(json.dumps(data, separators=(",", ":")), file=file, flush=True)
64
+
65
+
66
+ def error(text: str) -> None:
67
+ """Write error message to stderr."""
68
+ print(f"error: {text}", file=sys.stderr, flush=True)
69
+
70
+
71
+ def cmd_encode(args: argparse.Namespace) -> int:
72
+ """Handle encode command."""
73
+ fancy = getattr(args, 'fancy', False)
74
+ show_visual = getattr(args, 'visual', False)
75
+ lang = getattr(args, 'lang', DEFAULT_LANG)
76
+
77
+ try:
78
+ # Validate ranges
79
+ if not (0 <= args.shell <= MAX_SHELL):
80
+ error(f"shell must be 0-{MAX_SHELL}")
81
+ return EXIT_INVALID_INPUT
82
+ if not (0 <= args.theta <= MAX_THETA):
83
+ error(f"theta must be 0-{MAX_THETA}")
84
+ return EXIT_INVALID_INPUT
85
+ if not (0 <= args.phi <= MAX_PHI):
86
+ error(f"phi must be 0-{MAX_PHI}")
87
+ return EXIT_INVALID_INPUT
88
+ if not (0 <= args.harmonic <= MAX_HARMONIC):
89
+ error(f"harmonic must be 0-{MAX_HARMONIC}")
90
+ return EXIT_INVALID_INPUT
91
+
92
+ # Encode
93
+ addr = from_components(args.shell, args.theta, args.phi, args.harmonic)
94
+
95
+ if args.json:
96
+ output_json(addr.to_dict())
97
+ else:
98
+ # Get translated names
99
+ shell_name = t(f"shell_{addr.shell_name.lower()}", lang)
100
+ sector_name = t(f"sector_{addr.sector_name.lower()}", lang)
101
+ grounding = t(f"grounding_{addr.grounding_level.lower()}", lang)
102
+
103
+ # Show operation status
104
+ output(visual.operation_status(t("encode", lang), True, fancy))
105
+ output("")
106
+
107
+ if show_visual:
108
+ # Show full bit layout diagram
109
+ output(visual.bit_layout_diagram(
110
+ addr.shell, addr.theta, addr.phi, addr.harmonic,
111
+ addr.raw, fancy
112
+ ))
113
+ output("")
114
+ # Show consent meter
115
+ output(visual.consent_meter(addr.phi, fancy))
116
+ output("")
117
+ # Show shell tier
118
+ output(f"{t('shell', lang).title()}:")
119
+ output(visual.shell_tier_visual(addr.shell, fancy))
120
+ else:
121
+ # Compact output with mini visual
122
+ output(visual.address_mini(
123
+ addr.to_hex(), shell_name,
124
+ sector_name, grounding, fancy
125
+ ))
126
+ output("")
127
+ output(f" {t('shell', lang)}: {addr.shell} ({shell_name})")
128
+ output(f" {t('theta', lang)}: {addr.theta} ({sector_name})")
129
+ output(f" {t('phi', lang)}: {addr.phi} ({grounding})")
130
+ output(f" {t('harmonic', lang)}: {addr.harmonic}")
131
+
132
+ return EXIT_SUCCESS
133
+
134
+ except ValueError as e:
135
+ error(str(e))
136
+ return EXIT_INVALID_INPUT
137
+ except Exception as e:
138
+ error(f"internal error: {e}")
139
+ return EXIT_ERROR
140
+
141
+
142
+ def cmd_decode(args: argparse.Namespace) -> int:
143
+ """Handle decode command."""
144
+ fancy = getattr(args, 'fancy', False)
145
+ show_visual = getattr(args, 'visual', False)
146
+ lang = getattr(args, 'lang', DEFAULT_LANG)
147
+
148
+ try:
149
+ # Parse address (handles hex or decimal)
150
+ raw = parse_address(args.address)
151
+ addr = from_raw(raw)
152
+
153
+ if args.json:
154
+ output_json(addr.to_dict())
155
+ else:
156
+ # Get translated names
157
+ shell_name = t(f"shell_{addr.shell_name.lower()}", lang)
158
+ sector_name = t(f"sector_{addr.sector_name.lower()}", lang)
159
+ grounding = t(f"grounding_{addr.grounding_level.lower()}", lang)
160
+
161
+ # Show operation status
162
+ output(visual.operation_status(t("decode", lang), True, fancy))
163
+ output("")
164
+
165
+ if show_visual:
166
+ # Show full bit layout diagram
167
+ output(visual.bit_layout_diagram(
168
+ addr.shell, addr.theta, addr.phi, addr.harmonic,
169
+ addr.raw, fancy
170
+ ))
171
+ output("")
172
+ # Show consent meter
173
+ output(visual.consent_meter(addr.phi, fancy))
174
+ output("")
175
+ # Show theta wheel
176
+ output(visual.theta_wheel(addr.theta, fancy))
177
+ else:
178
+ # Compact output with mini visual
179
+ output(visual.address_mini(
180
+ addr.to_hex(), shell_name,
181
+ sector_name, grounding, fancy
182
+ ))
183
+ output("")
184
+ output(f" {t('shell', lang)}: {addr.shell} ({shell_name})")
185
+ output(f" {t('theta', lang)}: {addr.theta} ({sector_name})")
186
+ output(f" {t('phi', lang)}: {addr.phi} ({grounding})")
187
+ output(f" {t('harmonic', lang)}: {addr.harmonic}")
188
+
189
+ return EXIT_SUCCESS
190
+
191
+ except ValueError as e:
192
+ error(str(e))
193
+ return EXIT_INVALID_INPUT
194
+ except Exception as e:
195
+ error(f"internal error: {e}")
196
+ return EXIT_ERROR
197
+
198
+
199
+ def cmd_resolve(args: argparse.Namespace) -> int:
200
+ """Handle resolve command."""
201
+ fancy = getattr(args, 'fancy', False)
202
+ show_visual = getattr(args, 'visual', False)
203
+ lang = getattr(args, 'lang', DEFAULT_LANG)
204
+
205
+ try:
206
+ # Parse address
207
+ raw = parse_address(args.address)
208
+ addr = from_raw(raw)
209
+
210
+ # Build context
211
+ context = {}
212
+ if args.consent:
213
+ context["consent"] = args.consent
214
+ if args.emergency:
215
+ context["emergency_override"] = True
216
+
217
+ # Resolve
218
+ result = resolve(raw, operation=args.operation, context=context)
219
+
220
+ if args.json:
221
+ output_json(result.to_dict())
222
+ else:
223
+ # Show operation status
224
+ output(visual.operation_status(t("resolve", lang), result.allowed, fancy))
225
+ output("")
226
+
227
+ if show_visual:
228
+ # Show routing diagram
229
+ output(visual.routing_diagram(
230
+ addr.shell, result.allowed, result.route,
231
+ result.reason, fancy
232
+ ))
233
+ output("")
234
+ # Show consent meter for context
235
+ output(visual.consent_meter(addr.phi, fancy))
236
+ else:
237
+ # Compact output (ASCII-safe for Windows)
238
+ output(f" {t('allowed', lang)}: {str(result.allowed).lower()}")
239
+ output(f" {t('route', lang)}: {result.route if result.route else 'null'}")
240
+ output(f" {t('reason', lang)}: {result.reason}")
241
+
242
+ # Exit code based on result
243
+ if result.allowed:
244
+ return EXIT_SUCCESS
245
+ else:
246
+ return EXIT_DENIED
247
+
248
+ except ValueError as e:
249
+ error(str(e))
250
+ return EXIT_INVALID_INPUT
251
+ except Exception as e:
252
+ error(f"internal error: {e}")
253
+ return EXIT_ERROR
254
+
255
+
256
+ def cmd_demo(args: argparse.Namespace) -> int:
257
+ """Run demonstration of the three core scenarios."""
258
+ fancy = getattr(args, 'fancy', False)
259
+ lang = getattr(args, 'lang', DEFAULT_LANG)
260
+
261
+ # Show banner
262
+ output(visual.demo_banner(fancy))
263
+ output("")
264
+
265
+ # Scenario 1: Allowed read (low phi)
266
+ output("=" * 60)
267
+ output(f" {t('scenario_1_title', lang)}")
268
+ output("=" * 60)
269
+ output("")
270
+ addr1 = from_components(shell=0, theta=12, phi=40, harmonic=1)
271
+ output(visual.bit_layout_diagram(
272
+ addr1.shell, addr1.theta, addr1.phi, addr1.harmonic,
273
+ addr1.raw, fancy
274
+ ))
275
+ output("")
276
+ result1 = resolve(addr1.raw, operation="read")
277
+ output(visual.routing_diagram(
278
+ addr1.shell, result1.allowed, result1.route,
279
+ result1.reason, fancy
280
+ ))
281
+ output("")
282
+ output(visual.consent_meter(addr1.phi, fancy))
283
+ output("")
284
+
285
+ # Scenario 2: Denied write (high phi)
286
+ output("=" * 60)
287
+ output(f" {t('scenario_2_title', lang)}")
288
+ output("=" * 60)
289
+ output("")
290
+ addr2 = from_components(shell=0, theta=100, phi=450, harmonic=64)
291
+ output(visual.bit_layout_diagram(
292
+ addr2.shell, addr2.theta, addr2.phi, addr2.harmonic,
293
+ addr2.raw, fancy
294
+ ))
295
+ output("")
296
+ result2 = resolve(addr2.raw, operation="write")
297
+ output(visual.routing_diagram(
298
+ addr2.shell, result2.allowed, result2.route,
299
+ result2.reason, fancy
300
+ ))
301
+ output("")
302
+ output(visual.consent_meter(addr2.phi, fancy))
303
+ output("")
304
+
305
+ # Scenario 3: Routed to archive (cold shell)
306
+ output("=" * 60)
307
+ output(f" {t('scenario_3_title', lang)}")
308
+ output("=" * 60)
309
+ output("")
310
+ addr3 = from_components(shell=2, theta=200, phi=128, harmonic=32)
311
+ output(visual.bit_layout_diagram(
312
+ addr3.shell, addr3.theta, addr3.phi, addr3.harmonic,
313
+ addr3.raw, fancy
314
+ ))
315
+ output("")
316
+ result3 = resolve(addr3.raw, operation="read")
317
+ output(visual.routing_diagram(
318
+ addr3.shell, result3.allowed, result3.route,
319
+ result3.reason, fancy
320
+ ))
321
+ output("")
322
+ output("Shell Tiers:")
323
+ output(visual.shell_tier_visual(addr3.shell, fancy))
324
+ output("")
325
+
326
+ # Summary
327
+ output("=" * 60)
328
+ output(visual.success_box(t("demonstration_complete", lang), fancy))
329
+ output("")
330
+ output("Key takeaways:")
331
+ output(f" * {t('takeaway_grounded', lang)}")
332
+ output(f" * {t('takeaway_ethereal', lang)}")
333
+ output(f" * {t('takeaway_cold', lang)}")
334
+ output("")
335
+
336
+ return EXIT_SUCCESS
337
+
338
+
339
+ def cmd_version(args: argparse.Namespace) -> int:
340
+ """Show version."""
341
+ from rpp import __version__
342
+ output(f"rpp {__version__}")
343
+ return EXIT_SUCCESS
344
+
345
+
346
+ def cmd_tutorial(args: argparse.Namespace) -> int:
347
+ """Run interactive tutorial explaining RPP concepts."""
348
+ fancy = getattr(args, 'fancy', False)
349
+ lang = getattr(args, 'lang', DEFAULT_LANG)
350
+
351
+ output(visual.demo_banner(fancy))
352
+ output("")
353
+ output("=" * 60)
354
+ output(f" {t('tutorial_welcome', lang)}")
355
+ output("=" * 60)
356
+ output("")
357
+
358
+ # Section 1: What is RPP?
359
+ output("-" * 60)
360
+ output(f" SECTION 1: {t('tutorial_what_is', lang)}")
361
+ output("-" * 60)
362
+ output("")
363
+ output("RPP (Rotational Packet Protocol) encodes MEANING directly")
364
+ output("into addresses. Instead of opaque memory locations, every")
365
+ output("28-bit address carries semantic information:")
366
+ output("")
367
+ output(" * WHERE data lives (Shell: storage tier)")
368
+ output(" * WHAT type it is (Theta: semantic sector)")
369
+ output(" * WHO can access it (Phi: consent/grounding level)")
370
+ output(" * HOW it behaves (Harmonic: frequency/mode)")
371
+ output("")
372
+
373
+ # Section 2: The 28-bit Structure
374
+ output("-" * 60)
375
+ output(f" SECTION 2: {t('tutorial_address', lang)}")
376
+ output("-" * 60)
377
+ output("")
378
+ output("Every RPP address is exactly 28 bits:")
379
+ output("")
380
+ output(" +--------+-------+---------+---------+----------+")
381
+ output(" | Reserved| Shell | Theta | Phi | Harmonic |")
382
+ output(" | 4 bits | 2 bits| 9 bits | 9 bits | 8 bits |")
383
+ output(" +--------+-------+---------+---------+----------+")
384
+ output("")
385
+
386
+ # Show example
387
+ example_addr = from_components(shell=1, theta=150, phi=200, harmonic=42)
388
+ output("Example: Encoding shell=1, theta=150, phi=200, harmonic=42")
389
+ output("")
390
+ output(visual.bit_layout_diagram(
391
+ example_addr.shell, example_addr.theta, example_addr.phi,
392
+ example_addr.harmonic, example_addr.raw, fancy
393
+ ))
394
+ output("")
395
+
396
+ # Section 3: Shell Tiers
397
+ output("-" * 60)
398
+ output(" SECTION 3: Shell Tiers (Storage Routing)")
399
+ output("-" * 60)
400
+ output("")
401
+ output("The SHELL (2 bits) determines storage tier routing:")
402
+ output("")
403
+ output(" Shell 0 (Hot): In-memory, immediate access")
404
+ output(" Shell 1 (Warm): Fast storage, quick retrieval")
405
+ output(" Shell 2 (Cold): Archive storage, slower access")
406
+ output(" Shell 3 (Frozen): Deep archive, explicit consent required")
407
+ output("")
408
+ output(visual.shell_tier_visual(1, fancy))
409
+ output("")
410
+
411
+ # Section 4: Theta Sectors
412
+ output("-" * 60)
413
+ output(" SECTION 4: Theta Sectors (Semantic Classification)")
414
+ output("-" * 60)
415
+ output("")
416
+ output("The THETA (9 bits, 0-511) classifies data semantically:")
417
+ output("")
418
+ output(" 0-63: Gene - Core identity, genetic data")
419
+ output(" 64-127: Memory - Experiential records")
420
+ output(" 128-191: Witness - Observational data")
421
+ output(" 192-255: Dream - Aspirational/creative content")
422
+ output(" 256-319: Bridge - Connective/relational data")
423
+ output(" 320-383: Guardian - Protective/security data")
424
+ output(" 384-447: Emergence - Evolving/transforming content")
425
+ output(" 448-511: Meta - Self-referential/meta data")
426
+ output("")
427
+ output(visual.theta_wheel(150, fancy))
428
+ output("")
429
+
430
+ # Section 5: Phi Grounding
431
+ output("-" * 60)
432
+ output(" SECTION 5: Phi Grounding (Consent Levels)")
433
+ output("-" * 60)
434
+ output("")
435
+ output("The PHI (9 bits, 0-511) determines consent requirements:")
436
+ output("")
437
+ output(" 0-127: Grounded - Open access, no consent needed")
438
+ output(" 128-255: Transitional - Basic consent for modifications")
439
+ output(" 256-383: Abstract - Elevated consent required")
440
+ output(" 384-511: Ethereal - Explicit consent for all ops")
441
+ output("")
442
+ output("Low phi (grounded):")
443
+ output(visual.consent_meter(50, fancy))
444
+ output("")
445
+ output("High phi (ethereal):")
446
+ output(visual.consent_meter(450, fancy))
447
+ output("")
448
+
449
+ # Section 6: Resolution
450
+ output("-" * 60)
451
+ output(f" SECTION 6: {t('tutorial_resolver', lang)}")
452
+ output("-" * 60)
453
+ output("")
454
+ output("When you access an address, the RESOLVER checks:")
455
+ output("")
456
+ output(" 1. Is the operation (read/write/delete) allowed?")
457
+ output(" 2. Does the phi level require consent?")
458
+ output(" 3. Which storage backend should handle it?")
459
+ output("")
460
+
461
+ # Show allowed vs denied
462
+ allowed_addr = from_components(shell=0, theta=12, phi=40, harmonic=1)
463
+ result_ok = resolve(allowed_addr.raw, operation="read")
464
+ output("Example: Reading from grounded zone (phi=40)")
465
+ output(visual.routing_diagram(
466
+ allowed_addr.shell, result_ok.allowed, result_ok.route,
467
+ result_ok.reason, fancy
468
+ ))
469
+ output("")
470
+
471
+ denied_addr = from_components(shell=0, theta=100, phi=450, harmonic=64)
472
+ result_denied = resolve(denied_addr.raw, operation="write")
473
+ output("Example: Writing to ethereal zone (phi=450)")
474
+ output(visual.routing_diagram(
475
+ denied_addr.shell, result_denied.allowed, result_denied.route,
476
+ result_denied.reason, fancy
477
+ ))
478
+ output("")
479
+
480
+ # Summary
481
+ output("=" * 60)
482
+ output(visual.success_box(t("demonstration_complete", lang), fancy))
483
+ output("")
484
+ output(f"{t('tutorial_try_it', lang)}:")
485
+ output(" rpp encode --shell 0 --theta 12 --phi 40 --harmonic 1")
486
+ output(" rpp decode --address 0x0182801")
487
+ output(" rpp resolve --address 0x0182801 --operation read")
488
+ output(" rpp demo")
489
+ output("")
490
+ output("Add --visual for detailed diagrams, --fancy for colors!")
491
+ output("")
492
+
493
+ return EXIT_SUCCESS
494
+
495
+
496
+ def build_parser() -> argparse.ArgumentParser:
497
+ """Build the argument parser."""
498
+ parser = argparse.ArgumentParser(
499
+ prog="rpp",
500
+ description="RPP - Rotational Packet Protocol CLI",
501
+ epilog="See https://github.com/anywave/rpp-spec for documentation.",
502
+ )
503
+
504
+ parser.add_argument(
505
+ "--version", "-v",
506
+ action="store_true",
507
+ help="Show version and exit",
508
+ )
509
+
510
+ parser.add_argument(
511
+ "--fancy", "-f",
512
+ action="store_true",
513
+ help="Enable ANSI colors for modern terminals",
514
+ )
515
+
516
+ parser.add_argument(
517
+ "--visual", "-V",
518
+ action="store_true",
519
+ help="Show detailed ASCII diagrams and visual feedback",
520
+ )
521
+
522
+ parser.add_argument(
523
+ "--lang", "-l",
524
+ type=str,
525
+ default=DEFAULT_LANG,
526
+ choices=get_supported_languages(),
527
+ help="Output language (en, ar-gulf, ar-hejaz, es, ru)",
528
+ )
529
+
530
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
531
+
532
+ # Encode command
533
+ encode_parser = subparsers.add_parser(
534
+ "encode",
535
+ help="Encode components into an RPP address",
536
+ )
537
+ encode_parser.add_argument("--shell", "-s", type=int, required=True, help=f"Shell tier (0-{MAX_SHELL})")
538
+ encode_parser.add_argument("--theta", "-t", type=int, required=True, help=f"Theta sector (0-{MAX_THETA})")
539
+ encode_parser.add_argument("--phi", "-p", type=int, required=True, help=f"Phi grounding (0-{MAX_PHI})")
540
+ encode_parser.add_argument("--harmonic", "-H", type=int, required=True, help=f"Harmonic (0-{MAX_HARMONIC})")
541
+ encode_parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
542
+ encode_parser.set_defaults(func=cmd_encode)
543
+
544
+ # Decode command
545
+ decode_parser = subparsers.add_parser(
546
+ "decode",
547
+ help="Decode an RPP address into components",
548
+ )
549
+ decode_parser.add_argument("--address", "-a", type=str, required=True, help="Address (hex or decimal)")
550
+ decode_parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
551
+ decode_parser.set_defaults(func=cmd_decode)
552
+
553
+ # Resolve command
554
+ resolve_parser = subparsers.add_parser(
555
+ "resolve",
556
+ help="Resolve an RPP address to a routing decision",
557
+ )
558
+ resolve_parser.add_argument("--address", "-a", type=str, required=True, help="Address (hex or decimal)")
559
+ resolve_parser.add_argument("--operation", "-o", type=str, default="read", choices=["read", "write", "delete"], help="Operation type")
560
+ resolve_parser.add_argument("--consent", "-c", type=str, choices=["none", "diminished", "full", "explicit"], help="Consent level")
561
+ resolve_parser.add_argument("--emergency", "-e", action="store_true", help="Emergency override")
562
+ resolve_parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
563
+ resolve_parser.set_defaults(func=cmd_resolve)
564
+
565
+ # Demo command
566
+ demo_parser = subparsers.add_parser(
567
+ "demo",
568
+ help="Run demonstration of core scenarios",
569
+ )
570
+ demo_parser.set_defaults(func=cmd_demo)
571
+
572
+ # Version command
573
+ version_parser = subparsers.add_parser(
574
+ "version",
575
+ help="Show version",
576
+ )
577
+ version_parser.set_defaults(func=cmd_version)
578
+
579
+ # Tutorial command
580
+ tutorial_parser = subparsers.add_parser(
581
+ "tutorial",
582
+ help="Interactive tutorial explaining RPP concepts",
583
+ )
584
+ tutorial_parser.set_defaults(func=cmd_tutorial)
585
+
586
+ return parser
587
+
588
+
589
+ def main(argv: Optional[List[str]] = None) -> int:
590
+ """Main entry point."""
591
+ parser = build_parser()
592
+ args = parser.parse_args(argv)
593
+
594
+ # Handle --version flag at top level
595
+ if args.version:
596
+ return cmd_version(args)
597
+
598
+ # No command specified
599
+ if args.command is None:
600
+ parser.print_help()
601
+ return EXIT_SUCCESS
602
+
603
+ # Run command
604
+ return args.func(args)
605
+
606
+
607
+ if __name__ == "__main__":
608
+ sys.exit(main())