portacode 1.4.17.dev11__py3-none-any.whl → 1.4.18__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.17.dev11'
32
- __version_tuple__ = version_tuple = (1, 4, 17, 'dev11')
31
+ __version__ = version = '1.4.18'
32
+ __version_tuple__ = version_tuple = (1, 4, 18)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -408,7 +408,7 @@ Deletes a managed container from Proxmox (stopping it first if necessary) and re
408
408
 
409
409
  **Payload Fields:**
410
410
 
411
- * `ctid` (string, required): Identifier of the container to delete.
411
+ * `ctid` (string, optional): Identifier of the container to delete. If omitted, the handler resolves the CTID from the managed container records using `child_device_id`.
412
412
  * `child_device_id` (string, required): Dashboard `Device.id` that should own the container metadata being purged.
413
413
 
414
414
  **Responses:**
@@ -62,16 +62,18 @@ class BaseHandler(ABC):
62
62
  # Get client session manager from context
63
63
  client_session_manager = self.context.get("client_session_manager")
64
64
 
65
+ bypass_session_gate = bool(payload.get("bypass_session_gate"))
65
66
  if client_session_manager and client_session_manager.has_interested_clients():
66
67
  # Get target sessions
67
68
  target_sessions = client_session_manager.get_target_sessions(project_id)
68
- if not target_sessions:
69
+ if not target_sessions and not bypass_session_gate:
69
70
  logger.debug("handler: No target sessions found, skipping response send")
70
71
  return
71
72
 
72
73
  # Add session targeting information
73
74
  enhanced_payload = dict(payload)
74
- enhanced_payload["client_sessions"] = target_sessions
75
+ if target_sessions:
76
+ enhanced_payload["client_sessions"] = target_sessions
75
77
 
76
78
  # Add backward compatibility reply_channel (first session if not provided)
77
79
  if not reply_channel:
@@ -216,4 +218,4 @@ class SyncHandler(BaseHandler):
216
218
  logger.exception("Error in sync handler %s: %s", self.command_name, exc)
217
219
  # Extract project_id from original message for error targeting
218
220
  project_id = message.get("project_id")
219
- await self.send_error(str(exc), reply_channel, project_id)
221
+ await self.send_error(str(exc), reply_channel, project_id)
@@ -116,6 +116,7 @@ def _emit_progress_event(
116
116
  payload["details"] = details
117
117
  if on_behalf_of_device:
118
118
  payload["on_behalf_of_device"] = str(on_behalf_of_device)
119
+ payload["bypass_session_gate"] = True
119
120
 
120
121
  future = asyncio.run_coroutine_threadsafe(handler.send_response(payload), loop)
121
122
  future.add_done_callback(
@@ -1071,7 +1072,7 @@ _PACKAGE_MANAGER_PROFILES: Dict[str, Dict[str, Any]] = {
1071
1072
  "zypper": {
1072
1073
  "update_cmd": "zypper refresh",
1073
1074
  "update_step_name": "zypper_update",
1074
- "install_cmd": "zypper install -y python3 python3-pip sudo",
1075
+ "install_cmd": "zypper install -y python311 python311-pip sudo",
1075
1076
  "install_step_name": "install_deps",
1076
1077
  "update_retries": 3,
1077
1078
  "install_retries": 5,
@@ -1101,6 +1102,7 @@ def _build_bootstrap_steps(
1101
1102
  package_manager: str = "apt",
1102
1103
  ) -> List[Dict[str, Any]]:
1103
1104
  profile = _PACKAGE_MANAGER_PROFILES.get(package_manager, _PACKAGE_MANAGER_PROFILES["apt"])
1105
+ python_cmd = "python3.11" if package_manager == "zypper" else "python3"
1104
1106
  steps: List[Dict[str, Any]] = [
1105
1107
  {"name": "wait_for_network", "cmd": _NETWORK_WAIT_CMD, "retries": 0},
1106
1108
  ]
@@ -1178,14 +1180,19 @@ def _build_bootstrap_steps(
1178
1180
  steps.append(
1179
1181
  {
1180
1182
  "name": "add_ssh_key",
1181
- "cmd": f"install -d -m 700 /home/{user}/.ssh && echo '{ssh_key}' >> /home/{user}/.ssh/authorized_keys && chown -R {user}:{user} /home/{user}/.ssh",
1183
+ "cmd": f"install -d -m 700 /home/{user}/.ssh && echo '{ssh_key}' >> /home/{user}/.ssh/authorized_keys && chown -R {user}:$(id -gn {shlex.quote(user)}) /home/{user}/.ssh",
1182
1184
  "retries": 0,
1183
1185
  }
1184
1186
  )
1185
1187
  steps.extend(
1186
1188
  [
1187
- {"name": "pip_upgrade", "cmd": "python3 -m pip install --upgrade pip", "retries": 0},
1188
- {"name": "install_portacode", "cmd": "python3 -m pip install --upgrade portacode", "retries": 0},
1189
+ {"name": "pip_upgrade", "cmd": f"{python_cmd} -m pip install --upgrade pip", "retries": 0},
1190
+ {"name": "install_portacode", "cmd": f"{python_cmd} -m pip install --upgrade portacode", "retries": 0},
1191
+ {
1192
+ "name": "ensure_portacode_path",
1193
+ "cmd": "if [ ! -f /etc/profile.d/portacode_path.sh ]; then printf 'export PATH=/usr/local/bin:$PATH\\n' >/etc/profile.d/portacode_path.sh; fi",
1194
+ "retries": 0,
1195
+ },
1189
1196
  ]
1190
1197
  )
1191
1198
  if include_portacode_connect:
@@ -1393,6 +1400,25 @@ def _parse_ctid(message: Dict[str, Any]) -> int:
1393
1400
  raise ValueError("ctid is required")
1394
1401
 
1395
1402
 
1403
+ def _resolve_vmid_for_device(device_id: str) -> int:
1404
+ _initialize_managed_containers_state()
1405
+ records = list(_MANAGED_CONTAINERS_STATE.get("records", {}).values())
1406
+ for record in records:
1407
+ record_device_id = record.get("device_id")
1408
+ if record_device_id is None:
1409
+ continue
1410
+ if str(record_device_id) != str(device_id):
1411
+ continue
1412
+ vmid = record.get("vmid")
1413
+ if vmid is None:
1414
+ continue
1415
+ try:
1416
+ return int(str(vmid).strip())
1417
+ except ValueError:
1418
+ raise ValueError("ctid must be an integer") from None
1419
+ raise ValueError("ctid is required for remove_proxmox_container")
1420
+
1421
+
1396
1422
  def _ensure_container_managed(
1397
1423
  proxmox: Any, node: str, vmid: int, *, device_id: Optional[str] = None
1398
1424
  ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
@@ -1438,6 +1464,14 @@ def _su_command(user: str, command: str) -> str:
1438
1464
  return f"su - {user} -s /bin/sh -c {shlex.quote(command)}"
1439
1465
 
1440
1466
 
1467
+ def _resolve_user_group(vmid: int, user: str) -> str:
1468
+ res = _run_pct(vmid, f"id -gn {shlex.quote(user)}")
1469
+ group = (res.get("stdout") or "").strip()
1470
+ if group:
1471
+ return f"{user}:{group}"
1472
+ return f"{user}:{user}"
1473
+
1474
+
1441
1475
  def _resolve_portacode_cli_path(vmid: int, user: str) -> str:
1442
1476
  """Resolve the full path to the portacode CLI inside the container."""
1443
1477
  res = _run_pct(vmid, _su_command(user, "command -v portacode"))
@@ -1474,12 +1508,13 @@ def _push_bytes_to_container(
1474
1508
  ) -> None:
1475
1509
  logger.debug("Preparing to push %d bytes to container vmid=%s path=%s for user=%s", len(data), vmid, path, user)
1476
1510
  tmp_path: Optional[str] = None
1511
+ owner = _resolve_user_group(vmid, user)
1477
1512
  try:
1478
1513
  parent = Path(path).parent
1479
1514
  parent_str = parent.as_posix()
1480
1515
  if parent_str not in {"", ".", "/"}:
1481
1516
  _run_pct_exec_check(vmid, ["mkdir", "-p", parent_str])
1482
- _run_pct_exec_check(vmid, ["chown", "-R", f"{user}:{user}", parent_str])
1517
+ _run_pct_exec_check(vmid, ["chown", "-R", owner, parent_str])
1483
1518
 
1484
1519
  with tempfile.NamedTemporaryFile(delete=False) as tmp:
1485
1520
  tmp.write(data)
@@ -1491,7 +1526,7 @@ def _push_bytes_to_container(
1491
1526
  if push_res.returncode != 0:
1492
1527
  raise RuntimeError(push_res.stderr or push_res.stdout or f"pct push returned {push_res.returncode}")
1493
1528
 
1494
- _run_pct_exec_check(vmid, ["chown", f"{user}:{user}", path])
1529
+ _run_pct_exec_check(vmid, ["chown", owner, path])
1495
1530
  _run_pct_exec_check(vmid, ["chmod", format(mode, "o"), path])
1496
1531
  logger.debug("Successfully pushed %d bytes to vmid=%s path=%s", len(data), vmid, path)
1497
1532
  except Exception as exc:
@@ -1510,7 +1545,8 @@ def _resolve_portacode_key_dir(vmid: int, user: str) -> str:
1510
1545
  data_home = _run_pct_check(vmid, data_dir_cmd)["stdout"].strip()
1511
1546
  portacode_dir = f"{data_home}/portacode"
1512
1547
  _run_pct_exec_check(vmid, ["mkdir", "-p", portacode_dir])
1513
- _run_pct_exec_check(vmid, ["chown", "-R", f"{user}:{user}", portacode_dir])
1548
+ owner = _resolve_user_group(vmid, user)
1549
+ _run_pct_exec_check(vmid, ["chown", "-R", owner, portacode_dir])
1514
1550
  return f"{portacode_dir}/keys"
1515
1551
 
1516
1552
 
@@ -1523,7 +1559,8 @@ def _deploy_device_keypair(vmid: int, user: str, private_key: str, public_key: s
1523
1559
 
1524
1560
 
1525
1561
  def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -> Dict[str, Any]:
1526
- su_connect_cmd = _su_command(user, "portacode connect")
1562
+ cli_path = _resolve_portacode_cli_path(vmid, user)
1563
+ su_connect_cmd = _su_command(user, f"{shlex.quote(cli_path)} connect")
1527
1564
  cmd = ["pct", "exec", str(vmid), "--", "/bin/sh", "-c", su_connect_cmd]
1528
1565
  proc = subprocess.Popen(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1529
1566
  start = time.time()
@@ -2314,6 +2351,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
2314
2351
  "setup_steps": steps,
2315
2352
  "device_id": device_id,
2316
2353
  "on_behalf_of_device": device_id,
2354
+ "bypass_session_gate": True,
2317
2355
  "service_installed": service_installed,
2318
2356
  "request_id": request_id,
2319
2357
  },
@@ -2340,7 +2378,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
2340
2378
  "event": "proxmox_container_accepted",
2341
2379
  "success": True,
2342
2380
  "message": "Provisioning accepted; resources reserved.",
2343
- "device_id": device_id,
2344
2381
  "request_id": request_id,
2345
2382
  }
2346
2383
 
@@ -2540,10 +2577,13 @@ class RemoveProxmoxContainerHandler(SyncHandler):
2540
2577
  return "remove_proxmox_container"
2541
2578
 
2542
2579
  def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
2543
- vmid = _parse_ctid(message)
2544
2580
  child_device_id = (message.get("child_device_id") or "").strip()
2545
2581
  if not child_device_id:
2546
2582
  raise ValueError("child_device_id is required for remove_proxmox_container")
2583
+ try:
2584
+ vmid = _parse_ctid(message)
2585
+ except ValueError:
2586
+ vmid = _resolve_vmid_for_device(child_device_id)
2547
2587
  config = _ensure_infra_configured()
2548
2588
  proxmox = _connect_proxmox(config)
2549
2589
  node = _get_node_from_config(config)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.17.dev11
3
+ Version: 1.4.18
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=Lg3L_Yd907oy5QaxmgSlo_Rw45Rv_u8XLVUuKTmx3Gg,721
4
+ portacode/_version.py,sha256=ver_5YwV6iEZGHA-LSfN0xE_LIPi_0Lc9SHbQX2D7zw,706
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
@@ -14,15 +14,15 @@ portacode/connection/client.py,sha256=jtLb9_YufqPkzi9t8VQH3iz_JEMisbtY6a8L9U5wei
14
14
  portacode/connection/multiplex.py,sha256=L-TxqJ_ZEbfNEfu1cwxgJ5vUdyRzZjsMy2Kx1diiZys,5237
15
15
  portacode/connection/terminal.py,sha256=n1Uu92JacV5K6d1Qwx94Tw9OB2Tpke5HqsW2NDn76Ls,49032
16
16
  portacode/connection/handlers/README.md,sha256=HsLZG1QK1JNm67HsgL6WoDg9nxzKXxwkc5fJPFJdX5g,12169
17
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=R9trHDWDuADIY2Xs2LrpIw1himtxE-NDb_6CzNVNVcM,103625
17
+ portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=Btbt3UVUvXG5t2HB0_ani0eQNnUPUCLWaqlvCHTaWfo,103727
18
18
  portacode/connection/handlers/__init__.py,sha256=WSeBmi65GWFQPYt9M3E10rn0uZ_EPCJzNJOzSf2HZyw,2921
19
- portacode/connection/handlers/base.py,sha256=oENFb-Fcfzwk99Qx8gJQriEMiwSxwygwjOiuCH36hM4,10231
19
+ portacode/connection/handlers/base.py,sha256=OPg4EsMqqR_59-uQ6UYloFU5RX0474m4Ot4oXOuWals,10367
20
20
  portacode/connection/handlers/chunked_content.py,sha256=h6hXRmxSeOgnIxoU8CkmvEf2Odv-ajPrpHIe_W3GKcA,9251
21
21
  portacode/connection/handlers/diff_handlers.py,sha256=iYTIRCcpEQ03vIPKZCsMTE5aZbQw6sF04M3dM6rUV8Q,24477
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=6Av5QeNQGh38U0kRzoB4ZICmSrRaWLT3YXHoAqUisv4,99102
25
+ portacode/connection/handlers/proxmox_infra.py,sha256=y7Vx8OCGw5OUV00lT946yOjXdDed2CxenveffSnGAlI,100690
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.17.dev11.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
68
+ portacode-1.4.18.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.17.dev11.dist-info/METADATA,sha256=qpoxNNnCxvUgnxqz8EyYsPLpSa6TQIC9YeHhTC-DGH0,13052
95
- portacode-1.4.17.dev11.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
- portacode-1.4.17.dev11.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
- portacode-1.4.17.dev11.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
- portacode-1.4.17.dev11.dist-info/RECORD,,
94
+ portacode-1.4.18.dist-info/METADATA,sha256=Umbaduyx9KBfBweiAoFPYo6JFM0IIUxCM8FjNh3ePi8,13046
95
+ portacode-1.4.18.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
+ portacode-1.4.18.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
+ portacode-1.4.18.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
+ portacode-1.4.18.dist-info/RECORD,,