unitlab 2.3.29__tar.gz → 2.3.32__tar.gz

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.
Files changed (28) hide show
  1. {unitlab-2.3.29/src/unitlab.egg-info → unitlab-2.3.32}/PKG-INFO +1 -1
  2. {unitlab-2.3.29 → unitlab-2.3.32}/setup.py +1 -1
  3. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/client.py +9 -12
  4. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/persistent_tunnel.py +107 -33
  5. {unitlab-2.3.29 → unitlab-2.3.32/src/unitlab.egg-info}/PKG-INFO +1 -1
  6. {unitlab-2.3.29 → unitlab-2.3.32}/LICENSE.md +0 -0
  7. {unitlab-2.3.29 → unitlab-2.3.32}/README.md +0 -0
  8. {unitlab-2.3.29 → unitlab-2.3.32}/setup.cfg +0 -0
  9. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/__init__.py +0 -0
  10. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/__main__.py +0 -0
  11. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/api_tunnel.py +0 -0
  12. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/auto_tunnel.py +0 -0
  13. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/binary_manager.py +0 -0
  14. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/cloudflare_api_tunnel.py +0 -0
  15. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/cloudflare_api_tunnel_backup.py +0 -0
  16. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/dynamic_tunnel.py +0 -0
  17. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/easy_tunnel.py +0 -0
  18. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/exceptions.py +0 -0
  19. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/main.py +0 -0
  20. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/simple_tunnel.py +0 -0
  21. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/tunnel_config.py +0 -0
  22. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/tunnel_service_token.py +0 -0
  23. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/utils.py +0 -0
  24. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab.egg-info/SOURCES.txt +0 -0
  25. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab.egg-info/dependency_links.txt +0 -0
  26. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab.egg-info/entry_points.txt +0 -0
  27. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab.egg-info/requires.txt +0 -0
  28. {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unitlab
3
- Version: 2.3.29
3
+ Version: 2.3.32
4
4
  Home-page: https://github.com/teamunitlab/unitlab-sdk
5
5
  Author: Unitlab Inc.
6
6
  Author-email: team@unitlab.ai
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
2
2
 
3
3
  setup(
4
4
  name="unitlab",
5
- version="2.3.29",
5
+ version="2.3.32",
6
6
  license="MIT",
7
7
  author="Unitlab Inc.",
8
8
  author_email="team@unitlab.ai",
@@ -286,21 +286,14 @@ class UnitlabClient:
286
286
  from .persistent_tunnel import PersistentTunnel
287
287
  logger.info("Using Persistent Tunnel with Cloudflare API")
288
288
  self.tunnel_manager = PersistentTunnel(device_id=self.device_id)
289
- # URLs will be set after tunnel starts
290
- self.jupyter_url = self.tunnel_manager.jupyter_url
291
- self.ssh_url = self.jupyter_url
289
+ # Don't call run() here - it has infinite loop. Call start() in setup_tunnels()
290
+ self.jupyter_url = None
291
+ self.ssh_url = None
292
+
292
293
  except ImportError as e:
293
294
  logger.warning(f"Could not import PersistentTunnel: {e}")
294
295
  # Fallback to easy tunnel
295
- try:
296
- from .easy_tunnel import EasyTunnelAdapter
297
- logger.info("Using Easy Dynamic Tunnel (random URL)")
298
- self.tunnel_manager = EasyTunnelAdapter(device_id=self.device_id)
299
- self.jupyter_url = None
300
- self.ssh_url = None
301
- except ImportError:
302
- logger.error("No tunnel implementation available!")
303
- raise
296
+
304
297
 
305
298
  # Setup signal handlers
306
299
  signal.signal(signal.SIGINT, self._handle_shutdown)
@@ -410,6 +403,10 @@ class UnitlabClient:
410
403
  if self.tunnel_manager.start():
411
404
  # Store the processes for monitoring
412
405
  self.jupyter_proc = self.tunnel_manager.jupyter_process
406
+ # Update URLs after tunnel starts successfully
407
+ self.jupyter_url = self.tunnel_manager.jupyter_url
408
+ self.ssh_url = self.tunnel_manager.jupyter_url
409
+ logger.info(f"Tunnel started successfully at {self.jupyter_url}")
413
410
  self.tunnel_proc = self.tunnel_manager.tunnel_process
414
411
  self.jupyter_port = "8888" # Both use fixed port
415
412
 
@@ -67,9 +67,42 @@ class PersistentTunnel:
67
67
  "Content-Type": "application/json"
68
68
  }
69
69
 
70
- def create_tunnel(self):
71
- """Create a new tunnel via API"""
72
- print("🔧 Creating tunnel: {}...".format(self.tunnel_name))
70
+ def get_or_create_tunnel(self):
71
+ """Get existing tunnel or create a new one"""
72
+ # First, check if tunnel already exists
73
+ print("🔍 Checking for existing tunnel: {}...".format(self.tunnel_name))
74
+
75
+ list_url = "https://api.cloudflare.com/client/v4/accounts/{}/cfd_tunnel".format(self.cf_account_id)
76
+ headers = self._get_headers()
77
+
78
+ # Check if tunnel exists
79
+ response = requests.get(list_url, headers=headers)
80
+ if response.status_code == 200:
81
+ tunnels = response.json().get("result", [])
82
+ for tunnel in tunnels:
83
+ if tunnel["name"] == self.tunnel_name:
84
+ print("✅ Found existing tunnel: {}".format(tunnel["id"]))
85
+ self.tunnel_id = tunnel["id"]
86
+
87
+ # For persistent device IDs, always recreate to ensure fresh state
88
+ print("🔄 Recreating tunnel for persistent device...")
89
+ delete_url = "https://api.cloudflare.com/client/v4/accounts/{}/cfd_tunnel/{}".format(
90
+ self.cf_account_id, tunnel["id"]
91
+ )
92
+ del_resp = requests.delete(delete_url, headers=headers)
93
+ if del_resp.status_code in [200, 204]:
94
+ print("✅ Deleted old tunnel")
95
+ time.sleep(2)
96
+ else:
97
+ print("⚠️ Could not delete old tunnel, trying to create new one anyway")
98
+ break
99
+
100
+ # Create new tunnel
101
+ return self.create_new_tunnel()
102
+
103
+ def create_new_tunnel(self):
104
+ """Create a brand new tunnel"""
105
+ print("🔧 Creating new tunnel: {}...".format(self.tunnel_name))
73
106
 
74
107
  # Generate random tunnel secret (32 bytes)
75
108
  import secrets
@@ -96,8 +129,8 @@ class PersistentTunnel:
96
129
  "TunnelID": self.tunnel_id
97
130
  }
98
131
 
99
- # Save credentials to file
100
- cred_file = "/tmp/tunnel-{}.json".format(self.tunnel_id)
132
+ # Save credentials to file with tunnel name (not ID) for consistency
133
+ cred_file = "/tmp/tunnel-{}.json".format(self.tunnel_name)
101
134
  with open(cred_file, 'w') as f:
102
135
  json.dump(self.tunnel_credentials, f)
103
136
 
@@ -143,19 +176,7 @@ class PersistentTunnel:
143
176
 
144
177
  def create_tunnel_config(self, cred_file):
145
178
  """Create tunnel config file"""
146
- config = {
147
- "ingress": [
148
- {
149
- "hostname": "{}.{}".format(self.subdomain, self.domain),
150
- "service": "http://localhost:8888"
151
- },
152
- {
153
- "service": "http_status:404"
154
- }
155
- ]
156
- }
157
-
158
- config_file = "/tmp/tunnel-config-{}.yml".format(self.tunnel_id)
179
+ config_file = "/tmp/tunnel-config-{}.yml".format(self.tunnel_name)
159
180
  with open(config_file, 'w') as f:
160
181
  f.write("tunnel: {}\n".format(self.tunnel_id))
161
182
  f.write("credentials-file: {}\n\n".format(cred_file))
@@ -167,25 +188,75 @@ class PersistentTunnel:
167
188
  return config_file
168
189
 
169
190
  def get_cloudflared_path(self):
170
- """Get or download cloudflared"""
191
+ """Get or download cloudflared for any platform"""
171
192
  import shutil
193
+ import platform
194
+
195
+ # Check if already in system PATH
172
196
  if shutil.which("cloudflared"):
173
197
  return "cloudflared"
174
198
 
175
- local_bin = os.path.expanduser("~/.local/bin/cloudflared")
199
+ # Determine binary location based on OS
200
+ system = platform.system().lower()
201
+ machine = platform.machine().lower()
202
+
203
+ if system == "windows":
204
+ local_bin = os.path.expanduser("~/cloudflared/cloudflared.exe")
205
+ else:
206
+ local_bin = os.path.expanduser("~/.local/bin/cloudflared")
207
+
208
+ # Check if already downloaded
176
209
  if os.path.exists(local_bin):
177
210
  return local_bin
178
211
 
179
- # Download
180
- print("📦 Downloading cloudflared...")
181
- import platform
182
- system = platform.system().lower()
183
- arch = "amd64" if "x86" in platform.machine() else "arm64"
184
- url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
212
+ # Download based on platform
213
+ print("📦 Downloading cloudflared for {}...".format(system))
214
+
215
+ if system == "linux":
216
+ # Linux: detect architecture
217
+ if "arm" in machine or "aarch64" in machine:
218
+ arch = "arm64"
219
+ elif "386" in machine or "i686" in machine:
220
+ arch = "386"
221
+ else:
222
+ arch = "amd64"
223
+ url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
224
+
225
+ os.makedirs(os.path.dirname(local_bin), exist_ok=True)
226
+ subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
227
+ subprocess.run("chmod +x {}".format(local_bin), shell=True)
228
+
229
+ elif system == "darwin":
230
+ # macOS: supports both Intel and Apple Silicon
231
+ if "arm" in machine:
232
+ url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64.tgz"
233
+ else:
234
+ url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz"
235
+
236
+ os.makedirs(os.path.dirname(local_bin), exist_ok=True)
237
+ # Download and extract tar.gz
238
+ subprocess.run("curl -L {} | tar xz -C {}".format(url, os.path.dirname(local_bin)), shell=True, capture_output=True)
239
+ subprocess.run("chmod +x {}".format(local_bin), shell=True)
240
+
241
+ elif system == "windows":
242
+ # Windows: typically amd64
243
+ if "arm" in machine:
244
+ arch = "arm64"
245
+ elif "386" in machine:
246
+ arch = "386"
247
+ else:
248
+ arch = "amd64"
249
+ url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-{}.exe".format(arch)
250
+
251
+ os.makedirs(os.path.dirname(local_bin), exist_ok=True)
252
+ # Use PowerShell on Windows to download
253
+ subprocess.run("powershell -Command \"Invoke-WebRequest -Uri {} -OutFile {}\"".format(url, local_bin), shell=True, capture_output=True)
254
+
255
+ else:
256
+ print("❌ Unsupported platform: {}".format(system))
257
+ raise Exception("Platform {} not supported".format(system))
185
258
 
186
- os.makedirs(os.path.dirname(local_bin), exist_ok=True)
187
- subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
188
- subprocess.run("chmod +x {}".format(local_bin), shell=True)
259
+ print("✅ cloudflared downloaded successfully")
189
260
  return local_bin
190
261
 
191
262
  def start_jupyter(self):
@@ -198,7 +269,10 @@ class PersistentTunnel:
198
269
  "--no-browser",
199
270
  "--ip", "0.0.0.0",
200
271
  "--NotebookApp.token=''",
201
- "--NotebookApp.password=''"
272
+ "--NotebookApp.password=''",
273
+ "--NotebookApp.allow_origin='*'"
274
+
275
+
202
276
  ]
203
277
 
204
278
  self.jupyter_process = subprocess.Popen(
@@ -241,8 +315,8 @@ class PersistentTunnel:
241
315
 
242
316
  # API credentials are hardcoded, so we're ready to go
243
317
 
244
- # 1. Create tunnel via API
245
- cred_file = self.create_tunnel()
318
+ # 1. Get existing or create new tunnel via API
319
+ cred_file = self.get_or_create_tunnel()
246
320
  if not cred_file:
247
321
  print("⚠️ Falling back to quick tunnel")
248
322
  return self.start_quick_tunnel()
@@ -316,7 +390,7 @@ class PersistentTunnel:
316
390
  )
317
391
  requests.delete(url, headers=self._get_headers())
318
392
  print("🗑️ Tunnel deleted")
319
- except Exception as e:
393
+ except Exception:
320
394
  pass # Ignore cleanup errors
321
395
 
322
396
  def run(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unitlab
3
- Version: 2.3.29
3
+ Version: 2.3.32
4
4
  Home-page: https://github.com/teamunitlab/unitlab-sdk
5
5
  Author: Unitlab Inc.
6
6
  Author-email: team@unitlab.ai
File without changes
File without changes
File without changes
File without changes
File without changes