portacode 1.4.17.dev10__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.dev10'
32
- __version_tuple__ = version_tuple = (1, 4, 17, 'dev10')
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,23 @@ 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
+
1475
+ def _resolve_portacode_cli_path(vmid: int, user: str) -> str:
1476
+ """Resolve the full path to the portacode CLI inside the container."""
1477
+ res = _run_pct(vmid, _su_command(user, "command -v portacode"))
1478
+ path = (res.get("stdout") or "").strip()
1479
+ if path:
1480
+ return path
1481
+ return "portacode"
1482
+
1483
+
1441
1484
  def _run_pct_check(vmid: int, cmd: str) -> Dict[str, Any]:
1442
1485
  res = _run_pct(vmid, cmd)
1443
1486
  if res["returncode"] != 0:
@@ -1465,12 +1508,13 @@ def _push_bytes_to_container(
1465
1508
  ) -> None:
1466
1509
  logger.debug("Preparing to push %d bytes to container vmid=%s path=%s for user=%s", len(data), vmid, path, user)
1467
1510
  tmp_path: Optional[str] = None
1511
+ owner = _resolve_user_group(vmid, user)
1468
1512
  try:
1469
1513
  parent = Path(path).parent
1470
1514
  parent_str = parent.as_posix()
1471
1515
  if parent_str not in {"", ".", "/"}:
1472
1516
  _run_pct_exec_check(vmid, ["mkdir", "-p", parent_str])
1473
- _run_pct_exec_check(vmid, ["chown", "-R", f"{user}:{user}", parent_str])
1517
+ _run_pct_exec_check(vmid, ["chown", "-R", owner, parent_str])
1474
1518
 
1475
1519
  with tempfile.NamedTemporaryFile(delete=False) as tmp:
1476
1520
  tmp.write(data)
@@ -1482,7 +1526,7 @@ def _push_bytes_to_container(
1482
1526
  if push_res.returncode != 0:
1483
1527
  raise RuntimeError(push_res.stderr or push_res.stdout or f"pct push returned {push_res.returncode}")
1484
1528
 
1485
- _run_pct_exec_check(vmid, ["chown", f"{user}:{user}", path])
1529
+ _run_pct_exec_check(vmid, ["chown", owner, path])
1486
1530
  _run_pct_exec_check(vmid, ["chmod", format(mode, "o"), path])
1487
1531
  logger.debug("Successfully pushed %d bytes to vmid=%s path=%s", len(data), vmid, path)
1488
1532
  except Exception as exc:
@@ -1501,7 +1545,8 @@ def _resolve_portacode_key_dir(vmid: int, user: str) -> str:
1501
1545
  data_home = _run_pct_check(vmid, data_dir_cmd)["stdout"].strip()
1502
1546
  portacode_dir = f"{data_home}/portacode"
1503
1547
  _run_pct_exec_check(vmid, ["mkdir", "-p", portacode_dir])
1504
- _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])
1505
1550
  return f"{portacode_dir}/keys"
1506
1551
 
1507
1552
 
@@ -1514,7 +1559,8 @@ def _deploy_device_keypair(vmid: int, user: str, private_key: str, public_key: s
1514
1559
 
1515
1560
 
1516
1561
  def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -> Dict[str, Any]:
1517
- 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")
1518
1564
  cmd = ["pct", "exec", str(vmid), "--", "/bin/sh", "-c", su_connect_cmd]
1519
1565
  proc = subprocess.Popen(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1520
1566
  start = time.time()
@@ -2240,7 +2286,11 @@ class CreateProxmoxContainerHandler(SyncHandler):
2240
2286
  on_behalf_of_device=device_id,
2241
2287
  )
2242
2288
 
2243
- cmd = _su_command(payload_local["username"], "sudo -S portacode service install")
2289
+ cli_path = _resolve_portacode_cli_path(vmid, payload_local["username"])
2290
+ cmd = _su_command(
2291
+ payload_local["username"],
2292
+ f"sudo -S {shlex.quote(cli_path)} service install",
2293
+ )
2244
2294
  res = _run_pct(vmid, cmd, input_text=payload_local["password"] + "\n")
2245
2295
 
2246
2296
  if res["returncode"] != 0:
@@ -2301,6 +2351,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
2301
2351
  "setup_steps": steps,
2302
2352
  "device_id": device_id,
2303
2353
  "on_behalf_of_device": device_id,
2354
+ "bypass_session_gate": True,
2304
2355
  "service_installed": service_installed,
2305
2356
  "request_id": request_id,
2306
2357
  },
@@ -2327,7 +2378,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
2327
2378
  "event": "proxmox_container_accepted",
2328
2379
  "success": True,
2329
2380
  "message": "Provisioning accepted; resources reserved.",
2330
- "device_id": device_id,
2331
2381
  "request_id": request_id,
2332
2382
  }
2333
2383
 
@@ -2403,7 +2453,8 @@ class StartPortacodeServiceHandler(SyncHandler):
2403
2453
  on_behalf_of_device=on_behalf_of_device,
2404
2454
  )
2405
2455
 
2406
- cmd = _su_command(user, "sudo -S portacode service install")
2456
+ cli_path = _resolve_portacode_cli_path(vmid, user)
2457
+ cmd = _su_command(user, f"sudo -S {shlex.quote(cli_path)} service install")
2407
2458
  res = _run_pct(vmid, cmd, input_text=password + "\n")
2408
2459
 
2409
2460
  if res["returncode"] != 0:
@@ -2526,10 +2577,13 @@ class RemoveProxmoxContainerHandler(SyncHandler):
2526
2577
  return "remove_proxmox_container"
2527
2578
 
2528
2579
  def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
2529
- vmid = _parse_ctid(message)
2530
2580
  child_device_id = (message.get("child_device_id") or "").strip()
2531
2581
  if not child_device_id:
2532
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)
2533
2587
  config = _ensure_infra_configured()
2534
2588
  proxmox = _connect_proxmox(config)
2535
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.dev10
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=HLYWuN3eL2K1B7JHO-p2jakoiL3KWtYprDdYTrjf-dI,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=dxn6m7buw_GHbhkhWX6J7gDpqCpXmmBxfqSb-6aQiPg,98542
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.dev10.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.dev10.dist-info/METADATA,sha256=x0izWAdZ8_roBHdh2ZwXj2PvYX62ZscViAtNUNX21U0,13052
95
- portacode-1.4.17.dev10.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
- portacode-1.4.17.dev10.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
- portacode-1.4.17.dev10.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
- portacode-1.4.17.dev10.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,,