portacode 1.4.15.dev10__py3-none-any.whl → 1.4.15.dev23__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.
portacode/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.4.15.dev10'
32
- __version_tuple__ = version_tuple = (1, 4, 15, 'dev10')
31
+ __version__ = version = '1.4.15.dev23'
32
+ __version_tuple__ = version_tuple = (1, 4, 15, 'dev23')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -16,7 +16,7 @@ import sys
16
16
  import tempfile
17
17
  import time
18
18
  import threading
19
- from datetime import datetime
19
+ from datetime import datetime, timezone
20
20
  from pathlib import Path
21
21
  from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
22
22
 
@@ -47,7 +47,7 @@ UNIT_DIR = Path("/etc/systemd/system")
47
47
  _MANAGED_CONTAINERS_CACHE_TTL_S = 30.0
48
48
  _MANAGED_CONTAINERS_CACHE: Dict[str, Any] = {"timestamp": 0.0, "summary": None}
49
49
  _MANAGED_CONTAINERS_CACHE_LOCK = threading.Lock()
50
- _STARTUP_TEMPLATES_REFRESHED = False
50
+ TEMPLATES_REFRESH_INTERVAL_S = 300
51
51
 
52
52
  ProgressCallback = Callable[[int, int, Dict[str, Any], str, Optional[Dict[str, Any]]], None]
53
53
 
@@ -197,10 +197,33 @@ def _build_proxmox_client_from_config(config: Dict[str, Any]):
197
197
  )
198
198
 
199
199
 
200
+ def _current_time_iso() -> str:
201
+ return datetime.now(timezone.utc).isoformat()
202
+
203
+
204
+ def _parse_iso_timestamp(value: str) -> Optional[datetime]:
205
+ if not value:
206
+ return None
207
+ text = value
208
+ if text.endswith("Z"):
209
+ text = text[:-1] + "+00:00"
210
+ try:
211
+ return datetime.fromisoformat(text)
212
+ except ValueError:
213
+ return None
214
+
215
+
216
+ def _templates_need_refresh(config: Dict[str, Any]) -> bool:
217
+ if not config or not config.get("token_value"):
218
+ return False
219
+ last = _parse_iso_timestamp(config.get("templates_last_refreshed") or "")
220
+ if not last:
221
+ return True
222
+ return (datetime.now(timezone.utc) - last).total_seconds() >= TEMPLATES_REFRESH_INTERVAL_S
223
+
224
+
200
225
  def _ensure_templates_refreshed_on_startup(config: Dict[str, Any]) -> None:
201
- global _STARTUP_TEMPLATES_REFRESHED
202
- if _STARTUP_TEMPLATES_REFRESHED or not config or not config.get("token_value"):
203
- _STARTUP_TEMPLATES_REFRESHED = True
226
+ if not _templates_need_refresh(config):
204
227
  return
205
228
  try:
206
229
  client = _build_proxmox_client_from_config(config)
@@ -209,10 +232,10 @@ def _ensure_templates_refreshed_on_startup(config: Dict[str, Any]) -> None:
209
232
  templates = _list_templates(client, node, storages)
210
233
  if templates:
211
234
  config["templates"] = templates
235
+ config["templates_last_refreshed"] = _current_time_iso()
236
+ _save_config(config)
212
237
  except Exception as exc:
213
238
  logger.warning("Unable to refresh Proxmox templates on startup: %s", exc)
214
- finally:
215
- _STARTUP_TEMPLATES_REFRESHED = True
216
239
 
217
240
 
218
241
  def _pick_storage(storages: Iterable[Dict[str, Any]]) -> str:
@@ -473,48 +496,160 @@ def _friendly_step_label(step_name: str) -> str:
473
496
  return normalized.capitalize()
474
497
 
475
498
 
499
+ _NETWORK_WAIT_CMD = (
500
+ "count=0; "
501
+ "while [ \"$count\" -lt 20 ]; do "
502
+ " if command -v ip >/dev/null 2>&1 && ip route get 1.1.1.1 >/dev/null 2>&1; then break; fi; "
503
+ " if [ -f /proc/net/route ] && grep -q '^00000000' /proc/net/route >/dev/null 2>&1; then break; fi; "
504
+ " sleep 1; "
505
+ " count=$((count+1)); "
506
+ "done"
507
+ )
508
+
509
+ _PACKAGE_MANAGER_PROFILES: Dict[str, Dict[str, Any]] = {
510
+ "apt": {
511
+ "update_cmd": "apt-get update -y",
512
+ "update_step_name": "apt_update",
513
+ "install_cmd": "apt-get install -y python3 python3-pip sudo --fix-missing",
514
+ "install_step_name": "install_deps",
515
+ "update_retries": 4,
516
+ "install_retries": 5,
517
+ },
518
+ "dnf": {
519
+ "update_cmd": "dnf check-update || true",
520
+ "update_step_name": "dnf_update",
521
+ "install_cmd": "dnf install -y python3 python3-pip sudo",
522
+ "install_step_name": "install_deps",
523
+ "update_retries": 3,
524
+ "install_retries": 5,
525
+ },
526
+ "yum": {
527
+ "update_cmd": "yum makecache",
528
+ "update_step_name": "yum_update",
529
+ "install_cmd": "yum install -y python3 python3-pip sudo",
530
+ "install_step_name": "install_deps",
531
+ "update_retries": 3,
532
+ "install_retries": 5,
533
+ },
534
+ "apk": {
535
+ "update_cmd": "apk update",
536
+ "update_step_name": "apk_update",
537
+ "install_cmd": "apk add --no-cache python3 py3-pip sudo shadow",
538
+ "install_step_name": "install_deps",
539
+ "update_retries": 3,
540
+ "install_retries": 5,
541
+ },
542
+ "pacman": {
543
+ "update_cmd": "pacman -Sy --noconfirm",
544
+ "update_step_name": "pacman_update",
545
+ "install_cmd": "pacman -S --noconfirm python python-pip sudo",
546
+ "install_step_name": "install_deps",
547
+ "update_retries": 3,
548
+ "install_retries": 5,
549
+ },
550
+ "zypper": {
551
+ "update_cmd": "zypper refresh",
552
+ "update_step_name": "zypper_update",
553
+ "install_cmd": "zypper install -y python3 python3-pip sudo",
554
+ "install_step_name": "install_deps",
555
+ "update_retries": 3,
556
+ "install_retries": 5,
557
+ },
558
+ }
559
+
560
+ _UPDATE_RETRY_ON = [
561
+ "Temporary failure resolving",
562
+ "Could not resolve",
563
+ "Failed to fetch",
564
+ ]
565
+
566
+ _INSTALL_RETRY_ON = [
567
+ "lock-frontend",
568
+ "Unable to acquire the dpkg frontend lock",
569
+ "Temporary failure resolving",
570
+ "Could not resolve",
571
+ "Failed to fetch",
572
+ ]
573
+
574
+
476
575
  def _build_bootstrap_steps(
477
576
  user: str,
478
577
  password: str,
479
578
  ssh_key: str,
480
579
  include_portacode_connect: bool = True,
580
+ package_manager: str = "apt",
481
581
  ) -> List[Dict[str, Any]]:
482
- steps = [
483
- {
484
- "name": "apt_update",
485
- "cmd": "apt-get update -y",
486
- "retries": 4,
487
- "retry_delay_s": 5,
488
- "retry_on": [
489
- "Temporary failure resolving",
490
- "Could not resolve",
491
- "Failed to fetch",
492
- ],
493
- },
582
+ profile = _PACKAGE_MANAGER_PROFILES.get(package_manager, _PACKAGE_MANAGER_PROFILES["apt"])
583
+ steps: List[Dict[str, Any]] = [
584
+ {"name": "wait_for_network", "cmd": _NETWORK_WAIT_CMD, "retries": 0},
585
+ ]
586
+ update_cmd = profile.get("update_cmd")
587
+ if update_cmd:
588
+ steps.append(
589
+ {
590
+ "name": profile.get("update_step_name", "package_update"),
591
+ "cmd": update_cmd,
592
+ "retries": profile.get("update_retries", 3),
593
+ "retry_delay_s": 5,
594
+ "retry_on": _UPDATE_RETRY_ON,
595
+ }
596
+ )
597
+ install_cmd = profile.get("install_cmd")
598
+ if install_cmd:
599
+ steps.append(
600
+ {
601
+ "name": profile.get("install_step_name", "install_deps"),
602
+ "cmd": install_cmd,
603
+ "retries": profile.get("install_retries", 5),
604
+ "retry_delay_s": 5,
605
+ "retry_on": _INSTALL_RETRY_ON,
606
+ }
607
+ )
608
+ steps.extend(
609
+ [
610
+ {"name": "user_exists", "cmd": f"id -u {user} >/dev/null 2>&1 || adduser --disabled-password --gecos '' {user}", "retries": 0},
611
+ {
612
+ "name": "add_sudo",
613
+ "cmd": (
614
+ f"if command -v usermod >/dev/null 2>&1; then "
615
+ " if ! getent group sudo >/dev/null 2>&1; then "
616
+ " if command -v groupadd >/dev/null 2>&1; then "
617
+ " groupadd sudo >/dev/null 2>&1 || true; "
618
+ " fi; "
619
+ " fi; "
620
+ f" usermod -aG sudo {user}; "
621
+ "else "
622
+ " for grp in wheel sudo; do "
623
+ " if ! getent group \"$grp\" >/dev/null 2>&1 && command -v groupadd >/dev/null 2>&1; then "
624
+ " groupadd \"$grp\" >/dev/null 2>&1 || true; "
625
+ " fi; "
626
+ " addgroup \"$grp\" >/dev/null 2>&1 || true; "
627
+ f" addgroup {user} \"$grp\" >/dev/null 2>&1 || true; "
628
+ " done; "
629
+ "fi"
630
+ ),
631
+ "retries": 0,
632
+ },
494
633
  {
495
- "name": "install_deps",
496
- "cmd": "apt-get install -y python3 python3-pip sudo --fix-missing",
497
- "retries": 5,
498
- "retry_delay_s": 5,
499
- "retry_on": [
500
- "lock-frontend",
501
- "Unable to acquire the dpkg frontend lock",
502
- "Temporary failure resolving",
503
- "Could not resolve",
504
- "Failed to fetch",
505
- ],
634
+ "name": "add_sudoers",
635
+ "cmd": (
636
+ f"printf '%s ALL=(ALL) NOPASSWD:ALL\n' {shlex.quote(user)} >/etc/sudoers.d/portacode && "
637
+ "chmod 0440 /etc/sudoers.d/portacode"
638
+ ),
639
+ "retries": 0,
506
640
  },
507
- {"name": "user_exists", "cmd": f"id -u {user} >/dev/null 2>&1 || adduser --disabled-password --gecos '' {user}", "retries": 0},
508
- {"name": "add_sudo", "cmd": f"usermod -aG sudo {user}", "retries": 0},
509
- ]
641
+ ]
642
+ )
510
643
  if password:
511
644
  steps.append({"name": "set_password", "cmd": f"echo '{user}:{password}' | chpasswd", "retries": 0})
512
645
  if ssh_key:
513
- steps.append({
514
- "name": "add_ssh_key",
515
- "cmd": f"install -d -m 700 /home/{user}/.ssh && echo '{ssh_key}' >> /home/{user}/.ssh/authorized_keys && chown -R {user}:{user} /home/{user}/.ssh",
516
- "retries": 0,
517
- })
646
+ steps.append(
647
+ {
648
+ "name": "add_ssh_key",
649
+ "cmd": f"install -d -m 700 /home/{user}/.ssh && echo '{ssh_key}' >> /home/{user}/.ssh/authorized_keys && chown -R {user}:{user} /home/{user}/.ssh",
650
+ "retries": 0,
651
+ }
652
+ )
518
653
  steps.extend(
519
654
  [
520
655
  {"name": "pip_upgrade", "cmd": "python3 -m pip install --upgrade pip", "retries": 0},
@@ -526,6 +661,45 @@ def _build_bootstrap_steps(
526
661
  return steps
527
662
 
528
663
 
664
+ def _guess_package_manager_from_template(template: str) -> str:
665
+ normalized = (template or "").lower()
666
+ if "alpine" in normalized:
667
+ return "apk"
668
+ if "archlinux" in normalized:
669
+ return "pacman"
670
+ if "centos-7" in normalized:
671
+ return "yum"
672
+ if any(keyword in normalized for keyword in ("centos-8", "centos-9", "centos-9-stream", "centos-8-stream")):
673
+ return "dnf"
674
+ if any(keyword in normalized for keyword in ("rockylinux", "almalinux", "fedora")):
675
+ return "dnf"
676
+ if "opensuse" in normalized or "suse" in normalized:
677
+ return "zypper"
678
+ if any(keyword in normalized for keyword in ("debian", "ubuntu", "devuan", "turnkeylinux")):
679
+ return "apt"
680
+ if normalized.startswith("system/") and "linux" in normalized:
681
+ return "apt"
682
+ return "apt"
683
+
684
+
685
+ def _detect_package_manager(vmid: int) -> str:
686
+ candidates = [
687
+ ("apt", "apt-get"),
688
+ ("dnf", "dnf"),
689
+ ("yum", "yum"),
690
+ ("apk", "apk"),
691
+ ("pacman", "pacman"),
692
+ ("zypper", "zypper"),
693
+ ]
694
+ for name, binary in candidates:
695
+ res = _run_pct(vmid, f"command -v {binary} >/dev/null 2>&1")
696
+ if res.get("returncode") == 0:
697
+ logger.debug("Detected package manager %s inside container %s", name, vmid)
698
+ return name
699
+ logger.warning("Unable to detect package manager inside container %s; defaulting to apt", vmid)
700
+ return "apt"
701
+
702
+
529
703
  def _get_storage_type(storages: Iterable[Dict[str, Any]], storage_name: str) -> str:
530
704
  for entry in storages:
531
705
  if entry.get("storage") == storage_name:
@@ -713,7 +887,8 @@ def _connect_proxmox(config: Dict[str, Any]) -> Any:
713
887
 
714
888
 
715
889
  def _run_pct(vmid: int, cmd: str, input_text: Optional[str] = None) -> Dict[str, Any]:
716
- full = ["pct", "exec", str(vmid), "--", "bash", "-lc", cmd]
890
+ shell = "/bin/sh"
891
+ full = ["pct", "exec", str(vmid), "--", shell, "-c", cmd]
717
892
  start = time.time()
718
893
  proc = subprocess.run(full, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, input=input_text)
719
894
  return {
@@ -725,6 +900,10 @@ def _run_pct(vmid: int, cmd: str, input_text: Optional[str] = None) -> Dict[str,
725
900
  }
726
901
 
727
902
 
903
+ def _su_command(user: str, command: str) -> str:
904
+ return f"su - {user} -s /bin/sh -c {shlex.quote(command)}"
905
+
906
+
728
907
  def _run_pct_check(vmid: int, cmd: str) -> Dict[str, Any]:
729
908
  res = _run_pct(vmid, cmd)
730
909
  if res["returncode"] != 0:
@@ -784,7 +963,7 @@ def _push_bytes_to_container(
784
963
 
785
964
 
786
965
  def _resolve_portacode_key_dir(vmid: int, user: str) -> str:
787
- data_dir_cmd = f"su - {user} -c 'echo -n ${{XDG_DATA_HOME:-$HOME/.local/share}}'"
966
+ data_dir_cmd = _su_command(user, "echo -n ${XDG_DATA_HOME:-$HOME/.local/share}")
788
967
  data_home = _run_pct_check(vmid, data_dir_cmd)["stdout"].strip()
789
968
  portacode_dir = f"{data_home}/portacode"
790
969
  _run_pct_exec_check(vmid, ["mkdir", "-p", portacode_dir])
@@ -801,18 +980,19 @@ def _deploy_device_keypair(vmid: int, user: str, private_key: str, public_key: s
801
980
 
802
981
 
803
982
  def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -> Dict[str, Any]:
804
- cmd = ["pct", "exec", str(vmid), "--", "bash", "-lc", f"su - {user} -c 'portacode connect'"]
983
+ su_connect_cmd = _su_command(user, "portacode connect")
984
+ cmd = ["pct", "exec", str(vmid), "--", "/bin/sh", "-c", su_connect_cmd]
805
985
  proc = subprocess.Popen(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
806
986
  start = time.time()
807
987
 
808
- data_dir_cmd = f"su - {user} -c 'echo -n ${{XDG_DATA_HOME:-$HOME/.local/share}}'"
988
+ data_dir_cmd = _su_command(user, "echo -n ${XDG_DATA_HOME:-$HOME/.local/share}")
809
989
  data_dir = _run_pct_check(vmid, data_dir_cmd)["stdout"].strip()
810
990
  key_dir = f"{data_dir}/portacode/keys"
811
991
  pub_path = f"{key_dir}/id_portacode.pub"
812
992
  priv_path = f"{key_dir}/id_portacode"
813
993
 
814
994
  def file_size(path: str) -> Optional[int]:
815
- stat_cmd = f"su - {user} -c 'test -s {path} && stat -c %s {path}'"
995
+ stat_cmd = _su_command(user, f"test -s {path} && stat -c %s {path}")
816
996
  res = _run_pct(vmid, stat_cmd)
817
997
  if res["returncode"] != 0:
818
998
  return None
@@ -870,7 +1050,7 @@ def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -
870
1050
  final_pub = file_size(pub_path)
871
1051
  final_priv = file_size(priv_path)
872
1052
  if final_pub and final_priv:
873
- key_res = _run_pct(vmid, f"su - {user} -c 'cat {pub_path}'")
1053
+ key_res = _run_pct(vmid, _su_command(user, f"cat {pub_path}"))
874
1054
  if not process_exited:
875
1055
  proc.terminate()
876
1056
  try:
@@ -910,7 +1090,7 @@ def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -
910
1090
  except subprocess.TimeoutExpired:
911
1091
  proc.kill()
912
1092
 
913
- key_res = _run_pct(vmid, f"su - {user} -c 'cat {pub_path}'")
1093
+ key_res = _run_pct(vmid, _su_command(user, f"cat {pub_path}"))
914
1094
  return {
915
1095
  "ok": True,
916
1096
  "public_key": key_res["stdout"].strip(),
@@ -1003,7 +1183,16 @@ def _bootstrap_portacode(
1003
1183
  total_steps: Optional[int] = None,
1004
1184
  default_public_key: Optional[str] = None,
1005
1185
  ) -> Tuple[str, List[Dict[str, Any]]]:
1006
- actual_steps = steps if steps is not None else _build_bootstrap_steps(user, password, ssh_key)
1186
+ if steps is not None:
1187
+ actual_steps = steps
1188
+ else:
1189
+ detected_manager = _detect_package_manager(vmid)
1190
+ actual_steps = _build_bootstrap_steps(
1191
+ user,
1192
+ password,
1193
+ ssh_key,
1194
+ package_manager=detected_manager,
1195
+ )
1007
1196
  results, ok = _run_setup_steps(
1008
1197
  vmid,
1009
1198
  actual_steps,
@@ -1027,6 +1216,15 @@ def _bootstrap_portacode(
1027
1216
  else:
1028
1217
  command_text = str(command)
1029
1218
  command_suffix = f" command={command_text}" if command_text else ""
1219
+ stdout = details.get("stdout")
1220
+ stderr = details.get("stderr")
1221
+ if stdout or stderr:
1222
+ logger.debug(
1223
+ "Bootstrap command output%s%s%s",
1224
+ f" stdout={stdout!r}" if stdout else "",
1225
+ " " if stdout and stderr else "",
1226
+ f"stderr={stderr!r}" if stderr else "",
1227
+ )
1030
1228
  if summary:
1031
1229
  logger.warning(
1032
1230
  "Portacode bootstrap failure summary=%s%s%s",
@@ -1034,10 +1232,15 @@ def _bootstrap_portacode(
1034
1232
  f" history_len={len(history)}" if history else "",
1035
1233
  f" command={command_text}" if command_text else "",
1036
1234
  )
1037
- raise RuntimeError(
1038
- f"Portacode bootstrap steps failed: {summary}{history_snippet}{command_suffix}"
1039
- )
1040
- raise RuntimeError("Portacode bootstrap steps failed.")
1235
+ logger.error(
1236
+ "Portacode bootstrap command failed%s%s%s",
1237
+ f" command={command_text}" if command_text else "",
1238
+ f" stdout={stdout!r}" if stdout else "",
1239
+ f" stderr={stderr!r}" if stderr else "",
1240
+ )
1241
+ raise RuntimeError(
1242
+ f"Portacode bootstrap steps failed: {summary}{history_snippet}{command_suffix}"
1243
+ )
1041
1244
  key_step = next((entry for entry in results if entry.get("name") == "portacode_connect"), None)
1042
1245
  public_key = key_step.get("public_key") if key_step else default_public_key
1043
1246
  if not public_key:
@@ -1104,8 +1307,9 @@ def configure_infrastructure(token_identifier: str, token_value: str, verify_ssl
1104
1307
  "token_value": token_value,
1105
1308
  "verify_ssl": verify_ssl,
1106
1309
  "default_storage": default_storage,
1107
- "templates": templates,
1108
1310
  "last_verified": datetime.utcnow().isoformat() + "Z",
1311
+ "templates": templates,
1312
+ "templates_last_refreshed": _current_time_iso(),
1109
1313
  "network": network,
1110
1314
  "node_status": status,
1111
1315
  }
@@ -1189,12 +1393,17 @@ class CreateProxmoxContainerHandler(SyncHandler):
1189
1393
  device_public_key = (message.get("device_public_key") or "").strip()
1190
1394
  device_private_key = (message.get("device_private_key") or "").strip()
1191
1395
  has_device_keypair = bool(device_public_key and device_private_key)
1396
+ config_guess = _load_config()
1397
+ template_candidates = config_guess.get("templates") or []
1398
+ template_hint = (message.get("template") or (template_candidates[0] if template_candidates else "")).strip()
1399
+ package_manager = _guess_package_manager_from_template(template_hint)
1192
1400
  bootstrap_user, bootstrap_password, bootstrap_ssh_key = _get_provisioning_user_info(message)
1193
1401
  bootstrap_steps = _build_bootstrap_steps(
1194
1402
  bootstrap_user,
1195
1403
  bootstrap_password,
1196
1404
  bootstrap_ssh_key,
1197
1405
  include_portacode_connect=not has_device_keypair,
1406
+ package_manager=package_manager,
1198
1407
  )
1199
1408
  total_steps = 3 + len(bootstrap_steps) + 2
1200
1409
  current_step_index = 1
@@ -1424,7 +1633,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
1424
1633
  on_behalf_of_device=device_id,
1425
1634
  )
1426
1635
 
1427
- cmd = f"su - {payload['username']} -c 'sudo -S portacode service install'"
1636
+ cmd = _su_command(payload["username"], "sudo -S portacode service install")
1428
1637
  res = _run_pct(vmid, cmd, input_text=payload["password"] + "\n")
1429
1638
 
1430
1639
  if res["returncode"] != 0:
@@ -1556,7 +1765,7 @@ class StartPortacodeServiceHandler(SyncHandler):
1556
1765
  on_behalf_of_device=on_behalf_of_device,
1557
1766
  )
1558
1767
 
1559
- cmd = f"su - {user} -c 'sudo -S portacode service install'"
1768
+ cmd = _su_command(user, "sudo -S portacode service install")
1560
1769
  res = _run_pct(vmid, cmd, input_text=password + "\n")
1561
1770
 
1562
1771
  if res["returncode"] != 0:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.15.dev10
3
+ Version: 1.4.15.dev23
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -1,7 +1,7 @@
1
1
  portacode/README.md,sha256=4dKtpvR8LNgZPVz37GmkQCMWIr_u25Ao63iW56s7Ke4,775
2
2
  portacode/__init__.py,sha256=oB3sV1wXr-um-RXio73UG8E5Xx6cF2ZVJveqjNmC-vQ,1086
3
3
  portacode/__main__.py,sha256=jmHTGC1hzmo9iKJLv-SSYe9BSIbPPZ2IOpecI03PlTs,296
4
- portacode/_version.py,sha256=OsU3LmSVwSb16YuRZZoOGhx4fmAppA2W-vOQo60j6hQ,721
4
+ portacode/_version.py,sha256=8YGMPC5_rqCpJnjJF3sLM5flS3H0d8do1siDivvmwVs,721
5
5
  portacode/cli.py,sha256=mGLKoZ-T2FBF7IA9wUq0zyG0X9__-A1ao7gajjcVRH8,21828
6
6
  portacode/data.py,sha256=5-s291bv8J354myaHm1Y7CQZTZyRzMU3TGe5U4hb-FA,1591
7
7
  portacode/keypair.py,sha256=0OO4vHDcF1XMxCDqce61xFTlFwlTcmqe5HyGsXFEt7s,5838
@@ -22,7 +22,7 @@ portacode/connection/handlers/diff_handlers.py,sha256=iYTIRCcpEQ03vIPKZCsMTE5aZb
22
22
  portacode/connection/handlers/file_handlers.py,sha256=nAJH8nXnX07xxD28ngLpgIUzcTuRwZBNpEGEKdRqohw,39507
23
23
  portacode/connection/handlers/project_aware_file_handlers.py,sha256=AqgMnDqX2893T2NsrvUSCwjN5VKj4Pb2TN0S_SuboOE,9803
24
24
  portacode/connection/handlers/project_state_handlers.py,sha256=v6ZefGW9i7n1aZLq2jOGumJIjYb6aHlPI4m1jkYewm8,1686
25
- portacode/connection/handlers/proxmox_infra.py,sha256=5DAHhzbegCI0fKdekP8Or6jrGZptmthLdq-RMtOPZDc,63843
25
+ portacode/connection/handlers/proxmox_infra.py,sha256=cAueQb_D3COmPI84LfI1bqHOcgt02BOgho_pCRGeLWE,71132
26
26
  portacode/connection/handlers/registry.py,sha256=qXGE60sYEWg6ZtVQzFcZ5YI2XWR6lMgw4hAL9x5qR1I,6181
27
27
  portacode/connection/handlers/session.py,sha256=uNGfiO_1B9-_yjJKkpvmbiJhIl6b-UXlT86UTfd6WYE,42219
28
28
  portacode/connection/handlers/system_handlers.py,sha256=fr12QpOr_Z8KYGUU-AYrTQwRPAcrLK85hvj3SEq1Kw8,14757
@@ -65,7 +65,7 @@ portacode/utils/__init__.py,sha256=NgBlWTuNJESfIYJzP_3adI1yJQJR0XJLRpSdVNaBAN0,3
65
65
  portacode/utils/diff_apply.py,sha256=4Oi7ft3VUCKmiUE4VM-OeqO7Gk6H7PF3WnN4WHXtjxI,15157
66
66
  portacode/utils/diff_renderer.py,sha256=S76StnQ2DLfsz4Gg0m07UwPfRp8270PuzbNaQq-rmYk,13850
67
67
  portacode/utils/ntp_clock.py,sha256=VqCnWCTehCufE43W23oB-WUdAZGeCcLxkmIOPwInYHc,2499
68
- portacode-1.4.15.dev10.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
68
+ portacode-1.4.15.dev23.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
69
69
  test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
70
70
  test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
71
71
  test_modules/test_device_online.py,sha256=QtYq0Dq9vME8Gp2O4fGSheqVf8LUtpsSKosXXk56gGM,1654
@@ -91,8 +91,8 @@ testing_framework/core/playwright_manager.py,sha256=Tw46qwxIhOFkS48C2IWIQHHNpEe-
91
91
  testing_framework/core/runner.py,sha256=j2QwNJmAxVBmJvcbVS7DgPJUKPNzqfLmt_4NNdaKmZU,19297
92
92
  testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
93
93
  testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
94
- portacode-1.4.15.dev10.dist-info/METADATA,sha256=FZkeltpJrx27P2fnujDDrYsinIFjnm1VsnnqTSVBk1g,13052
95
- portacode-1.4.15.dev10.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
- portacode-1.4.15.dev10.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
- portacode-1.4.15.dev10.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
- portacode-1.4.15.dev10.dist-info/RECORD,,
94
+ portacode-1.4.15.dev23.dist-info/METADATA,sha256=1pp2pJiX9YjuOBghWO4mYN7fkpK0dqJSv6CdFMgbwOQ,13052
95
+ portacode-1.4.15.dev23.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
+ portacode-1.4.15.dev23.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
+ portacode-1.4.15.dev23.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
+ portacode-1.4.15.dev23.dist-info/RECORD,,