ravmem-cli 1.2.0__tar.gz → 1.3.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.
- {ravmem_cli-1.2.0/ravmem_cli.egg-info → ravmem_cli-1.3.0}/PKG-INFO +1 -1
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/pyproject.toml +1 -1
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/license.py +158 -76
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/main.py +10 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0/ravmem_cli.egg-info}/PKG-INFO +1 -1
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/LICENSE +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/README.md +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/__init__.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/_common.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/_errors.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/analyze.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/audit.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/auth.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/browse.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/config_cmd.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/diff.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/doctor.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/git_local.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/impact.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/info.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/kg.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/remote.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/search.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/cli/serve.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/client/__init__.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/client/api.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/client/config.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/config.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/config_loader.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/kg_config.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem/kg_init.py +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem_cli.egg-info/SOURCES.txt +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem_cli.egg-info/dependency_links.txt +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem_cli.egg-info/entry_points.txt +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem_cli.egg-info/requires.txt +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/ravmem_cli.egg-info/top_level.txt +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/setup.cfg +0 -0
- {ravmem_cli-1.2.0 → ravmem_cli-1.3.0}/tests/test_smoke.py +0 -0
|
@@ -47,7 +47,7 @@ def _post_with_key_fallback(client, con: Console, path: str, body: Optional[dict
|
|
|
47
47
|
try:
|
|
48
48
|
return client.post(path, json=body)
|
|
49
49
|
except RavMemAPIError as exc:
|
|
50
|
-
if exc.status == 400 and "RAVMEM_LICENSE_KEY is not set" in (exc.detail or ""):
|
|
50
|
+
if exc.status == 400 and "RAVMEM_LICENSE_KEY is not set" in (exc.detail or "") and not body.get("license_key"):
|
|
51
51
|
con.print("[yellow]Server has no license key on file — enter it to proceed.[/yellow]")
|
|
52
52
|
key = typer.prompt("License key", hide_input=True).strip()
|
|
53
53
|
if not key:
|
|
@@ -57,6 +57,37 @@ def _post_with_key_fallback(client, con: Console, path: str, body: Optional[dict
|
|
|
57
57
|
raise
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
def _license_hint(detail: str) -> Optional[str]:
|
|
61
|
+
"""Map a license-server refusal to the concrete next step for the operator."""
|
|
62
|
+
d = detail or ""
|
|
63
|
+
if "LICENSE_REVOKED" in d:
|
|
64
|
+
return ("This license was revoked. Rotate the key in the console (that un-revokes it and "
|
|
65
|
+
"issues a new key), then run: ravmem license activate <new-key>")
|
|
66
|
+
if "LICENSE_KEY_NOT_FOUND" in d or "License key not found" in d or "No account found" in d:
|
|
67
|
+
return ("The license server doesn't recognise this key. If it was rotated in the console, "
|
|
68
|
+
"run: ravmem license activate <new-key>")
|
|
69
|
+
if "ACTIVATION_LIMIT_REACHED" in d:
|
|
70
|
+
return ("Every seat on this license is in use — possibly by this same machine under a "
|
|
71
|
+
"previous key. Deactivate the old machine in the console, then retry.")
|
|
72
|
+
if "SUBSCRIPTION_SUSPENDED" in d:
|
|
73
|
+
return "The subscription is suspended. Resolve billing in the console, then run: ravmem license renew"
|
|
74
|
+
if "SUBSCRIPTION_EXPIRED" in d:
|
|
75
|
+
return "The subscription has expired. Renew it in the console, then run: ravmem license renew"
|
|
76
|
+
if "No license record found" in d:
|
|
77
|
+
return "Run: ravmem license activate <key>"
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _fail(con: Console, prefix: str, exc: Exception, note: Optional[str] = None) -> None:
|
|
82
|
+
con.print(f"[red]{prefix}: {exc}[/red]")
|
|
83
|
+
if note:
|
|
84
|
+
con.print(f"[dim]{note}[/dim]")
|
|
85
|
+
hint = _license_hint(getattr(exc, "detail", "") or str(exc))
|
|
86
|
+
if hint:
|
|
87
|
+
con.print(f"[yellow]-> {hint}[/yellow]")
|
|
88
|
+
raise typer.Exit(1)
|
|
89
|
+
|
|
90
|
+
|
|
60
91
|
# ---------------------------------------------------------------------------
|
|
61
92
|
# ravmem license init
|
|
62
93
|
# ---------------------------------------------------------------------------
|
|
@@ -146,19 +177,25 @@ def activate_cmd(
|
|
|
146
177
|
# that is fine — we just continue to the entitlement step.
|
|
147
178
|
con.print("Sealing license to this machine…")
|
|
148
179
|
sealed_now = False
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
180
|
+
for attempt in range(3):
|
|
181
|
+
try:
|
|
182
|
+
client.post("/activate", json={"license_key": key})
|
|
183
|
+
sealed_now = True
|
|
184
|
+
con.print("[green]K3 sealed. Waiting for the server to load modules…[/green]")
|
|
185
|
+
break
|
|
186
|
+
except RavMemAPIError as exc:
|
|
187
|
+
if exc.status == 404:
|
|
188
|
+
con.print("[dim]Server already activated — proceeding to entitlement init.[/dim]")
|
|
189
|
+
break
|
|
190
|
+
_fail(con, "Activation failed", exc)
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
# Transient: server still booting, or a network blip to it.
|
|
193
|
+
if attempt < 2:
|
|
194
|
+
time.sleep(3)
|
|
195
|
+
continue
|
|
196
|
+
con.print(f"[red]Could not reach the server: {exc}[/red]")
|
|
197
|
+
con.print("[yellow]-> Check it's running ('docker logs ravmem'), then retry.[/yellow]")
|
|
158
198
|
raise typer.Exit(1)
|
|
159
|
-
except Exception as exc:
|
|
160
|
-
con.print(f"[red]Could not reach the server: {exc}[/red]")
|
|
161
|
-
raise typer.Exit(1)
|
|
162
199
|
|
|
163
200
|
# Phase 2 — wait for the re-exec'd full server to finish loading modules.
|
|
164
201
|
if sealed_now:
|
|
@@ -181,44 +218,57 @@ def activate_cmd(
|
|
|
181
218
|
|
|
182
219
|
# Phase 3 — store the signed entitlement JWT (idempotent: 409 = already done).
|
|
183
220
|
con.print("Activating entitlements…")
|
|
184
|
-
init_payload: dict = {"key_hash": key_hash}
|
|
221
|
+
init_payload: dict = {"key_hash": key_hash, "license_key": key}
|
|
185
222
|
if server_url:
|
|
186
223
|
init_payload["server_url"] = server_url
|
|
187
224
|
try:
|
|
188
225
|
result = client.post("/api/license/init", json=init_payload)
|
|
189
226
|
except RavMemAPIError as exc:
|
|
190
|
-
if exc.status
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
227
|
+
if exc.status != 409:
|
|
228
|
+
_fail(con, "Entitlement init failed", exc)
|
|
229
|
+
|
|
230
|
+
# Already initialized. That alone doesn't mean everything is fine: the
|
|
231
|
+
# stored token may be broken (recreate), the license may be
|
|
232
|
+
# deactivated/revoked, or the key typed here may differ from the one
|
|
233
|
+
# this server holds (rotated key, or a typo). Only report "ready" when
|
|
234
|
+
# all three check out; otherwise re-fetch with the key given here —
|
|
235
|
+
# the license server validates it either way, and on refusal nothing
|
|
236
|
+
# stored locally is changed.
|
|
237
|
+
con.print("[dim]License already initialized — verifying…[/dim]")
|
|
238
|
+
try:
|
|
239
|
+
status_data = client.get("/api/license/status")
|
|
240
|
+
except RavMemAPIError as exc2:
|
|
241
|
+
_fail(con, "Could not verify license status", exc2)
|
|
201
242
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
243
|
+
integrity = status_data.get("token_integrity")
|
|
244
|
+
status = status_data.get("status")
|
|
245
|
+
stored_prefix = status_data.get("key_hash_prefix")
|
|
246
|
+
key_differs = bool(stored_prefix) and not key_hash.startswith(stored_prefix)
|
|
206
247
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
# Already have the key in scope from this activate call — no
|
|
210
|
-
# need to prompt again even if the server has nothing on file.
|
|
211
|
-
repair_result = client.post("/api/license/repair", json={"license_key": key})
|
|
212
|
-
except RavMemAPIError as exc3:
|
|
213
|
-
con.print(f"[red]Repair failed: {exc3}[/red]")
|
|
214
|
-
raise typer.Exit(1)
|
|
215
|
-
|
|
216
|
-
con.print("\n[bold green]License repaired. ravmem is ready.[/bold green]")
|
|
217
|
-
con.print(f" Status: {repair_result.get('status')}")
|
|
218
|
-
con.print(f" Expires: {repair_result.get('expiry_ts')}")
|
|
248
|
+
if integrity == "ok" and status in ("ACTIVE", "GRACE_PERIOD") and not key_differs:
|
|
249
|
+
con.print("[bold green]License already initialized — ravmem is ready.[/bold green]")
|
|
219
250
|
return
|
|
220
|
-
|
|
221
|
-
|
|
251
|
+
|
|
252
|
+
if key_differs:
|
|
253
|
+
con.print("[yellow]This key differs from the one this server is activated with — "
|
|
254
|
+
"switching to it…[/yellow]")
|
|
255
|
+
elif status not in ("ACTIVE", "GRACE_PERIOD"):
|
|
256
|
+
con.print(f"[yellow]License status is {status} — re-activating…[/yellow]")
|
|
257
|
+
else:
|
|
258
|
+
con.print(f"[yellow]Stored token integrity is '{integrity}' — repairing…[/yellow]")
|
|
259
|
+
try:
|
|
260
|
+
repair_result = client.post("/api/license/repair", json={"license_key": key})
|
|
261
|
+
except RavMemAPIError as exc3:
|
|
262
|
+
_fail(con, "Activation failed", exc3,
|
|
263
|
+
note="Your existing license on this server was left unchanged." if key_differs else None)
|
|
264
|
+
|
|
265
|
+
if repair_result.get("status") == "rebind_pending":
|
|
266
|
+
con.print(f"[yellow]{repair_result.get('message')}[/yellow]")
|
|
267
|
+
raise typer.Exit(1)
|
|
268
|
+
con.print("\n[bold green]License activated. ravmem is ready.[/bold green]")
|
|
269
|
+
con.print(f" Status: {repair_result.get('status')}")
|
|
270
|
+
con.print(f" Expires: {repair_result.get('expiry_ts')}")
|
|
271
|
+
return
|
|
222
272
|
|
|
223
273
|
con.print("\n[bold green]License activated. ravmem is ready.[/bold green]")
|
|
224
274
|
con.print(f" Status: {result.get('status')}")
|
|
@@ -382,6 +432,8 @@ def status_cmd(
|
|
|
382
432
|
expiry_ts = data.get("expiry_ts")
|
|
383
433
|
table.add_row("Expires", _fmt_ts(expiry_ts) + _days_until(expiry_ts))
|
|
384
434
|
table.add_row("Last heartbeat", _fmt_ts(data.get("last_heartbeat")))
|
|
435
|
+
if data.get("heartbeat_interval"):
|
|
436
|
+
table.add_row("Heartbeat every", f"{data['heartbeat_interval']}s (set in the license console)")
|
|
385
437
|
table.add_row("Last server time", _fmt_ts(data.get("last_server_time")))
|
|
386
438
|
|
|
387
439
|
grace_deadline = data.get("grace_deadline")
|
|
@@ -405,6 +457,8 @@ def status_cmd(
|
|
|
405
457
|
)
|
|
406
458
|
elif status == "GRACE_PERIOD":
|
|
407
459
|
con.print("\n[yellow]⚠ License in grace period. Renew before the grace window closes.[/yellow]")
|
|
460
|
+
else:
|
|
461
|
+
_status_hint(con, status)
|
|
408
462
|
|
|
409
463
|
if status not in ("ACTIVE", "GRACE_PERIOD", "DEACTIVATED"):
|
|
410
464
|
raise typer.Exit(1)
|
|
@@ -414,16 +468,31 @@ def status_cmd(
|
|
|
414
468
|
# ravmem license renew
|
|
415
469
|
# ---------------------------------------------------------------------------
|
|
416
470
|
|
|
471
|
+
_KEY_OPTION_HELP = (
|
|
472
|
+
"License key to use. Only needed if the key was rotated in the console "
|
|
473
|
+
"(this server is then switched onto it), or if the server has no key on file."
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _run_recovery(con: Console, path: str, key: Optional[str], failed: str) -> dict:
|
|
478
|
+
client = _get_client()
|
|
479
|
+
body = {"license_key": key.strip()} if key and key.strip() else {}
|
|
480
|
+
try:
|
|
481
|
+
return _post_with_key_fallback(client, con, path, body)
|
|
482
|
+
except RavMemAPIError as exc:
|
|
483
|
+
_fail(con, failed, exc)
|
|
484
|
+
|
|
485
|
+
|
|
417
486
|
@app.command("renew")
|
|
418
|
-
def renew_cmd(
|
|
487
|
+
def renew_cmd(
|
|
488
|
+
key: Optional[str] = typer.Option(None, "--key", help=_KEY_OPTION_HELP),
|
|
489
|
+
) -> None:
|
|
419
490
|
"""Re-activate an expired license against the live license server."""
|
|
420
491
|
con = _console()
|
|
421
|
-
client = _get_client()
|
|
422
492
|
con.print("Renewing license…")
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
con.print(f"[red]Renewal failed: {exc}[/red]")
|
|
493
|
+
result = _run_recovery(con, "/api/license/renew", key, "Renewal failed")
|
|
494
|
+
if result.get("status") == "rebind_pending":
|
|
495
|
+
con.print(f"[yellow]{result.get('message')}[/yellow]")
|
|
427
496
|
raise typer.Exit(1)
|
|
428
497
|
con.print(f"[green]Renewed. Status: {result.get('status')} | Expiry: {result.get('expiry_ts')}[/green]")
|
|
429
498
|
|
|
@@ -433,20 +502,19 @@ def renew_cmd() -> None:
|
|
|
433
502
|
# ---------------------------------------------------------------------------
|
|
434
503
|
|
|
435
504
|
@app.command("repair")
|
|
436
|
-
def repair_cmd(
|
|
437
|
-
|
|
505
|
+
def repair_cmd(
|
|
506
|
+
key: Optional[str] = typer.Option(None, "--key", help=_KEY_OPTION_HELP),
|
|
507
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt"),
|
|
508
|
+
) -> None:
|
|
509
|
+
"""Re-fetch a valid license token for this server (broken token, recreated
|
|
510
|
+
container, deactivated machine, or a key rotated in the console)."""
|
|
438
511
|
con = _console()
|
|
439
|
-
|
|
440
|
-
"[yellow]This
|
|
441
|
-
"
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
client = _get_client()
|
|
446
|
-
try:
|
|
447
|
-
result = _post_with_key_fallback(client, con, "/api/license/repair")
|
|
448
|
-
except RavMemAPIError as exc:
|
|
449
|
-
con.print(f"[red]Repair failed: {exc}[/red]")
|
|
512
|
+
if not yes:
|
|
513
|
+
con.print("[yellow]This re-contacts the license server and replaces this server's stored token.[/yellow]")
|
|
514
|
+
typer.confirm("Continue?", abort=True)
|
|
515
|
+
result = _run_recovery(con, "/api/license/repair", key, "Repair failed")
|
|
516
|
+
if result.get("status") == "rebind_pending":
|
|
517
|
+
con.print(f"[yellow]{result.get('message')}[/yellow]")
|
|
450
518
|
raise typer.Exit(1)
|
|
451
519
|
con.print(f"[green]Repair complete. Status: {result.get('status')}[/green]")
|
|
452
520
|
|
|
@@ -489,29 +557,43 @@ def check_cmd() -> None:
|
|
|
489
557
|
table.add_row("Revision", str(data.get("revision", "—")))
|
|
490
558
|
table.add_row("Expires", _fmt_ts(data.get("expiry_ts")))
|
|
491
559
|
table.add_row("Last heartbeat", _fmt_ts(data.get("last_heartbeat")))
|
|
560
|
+
if data.get("heartbeat_interval"):
|
|
561
|
+
table.add_row("Heartbeat every", f"{data['heartbeat_interval']}s (set in the license console)")
|
|
492
562
|
grace_deadline = data.get("grace_deadline")
|
|
493
563
|
if grace_deadline:
|
|
494
564
|
table.add_row("[yellow]Grace deadline[/yellow]", f"[yellow]{_fmt_ts(grace_deadline)}[/yellow]")
|
|
495
565
|
con.print(table)
|
|
566
|
+
_status_hint(con, status)
|
|
496
567
|
|
|
497
568
|
if status not in ("ACTIVE", "GRACE_PERIOD", "DEACTIVATED", "SUSPENDED"):
|
|
498
569
|
raise typer.Exit(1)
|
|
499
570
|
|
|
500
571
|
|
|
572
|
+
def _status_hint(con: Console, status: str) -> None:
|
|
573
|
+
hints = {
|
|
574
|
+
"REVOKED": _license_hint("LICENSE_REVOKED"),
|
|
575
|
+
"REQUIRES_REACTIVATION": "Run: ravmem license repair (or: ravmem license activate <key>)",
|
|
576
|
+
"TAMPERED": "Run: ravmem license repair",
|
|
577
|
+
"DEACTIVATED": "This machine was deactivated. To keep it, run: ravmem license activate <key>",
|
|
578
|
+
"SUSPENDED": _license_hint("SUBSCRIPTION_SUSPENDED"),
|
|
579
|
+
"UNINITIALIZED": "Run: ravmem license activate <key>",
|
|
580
|
+
}
|
|
581
|
+
if hints.get(status):
|
|
582
|
+
con.print(f"[yellow]-> {hints[status]}[/yellow]")
|
|
583
|
+
|
|
584
|
+
|
|
501
585
|
@app.command("rebind")
|
|
502
|
-
def rebind_cmd(
|
|
503
|
-
|
|
586
|
+
def rebind_cmd(
|
|
587
|
+
key: Optional[str] = typer.Option(None, "--key", help=_KEY_OPTION_HELP),
|
|
588
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt"),
|
|
589
|
+
) -> None:
|
|
590
|
+
"""Move this license's seat onto this machine (after migration/recreate)."""
|
|
504
591
|
con = _console()
|
|
505
|
-
|
|
506
|
-
"[yellow]This
|
|
507
|
-
"
|
|
508
|
-
)
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
client = _get_client()
|
|
512
|
-
try:
|
|
513
|
-
result = _post_with_key_fallback(client, con, "/api/license/rebind")
|
|
514
|
-
except RavMemAPIError as exc:
|
|
515
|
-
con.print(f"[red]Rebind request failed: {exc}[/red]")
|
|
592
|
+
if not yes:
|
|
593
|
+
con.print("[yellow]This moves the license seat onto this machine's current identity.[/yellow]")
|
|
594
|
+
typer.confirm("Continue?", abort=True)
|
|
595
|
+
result = _run_recovery(con, "/api/license/rebind", key, "Rebind failed")
|
|
596
|
+
if result.get("status") == "rebind_pending":
|
|
597
|
+
con.print(f"[yellow]{result.get('message')}[/yellow]")
|
|
516
598
|
raise typer.Exit(1)
|
|
517
|
-
con.print(f"[green]{result.get('message', '
|
|
599
|
+
con.print(f"[green]{result.get('message', 'Rebound.')}[/green]")
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
"""CLI entry point — registers all subcommands."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import sys
|
|
4
5
|
from typing import Optional
|
|
5
6
|
|
|
6
7
|
import typer
|
|
7
8
|
|
|
9
|
+
# Legacy Windows consoles (cp1252) can't encode the ⚠/…/— glyphs used in
|
|
10
|
+
# output; without this, e.g. `ravmem license status` crashed with a
|
|
11
|
+
# UnicodeEncodeError *after* printing, exiting non-zero on a healthy license.
|
|
12
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
13
|
+
try:
|
|
14
|
+
_stream.reconfigure(errors="replace")
|
|
15
|
+
except Exception:
|
|
16
|
+
pass
|
|
17
|
+
|
|
8
18
|
|
|
9
19
|
def setup_logging(verbose: bool = False) -> None:
|
|
10
20
|
"""Configure root logger based on verbosity or RAVMEM_LOG_LEVEL env var.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|