portacode 1.4.15.dev1__py3-none-any.whl → 1.4.15.dev2__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.dev1'
32
- __version_tuple__ = version_tuple = (1, 4, 15, 'dev1')
31
+ __version__ = version = '1.4.15.dev2'
32
+ __version_tuple__ = version_tuple = (1, 4, 15, 'dev2')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -420,7 +420,7 @@ Creates or updates a Cloudflare tunnel in front of a managed container. The cont
420
420
 
421
421
  * `ctid` (string, required): Identifier of the container.
422
422
  * `container_port` (integer, required): Port inside the container to expose through Cloudflare.
423
- * `cloudflare_url` (string, required): Hostname (e.g., `app.example.com`) that the tunnel should serve.
423
+ * `cloudflare_url` (string, optional): Hostname (e.g., `app.example.com`) that the tunnel should serve. Leave blank to let Cloudflare create a quick tunnel (`*.cfargotunnel.com`) automatically.
424
424
  * `protocol` (string, optional): One of `http`, `https`, or `tcp` (defaults to `http`).
425
425
 
426
426
  **Responses:**
@@ -436,7 +436,7 @@ Refreshes the tunnel configuration for an existing tunnel (different port, URL,
436
436
 
437
437
  * `ctid` (string, required): Identifier of the container.
438
438
  * `container_port` (integer, optional): New container port.
439
- * `cloudflare_url` (string, optional): New hostname.
439
+ * `cloudflare_url` (string, optional): New hostname (leave blank to keep the current hostname or let Cloudflare assign a quick tunnel).
440
440
  * `protocol` (string, optional): New protocol (`http`, `https`, or `tcp`).
441
441
 
442
442
  **Responses:**
@@ -1273,7 +1273,7 @@ Submitted after a successful `create_cloudflare_tunnel` action.
1273
1273
  * `ctid` (string): Container ID associated with the tunnel.
1274
1274
  * `success` (boolean): True when the tunnel is running.
1275
1275
  * `message` (string): Summary text.
1276
- * `tunnel` (object): Tunnel metadata matching the manager view.
1276
+ * `tunnel` (object): Tunnel metadata matching the manager view. When using Cloudflare Quick Tunnel (no hostname supplied), `tunnel.url` holds the autogenerated `*.cfargotunnel.com` hostname.
1277
1277
 
1278
1278
  ### `cloudflare_tunnel_updated`
1279
1279
 
@@ -8,6 +8,8 @@ import logging
8
8
  import os
9
9
  import secrets
10
10
  import shlex
11
+ import re
12
+ import select
11
13
  import shutil
12
14
  import stat
13
15
  import subprocess
@@ -637,19 +639,19 @@ def _launch_container_tunnel(proxmox: Any, node: str, vmid: int, tunnel: Dict[st
637
639
  port = int(tunnel.get("container_port") or 0)
638
640
  if not port:
639
641
  raise ValueError("container_port is required to create a tunnel.")
640
- hostname = tunnel.get("url")
641
- if not hostname:
642
- raise ValueError("cloudflare_url is required to expose the tunnel.")
642
+ requested_hostname = tunnel.get("url") or None
643
643
  protocol = (tunnel.get("protocol") or "http").lower()
644
644
  ip_address = _resolve_container_ip(proxmox, node, vmid)
645
645
  target_url = f"{protocol}://{ip_address}:{port}"
646
646
  name = tunnel.get("name") or _build_tunnel_name(vmid, port)
647
647
  _stop_cloudflare_process(name)
648
- proc = _start_cloudflare_process(name, hostname, target_url)
648
+ proc, assigned_url = _start_cloudflare_process(name, target_url, hostname=requested_hostname)
649
+ if not assigned_url:
650
+ raise RuntimeError("Failed to determine Cloudflare hostname for the tunnel.")
649
651
  updated = {
650
652
  "name": name,
651
653
  "container_port": port,
652
- "url": hostname,
654
+ "url": assigned_url,
653
655
  "protocol": protocol,
654
656
  "status": "running",
655
657
  "pid": proc.pid,
@@ -671,13 +673,14 @@ def _stop_container_tunnel(vmid: int) -> None:
671
673
  return
672
674
  name = tunnel.get("name") or _build_tunnel_name(vmid, int(tunnel.get("container_port") or 0))
673
675
  stopped = _stop_cloudflare_process(name)
674
- if stopped or tunnel.get("status") != "stopped":
675
- tunnel_update = {
676
- **tunnel,
677
- "status": "stopped",
678
- "pid": None,
679
- "last_updated": datetime.utcnow().isoformat() + "Z",
680
- }
676
+ if not stopped and tunnel.get("status") == "stopped":
677
+ return
678
+ tunnel_update = {
679
+ **tunnel,
680
+ "status": "stopped",
681
+ "pid": None,
682
+ "last_updated": datetime.utcnow().isoformat() + "Z",
683
+ }
681
684
  _update_container_tunnel(vmid, tunnel_update)
682
685
 
683
686
 
@@ -827,25 +830,67 @@ def _get_cloudflared_binary() -> str:
827
830
  return binary
828
831
 
829
832
 
830
- def _start_cloudflare_process(name: str, hostname: str, target_url: str) -> subprocess.Popen:
833
+ def _drain_stream(stream: Optional[Any]) -> None:
834
+ if stream is None:
835
+ return
836
+ try:
837
+ for _ in iter(stream.readline, ""):
838
+ continue
839
+ except Exception:
840
+ pass
841
+ finally:
842
+ try:
843
+ stream.close()
844
+ except Exception:
845
+ pass
846
+
847
+
848
+ def _await_quick_tunnel_url(proc: subprocess.Popen, timeout: float = 15.0) -> Optional[str]:
849
+ if not proc.stdout:
850
+ return None
851
+ cf_re = re.compile(r"https://[A-Za-z0-9\-.]+\.cfargotunnel\.com")
852
+ deadline = time.time() + timeout
853
+ while time.time() < deadline:
854
+ ready, _, _ = select.select([proc.stdout], [], [], 1)
855
+ if not ready:
856
+ continue
857
+ line = proc.stdout.readline()
858
+ if not line:
859
+ continue
860
+ match = cf_re.search(line)
861
+ if match:
862
+ return match.group(0)
863
+ return None
864
+
865
+
866
+ def _start_cloudflare_process(name: str, target_url: str, hostname: Optional[str] = None) -> Tuple[subprocess.Popen, Optional[str]]:
831
867
  binary = _get_cloudflared_binary()
832
868
  cmd = [
833
869
  binary,
834
870
  "tunnel",
835
- "--hostname",
836
- hostname,
837
871
  "--url",
838
872
  target_url,
839
873
  "--no-autoupdate",
840
874
  ]
875
+ if hostname:
876
+ cmd.extend(["--hostname", hostname])
877
+ stdout_target = subprocess.DEVNULL
878
+ else:
879
+ stdout_target = subprocess.PIPE
841
880
  proc = subprocess.Popen(
842
881
  cmd,
843
- stdout=subprocess.DEVNULL,
844
- stderr=subprocess.DEVNULL,
882
+ stdout=stdout_target,
883
+ stderr=subprocess.PIPE,
884
+ text=True,
845
885
  )
846
886
  with _CLOUDFLARE_TUNNELS_LOCK:
847
887
  _CLOUDFLARE_TUNNEL_PROCESSES[name] = proc
848
- return proc
888
+ assigned_url = hostname
889
+ if not hostname:
890
+ assigned_url = _await_quick_tunnel_url(proc)
891
+ threading.Thread(target=_drain_stream, args=(proc.stdout,), daemon=True).start()
892
+ threading.Thread(target=_drain_stream, args=(proc.stderr,), daemon=True).start()
893
+ return proc, assigned_url
849
894
 
850
895
 
851
896
  def _stop_cloudflare_process(name: str) -> bool:
@@ -1884,8 +1929,7 @@ class CreateCloudflareTunnelHandler(SyncHandler):
1884
1929
  if container_port <= 0:
1885
1930
  raise ValueError("container_port is required and must be greater than zero.")
1886
1931
  hostname = (message.get("cloudflare_url") or message.get("hostname") or "").strip()
1887
- if not hostname:
1888
- raise ValueError("cloudflare_url is required.")
1932
+ hostname = hostname or None
1889
1933
  protocol = (message.get("protocol") or "http").strip().lower()
1890
1934
  if protocol not in {"http", "https", "tcp"}:
1891
1935
  raise ValueError("protocol must be one of http, https, or tcp.")
@@ -1899,9 +1943,10 @@ class CreateCloudflareTunnelHandler(SyncHandler):
1899
1943
  raise RuntimeError("Container must be running to create a tunnel.")
1900
1944
  tunnel = {
1901
1945
  "container_port": container_port,
1902
- "url": hostname,
1903
1946
  "protocol": protocol,
1904
1947
  }
1948
+ if hostname:
1949
+ tunnel["url"] = hostname
1905
1950
  created = _launch_container_tunnel(proxmox, node, vmid, tunnel)
1906
1951
  return {
1907
1952
  "event": "cloudflare_tunnel_created",
@@ -1934,16 +1979,16 @@ class UpdateCloudflareTunnelHandler(SyncHandler):
1934
1979
  if container_port <= 0:
1935
1980
  raise ValueError("container_port must be greater than zero.")
1936
1981
  hostname = (message.get("cloudflare_url") or tunnel.get("url") or "").strip()
1937
- if not hostname:
1938
- raise ValueError("cloudflare_url is required.")
1982
+ hostname = hostname or None
1939
1983
  protocol = (message.get("protocol") or tunnel.get("protocol") or "http").strip().lower()
1940
1984
  if protocol not in {"http", "https", "tcp"}:
1941
1985
  raise ValueError("protocol must be one of http, https, or tcp.")
1942
1986
  updated_tunnel = {
1943
1987
  "container_port": container_port,
1944
- "url": hostname,
1945
1988
  "protocol": protocol,
1946
1989
  }
1990
+ if hostname:
1991
+ updated_tunnel["url"] = hostname
1947
1992
  result = _launch_container_tunnel(proxmox, node, vmid, updated_tunnel)
1948
1993
  return {
1949
1994
  "event": "cloudflare_tunnel_updated",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.15.dev1
3
+ Version: 1.4.15.dev2
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=Gl8_1Kg3vuWq9htOanvxRd3kbf_a48CP9mpOXGbiomk,719
4
+ portacode/_version.py,sha256=oWFHpNG7I0w-78ytqCCXMciE0ywZlQquvI0R-kyOX34,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,7 +14,7 @@ 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=oyLPOVLPlUuN_eRvHPGazB51yi8W8JEF3oOEYxucGTE,45069
16
16
  portacode/connection/handlers/README.md,sha256=HsLZG1QK1JNm67HsgL6WoDg9nxzKXxwkc5fJPFJdX5g,12169
17
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=nbu1iQkqCCuUOyCdItBAETO0ab2UNWV060VpS8WyAhY,102810
17
+ portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=IZMuYF5wc23_ovEzXAvlLZLW1WXmqUUlrh_feTSsQfE,103110
18
18
  portacode/connection/handlers/__init__.py,sha256=WSeBmi65GWFQPYt9M3E10rn0uZ_EPCJzNJOzSf2HZyw,2921
19
19
  portacode/connection/handlers/base.py,sha256=oENFb-Fcfzwk99Qx8gJQriEMiwSxwygwjOiuCH36hM4,10231
20
20
  portacode/connection/handlers/chunked_content.py,sha256=h6hXRmxSeOgnIxoU8CkmvEf2Odv-ajPrpHIe_W3GKcA,9251
@@ -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=73Lvf_VjiXLAfVlegBDH9VXr7JzXyN3SKESHhyd-v6s,73699
25
+ portacode/connection/handlers/proxmox_infra.py,sha256=4XxGeHAppuvVdqTQI-UoYTunS78uI8_NSR5Rqtqexso,75073
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.dev1.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
68
+ portacode-1.4.15.dev2.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.dev1.dist-info/METADATA,sha256=dDo_VYpC29NGZXMz95iwBAedzWnWhDFiU69MnZNSMwU,13051
95
- portacode-1.4.15.dev1.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
96
- portacode-1.4.15.dev1.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
- portacode-1.4.15.dev1.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
- portacode-1.4.15.dev1.dist-info/RECORD,,
94
+ portacode-1.4.15.dev2.dist-info/METADATA,sha256=iZsuq2JwkRIK8b60HLPQ4kMUH5-oXoDaVMCFPGgfm6c,13051
95
+ portacode-1.4.15.dev2.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
96
+ portacode-1.4.15.dev2.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
+ portacode-1.4.15.dev2.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
+ portacode-1.4.15.dev2.dist-info/RECORD,,