portacode 1.4.15.dev11__py3-none-any.whl → 1.4.15.dev13__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.dev11'
32
- __version_tuple__ = version_tuple = (1, 4, 15, 'dev11')
31
+ __version__ = version = '1.4.15.dev13'
32
+ __version_tuple__ = version_tuple = (1, 4, 15, 'dev13')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -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.utcnow().isoformat() + "Z"
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.utcnow() - 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:
@@ -618,6 +641,24 @@ def _guess_package_manager_from_template(template: str) -> str:
618
641
  return "apt"
619
642
 
620
643
 
644
+ def _detect_package_manager(vmid: int) -> str:
645
+ candidates = [
646
+ ("apt", "apt-get"),
647
+ ("dnf", "dnf"),
648
+ ("yum", "yum"),
649
+ ("apk", "apk"),
650
+ ("pacman", "pacman"),
651
+ ("zypper", "zypper"),
652
+ ]
653
+ for name, binary in candidates:
654
+ res = _run_pct(vmid, f"command -v {binary} >/dev/null 2>&1")
655
+ if res.get("returncode") == 0:
656
+ logger.debug("Detected package manager %s inside container %s", name, vmid)
657
+ return name
658
+ logger.warning("Unable to detect package manager inside container %s; defaulting to apt", vmid)
659
+ return "apt"
660
+
661
+
621
662
  def _get_storage_type(storages: Iterable[Dict[str, Any]], storage_name: str) -> str:
622
663
  for entry in storages:
623
664
  if entry.get("storage") == storage_name:
@@ -1095,36 +1136,60 @@ def _bootstrap_portacode(
1095
1136
  total_steps: Optional[int] = None,
1096
1137
  default_public_key: Optional[str] = None,
1097
1138
  ) -> Tuple[str, List[Dict[str, Any]]]:
1098
- actual_steps = steps if steps is not None else _build_bootstrap_steps(user, password, ssh_key)
1099
- results, ok = _run_setup_steps(
1100
- vmid,
1101
- actual_steps,
1102
- user,
1103
- progress_callback=progress_callback,
1104
- start_index=start_index,
1105
- total_steps=total_steps,
1106
- )
1107
- if not ok:
1108
- details = results[-1] if results else {}
1109
- summary = details.get("error_summary") or details.get("stderr") or details.get("stdout") or details.get("name")
1110
- history = details.get("history")
1111
- history_snippet = ""
1112
- if isinstance(history, list) and history:
1113
- history_snippet = f" history={history[-3:]}"
1114
- command = details.get("cmd")
1115
- command_text = ""
1116
- if command:
1117
- if isinstance(command, (list, tuple)):
1118
- command_text = shlex.join(str(entry) for entry in command)
1119
- else:
1120
- command_text = str(command)
1121
- command_suffix = f" command={command_text}" if command_text else ""
1122
- if summary:
1123
- logger.warning(
1124
- "Portacode bootstrap failure summary=%s%s%s",
1125
- summary,
1126
- f" history_len={len(history)}" if history else "",
1139
+ if steps is not None:
1140
+ actual_steps = steps
1141
+ else:
1142
+ detected_manager = _detect_package_manager(vmid)
1143
+ actual_steps = _build_bootstrap_steps(
1144
+ user,
1145
+ password,
1146
+ ssh_key,
1147
+ package_manager=detected_manager,
1148
+ )
1149
+ results, ok = _run_setup_steps(
1150
+ vmid,
1151
+ actual_steps,
1152
+ user,
1153
+ progress_callback=progress_callback,
1154
+ start_index=start_index,
1155
+ total_steps=total_steps,
1156
+ )
1157
+ if not ok:
1158
+ details = results[-1] if results else {}
1159
+ summary = details.get("error_summary") or details.get("stderr") or details.get("stdout") or details.get("name")
1160
+ history = details.get("history")
1161
+ history_snippet = ""
1162
+ if isinstance(history, list) and history:
1163
+ history_snippet = f" history={history[-3:]}"
1164
+ command = details.get("cmd")
1165
+ command_text = ""
1166
+ if command:
1167
+ if isinstance(command, (list, tuple)):
1168
+ command_text = shlex.join(str(entry) for entry in command)
1169
+ else:
1170
+ command_text = str(command)
1171
+ command_suffix = f" command={command_text}" if command_text else ""
1172
+ stdout = details.get("stdout")
1173
+ stderr = details.get("stderr")
1174
+ if stdout or stderr:
1175
+ logger.debug(
1176
+ "Bootstrap command output%s%s%s",
1177
+ f" stdout={stdout!r}" if stdout else "",
1178
+ " " if stdout and stderr else "",
1179
+ f"stderr={stderr!r}" if stderr else "",
1180
+ )
1181
+ if summary:
1182
+ logger.warning(
1183
+ "Portacode bootstrap failure summary=%s%s%s",
1184
+ summary,
1185
+ f" history_len={len(history)}" if history else "",
1186
+ f" command={command_text}" if command_text else "",
1187
+ )
1188
+ logger.error(
1189
+ "Portacode bootstrap command failed%s%s%s",
1127
1190
  f" command={command_text}" if command_text else "",
1191
+ f" stdout={stdout!r}" if stdout else "",
1192
+ f" stderr={stderr!r}" if stderr else "",
1128
1193
  )
1129
1194
  raise RuntimeError(
1130
1195
  f"Portacode bootstrap steps failed: {summary}{history_snippet}{command_suffix}"
@@ -1196,8 +1261,9 @@ def configure_infrastructure(token_identifier: str, token_value: str, verify_ssl
1196
1261
  "token_value": token_value,
1197
1262
  "verify_ssl": verify_ssl,
1198
1263
  "default_storage": default_storage,
1199
- "templates": templates,
1200
1264
  "last_verified": datetime.utcnow().isoformat() + "Z",
1265
+ "templates": templates,
1266
+ "templates_last_refreshed": _current_time_iso(),
1201
1267
  "network": network,
1202
1268
  "node_status": status,
1203
1269
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.15.dev11
3
+ Version: 1.4.15.dev13
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=xmWnbzztE5Rc80b-QMEet19V4hm80qIDp1evukvT-mY,721
4
+ portacode/_version.py,sha256=FcKIi3ar9MBpxd5ZXOee5GCUDMFfUL8Remra4zRx4uo,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=GX2IE1J0YX5GHnqqn__e_wpag_Lsa-cWVRQ82MZnbjU,67267
25
+ portacode/connection/handlers/proxmox_infra.py,sha256=nkOuMT1vNKhGs7sHYrqDBju7qGAVC7PWasgd-QNqDUI,69529
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.dev11.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
68
+ portacode-1.4.15.dev13.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.dev11.dist-info/METADATA,sha256=YjU_cRf0I5P6zz-KmGOslg3APj4CL8IgOHbP13Z6T1U,13052
95
- portacode-1.4.15.dev11.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
- portacode-1.4.15.dev11.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
- portacode-1.4.15.dev11.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
- portacode-1.4.15.dev11.dist-info/RECORD,,
94
+ portacode-1.4.15.dev13.dist-info/METADATA,sha256=Hz8hOfUj5NBoq6aU7rG1ceRGPkwPlbqnD9of5XRWgQw,13052
95
+ portacode-1.4.15.dev13.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
+ portacode-1.4.15.dev13.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
+ portacode-1.4.15.dev13.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
+ portacode-1.4.15.dev13.dist-info/RECORD,,