portacode 1.4.18.dev1__py3-none-any.whl → 1.4.19.dev0__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.18.dev1'
32
- __version_tuple__ = version_tuple = (1, 4, 18, 'dev1')
31
+ __version__ = version = '1.4.19.dev0'
32
+ __version_tuple__ = version_tuple = (1, 4, 19, 'dev0')
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:
@@ -89,15 +91,24 @@ class BaseHandler(ABC):
89
91
  payload["reply_channel"] = reply_channel
90
92
  await self.control_channel.send(payload)
91
93
 
92
- async def send_error(self, message: str, reply_channel: Optional[str] = None, project_id: str = None) -> None:
94
+ async def send_error(
95
+ self,
96
+ message: str,
97
+ reply_channel: Optional[str] = None,
98
+ project_id: str = None,
99
+ request_id: Optional[str] = None,
100
+ ) -> None:
93
101
  """Send an error response with client session awareness.
94
102
 
95
103
  Args:
96
104
  message: Error message
97
105
  reply_channel: Optional reply channel for backward compatibility
98
106
  project_id: Optional project filter for targeting specific sessions
107
+ request_id: Optional request_id to correlate error with a request
99
108
  """
100
109
  payload = {"event": "error", "message": message}
110
+ if request_id:
111
+ payload["request_id"] = request_id
101
112
  await self.send_response(payload, reply_channel, project_id)
102
113
 
103
114
 
@@ -163,7 +174,12 @@ class AsyncHandler(BaseHandler):
163
174
  logger.exception("handler: Error in async handler %s: %s", self.command_name, exc)
164
175
  # Extract project_id from original message for error targeting
165
176
  project_id = message.get("project_id")
166
- await self.send_error(str(exc), reply_channel, project_id)
177
+ await self.send_error(
178
+ str(exc),
179
+ reply_channel,
180
+ project_id,
181
+ request_id=message.get("request_id"),
182
+ )
167
183
 
168
184
 
169
185
  class SyncHandler(BaseHandler):
@@ -216,4 +232,9 @@ class SyncHandler(BaseHandler):
216
232
  logger.exception("Error in sync handler %s: %s", self.command_name, exc)
217
233
  # Extract project_id from original message for error targeting
218
234
  project_id = message.get("project_id")
219
- await self.send_error(str(exc), reply_channel, project_id)
235
+ await self.send_error(
236
+ str(exc),
237
+ reply_channel,
238
+ project_id,
239
+ request_id=message.get("request_id"),
240
+ )
@@ -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(
@@ -1399,6 +1400,25 @@ def _parse_ctid(message: Dict[str, Any]) -> int:
1399
1400
  raise ValueError("ctid is required")
1400
1401
 
1401
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
+
1402
1422
  def _ensure_container_managed(
1403
1423
  proxmox: Any, node: str, vmid: int, *, device_id: Optional[str] = None
1404
1424
  ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
@@ -2331,6 +2351,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
2331
2351
  "setup_steps": steps,
2332
2352
  "device_id": device_id,
2333
2353
  "on_behalf_of_device": device_id,
2354
+ "bypass_session_gate": True,
2334
2355
  "service_installed": service_installed,
2335
2356
  "request_id": request_id,
2336
2357
  },
@@ -2357,7 +2378,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
2357
2378
  "event": "proxmox_container_accepted",
2358
2379
  "success": True,
2359
2380
  "message": "Provisioning accepted; resources reserved.",
2360
- "device_id": device_id,
2361
2381
  "request_id": request_id,
2362
2382
  }
2363
2383
 
@@ -2557,10 +2577,14 @@ class RemoveProxmoxContainerHandler(SyncHandler):
2557
2577
  return "remove_proxmox_container"
2558
2578
 
2559
2579
  def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
2560
- vmid = _parse_ctid(message)
2561
2580
  child_device_id = (message.get("child_device_id") or "").strip()
2562
2581
  if not child_device_id:
2563
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)
2587
+ request_id = message.get("request_id")
2564
2588
  config = _ensure_infra_configured()
2565
2589
  proxmox = _connect_proxmox(config)
2566
2590
  node = _get_node_from_config(config)
@@ -2582,6 +2606,9 @@ class RemoveProxmoxContainerHandler(SyncHandler):
2582
2606
  "delete_exitstatus": delete_status.get("exitstatus"),
2583
2607
  },
2584
2608
  "status": "deleted",
2609
+ "child_device_id": child_device_id,
2610
+ "on_behalf_of_device": child_device_id,
2611
+ "request_id": request_id,
2585
2612
  "infra": infra,
2586
2613
  }
2587
2614
 
@@ -106,12 +106,25 @@ class CommandRegistry:
106
106
  except Exception as exc:
107
107
  logger.exception("registry: Error dispatching command %s: %s", command_name, exc)
108
108
  # Send session-aware error response
109
- await self._send_session_aware_error(str(exc), reply_channel, message.get("project_id"))
109
+ await self._send_session_aware_error(
110
+ str(exc),
111
+ reply_channel,
112
+ message.get("project_id"),
113
+ request_id=message.get("request_id"),
114
+ )
110
115
  return False
111
116
 
112
- async def _send_session_aware_error(self, message: str, reply_channel: Optional[str] = None, project_id: str = None) -> None:
117
+ async def _send_session_aware_error(
118
+ self,
119
+ message: str,
120
+ reply_channel: Optional[str] = None,
121
+ project_id: str = None,
122
+ request_id: Optional[str] = None,
123
+ ) -> None:
113
124
  """Send an error response with client session awareness."""
114
125
  error_payload = {"event": "error", "message": message}
126
+ if request_id:
127
+ error_payload["request_id"] = request_id
115
128
 
116
129
  # Get client session manager from context
117
130
  client_session_manager = self.context.get("client_session_manager")
@@ -151,4 +164,4 @@ class CommandRegistry:
151
164
 
152
165
  # Update context for all existing handlers
153
166
  for handler in self._handlers.values():
154
- handler.context = self.context
167
+ handler.context = self.context
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.18.dev1
3
+ Version: 1.4.19.dev0
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=5dOvBCvz5Pakc6MOjfghJssGskaXc70INnpW2F-9DbA,719
4
+ portacode/_version.py,sha256=Mg7-aZdTDXntrWHuweCpOdxOGlkCkhdNJGq-1P_CTYQ,719
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,16 +14,16 @@ 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=tMsKJmnNhXA3a4YZA55a0WO2e8-4Uh4BsG9lWAr3Ris,10829
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=dP0ldWSC7khwjlHGa-Ujmlp9ZCYbAt6QuE5j16IuGTY,99843
26
- portacode/connection/handlers/registry.py,sha256=qXGE60sYEWg6ZtVQzFcZ5YI2XWR6lMgw4hAL9x5qR1I,6181
25
+ portacode/connection/handlers/proxmox_infra.py,sha256=rO7y9TGC2_u5Qazi3ukiXhiA0-nC_Gyi_FuXJSvVX80,100875
26
+ portacode/connection/handlers/registry.py,sha256=elISoWpIXapMuY28x1hzXV-uErtAklsFzMb8yVgp7NU,6456
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
29
29
  portacode/connection/handlers/tab_factory.py,sha256=yn93h6GASjD1VpvW1oqpax3EpoT0r7r97zFXxML1wdA,16173
@@ -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.18.dev1.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
68
+ portacode-1.4.19.dev0.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.18.dev1.dist-info/METADATA,sha256=ubjouOQTc91PCVLmKmJJKm3KPOqVe5jI_oAlARVTrqc,13051
95
- portacode-1.4.18.dev1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
- portacode-1.4.18.dev1.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
- portacode-1.4.18.dev1.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
- portacode-1.4.18.dev1.dist-info/RECORD,,
94
+ portacode-1.4.19.dev0.dist-info/METADATA,sha256=Wb-KIopzQn0baUy96Wah9IBrH12vzbNys9K0qkastxw,13051
95
+ portacode-1.4.19.dev0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
+ portacode-1.4.19.dev0.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
+ portacode-1.4.19.dev0.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
+ portacode-1.4.19.dev0.dist-info/RECORD,,