portacode 1.4.16.dev10__py3-none-any.whl → 1.4.16.dev11__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 +2 -2
- portacode/connection/handlers/proxmox_infra.py +285 -328
- {portacode-1.4.16.dev10.dist-info → portacode-1.4.16.dev11.dist-info}/METADATA +1 -1
- {portacode-1.4.16.dev10.dist-info → portacode-1.4.16.dev11.dist-info}/RECORD +8 -8
- {portacode-1.4.16.dev10.dist-info → portacode-1.4.16.dev11.dist-info}/WHEEL +0 -0
- {portacode-1.4.16.dev10.dist-info → portacode-1.4.16.dev11.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.16.dev10.dist-info → portacode-1.4.16.dev11.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.16.dev10.dist-info → portacode-1.4.16.dev11.dist-info}/top_level.txt +0 -0
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.16.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 4, 16, '
|
|
31
|
+
__version__ = version = '1.4.16.dev11'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 16, 'dev11')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -19,7 +19,7 @@ import time
|
|
|
19
19
|
import threading
|
|
20
20
|
from datetime import datetime, timezone
|
|
21
21
|
from pathlib import Path
|
|
22
|
-
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence,
|
|
22
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
23
23
|
|
|
24
24
|
import platformdirs
|
|
25
25
|
|
|
@@ -202,67 +202,66 @@ def _current_time_iso() -> str:
|
|
|
202
202
|
return datetime.now(timezone.utc).isoformat()
|
|
203
203
|
|
|
204
204
|
|
|
205
|
-
def
|
|
205
|
+
def _parse_iso_timestamp(value: str) -> Optional[datetime]:
|
|
206
|
+
if not value:
|
|
207
|
+
return None
|
|
208
|
+
text = value
|
|
209
|
+
if text.endswith("Z"):
|
|
210
|
+
text = text[:-1] + "+00:00"
|
|
206
211
|
try:
|
|
207
|
-
return
|
|
208
|
-
except
|
|
209
|
-
return
|
|
212
|
+
return datetime.fromisoformat(text)
|
|
213
|
+
except ValueError:
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _templates_need_refresh(config: Dict[str, Any]) -> bool:
|
|
218
|
+
if not config or not config.get("token_value"):
|
|
219
|
+
return False
|
|
220
|
+
last = _parse_iso_timestamp(config.get("templates_last_refreshed") or "")
|
|
221
|
+
if not last:
|
|
222
|
+
return True
|
|
223
|
+
return (datetime.now(timezone.utc) - last).total_seconds() >= TEMPLATES_REFRESH_INTERVAL_S
|
|
210
224
|
|
|
211
225
|
|
|
212
|
-
def
|
|
226
|
+
def _ensure_templates_refreshed_on_startup(config: Dict[str, Any]) -> None:
|
|
227
|
+
if not _templates_need_refresh(config):
|
|
228
|
+
return
|
|
213
229
|
try:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
230
|
+
client = _build_proxmox_client_from_config(config)
|
|
231
|
+
node = config.get("node") or _pick_node(client)
|
|
232
|
+
storages = client.nodes(node).storage.get()
|
|
233
|
+
templates = _list_templates(client, node, storages)
|
|
234
|
+
if templates:
|
|
235
|
+
config["templates"] = templates
|
|
236
|
+
config["templates_last_refreshed"] = _current_time_iso()
|
|
237
|
+
_save_config(config)
|
|
238
|
+
except Exception as exc:
|
|
239
|
+
logger.warning("Unable to refresh Proxmox templates on startup: %s", exc)
|
|
217
240
|
|
|
218
241
|
|
|
219
|
-
def
|
|
220
|
-
if
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
242
|
+
def _pick_storage(storages: Iterable[Dict[str, Any]]) -> str:
|
|
243
|
+
candidates = [s for s in storages if "rootdir" in s.get("content", "") and s.get("avail", 0) > 0]
|
|
244
|
+
if not candidates:
|
|
245
|
+
candidates = [s for s in storages if "rootdir" in s.get("content", "")]
|
|
246
|
+
if not candidates:
|
|
247
|
+
return ""
|
|
248
|
+
candidates.sort(key=lambda entry: entry.get("avail", 0), reverse=True)
|
|
249
|
+
return candidates[0].get("storage", "")
|
|
224
250
|
|
|
225
251
|
|
|
226
|
-
def
|
|
227
|
-
if isinstance(value, (int, float)):
|
|
228
|
-
return float(value)
|
|
229
|
-
text = str(value or "").strip()
|
|
230
|
-
if not text:
|
|
231
|
-
return 0.0
|
|
232
|
-
match = re.match(r"(?i)^\s*([0-9]*\.?[0-9]+)\s*([kmgtp]?i?b?)?\s*$", text)
|
|
233
|
-
if not match:
|
|
234
|
-
return 0.0
|
|
235
|
-
number = match.group(1)
|
|
236
|
-
unit = (match.group(2) or "").lower()
|
|
252
|
+
def _bytes_to_gib(value: Any) -> float:
|
|
237
253
|
try:
|
|
238
|
-
|
|
239
|
-
except ValueError:
|
|
254
|
+
return float(value) / 1024**3
|
|
255
|
+
except (TypeError, ValueError):
|
|
240
256
|
return 0.0
|
|
241
|
-
if unit.startswith("k"):
|
|
242
|
-
return value * 1024
|
|
243
|
-
if unit.startswith("m"):
|
|
244
|
-
return value * 1024**2
|
|
245
|
-
if unit.startswith("g"):
|
|
246
|
-
return value * 1024**3
|
|
247
|
-
if unit.startswith("t"):
|
|
248
|
-
return value * 1024**4
|
|
249
|
-
if unit.startswith("p"):
|
|
250
|
-
return value * 1024**5
|
|
251
|
-
return value
|
|
252
|
-
|
|
253
257
|
|
|
254
|
-
def _bytes_to_mib(value: Any) -> int:
|
|
255
|
-
return int(round(_normalize_bytes(value) / (1024**2)))
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def _bytes_to_gib(value: Any) -> int:
|
|
259
|
-
return int(round(_normalize_bytes(value) / (1024**3)))
|
|
260
258
|
|
|
259
|
+
def _bytes_to_mib(value: Any) -> float:
|
|
260
|
+
try:
|
|
261
|
+
return float(value) / 1024**2
|
|
262
|
+
except (TypeError, ValueError):
|
|
263
|
+
return 0.0
|
|
261
264
|
|
|
262
|
-
def _normalize_storage_name(name: Any) -> str:
|
|
263
|
-
if not name:
|
|
264
|
-
return ""
|
|
265
|
-
return str(name).strip().lower()
|
|
266
265
|
|
|
267
266
|
def _size_token_to_gib(token: str) -> float:
|
|
268
267
|
match = re.match(r"^\s*([0-9]+(?:\.[0-9]+)?)\s*([KMGTP])?([iI]?[bB])?\s*$", token)
|
|
@@ -272,11 +271,11 @@ def _size_token_to_gib(token: str) -> float:
|
|
|
272
271
|
unit = (match.group(2) or "").upper()
|
|
273
272
|
scale = {
|
|
274
273
|
"": 1,
|
|
275
|
-
"K": 1024
|
|
276
|
-
"M": 1024
|
|
277
|
-
"G": 1024
|
|
278
|
-
"T": 1024
|
|
279
|
-
"P": 1024
|
|
274
|
+
"K": 1024**1,
|
|
275
|
+
"M": 1024**2,
|
|
276
|
+
"G": 1024**3,
|
|
277
|
+
"T": 1024**4,
|
|
278
|
+
"P": 1024**5,
|
|
280
279
|
}.get(unit, 1)
|
|
281
280
|
return (number * scale) / 1024**3
|
|
282
281
|
|
|
@@ -292,7 +291,7 @@ def _extract_size_gib(value: Any) -> float:
|
|
|
292
291
|
return _size_token_to_gib(text)
|
|
293
292
|
|
|
294
293
|
|
|
295
|
-
def
|
|
294
|
+
def _extract_storage_token(value: Any) -> str:
|
|
296
295
|
if not value:
|
|
297
296
|
return "unknown"
|
|
298
297
|
text = str(value)
|
|
@@ -303,12 +302,12 @@ def _extract_storage(value: Any) -> str:
|
|
|
303
302
|
|
|
304
303
|
def _storage_from_lxc(cfg: Dict[str, Any], entry: Dict[str, Any]) -> str:
|
|
305
304
|
rootfs = cfg.get("rootfs") or entry.get("rootfs")
|
|
306
|
-
storage =
|
|
305
|
+
storage = _extract_storage_token(rootfs)
|
|
307
306
|
if storage != "unknown":
|
|
308
307
|
return storage
|
|
309
308
|
for idx in range(0, 10):
|
|
310
309
|
mp_value = cfg.get(f"mp{idx}")
|
|
311
|
-
storage =
|
|
310
|
+
storage = _extract_storage_token(mp_value)
|
|
312
311
|
if storage != "unknown":
|
|
313
312
|
return storage
|
|
314
313
|
return "unknown"
|
|
@@ -318,7 +317,7 @@ def _storage_from_qemu(cfg: Dict[str, Any]) -> str:
|
|
|
318
317
|
preferred_keys: List[str] = []
|
|
319
318
|
for prefix in ("scsi", "virtio", "sata", "ide"):
|
|
320
319
|
preferred_keys.extend(f"{prefix}{idx}" for idx in range(0, 6))
|
|
321
|
-
seen
|
|
320
|
+
seen = set()
|
|
322
321
|
for key in preferred_keys:
|
|
323
322
|
value = cfg.get(key)
|
|
324
323
|
if value is None:
|
|
@@ -327,7 +326,7 @@ def _storage_from_qemu(cfg: Dict[str, Any]) -> str:
|
|
|
327
326
|
text = str(value)
|
|
328
327
|
if "media=cdrom" in text or "cloudinit" in text:
|
|
329
328
|
continue
|
|
330
|
-
storage =
|
|
329
|
+
storage = _extract_storage_token(text)
|
|
331
330
|
if storage != "unknown":
|
|
332
331
|
return storage
|
|
333
332
|
for key in sorted(cfg.keys()):
|
|
@@ -341,11 +340,11 @@ def _storage_from_qemu(cfg: Dict[str, Any]) -> str:
|
|
|
341
340
|
text = str(value)
|
|
342
341
|
if "media=cdrom" in text or "cloudinit" in text:
|
|
343
342
|
continue
|
|
344
|
-
storage =
|
|
343
|
+
storage = _extract_storage_token(text)
|
|
345
344
|
if storage != "unknown":
|
|
346
345
|
return storage
|
|
347
346
|
for key in ("efidisk0", "tpmstate0"):
|
|
348
|
-
storage =
|
|
347
|
+
storage = _extract_storage_token(cfg.get(key))
|
|
349
348
|
if storage != "unknown":
|
|
350
349
|
return storage
|
|
351
350
|
return "unknown"
|
|
@@ -359,7 +358,7 @@ def _primary_qemu_disk(cfg: Dict[str, Any]) -> str:
|
|
|
359
358
|
preferred_keys: List[str] = []
|
|
360
359
|
for prefix in ("scsi", "virtio", "sata", "ide"):
|
|
361
360
|
preferred_keys.extend(f"{prefix}{idx}" for idx in range(0, 6))
|
|
362
|
-
seen
|
|
361
|
+
seen = set()
|
|
363
362
|
for key in preferred_keys:
|
|
364
363
|
value = cfg.get(key)
|
|
365
364
|
if value is None:
|
|
@@ -384,8 +383,8 @@ def _primary_qemu_disk(cfg: Dict[str, Any]) -> str:
|
|
|
384
383
|
return ""
|
|
385
384
|
|
|
386
385
|
|
|
387
|
-
def
|
|
388
|
-
storage =
|
|
386
|
+
def _pick_container_storage(kind: str, cfg: Dict[str, Any], entry: Dict[str, Any]) -> str:
|
|
387
|
+
storage = _extract_storage_token(cfg.get("storage") or entry.get("storage"))
|
|
389
388
|
if storage != "unknown":
|
|
390
389
|
return storage
|
|
391
390
|
if kind == "lxc":
|
|
@@ -393,7 +392,7 @@ def _pick_storage(kind: str, cfg: Dict[str, Any], entry: Dict[str, Any]) -> str:
|
|
|
393
392
|
return _storage_from_qemu(cfg)
|
|
394
393
|
|
|
395
394
|
|
|
396
|
-
def
|
|
395
|
+
def _pick_container_disk_gib(kind: str, cfg: Dict[str, Any], entry: Dict[str, Any]) -> float:
|
|
397
396
|
if kind == "lxc":
|
|
398
397
|
size = _extract_size_gib(_primary_lxc_disk(cfg, entry))
|
|
399
398
|
if size:
|
|
@@ -402,68 +401,63 @@ def _pick_disk_gib(kind: str, cfg: Dict[str, Any], entry: Dict[str, Any]) -> flo
|
|
|
402
401
|
size = _extract_size_gib(_primary_qemu_disk(cfg))
|
|
403
402
|
if size:
|
|
404
403
|
return size
|
|
405
|
-
for candidate in (entry.get("maxdisk"), entry.get("disk")):
|
|
406
|
-
if candidate
|
|
404
|
+
for candidate in (entry.get("maxdisk"), entry.get("disk"), cfg.get("disk")):
|
|
405
|
+
if candidate is None or candidate == 0:
|
|
407
406
|
continue
|
|
408
|
-
return
|
|
409
|
-
cfg_disk = cfg.get("disk")
|
|
410
|
-
if cfg_disk not in (None, 0):
|
|
411
|
-
return _normalize_bytes(cfg_disk) / 1024**3
|
|
407
|
+
return _bytes_to_gib(candidate)
|
|
412
408
|
return 0.0
|
|
413
409
|
|
|
414
410
|
|
|
415
|
-
def
|
|
416
|
-
if isinstance(value, bool):
|
|
417
|
-
return value
|
|
418
|
-
text = str(value or "").strip().lower()
|
|
419
|
-
return text in {"1", "true", "yes", "on"}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
def _parse_iso_timestamp(value: str) -> Optional[datetime]:
|
|
423
|
-
if not value:
|
|
424
|
-
return None
|
|
425
|
-
text = value
|
|
426
|
-
if text.endswith("Z"):
|
|
427
|
-
text = text[:-1] + "+00:00"
|
|
411
|
+
def _to_mib(value: Any) -> float:
|
|
428
412
|
try:
|
|
429
|
-
|
|
430
|
-
except ValueError:
|
|
431
|
-
return
|
|
413
|
+
val = float(value)
|
|
414
|
+
except (TypeError, ValueError):
|
|
415
|
+
return 0.0
|
|
416
|
+
if val <= 0:
|
|
417
|
+
return 0.0
|
|
418
|
+
# Heuristic: large values are bytes, smaller ones are already MiB.
|
|
419
|
+
return _bytes_to_mib(val) if val > 10000 else val
|
|
432
420
|
|
|
433
421
|
|
|
434
|
-
def
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
return (datetime.now(timezone.utc) - last).total_seconds() >= TEMPLATES_REFRESH_INTERVAL_S
|
|
422
|
+
def _pick_container_ram_mib(kind: str, cfg: Dict[str, Any], entry: Dict[str, Any]) -> float:
|
|
423
|
+
for candidate in (cfg.get("memory"), entry.get("maxmem"), entry.get("mem")):
|
|
424
|
+
ram = _to_mib(candidate)
|
|
425
|
+
if ram:
|
|
426
|
+
return ram
|
|
427
|
+
return 0.0
|
|
441
428
|
|
|
442
429
|
|
|
443
|
-
def
|
|
444
|
-
if not _templates_need_refresh(config):
|
|
445
|
-
return
|
|
430
|
+
def _safe_float(value: Any) -> float:
|
|
446
431
|
try:
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
templates = _list_templates(client, node, storages)
|
|
451
|
-
if templates:
|
|
452
|
-
config["templates"] = templates
|
|
453
|
-
config["templates_last_refreshed"] = _current_time_iso()
|
|
454
|
-
_save_config(config)
|
|
455
|
-
except Exception as exc:
|
|
456
|
-
logger.warning("Unable to refresh Proxmox templates on startup: %s", exc)
|
|
432
|
+
return float(value)
|
|
433
|
+
except (TypeError, ValueError):
|
|
434
|
+
return 0.0
|
|
457
435
|
|
|
458
436
|
|
|
459
|
-
def
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
437
|
+
def _pick_container_cpu_share(kind: str, cfg: Dict[str, Any], entry: Dict[str, Any]) -> float:
|
|
438
|
+
if kind == "lxc":
|
|
439
|
+
for key in ("cpulimit", "cores", "cpus"):
|
|
440
|
+
val = _safe_float(cfg.get(key))
|
|
441
|
+
if val:
|
|
442
|
+
return val
|
|
443
|
+
return _safe_float(entry.get("cpus"))
|
|
444
|
+
|
|
445
|
+
cores = _safe_float(cfg.get("cores"))
|
|
446
|
+
sockets = _safe_float(cfg.get("sockets")) or 1.0
|
|
447
|
+
if cores:
|
|
448
|
+
return cores * sockets
|
|
449
|
+
val = _safe_float(cfg.get("vcpus"))
|
|
450
|
+
if val:
|
|
451
|
+
return val
|
|
452
|
+
val = _safe_float(entry.get("cpus") or entry.get("maxcpu"))
|
|
453
|
+
if val:
|
|
454
|
+
return val
|
|
455
|
+
return 0.0
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _parse_onboot_flag(value: Any) -> bool:
|
|
459
|
+
text = str(value).strip().lower()
|
|
460
|
+
return text in {"1", "true", "yes", "on"}
|
|
467
461
|
|
|
468
462
|
|
|
469
463
|
def _write_bridge_config(bridge: str) -> None:
|
|
@@ -599,104 +593,35 @@ def _load_managed_container_records() -> List[Dict[str, Any]]:
|
|
|
599
593
|
return records
|
|
600
594
|
|
|
601
595
|
|
|
602
|
-
def
|
|
603
|
-
if not node_status:
|
|
604
|
-
return None, None, None
|
|
605
|
-
memory_total = node_status.get("memory", {}).get("total")
|
|
606
|
-
disk_total = node_status.get("disk", {}).get("total")
|
|
607
|
-
cpu_cores = node_status.get("cpuinfo", {}).get("cores")
|
|
608
|
-
host_ram = _bytes_to_mib(memory_total) if memory_total is not None else None
|
|
609
|
-
host_disk = _bytes_to_gib(disk_total) if disk_total is not None else None
|
|
610
|
-
host_cpu = _to_int(cpu_cores) if cpu_cores is not None else None
|
|
611
|
-
return host_ram, host_disk, host_cpu
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
def _build_unmanaged_container_entry(
|
|
615
|
-
ct: Dict[str, Any],
|
|
616
|
-
cfg: Dict[str, Any],
|
|
617
|
-
vmid: str,
|
|
618
|
-
default_storage: str | None,
|
|
619
|
-
*,
|
|
620
|
-
entry_type: str = "lxc",
|
|
621
|
-
) -> Dict[str, Any]:
|
|
622
|
-
ram_mib = _to_int(cfg.get("memory")) or _bytes_to_mib(ct.get("maxmem"))
|
|
623
|
-
disk_gib = int(round(_pick_disk_gib(entry_type, cfg, ct)))
|
|
624
|
-
cpu_share = _to_float(
|
|
625
|
-
cfg.get("cpulimit")
|
|
626
|
-
or cfg.get("cpus")
|
|
627
|
-
or cfg.get("cores")
|
|
628
|
-
or ct.get("cpus")
|
|
629
|
-
or ct.get("cpu")
|
|
630
|
-
)
|
|
631
|
-
hostname = ct.get("name") or cfg.get("hostname") or f"ct{vmid}"
|
|
632
|
-
storage = _pick_storage(entry_type, cfg, ct)
|
|
633
|
-
status = (ct.get("status") or "unknown").lower()
|
|
634
|
-
reserved = _parse_bool_flag(cfg.get("onboot"))
|
|
635
|
-
storage_matches_default = _storage_matches_default(storage, default_storage)
|
|
636
|
-
return {
|
|
637
|
-
"vmid": vmid,
|
|
638
|
-
"hostname": hostname,
|
|
639
|
-
"template": cfg.get("ostemplate"),
|
|
640
|
-
"storage": storage,
|
|
641
|
-
"disk_gib": disk_gib,
|
|
642
|
-
"ram_mib": ram_mib,
|
|
643
|
-
"cpu_share": cpu_share,
|
|
644
|
-
"reserve_on_boot": reserved,
|
|
645
|
-
"matches_default_storage": storage_matches_default,
|
|
646
|
-
"type": entry_type,
|
|
647
|
-
"status": status,
|
|
648
|
-
"managed": False,
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
def _get_storage_snapshot(proxmox: Any, node: str, storage_name: str | None) -> Dict[str, Any] | None:
|
|
653
|
-
if not storage_name:
|
|
654
|
-
return None
|
|
655
|
-
try:
|
|
656
|
-
storage = proxmox.nodes(node).storage(storage_name).status.get()
|
|
657
|
-
except Exception as exc:
|
|
658
|
-
logger.debug("Unable to read storage status %s:%s: %s", node, storage_name, exc)
|
|
659
|
-
return None
|
|
660
|
-
total_bytes = storage.get("total")
|
|
661
|
-
avail_bytes = storage.get("avail")
|
|
662
|
-
used_bytes = storage.get("used")
|
|
663
|
-
return {
|
|
664
|
-
"storage": storage_name,
|
|
665
|
-
"total_gib": _bytes_to_gib(total_bytes) if total_bytes is not None else None,
|
|
666
|
-
"avail_gib": _bytes_to_gib(avail_bytes) if avail_bytes is not None else None,
|
|
667
|
-
"used_gib": _bytes_to_gib(used_bytes) if used_bytes is not None else None,
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
def _storage_matches_default(storage_name: Any, default_storage: str | None) -> bool:
|
|
672
|
-
if not default_storage:
|
|
673
|
-
return True
|
|
674
|
-
return _normalize_storage_name(storage_name) == _normalize_storage_name(default_storage)
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
def _build_managed_containers_summary(
|
|
678
|
-
records: List[Dict[str, Any]],
|
|
679
|
-
unmanaged_records: List[Dict[str, Any]],
|
|
680
|
-
node_status: Dict[str, Any] | None,
|
|
681
|
-
storage_snapshot: Dict[str, Any] | None,
|
|
682
|
-
default_storage: str | None,
|
|
683
|
-
) -> Dict[str, Any]:
|
|
684
|
-
managed_containers: List[Dict[str, Any]] = []
|
|
596
|
+
def _build_managed_containers_summary(records: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
685
597
|
total_ram = 0
|
|
686
598
|
total_disk = 0
|
|
687
599
|
total_cpu_share = 0.0
|
|
600
|
+
containers: List[Dict[str, Any]] = []
|
|
688
601
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
602
|
+
def _as_int(value: Any) -> int:
|
|
603
|
+
try:
|
|
604
|
+
return int(value)
|
|
605
|
+
except (TypeError, ValueError):
|
|
606
|
+
return 0
|
|
607
|
+
|
|
608
|
+
def _as_float(value: Any) -> float:
|
|
609
|
+
try:
|
|
610
|
+
return float(value)
|
|
611
|
+
except (TypeError, ValueError):
|
|
612
|
+
return 0.0
|
|
613
|
+
|
|
614
|
+
for record in sorted(records, key=lambda entry: _as_int(entry.get("vmid"))):
|
|
615
|
+
ram_mib = _as_int(record.get("ram_mib"))
|
|
616
|
+
disk_gib = _as_int(record.get("disk_gib"))
|
|
617
|
+
cpu_share = _as_float(record.get("cpus"))
|
|
693
618
|
total_ram += ram_mib
|
|
694
619
|
total_disk += disk_gib
|
|
695
620
|
total_cpu_share += cpu_share
|
|
696
621
|
status = (record.get("status") or "unknown").lower()
|
|
697
|
-
|
|
622
|
+
containers.append(
|
|
698
623
|
{
|
|
699
|
-
"vmid": str(
|
|
624
|
+
"vmid": str(_as_int(record.get("vmid"))) if record.get("vmid") is not None else None,
|
|
700
625
|
"device_id": record.get("device_id"),
|
|
701
626
|
"hostname": record.get("hostname"),
|
|
702
627
|
"template": record.get("template"),
|
|
@@ -706,131 +631,177 @@ def _build_managed_containers_summary(
|
|
|
706
631
|
"cpu_share": cpu_share,
|
|
707
632
|
"created_at": record.get("created_at"),
|
|
708
633
|
"status": status,
|
|
709
|
-
"managed": True,
|
|
710
|
-
"matches_default_storage": _storage_matches_default(record.get("storage"), default_storage),
|
|
711
634
|
}
|
|
712
635
|
)
|
|
713
636
|
|
|
714
|
-
unmanaged_total_ram = sum(
|
|
715
|
-
_to_int(entry.get("ram_mib"))
|
|
716
|
-
for entry in unmanaged_records
|
|
717
|
-
if entry.get("reserve_on_boot")
|
|
718
|
-
)
|
|
719
|
-
unmanaged_total_disk = sum(
|
|
720
|
-
_to_int(entry.get("disk_gib"))
|
|
721
|
-
for entry in unmanaged_records
|
|
722
|
-
if entry.get("matches_default_storage")
|
|
723
|
-
)
|
|
724
|
-
unmanaged_total_cpu = sum(
|
|
725
|
-
_to_float(entry.get("cpu_share"))
|
|
726
|
-
for entry in unmanaged_records
|
|
727
|
-
if entry.get("reserve_on_boot")
|
|
728
|
-
)
|
|
729
|
-
|
|
730
|
-
allocated_ram = total_ram + unmanaged_total_ram
|
|
731
|
-
allocated_disk = total_disk + unmanaged_total_disk
|
|
732
|
-
allocated_cpu = total_cpu_share + unmanaged_total_cpu
|
|
733
|
-
|
|
734
|
-
host_ram_node, host_disk_node, host_cpu_node = _extract_host_totals(node_status)
|
|
735
|
-
storage_host_disk = storage_snapshot.get("total_gib") if storage_snapshot else None
|
|
736
|
-
host_ram = host_ram_node
|
|
737
|
-
host_disk = storage_host_disk if storage_host_disk is not None else host_disk_node
|
|
738
|
-
host_cpu = host_cpu_node
|
|
739
|
-
|
|
740
|
-
|
|
741
637
|
return {
|
|
742
638
|
"updated_at": datetime.utcnow().isoformat() + "Z",
|
|
743
|
-
"count": len(
|
|
639
|
+
"count": len(containers),
|
|
744
640
|
"total_ram_mib": total_ram,
|
|
745
641
|
"total_disk_gib": total_disk,
|
|
746
642
|
"total_cpu_share": round(total_cpu_share, 2),
|
|
747
|
-
"containers":
|
|
748
|
-
"unmanaged_containers": unmanaged_records,
|
|
749
|
-
"unmanaged_count": len(unmanaged_records),
|
|
750
|
-
"allocated_ram_mib": allocated_ram,
|
|
751
|
-
"allocated_disk_gib": allocated_disk,
|
|
752
|
-
"allocated_cpu_share": round(allocated_cpu, 2),
|
|
753
|
-
"available_ram_mib": _calculate_available(host_ram, allocated_ram) if host_ram is not None else None,
|
|
754
|
-
"available_disk_gib": _calculate_available(host_disk, allocated_disk) if host_disk is not None else None,
|
|
755
|
-
"available_cpu_share": _calculate_available(host_cpu, allocated_cpu) if host_cpu is not None else None,
|
|
756
|
-
"host_total_ram_mib": host_ram,
|
|
757
|
-
"host_total_disk_gib": host_disk,
|
|
758
|
-
"host_total_cpu_cores": host_cpu,
|
|
759
|
-
"default_storage": default_storage,
|
|
760
|
-
"default_storage_snapshot": storage_snapshot,
|
|
643
|
+
"containers": containers,
|
|
761
644
|
}
|
|
762
645
|
|
|
763
646
|
|
|
647
|
+
def _build_full_container_summary(records: List[Dict[str, Any]], config: Dict[str, Any]) -> Dict[str, Any]:
|
|
648
|
+
base_summary = _build_managed_containers_summary(records)
|
|
649
|
+
if not config or not config.get("token_value"):
|
|
650
|
+
return base_summary
|
|
651
|
+
|
|
652
|
+
try:
|
|
653
|
+
proxmox = _connect_proxmox(config)
|
|
654
|
+
node = _get_node_from_config(config)
|
|
655
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
656
|
+
logger.debug("Unable to extend container summary with Proxmox data: %s", exc)
|
|
657
|
+
return base_summary
|
|
658
|
+
|
|
659
|
+
default_storage = (config.get("default_storage") or "").strip()
|
|
660
|
+
record_map: Dict[str, Dict[str, Any]] = {}
|
|
661
|
+
for record in records:
|
|
662
|
+
vmid = record.get("vmid")
|
|
663
|
+
if vmid is None:
|
|
664
|
+
continue
|
|
665
|
+
try:
|
|
666
|
+
vmid_key = str(int(vmid))
|
|
667
|
+
except (ValueError, TypeError):
|
|
668
|
+
continue
|
|
669
|
+
record_map[vmid_key] = record
|
|
670
|
+
|
|
671
|
+
managed_entries: List[Dict[str, Any]] = []
|
|
672
|
+
unmanaged_entries: List[Dict[str, Any]] = []
|
|
673
|
+
allocated_ram = 0.0
|
|
674
|
+
allocated_disk = 0.0
|
|
675
|
+
allocated_cpu = 0.0
|
|
676
|
+
|
|
677
|
+
def _process_entries(kind: str, getter: str) -> None:
|
|
678
|
+
nonlocal allocated_ram, allocated_disk, allocated_cpu
|
|
679
|
+
entries = getattr(proxmox.nodes(node), getter).get()
|
|
680
|
+
for entry in entries:
|
|
681
|
+
vmid = entry.get("vmid")
|
|
682
|
+
if vmid is None:
|
|
683
|
+
continue
|
|
684
|
+
vmid_str = str(vmid)
|
|
685
|
+
cfg: Dict[str, Any] = {}
|
|
686
|
+
try:
|
|
687
|
+
cfg = getattr(proxmox.nodes(node), getter)(vmid_str).config.get() or {}
|
|
688
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
689
|
+
logger.debug("Failed to load %s config for %s: %s", kind, vmid_str, exc)
|
|
690
|
+
cfg = {}
|
|
691
|
+
|
|
692
|
+
record = record_map.get(vmid_str)
|
|
693
|
+
description = cfg.get("description") or ""
|
|
694
|
+
managed = bool(record) or MANAGED_MARKER in description
|
|
695
|
+
hostname = entry.get("name") or cfg.get("hostname") or (record.get("hostname") if record else None)
|
|
696
|
+
storage = _pick_container_storage(kind, cfg, entry)
|
|
697
|
+
disk_gib = _pick_container_disk_gib(kind, cfg, entry)
|
|
698
|
+
ram_mib = _pick_container_ram_mib(kind, cfg, entry)
|
|
699
|
+
cpu_share = _pick_container_cpu_share(kind, cfg, entry)
|
|
700
|
+
reserve_on_boot = _parse_onboot_flag(cfg.get("onboot"))
|
|
701
|
+
matches_default_storage = bool(default_storage and storage and storage.lower() == default_storage.lower())
|
|
702
|
+
|
|
703
|
+
base_entry = {
|
|
704
|
+
"type": kind,
|
|
705
|
+
"vmid": vmid_str,
|
|
706
|
+
"hostname": hostname,
|
|
707
|
+
"status": (entry.get("status") or "unknown").lower(),
|
|
708
|
+
"storage": storage,
|
|
709
|
+
"disk_gib": disk_gib,
|
|
710
|
+
"ram_mib": ram_mib,
|
|
711
|
+
"cpu_share": cpu_share,
|
|
712
|
+
"reserve_on_boot": reserve_on_boot,
|
|
713
|
+
"matches_default_storage": matches_default_storage,
|
|
714
|
+
"managed": managed,
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if managed:
|
|
718
|
+
merged = base_entry | {
|
|
719
|
+
"device_id": record.get("device_id") if record else None,
|
|
720
|
+
"template": record.get("template") if record else None,
|
|
721
|
+
"created_at": record.get("created_at") if record else None,
|
|
722
|
+
}
|
|
723
|
+
managed_entries.append(merged)
|
|
724
|
+
else:
|
|
725
|
+
unmanaged_entries.append(base_entry)
|
|
726
|
+
|
|
727
|
+
if managed or reserve_on_boot:
|
|
728
|
+
allocated_ram += ram_mib
|
|
729
|
+
allocated_cpu += cpu_share
|
|
730
|
+
if managed or matches_default_storage:
|
|
731
|
+
allocated_disk += disk_gib
|
|
732
|
+
|
|
733
|
+
_process_entries("lxc", "lxc")
|
|
734
|
+
_process_entries("qemu", "qemu")
|
|
735
|
+
|
|
736
|
+
memory_info = {}
|
|
737
|
+
cpu_info = {}
|
|
738
|
+
try:
|
|
739
|
+
node_status = proxmox.nodes(node).status.get()
|
|
740
|
+
memory_info = node_status.get("memory") or {}
|
|
741
|
+
cpu_info = node_status.get("cpuinfo") or {}
|
|
742
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
743
|
+
logger.debug("Unable to read node status for resource totals: %s", exc)
|
|
744
|
+
|
|
745
|
+
host_total_ram_mib = _bytes_to_mib(memory_info.get("total"))
|
|
746
|
+
used_ram_mib = _bytes_to_mib(memory_info.get("used"))
|
|
747
|
+
available_ram_mib = max(host_total_ram_mib - used_ram_mib, 0.0) if host_total_ram_mib else None
|
|
748
|
+
host_total_cpu_cores = _safe_float(cpu_info.get("cores"))
|
|
749
|
+
available_cpu_share = max(host_total_cpu_cores - allocated_cpu, 0.0) if host_total_cpu_cores else None
|
|
750
|
+
|
|
751
|
+
host_total_disk_gib = None
|
|
752
|
+
available_disk_gib = None
|
|
753
|
+
if default_storage:
|
|
754
|
+
try:
|
|
755
|
+
storage_status = proxmox.nodes(node).storage(default_storage).status.get()
|
|
756
|
+
host_total_disk_gib = _bytes_to_gib(storage_status.get("total"))
|
|
757
|
+
available_disk_gib = _bytes_to_gib(storage_status.get("avail"))
|
|
758
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
759
|
+
logger.debug("Unable to read storage status for %s: %s", default_storage, exc)
|
|
760
|
+
|
|
761
|
+
summary = base_summary.copy()
|
|
762
|
+
summary["containers"] = managed_entries
|
|
763
|
+
summary["count"] = len(managed_entries)
|
|
764
|
+
summary["total_ram_mib"] = int(sum(entry.get("ram_mib") or 0 for entry in managed_entries))
|
|
765
|
+
summary["total_disk_gib"] = int(sum(entry.get("disk_gib") or 0 for entry in managed_entries))
|
|
766
|
+
summary["total_cpu_share"] = round(sum(entry.get("cpu_share") or 0 for entry in managed_entries), 2)
|
|
767
|
+
summary["unmanaged_containers"] = unmanaged_entries
|
|
768
|
+
summary["allocated_ram_mib"] = round(allocated_ram, 2)
|
|
769
|
+
summary["allocated_disk_gib"] = round(allocated_disk, 2)
|
|
770
|
+
summary["allocated_cpu_share"] = round(allocated_cpu, 2)
|
|
771
|
+
summary["host_total_ram_mib"] = int(host_total_ram_mib) if host_total_ram_mib else None
|
|
772
|
+
summary["host_total_disk_gib"] = host_total_disk_gib
|
|
773
|
+
summary["host_total_cpu_cores"] = host_total_cpu_cores if host_total_cpu_cores else None
|
|
774
|
+
summary["available_ram_mib"] = int(available_ram_mib) if available_ram_mib is not None else None
|
|
775
|
+
summary["available_disk_gib"] = available_disk_gib
|
|
776
|
+
summary["available_cpu_share"] = available_cpu_share if available_cpu_share is not None else None
|
|
777
|
+
return summary
|
|
778
|
+
|
|
779
|
+
|
|
764
780
|
def _get_managed_containers_summary(force: bool = False) -> Dict[str, Any]:
|
|
765
|
-
def _refresh_container_statuses(
|
|
766
|
-
records
|
|
767
|
-
|
|
768
|
-
managed_vmids: Set[str],
|
|
769
|
-
default_storage: str | None,
|
|
770
|
-
) -> Tuple[Dict[str, str], List[Dict[str, Any]], Dict[str, Any] | None, Dict[str, Any] | None]:
|
|
771
|
-
statuses: Dict[str, str] = {}
|
|
772
|
-
unmanaged: List[Dict[str, Any]] = []
|
|
773
|
-
node_status: Dict[str, Any] | None = None
|
|
774
|
-
if not config:
|
|
775
|
-
return statuses, unmanaged, node_status, None
|
|
776
|
-
proxmox = None
|
|
777
|
-
node = None
|
|
781
|
+
def _refresh_container_statuses(records: List[Dict[str, Any]], config: Dict[str, Any] | None) -> None:
|
|
782
|
+
if not records or not config:
|
|
783
|
+
return
|
|
778
784
|
try:
|
|
779
785
|
proxmox = _connect_proxmox(config)
|
|
780
786
|
node = _get_node_from_config(config)
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
continue
|
|
786
|
-
vmid_key = str(_to_int(vmid_val))
|
|
787
|
-
statuses[vmid_key] = (ct.get("status") or "unknown").lower()
|
|
788
|
-
if vmid_key in managed_vmids:
|
|
789
|
-
continue
|
|
790
|
-
cfg: Dict[str, Any] = {}
|
|
791
|
-
try:
|
|
792
|
-
cfg = proxmox.nodes(node).lxc(vmid_key).config.get() or {}
|
|
793
|
-
except Exception as exc: # pragma: no cover - best effort
|
|
794
|
-
logger.debug("Failed to read config for container %s: %s", vmid_key, exc)
|
|
795
|
-
description = (cfg.get("description") or "")
|
|
796
|
-
if MANAGED_MARKER in description:
|
|
797
|
-
continue
|
|
798
|
-
unmanaged.append(
|
|
799
|
-
_build_unmanaged_container_entry(ct, cfg, vmid_key, default_storage, entry_type="lxc")
|
|
800
|
-
)
|
|
801
|
-
for vm in proxmox.nodes(node).qemu.get():
|
|
802
|
-
vmid_val = vm.get("vmid")
|
|
803
|
-
if vmid_val is None:
|
|
804
|
-
continue
|
|
805
|
-
vmid_key = str(_to_int(vmid_val))
|
|
806
|
-
statuses[vmid_key] = (vm.get("status") or "unknown").lower()
|
|
807
|
-
if vmid_key in managed_vmids:
|
|
808
|
-
continue
|
|
809
|
-
cfg: Dict[str, Any] = {}
|
|
810
|
-
try:
|
|
811
|
-
cfg = proxmox.nodes(node).qemu(vmid_key).config.get() or {}
|
|
812
|
-
except Exception as exc:
|
|
813
|
-
logger.debug("Failed to read config for VM %s: %s", vmid_key, exc)
|
|
814
|
-
description = (cfg.get("description") or "")
|
|
815
|
-
if MANAGED_MARKER in description:
|
|
816
|
-
continue
|
|
817
|
-
unmanaged.append(
|
|
818
|
-
_build_unmanaged_container_entry(
|
|
819
|
-
vm,
|
|
820
|
-
cfg,
|
|
821
|
-
vmid_key,
|
|
822
|
-
default_storage,
|
|
823
|
-
entry_type="qemu",
|
|
824
|
-
)
|
|
825
|
-
)
|
|
787
|
+
statuses = {
|
|
788
|
+
str(ct.get("vmid")): (ct.get("status") or "unknown").lower()
|
|
789
|
+
for ct in proxmox.nodes(node).lxc.get()
|
|
790
|
+
}
|
|
826
791
|
except Exception as exc: # pragma: no cover - best effort
|
|
827
792
|
logger.debug("Failed to refresh container statuses: %s", exc)
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
793
|
+
return
|
|
794
|
+
for record in records:
|
|
795
|
+
vmid = record.get("vmid")
|
|
796
|
+
if vmid is None:
|
|
797
|
+
continue
|
|
798
|
+
try:
|
|
799
|
+
vmid_key = str(int(vmid))
|
|
800
|
+
except (ValueError, TypeError):
|
|
801
|
+
continue
|
|
802
|
+
status = statuses.get(vmid_key)
|
|
803
|
+
if status:
|
|
804
|
+
record["status"] = status
|
|
834
805
|
|
|
835
806
|
now = time.monotonic()
|
|
836
807
|
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
@@ -840,22 +811,8 @@ def _get_managed_containers_summary(force: bool = False) -> Dict[str, Any]:
|
|
|
840
811
|
return cached
|
|
841
812
|
config = _load_config()
|
|
842
813
|
records = _load_managed_container_records()
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
}
|
|
846
|
-
default_storage = config.get("default_storage") if config else None
|
|
847
|
-
statuses, unmanaged, node_status, storage_snapshot = _refresh_container_statuses(
|
|
848
|
-
records, config, managed_vmids, default_storage
|
|
849
|
-
)
|
|
850
|
-
for record in records:
|
|
851
|
-
vmid = record.get("vmid")
|
|
852
|
-
if vmid is None:
|
|
853
|
-
continue
|
|
854
|
-
vmid_key = str(_to_int(vmid))
|
|
855
|
-
status = statuses.get(vmid_key)
|
|
856
|
-
if status:
|
|
857
|
-
record["status"] = status
|
|
858
|
-
summary = _build_managed_containers_summary(records, unmanaged, node_status, storage_snapshot, default_storage)
|
|
814
|
+
_refresh_container_statuses(records, config)
|
|
815
|
+
summary = _build_full_container_summary(records, config)
|
|
859
816
|
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
860
817
|
_MANAGED_CONTAINERS_CACHE["timestamp"] = now
|
|
861
818
|
_MANAGED_CONTAINERS_CACHE["summary"] = summary
|
|
@@ -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=
|
|
4
|
+
portacode/_version.py,sha256=ZsUlJUljVebwmjOdkxbUPcwi8iyVjIuyBU9g68awYpA,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
|
|
25
|
+
portacode/connection/handlers/proxmox_infra.py,sha256=-zAmaOTCTJaic8sclb5Z0DVCl_pU7XePo86NKohi3gc,85295
|
|
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.16.
|
|
68
|
+
portacode-1.4.16.dev11.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.16.
|
|
95
|
-
portacode-1.4.16.
|
|
96
|
-
portacode-1.4.16.
|
|
97
|
-
portacode-1.4.16.
|
|
98
|
-
portacode-1.4.16.
|
|
94
|
+
portacode-1.4.16.dev11.dist-info/METADATA,sha256=nEvtzLLbq9J8jG-kubMsNMRQ07YdtEczInIarfWpGhY,13052
|
|
95
|
+
portacode-1.4.16.dev11.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
96
|
+
portacode-1.4.16.dev11.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
97
|
+
portacode-1.4.16.dev11.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
98
|
+
portacode-1.4.16.dev11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|