unitlab 2.3.12__py3-none-any.whl → 2.3.13__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.
@@ -205,6 +205,106 @@ class CloudflareAPITunnel:
205
205
  print(" Assuming routes are configured in dashboard.")
206
206
  return True
207
207
 
208
+ def create_device_tunnel(self):
209
+ """
210
+ Create a unique tunnel for this device if it doesn't exist
211
+ """
212
+ tunnel_name = f"device-{self.clean_device_id}"
213
+ print(f"🔍 Checking for tunnel: {tunnel_name}")
214
+
215
+ # Check if tunnel already exists
216
+ list_url = f"{self.api_base}/accounts/{self.account_id}/tunnels"
217
+ response = requests.get(list_url, headers=self.headers)
218
+
219
+ if response.status_code == 200:
220
+ tunnels = response.json().get('result', [])
221
+ existing_tunnel = None
222
+
223
+ for tunnel in tunnels:
224
+ if tunnel['name'] == tunnel_name:
225
+ existing_tunnel = tunnel
226
+ print(f"✅ Found existing tunnel: {tunnel_name}")
227
+ break
228
+
229
+ if not existing_tunnel:
230
+ # Create new tunnel
231
+ print(f"📦 Creating new tunnel: {tunnel_name}")
232
+ create_url = f"{self.api_base}/accounts/{self.account_id}/tunnels"
233
+ create_data = {
234
+ "name": tunnel_name,
235
+ "tunnel_secret": os.urandom(32).hex() # Generate random secret
236
+ }
237
+
238
+ create_response = requests.post(create_url, headers=self.headers, json=create_data)
239
+
240
+ if create_response.status_code in [200, 201]:
241
+ existing_tunnel = create_response.json()['result']
242
+ print(f"✅ Created tunnel: {tunnel_name}")
243
+
244
+ # Save credentials for this tunnel
245
+ self._save_tunnel_credentials(existing_tunnel)
246
+
247
+ # Configure tunnel routes
248
+ self._configure_tunnel_routes(existing_tunnel['id'])
249
+
250
+ # Create DNS records for this device
251
+ self.create_dns_records()
252
+ else:
253
+ print(f"❌ Failed to create tunnel: {create_response.text}")
254
+ return None
255
+
256
+ return existing_tunnel
257
+
258
+ return None
259
+
260
+ def _configure_tunnel_routes(self, tunnel_id):
261
+ """
262
+ Configure ingress routes for the device tunnel
263
+ """
264
+ config_url = f"{self.api_base}/accounts/{self.account_id}/tunnels/{tunnel_id}/configurations"
265
+
266
+ config_data = {
267
+ "config": {
268
+ "ingress": [
269
+ {
270
+ "hostname": f"{self.jupyter_subdomain}.{self.base_domain}",
271
+ "service": "http://localhost:8888"
272
+ },
273
+ {
274
+ "service": "http_status:404"
275
+ }
276
+ ]
277
+ }
278
+ }
279
+
280
+ response = requests.put(config_url, headers=self.headers, json=config_data)
281
+ if response.status_code == 200:
282
+ print(f"✅ Configured tunnel routes")
283
+ else:
284
+ print(f"⚠️ Could not configure routes: {response.status_code}")
285
+
286
+ def _save_tunnel_credentials(self, tunnel_info):
287
+ """
288
+ Save tunnel credentials locally for this device
289
+ """
290
+ creds_dir = Path.home() / '.cloudflared'
291
+ creds_dir.mkdir(exist_ok=True)
292
+
293
+ creds_file = creds_dir / f"{tunnel_info['id']}.json"
294
+
295
+ credentials = {
296
+ "AccountTag": self.account_id,
297
+ "TunnelSecret": tunnel_info.get('tunnel_secret') or tunnel_info.get('secret'),
298
+ "TunnelID": tunnel_info['id']
299
+ }
300
+
301
+ import json
302
+ with open(creds_file, 'w') as f:
303
+ json.dump(credentials, f)
304
+
305
+ print(f"💾 Saved credentials to: {creds_file}")
306
+ return creds_file
307
+
208
308
  def start_tunnel_with_token(self):
209
309
  """
210
310
  Start tunnel using the existing service token
@@ -222,18 +322,65 @@ class CloudflareAPITunnel:
222
322
  if not cloudflared_path:
223
323
  raise RuntimeError("Failed to obtain cloudflared binary")
224
324
 
225
- # Use service token - simple and reliable
226
- # The dashboard must have *.1scan.uz -> localhost:8888 configured
227
- service_token = "eyJhIjoiYzkxMTkyYWUyMGE1ZDQzZjY1ZTA4NzU1MGQ4ZGM4OWIiLCJ0IjoiMDc3N2ZjMTAtNDljNC00NzJkLTg2NjEtZjYwZDgwZDYxODRkIiwicyI6Ik9XRTNaak5tTVdVdE1tWTRaUzAwTmpoakxTazBaalF0WXpjek1tSm1ZVGt4WlRRMCJ9"
325
+ # Create or get existing tunnel for this device
326
+ device_tunnel = self.create_device_tunnel()
228
327
 
229
- cmd = [
230
- cloudflared_path,
231
- "tunnel",
232
- "--no-autoupdate",
233
- "run",
234
- "--token",
235
- service_token
236
- ]
328
+ if not device_tunnel:
329
+ print("❌ Could not create/find device tunnel")
330
+ # Fallback to shared tunnel if API fails
331
+ print("⚠️ Falling back to shared tunnel...")
332
+ service_token = "eyJhIjoiYzkxMTkyYWUyMGE1ZDQzZjY1ZTA4NzU1MGQ4ZGM4OWIiLCJ0IjoiMDc3N2ZjMTAtNDljNC00NzJkLTg2NjEtZjYwZDgwZDYxODRkIiwicyI6Ik9XRTNaak5tTVdVdE1tWTRaUzAwTmpoakLTazBaalF0WXpjek1tSm1ZVGt4WlRRMCJ9"
333
+ cmd = [
334
+ cloudflared_path,
335
+ "tunnel",
336
+ "--no-autoupdate",
337
+ "run",
338
+ "--token",
339
+ service_token
340
+ ]
341
+ else:
342
+ tunnel_id = device_tunnel['id']
343
+ tunnel_name = device_tunnel['name']
344
+
345
+ print(f"🚇 Starting tunnel: {tunnel_name} ({tunnel_id})")
346
+
347
+ # Check if credentials file exists
348
+ creds_file = Path.home() / '.cloudflared' / f"{tunnel_id}.json"
349
+
350
+ if not creds_file.exists():
351
+ # Try to recreate credentials from stored secret
352
+ if device_tunnel.get('tunnel_secret'):
353
+ self._save_tunnel_credentials(device_tunnel)
354
+ else:
355
+ print("⚠️ No credentials found, requesting from API...")
356
+ # Get token for this tunnel
357
+ token_url = f"{self.api_base}/accounts/{self.account_id}/tunnels/{tunnel_id}/token"
358
+ token_response = requests.get(token_url, headers=self.headers)
359
+ if token_response.status_code == 200:
360
+ token = token_response.json()['result']
361
+ # Use token directly
362
+ cmd = [
363
+ cloudflared_path,
364
+ "tunnel",
365
+ "--no-autoupdate",
366
+ "run",
367
+ "--token",
368
+ token
369
+ ]
370
+ else:
371
+ print("❌ Could not get tunnel token")
372
+ return None
373
+
374
+ if creds_file.exists():
375
+ # Run tunnel with credentials file
376
+ cmd = [
377
+ cloudflared_path,
378
+ "tunnel",
379
+ "--no-autoupdate",
380
+ "--credentials-file", str(creds_file),
381
+ "run",
382
+ tunnel_id
383
+ ]
237
384
 
238
385
  self.tunnel_process = subprocess.Popen(
239
386
  cmd,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unitlab
3
- Version: 2.3.12
3
+ Version: 2.3.13
4
4
  Home-page: https://github.com/teamunitlab/unitlab-sdk
5
5
  Author: Unitlab Inc.
6
6
  Author-email: team@unitlab.ai
@@ -2,15 +2,15 @@ unitlab/__init__.py,sha256=Wtk5kQ_MTlxtd3mxJIn2qHVK5URrVcasMMPjD3BtrVM,214
2
2
  unitlab/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
3
3
  unitlab/binary_manager.py,sha256=Q1v2Odm0hk_3g7jfDUJQfkjEbUbSjtuyo2JDUyWjDrk,5468
4
4
  unitlab/client.py,sha256=V5fTgbprmMsnMwD_FPn7oZh0KK6hdnqB4BYuY4D-JRw,24558
5
- unitlab/cloudflare_api_tunnel.py,sha256=d95e4c4SvjpCtnQz_aGTvP-ma9fkxsbvV-oIq3aWxIk,15533
5
+ unitlab/cloudflare_api_tunnel.py,sha256=2rIGFAP9TMtOLGstqVUpVj5C0vCDdhkVRVHH41O5_MA,21624
6
6
  unitlab/exceptions.py,sha256=68Tr6LreEzjQ3Vns8HAaWdtewtkNUJOvPazbf6NSnXU,950
7
7
  unitlab/main.py,sha256=h1WG6up6STt-2fJZAkqnxenwJ7kmkqnkfq5Zs4xLSeI,5257
8
8
  unitlab/tunnel_config.py,sha256=7CiAqasfg26YQfJYXapCBQPSoqw4jIx6yR64saybLLo,8312
9
9
  unitlab/tunnel_service_token.py,sha256=ji96a4s4W2cFJrHZle0zBD85Ac_T862-gCKzBUomrxM,3125
10
10
  unitlab/utils.py,sha256=83ekAxxfXecFTg76Z62BGDybC_skKJHYoLyawCD9wGM,1920
11
- unitlab-2.3.12.dist-info/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
12
- unitlab-2.3.12.dist-info/METADATA,sha256=bMIkAY0bQjPXwYU6D1Rlrl47DoqwPZqW78OJP5NXIPA,844
13
- unitlab-2.3.12.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
14
- unitlab-2.3.12.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
15
- unitlab-2.3.12.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
16
- unitlab-2.3.12.dist-info/RECORD,,
11
+ unitlab-2.3.13.dist-info/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
12
+ unitlab-2.3.13.dist-info/METADATA,sha256=P46YSXOduMWVweKtJd5AfIJUq7sqNjHFgN_qd3o7Bh8,844
13
+ unitlab-2.3.13.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
14
+ unitlab-2.3.13.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
15
+ unitlab-2.3.13.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
16
+ unitlab-2.3.13.dist-info/RECORD,,