unitlab 2.3.29__py3-none-any.whl → 2.3.33__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.
unitlab/client.py CHANGED
@@ -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
 
unitlab/main.py CHANGED
@@ -151,22 +151,11 @@ def run_agent(
151
151
  # Try environment variable first
152
152
  device_id = os.getenv('DEVICE_ID')
153
153
  if not device_id:
154
- # Try to load saved device ID
155
- device_id_file = Path.home() / '.unitlab' / 'device_id'
156
- device_id_file.parent.mkdir(exist_ok=True, parents=True)
157
-
158
- if device_id_file.exists():
159
- device_id = device_id_file.read_text().strip()
160
- print(f"📌 Using saved device ID: {device_id}")
161
- else:
162
- # Generate a unique ID based on hostname and random UUID
163
- hostname = platform.node().replace('.', '-').replace(' ', '-')[:20]
164
- random_suffix = str(uuid.uuid4())[:8]
165
- device_id = f"{hostname}-{random_suffix}"
166
-
167
- # Save for future runs
168
- device_id_file.write_text(device_id)
169
- print(f"📝 Generated and saved device ID: {device_id}")
154
+ # Always generate a unique device ID (no saving/reusing)
155
+ hostname = platform.node().replace('.', '-').replace(' ', '-')[:20]
156
+ random_suffix = str(uuid.uuid4())[:8]
157
+ device_id = f"{hostname}-{random_suffix}"
158
+ print(f"📝 Generated unique device ID: {device_id}")
170
159
 
171
160
 
172
161
  # Create client and initialize device agent
@@ -67,9 +67,38 @@ 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
+ # Tunnel exists, create a new one with unique name
88
+ print("⚠️ Tunnel with this name already exists")
89
+ import uuid
90
+ unique_suffix = str(uuid.uuid4())[:8]
91
+ self.tunnel_name = "agent-{}-{}".format(self.device_id, unique_suffix)
92
+ print("🔄 Creating new tunnel with unique name: {}".format(self.tunnel_name))
93
+ # Don't break, let it continue to create new tunnel
94
+ return self.create_new_tunnel()
95
+
96
+ # Create new tunnel
97
+ return self.create_new_tunnel()
98
+
99
+ def create_new_tunnel(self):
100
+ """Create a brand new tunnel"""
101
+ print("🔧 Creating new tunnel: {}...".format(self.tunnel_name))
73
102
 
74
103
  # Generate random tunnel secret (32 bytes)
75
104
  import secrets
@@ -96,8 +125,8 @@ class PersistentTunnel:
96
125
  "TunnelID": self.tunnel_id
97
126
  }
98
127
 
99
- # Save credentials to file
100
- cred_file = "/tmp/tunnel-{}.json".format(self.tunnel_id)
128
+ # Save credentials to file with tunnel name (not ID) for consistency
129
+ cred_file = "/tmp/tunnel-{}.json".format(self.tunnel_name)
101
130
  with open(cred_file, 'w') as f:
102
131
  json.dump(self.tunnel_credentials, f)
103
132
 
@@ -143,19 +172,7 @@ class PersistentTunnel:
143
172
 
144
173
  def create_tunnel_config(self, cred_file):
145
174
  """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)
175
+ config_file = "/tmp/tunnel-config-{}.yml".format(self.tunnel_name)
159
176
  with open(config_file, 'w') as f:
160
177
  f.write("tunnel: {}\n".format(self.tunnel_id))
161
178
  f.write("credentials-file: {}\n\n".format(cred_file))
@@ -167,25 +184,75 @@ class PersistentTunnel:
167
184
  return config_file
168
185
 
169
186
  def get_cloudflared_path(self):
170
- """Get or download cloudflared"""
187
+ """Get or download cloudflared for any platform"""
171
188
  import shutil
189
+ import platform
190
+
191
+ # Check if already in system PATH
172
192
  if shutil.which("cloudflared"):
173
193
  return "cloudflared"
174
194
 
175
- local_bin = os.path.expanduser("~/.local/bin/cloudflared")
195
+ # Determine binary location based on OS
196
+ system = platform.system().lower()
197
+ machine = platform.machine().lower()
198
+
199
+ if system == "windows":
200
+ local_bin = os.path.expanduser("~/cloudflared/cloudflared.exe")
201
+ else:
202
+ local_bin = os.path.expanduser("~/.local/bin/cloudflared")
203
+
204
+ # Check if already downloaded
176
205
  if os.path.exists(local_bin):
177
206
  return local_bin
178
207
 
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)
208
+ # Download based on platform
209
+ print("📦 Downloading cloudflared for {}...".format(system))
210
+
211
+ if system == "linux":
212
+ # Linux: detect architecture
213
+ if "arm" in machine or "aarch64" in machine:
214
+ arch = "arm64"
215
+ elif "386" in machine or "i686" in machine:
216
+ arch = "386"
217
+ else:
218
+ arch = "amd64"
219
+ url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
220
+
221
+ os.makedirs(os.path.dirname(local_bin), exist_ok=True)
222
+ subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
223
+ subprocess.run("chmod +x {}".format(local_bin), shell=True)
224
+
225
+ elif system == "darwin":
226
+ # macOS: supports both Intel and Apple Silicon
227
+ if "arm" in machine:
228
+ url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64.tgz"
229
+ else:
230
+ url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz"
231
+
232
+ os.makedirs(os.path.dirname(local_bin), exist_ok=True)
233
+ # Download and extract tar.gz
234
+ subprocess.run("curl -L {} | tar xz -C {}".format(url, os.path.dirname(local_bin)), shell=True, capture_output=True)
235
+ subprocess.run("chmod +x {}".format(local_bin), shell=True)
236
+
237
+ elif system == "windows":
238
+ # Windows: typically amd64
239
+ if "arm" in machine:
240
+ arch = "arm64"
241
+ elif "386" in machine:
242
+ arch = "386"
243
+ else:
244
+ arch = "amd64"
245
+ url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-{}.exe".format(arch)
246
+
247
+ os.makedirs(os.path.dirname(local_bin), exist_ok=True)
248
+ # Use PowerShell on Windows to download
249
+ subprocess.run("powershell -Command \"Invoke-WebRequest -Uri {} -OutFile {}\"".format(url, local_bin), shell=True, capture_output=True)
250
+
251
+ else:
252
+ print("❌ Unsupported platform: {}".format(system))
253
+ raise Exception("Platform {} not supported".format(system))
185
254
 
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)
255
+ print("✅ cloudflared downloaded successfully")
189
256
  return local_bin
190
257
 
191
258
  def start_jupyter(self):
@@ -198,7 +265,10 @@ class PersistentTunnel:
198
265
  "--no-browser",
199
266
  "--ip", "0.0.0.0",
200
267
  "--NotebookApp.token=''",
201
- "--NotebookApp.password=''"
268
+ "--NotebookApp.password=''",
269
+ "--NotebookApp.allow_origin='*'"
270
+
271
+
202
272
  ]
203
273
 
204
274
  self.jupyter_process = subprocess.Popen(
@@ -241,8 +311,8 @@ class PersistentTunnel:
241
311
 
242
312
  # API credentials are hardcoded, so we're ready to go
243
313
 
244
- # 1. Create tunnel via API
245
- cred_file = self.create_tunnel()
314
+ # 1. Get existing or create new tunnel via API
315
+ cred_file = self.get_or_create_tunnel()
246
316
  if not cred_file:
247
317
  print("⚠️ Falling back to quick tunnel")
248
318
  return self.start_quick_tunnel()
@@ -316,7 +386,7 @@ class PersistentTunnel:
316
386
  )
317
387
  requests.delete(url, headers=self._get_headers())
318
388
  print("🗑️ Tunnel deleted")
319
- except Exception as e:
389
+ except Exception:
320
390
  pass # Ignore cleanup errors
321
391
 
322
392
  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.33
4
4
  Home-page: https://github.com/teamunitlab/unitlab-sdk
5
5
  Author: Unitlab Inc.
6
6
  Author-email: team@unitlab.ai
@@ -3,21 +3,21 @@ unitlab/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
3
3
  unitlab/api_tunnel.py,sha256=SzDKFmxUg713KTkysc8qUnSmkfRc_dS3Cqrw2ONjn8I,8259
4
4
  unitlab/auto_tunnel.py,sha256=Q4YyxrKOvM6jB1lQZd-QcHwt5SuMa60MpKWKEWF4fhY,5495
5
5
  unitlab/binary_manager.py,sha256=Q1v2Odm0hk_3g7jfDUJQfkjEbUbSjtuyo2JDUyWjDrk,5468
6
- unitlab/client.py,sha256=roVX8yq1x8LW1XEFuic4G-Cq1QEjsk2CySGkiJbxA78,25759
6
+ unitlab/client.py,sha256=ftiW_ZHCHiKUdCizGq1lsq2YnOCGjjqKm9E8vM9dHbg,25636
7
7
  unitlab/cloudflare_api_tunnel.py,sha256=XgDOQ-ISNDAJOlbKp96inGix3An_eBnAQ2pORcGBM40,14061
8
8
  unitlab/cloudflare_api_tunnel_backup.py,sha256=dG5Vax0JqrF2i-zxAFB-kNGyVSFR01-ovalwuJELqpo,28489
9
9
  unitlab/dynamic_tunnel.py,sha256=fHPMouaY2q1N7e4jyre34ZeWk2mx7MKanoPfRnLNmc8,8980
10
10
  unitlab/easy_tunnel.py,sha256=yfTGv7i9wtqMpMagpIrIQTrd3jknYwQ6IUgFGbcitKM,6735
11
11
  unitlab/exceptions.py,sha256=68Tr6LreEzjQ3Vns8HAaWdtewtkNUJOvPazbf6NSnXU,950
12
- unitlab/main.py,sha256=7gPZ_2n90sxDnq9oGZVKOkuifr-k7w2Tq3ZIldAUE8I,5877
13
- unitlab/persistent_tunnel.py,sha256=0ubhsUOJUpDKG0xo18e6mN1V4pxNQvNFjylC1J1QglA,11712
12
+ unitlab/main.py,sha256=ARgToqC63NPhSxtRxarhPT0P2LgH09_pZW0Xj8mFAVY,5366
13
+ unitlab/persistent_tunnel.py,sha256=usi7lAFC_Qhmmh1hTTmLHXlm5-EUiUXFgVDm4uXJqjw,15214
14
14
  unitlab/simple_tunnel.py,sha256=vWgVYFEbPoGCHmumujNrfBnDPuUCZgQJkVO3IvdygQA,6812
15
15
  unitlab/tunnel_config.py,sha256=7CiAqasfg26YQfJYXapCBQPSoqw4jIx6yR64saybLLo,8312
16
16
  unitlab/tunnel_service_token.py,sha256=ji96a4s4W2cFJrHZle0zBD85Ac_T862-gCKzBUomrxM,3125
17
17
  unitlab/utils.py,sha256=83ekAxxfXecFTg76Z62BGDybC_skKJHYoLyawCD9wGM,1920
18
- unitlab-2.3.29.dist-info/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
19
- unitlab-2.3.29.dist-info/METADATA,sha256=OG1XzALj4PXPvmcrJ4l15-DXdelWtgxm5y0Wo9k9uZ8,844
20
- unitlab-2.3.29.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
21
- unitlab-2.3.29.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
22
- unitlab-2.3.29.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
23
- unitlab-2.3.29.dist-info/RECORD,,
18
+ unitlab-2.3.33.dist-info/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
19
+ unitlab-2.3.33.dist-info/METADATA,sha256=zx2NFeFbXhodlUQcmG-1-B0YFFis8DCUTw62L-CeJcY,844
20
+ unitlab-2.3.33.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
21
+ unitlab-2.3.33.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
22
+ unitlab-2.3.33.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
23
+ unitlab-2.3.33.dist-info/RECORD,,