unitlab 2.3.33__py3-none-any.whl → 2.3.35__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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Persistent Tunnel - Each device gets deviceid.1scan.uz
3
+ Persistent Tunnel - Each device gets deviceid.unitlab-ai.com
4
4
  Uses Cloudflare API to create named tunnels
5
5
  """
6
6
 
@@ -10,18 +10,32 @@ import json
10
10
  import time
11
11
  import os
12
12
  import base64
13
+ from fastapi import FastAPI
14
+ import uvicorn
15
+ import threading
16
+ import psutil
17
+
18
+
19
+ api = FastAPI()
20
+
21
+ @api.get("/api-agent/")
22
+ def get_cpu_info():
23
+ cpu_usage_percent = psutil.cpu_percent(interval=1)
24
+ ram = psutil.virtual_memory()
25
+ return {"cpu_percentage": cpu_usage_percent, 'cpu_count': psutil.cpu_count(), 'ram_usage': ram.used }
26
+
13
27
 
14
28
  class PersistentTunnel:
15
29
  def __init__(self, device_id=None):
16
30
  """Initialize with device ID"""
17
31
 
18
32
  # Cloudflare credentials (hardcoded for simplicity)
19
- self.cf_email = "uone2323@gmail.com"
20
- self.cf_api_key = "1c634bd17ca6ade0eb91966323589fd98c72e" # Global API Key
33
+
34
+ self.cf_api_key = "RoIAn1t9rMqcGK7_Xja216pxbRTyFafC1jeRKIO3"
21
35
 
22
36
  # Account and Zone IDs
23
- self.cf_account_id = "c91192ae20a5d43f65e087550d8dc89b" # Your account ID
24
- self.cf_zone_id = "78182c3883adad79d8f1026851a68176" # Zone ID for 1scan.uz
37
+ self.cf_account_id = "29df28cf48a30be3b1aa344b840400e6" # Your account ID
38
+ self.cf_zone_id = "eae80a730730b3b218a80dace996535a" # Zone ID for unitlab-ai.com
25
39
 
26
40
  # Clean device ID for subdomain
27
41
  if device_id:
@@ -32,16 +46,19 @@ class PersistentTunnel:
32
46
 
33
47
  self.tunnel_name = "agent-{}".format(self.device_id)
34
48
  self.subdomain = self.device_id
35
- self.domain = "1scan.uz"
49
+ self.domain = "unitlab-ai.com"
36
50
  self.jupyter_url = "https://{}.{}".format(self.subdomain, self.domain)
37
-
51
+ self.api_expose_url = "https://{}.{}/api-agent/".format(self.subdomain, self.domain)
52
+ self.ssh_subdomain = "s{}".format(self.device_id) # Shorter SSH subdomain to avoid length issues
53
+ self.ssh_url = "{}.{}".format(self.ssh_subdomain, self.domain) # SSH on s{deviceid}.unitlab-ai.com
54
+
38
55
  self.tunnel_id = None
39
56
  self.tunnel_credentials = None
40
57
  self.jupyter_process = None
41
58
  self.tunnel_process = None
42
59
 
43
60
  def get_zone_id(self):
44
- """Get Zone ID for 1scan.uz"""
61
+ """Get Zone ID for unitlab-ai.com"""
45
62
  print("🔍 Getting Zone ID for {}...".format(self.domain))
46
63
 
47
64
  url = "https://api.cloudflare.com/client/v4/zones"
@@ -61,11 +78,12 @@ class PersistentTunnel:
61
78
 
62
79
  def _get_headers(self):
63
80
  """Get API headers for Global API Key"""
64
- return {
65
- "X-Auth-Email": self.cf_email,
66
- "X-Auth-Key": self.cf_api_key,
67
- "Content-Type": "application/json"
68
- }
81
+
82
+
83
+ return {
84
+ "Authorization": f"Bearer {self.cf_api_key}",
85
+ "Content-Type": "application/json"
86
+ }
69
87
 
70
88
  def get_or_create_tunnel(self):
71
89
  """Get existing tunnel or create a new one"""
@@ -133,15 +151,15 @@ class PersistentTunnel:
133
151
  print("✅ Tunnel created: {}".format(self.tunnel_id))
134
152
  return cred_file
135
153
  else:
136
- print("❌ Failed to create tunnel: {}".format(response.text[:200]))
154
+ print("❌ Failed to create tunnel: {}".format(response.text))
137
155
  return None
138
156
 
139
157
  def create_dns_record(self):
140
- """Create DNS CNAME record"""
158
+ """Create DNS CNAME records for main domain and SSH subdomain"""
141
159
  if not self.tunnel_id:
142
160
  return False
143
161
 
144
- print("🔧 Creating DNS record: {}.{}...".format(self.subdomain, self.domain))
162
+ print("🔧 Creating DNS records...")
145
163
 
146
164
  # Get zone ID if we don't have it
147
165
  if self.cf_zone_id == "NEED_ZONE_ID_FOR_1SCAN_UZ":
@@ -150,6 +168,7 @@ class PersistentTunnel:
150
168
  url = "https://api.cloudflare.com/client/v4/zones/{}/dns_records".format(self.cf_zone_id)
151
169
  headers = self._get_headers()
152
170
 
171
+ # Create main subdomain record for Jupyter and API
153
172
  data = {
154
173
  "type": "CNAME",
155
174
  "name": self.subdomain,
@@ -161,13 +180,83 @@ class PersistentTunnel:
161
180
  response = requests.post(url, headers=headers, json=data)
162
181
 
163
182
  if response.status_code in [200, 201]:
164
- print("✅ DNS record created")
165
- return True
183
+ print("✅ Main DNS record created: {}.{}".format(self.subdomain, self.domain))
166
184
  elif "already exists" in response.text:
167
- print("⚠️ DNS record already exists")
185
+ print("⚠️ Main DNS record already exists: {}.{}".format(self.subdomain, self.domain))
186
+ else:
187
+ print("❌ Failed to create main DNS: {}".format(response.text[:200]))
188
+ return False
189
+
190
+ # Create SSH subdomain record (s{deviceid}.unitlab-ai.com)
191
+ ssh_data = {
192
+ "type": "CNAME",
193
+ "name": self.ssh_subdomain,
194
+ "content": "{}.cfargotunnel.com".format(self.tunnel_id),
195
+ "proxied": True,
196
+ "ttl": 1
197
+ }
198
+
199
+ ssh_response = requests.post(url, headers=headers, json=ssh_data)
200
+
201
+ if ssh_response.status_code in [200, 201]:
202
+ print("✅ SSH DNS record created: {}.{}".format(self.ssh_subdomain, self.domain))
203
+ elif "already exists" in ssh_response.text:
204
+ print("⚠️ SSH DNS record already exists: {}.{}".format(self.ssh_subdomain, self.domain))
205
+ else:
206
+ print("⚠️ Could not create SSH DNS: {}".format(ssh_response.text[:200]))
207
+ # SSH is optional, so we continue even if SSH DNS fails
208
+
209
+ return True
210
+
211
+ def create_access_application(self):
212
+ """Create Cloudflare Access application for SSH with bypass policy"""
213
+ print("🔧 Creating Access application for SSH...")
214
+
215
+ # Create Access application
216
+ app_url = "https://api.cloudflare.com/client/v4/zones/{}/access/apps".format(self.cf_zone_id)
217
+ headers = self._get_headers()
218
+
219
+ app_data = {
220
+ "name": "SSH-{}".format(self.device_id),
221
+ "domain": "{}.{}".format(self.ssh_subdomain, self.domain),
222
+ "type": "ssh",
223
+ "session_duration": "24h",
224
+ "auto_redirect_to_identity": False
225
+ }
226
+
227
+ app_response = requests.post(app_url, headers=headers, json=app_data)
228
+
229
+ if app_response.status_code in [200, 201]:
230
+ app_id = app_response.json()["result"]["id"]
231
+ print("✅ Access application created: {}".format(app_id))
232
+
233
+ # Create bypass policy (no authentication required)
234
+ policy_url = "https://api.cloudflare.com/client/v4/zones/{}/access/apps/{}/policies".format(
235
+ self.cf_zone_id, app_id
236
+ )
237
+
238
+ policy_data = {
239
+ "name": "Public Access",
240
+ "decision": "bypass",
241
+ "include": [
242
+ {"everyone": {}}
243
+ ],
244
+ "precedence": 1
245
+ }
246
+
247
+ policy_response = requests.post(policy_url, headers=headers, json=policy_data)
248
+
249
+ if policy_response.status_code in [200, 201]:
250
+ print("✅ Bypass policy created - SSH is publicly accessible")
251
+ return True
252
+ else:
253
+ print("⚠️ Could not create bypass policy: {}".format(policy_response.text[:200]))
254
+ return False
255
+ elif "already exists" in app_response.text:
256
+ print("⚠️ Access application already exists")
168
257
  return True
169
258
  else:
170
- print(" Failed to create DNS: {}".format(response.text[:200]))
259
+ print("⚠️ Could not create Access application: {}".format(app_response.text[:200]))
171
260
  return False
172
261
 
173
262
  def create_tunnel_config(self, cred_file):
@@ -177,11 +266,24 @@ class PersistentTunnel:
177
266
  f.write("tunnel: {}\n".format(self.tunnel_id))
178
267
  f.write("credentials-file: {}\n\n".format(cred_file))
179
268
  f.write("ingress:\n")
269
+
270
+ # SSH service on dedicated subdomain (s{deviceid}.unitlab-ai.com)
271
+ f.write(" - hostname: {}.{}\n".format(self.ssh_subdomain, self.domain))
272
+ f.write(" service: ssh://localhost:22\n")
273
+
274
+ # API (more specific path goes first)
275
+ f.write(" - hostname: {}.{}\n".format(self.subdomain, self.domain))
276
+ f.write(" path: /api-agent/*\n")
277
+ f.write(" service: http://localhost:8001\n")
278
+
279
+ # Jupyter (general hostname for HTTP)
180
280
  f.write(" - hostname: {}.{}\n".format(self.subdomain, self.domain))
181
281
  f.write(" service: http://localhost:8888\n")
282
+
283
+ # Catch-all 404 (MUST be last!)
182
284
  f.write(" - service: http_status:404\n")
183
-
184
- return config_file
285
+ return config_file
286
+
185
287
 
186
288
  def get_cloudflared_path(self):
187
289
  """Get or download cloudflared for any platform"""
@@ -255,6 +357,9 @@ class PersistentTunnel:
255
357
  print("✅ cloudflared downloaded successfully")
256
358
  return local_bin
257
359
 
360
+
361
+
362
+
258
363
  def start_jupyter(self):
259
364
  """Start Jupyter"""
260
365
  print("🚀 Starting Jupyter...")
@@ -274,31 +379,59 @@ class PersistentTunnel:
274
379
  self.jupyter_process = subprocess.Popen(
275
380
  cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
276
381
  )
277
-
382
+
278
383
  time.sleep(3)
279
384
  print("✅ Jupyter started")
280
385
  return True
281
386
 
282
- def start_tunnel(self, config_file):
283
- """Start tunnel with config"""
284
- print("🔧 Starting tunnel...")
285
-
286
- cloudflared = self.get_cloudflared_path()
287
-
288
- cmd = [
289
- cloudflared,
290
- "tunnel",
291
- "--config", config_file,
292
- "run"
293
- ]
294
-
295
- self.tunnel_process = subprocess.Popen(
296
- cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
297
- )
298
-
299
- time.sleep(5)
300
- print("✅ Tunnel running at {}".format(self.jupyter_url))
301
- return True
387
+ def start_api(self):
388
+ def run_api():
389
+ uvicorn.run(
390
+ api,
391
+ port=8001
392
+ )
393
+
394
+ api_thread = threading.Thread(target=run_api, daemon=True)
395
+ api_thread.start()
396
+ print('API is started')
397
+
398
+ def start_tunnel(self, config_file):
399
+ """Start tunnel with config"""
400
+ print("🔧 Starting tunnel...")
401
+
402
+ cloudflared = self.get_cloudflared_path()
403
+
404
+ cmd = [
405
+ cloudflared,
406
+ "tunnel",
407
+ "--config", config_file,
408
+ "run"
409
+ ]
410
+
411
+ self.tunnel_process = subprocess.Popen(
412
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
413
+ )
414
+
415
+ time.sleep(2)
416
+
417
+ # Check if process is still running
418
+ if self.tunnel_process.poll() is not None:
419
+ print("❌ Tunnel process died!")
420
+ # Try to read error output
421
+ try:
422
+ stdout, stderr = self.tunnel_process.communicate(timeout=1)
423
+ if stderr:
424
+ print(f"Error: {stderr.decode()}")
425
+ if stdout:
426
+ print(f"Output: {stdout.decode()}")
427
+ except:
428
+ pass
429
+ return False
430
+
431
+ print("✅ Tunnel running at {}".format(self.jupyter_url))
432
+ print("✅ API running at {}".format(self.api_expose_url))
433
+ print("✅ SSH available at {}".format(self.ssh_url))
434
+ return True
302
435
 
303
436
  def start(self):
304
437
  """Main entry point"""
@@ -313,25 +446,37 @@ class PersistentTunnel:
313
446
 
314
447
  # 1. Get existing or create new tunnel via API
315
448
  cred_file = self.get_or_create_tunnel()
316
- if not cred_file:
317
- print("⚠️ Falling back to quick tunnel")
318
- return self.start_quick_tunnel()
449
+
319
450
 
320
451
  # 2. Create DNS record
321
452
  self.create_dns_record()
322
453
 
323
- # 3. Create config
454
+ # 3. Create Access application for SSH
455
+ self.create_access_application()
456
+
457
+ # 4. Create config
324
458
  config_file = self.create_tunnel_config(cred_file)
325
459
 
326
- # 4. Start services
460
+ # 5. Start services
327
461
  self.start_jupyter()
462
+ self.start_api()
328
463
  self.start_tunnel(config_file)
329
464
 
330
465
  print("\n" + "="*50)
331
- print("🎉 SUCCESS! Persistent URL created:")
332
- print(" {}".format(self.jupyter_url))
333
- print(" Tunnel ID: {}".format(self.tunnel_id))
466
+ print("🎉 SUCCESS! Persistent URLs created:")
467
+ print("📔 Jupyter: {}".format(self.jupyter_url))
468
+ print("🔧 API: {}".format(self.api_expose_url))
469
+ print("🔐 SSH: {}".format(self.ssh_url))
470
+ print("")
471
+ print("SSH Connection Command:")
472
+ import getpass
473
+ current_user = getpass.getuser()
474
+ print("ssh -o ProxyCommand='cloudflared access ssh --hostname {}' {}@{}".format(
475
+ self.ssh_url, current_user, self.ssh_url))
476
+ print("")
477
+ print("Tunnel ID: {}".format(self.tunnel_id))
334
478
  print("="*50)
479
+
335
480
 
336
481
  return True
337
482
 
unitlab/utils.py CHANGED
@@ -66,3 +66,5 @@ def get_api_url() -> str:
66
66
  except Exception:
67
67
  return "https://api.unitlab.ai"
68
68
 
69
+
70
+
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: unitlab
3
- Version: 2.3.33
3
+ Version: 2.3.35
4
4
  Home-page: https://github.com/teamunitlab/unitlab-sdk
5
5
  Author: Unitlab Inc.
6
6
  Author-email: team@unitlab.ai
@@ -25,4 +25,13 @@ Requires-Dist: psutil
25
25
  Requires-Dist: pyyaml
26
26
  Requires-Dist: jupyter
27
27
  Requires-Dist: python-dotenv
28
-
28
+ Requires-Dist: uvicorn
29
+ Requires-Dist: fastapi
30
+ Dynamic: author
31
+ Dynamic: author-email
32
+ Dynamic: classifier
33
+ Dynamic: home-page
34
+ Dynamic: keywords
35
+ Dynamic: license
36
+ Dynamic: license-file
37
+ Dynamic: requires-dist
@@ -0,0 +1,13 @@
1
+ unitlab/__init__.py,sha256=Wtk5kQ_MTlxtd3mxJIn2qHVK5URrVcasMMPjD3BtrVM,214
2
+ unitlab/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
3
+ unitlab/client.py,sha256=wqREtDuYc5ixeloPEGm0hp1sdUtB59sB1bIJjBcO1y0,25983
4
+ unitlab/exceptions.py,sha256=68Tr6LreEzjQ3Vns8HAaWdtewtkNUJOvPazbf6NSnXU,950
5
+ unitlab/main.py,sha256=kazASmzhPaAcf9hsPZdewcry_vplsrRLfziPxKlPT70,4425
6
+ unitlab/persistent_tunnel.py,sha256=AHiOTVkxAlwsV0-19wxuX6-I_1l3rwv9hQiCSyLOFrA,23410
7
+ unitlab/utils.py,sha256=9gPRu-d6pbhSoVdll1GXe4eoz_uFYOSbYArFDQdlUZs,1922
8
+ unitlab-2.3.35.dist-info/licenses/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
9
+ unitlab-2.3.35.dist-info/METADATA,sha256=x1Oucw_bpzTjYQbkIl56g2f7I-28eE5vmWFR0TqsKvA,1046
10
+ unitlab-2.3.35.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ unitlab-2.3.35.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
12
+ unitlab-2.3.35.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
13
+ unitlab-2.3.35.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.2)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
unitlab/api_tunnel.py DELETED
@@ -1,238 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Simple API-based Dynamic Tunnel - Each device gets deviceid.1scan.uz
4
- """
5
-
6
- import subprocess
7
- import requests
8
- import json
9
- import time
10
- import os
11
-
12
- class APITunnel:
13
- def __init__(self, device_id=None):
14
- """Initialize with device ID"""
15
- # Hardcoded Cloudflare credentials for simplicity
16
- self.cf_email = "muminovbobur93@gmail.com"
17
- self.cf_api_key = "1ae47782b5e2e639fb088ee73e17b74db4b4e" # Global API Key
18
- self.cf_account_id = "c91192ae20a5d43f65e087550d8dc89b"
19
- self.cf_zone_id = "06ebea0ee0b228c186f97fe9a0a7c83e" # for 1scan.uz
20
-
21
- # Clean device ID for subdomain
22
- if device_id:
23
- self.device_id = device_id.replace('-', '').replace('_', '').replace('.', '').lower()[:20]
24
- else:
25
- import uuid
26
- self.device_id = str(uuid.uuid4())[:8]
27
-
28
- self.tunnel_name = "agent-{}".format(self.device_id)
29
- self.subdomain = self.device_id
30
- self.jupyter_url = "https://{}.1scan.uz".format(self.subdomain)
31
-
32
- self.tunnel_id = None
33
- self.tunnel_token = None
34
- self.jupyter_process = None
35
- self.tunnel_process = None
36
-
37
- def create_tunnel_via_cli(self):
38
- """Create tunnel using cloudflared CLI (simpler than API)"""
39
- print("🔧 Creating tunnel: {}...".format(self.tunnel_name))
40
-
41
- cloudflared = self.get_cloudflared_path()
42
-
43
- # Login with cert (one-time if not logged in)
44
- # This uses the cert.pem file if it exists
45
- cert_path = os.path.expanduser("~/.cloudflared/cert.pem")
46
- if not os.path.exists(cert_path):
47
- print("📝 First time setup - logging in to Cloudflare...")
48
- # Use service token instead of interactive login
49
- # Or use the API to create tunnel
50
-
51
- # Create tunnel using CLI
52
- cmd = [cloudflared, "tunnel", "create", self.tunnel_name]
53
- result = subprocess.run(cmd, capture_output=True, text=True)
54
-
55
- if result.returncode == 0:
56
- # Extract tunnel ID from output
57
- import re
58
- match = re.search(r'Created tunnel .* with id ([a-f0-9-]+)', result.stdout)
59
- if match:
60
- self.tunnel_id = match.group(1)
61
- print("✅ Tunnel created: {}".format(self.tunnel_id))
62
-
63
- # Get the tunnel token
64
- token_cmd = [cloudflared, "tunnel", "token", self.tunnel_name]
65
- token_result = subprocess.run(token_cmd, capture_output=True, text=True)
66
- if token_result.returncode == 0:
67
- self.tunnel_token = token_result.stdout.strip()
68
- return True
69
-
70
- print("⚠️ Could not create tunnel via CLI, using quick tunnel instead")
71
- return False
72
-
73
- def create_dns_record(self):
74
- """Add DNS record for subdomain"""
75
- if not self.tunnel_id:
76
- return False
77
-
78
- print("🔧 Creating DNS: {}.1scan.uz...".format(self.subdomain))
79
-
80
- url = "https://api.cloudflare.com/client/v4/zones/{}/dns_records".format(self.cf_zone_id)
81
-
82
- headers = {
83
- "X-Auth-Email": self.cf_email,
84
- "X-Auth-Key": self.cf_api_key,
85
- "Content-Type": "application/json"
86
- }
87
-
88
- data = {
89
- "type": "CNAME",
90
- "name": self.subdomain,
91
- "content": "{}.cfargotunnel.com".format(self.tunnel_id),
92
- "proxied": True
93
- }
94
-
95
- response = requests.post(url, headers=headers, json=data)
96
- if response.status_code in [200, 409]: # 409 = already exists
97
- print("✅ DNS configured")
98
- return True
99
-
100
- print("⚠️ DNS setup failed: {}".format(response.text[:100]))
101
- return False
102
-
103
- def get_cloudflared_path(self):
104
- """Get or download cloudflared"""
105
- import shutil
106
- if shutil.which("cloudflared"):
107
- return "cloudflared"
108
-
109
- local_bin = os.path.expanduser("~/.local/bin/cloudflared")
110
- if os.path.exists(local_bin):
111
- return local_bin
112
-
113
- # Download
114
- print("📦 Downloading cloudflared...")
115
- import platform
116
- system = platform.system().lower()
117
- arch = "amd64" if "x86" in platform.machine() else "arm64"
118
- url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
119
-
120
- os.makedirs(os.path.dirname(local_bin), exist_ok=True)
121
- subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
122
- subprocess.run("chmod +x {}".format(local_bin), shell=True)
123
- return local_bin
124
-
125
- def start_jupyter(self):
126
- """Start Jupyter"""
127
- print("🚀 Starting Jupyter...")
128
-
129
- cmd = [
130
- "jupyter", "notebook",
131
- "--port", "8888",
132
- "--no-browser",
133
- "--ip", "0.0.0.0",
134
- "--NotebookApp.token=''",
135
- "--NotebookApp.password=''"
136
- ]
137
-
138
- self.jupyter_process = subprocess.Popen(
139
- cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
140
- )
141
-
142
- time.sleep(3)
143
- print("✅ Jupyter started")
144
- return True
145
-
146
- def start_tunnel(self):
147
- """Start tunnel - try with token first, fallback to quick tunnel"""
148
- cloudflared = self.get_cloudflared_path()
149
-
150
- if self.tunnel_token:
151
- # Use token-based tunnel
152
- print("🔧 Starting tunnel with token...")
153
- cmd = [cloudflared, "tunnel", "run", "--token", self.tunnel_token]
154
- elif self.tunnel_id:
155
- # Use tunnel ID
156
- print("🔧 Starting tunnel with ID...")
157
- cmd = [cloudflared, "tunnel", "run", "--url", "http://localhost:8888", self.tunnel_id]
158
- else:
159
- # Fallback to quick tunnel
160
- print("🔧 Starting quick tunnel (random URL)...")
161
- cmd = [cloudflared, "tunnel", "--url", "http://localhost:8888"]
162
- self.jupyter_url = "Check terminal output for URL"
163
-
164
- self.tunnel_process = subprocess.Popen(
165
- cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
166
- )
167
-
168
- time.sleep(5)
169
- print("✅ Tunnel running")
170
- return True
171
-
172
- def start(self):
173
- """Main entry point"""
174
- try:
175
- print("="*50)
176
- print("🌐 API-Based Dynamic Tunnel")
177
- print("Device: {}".format(self.device_id))
178
- print("="*50)
179
-
180
- # Try to create named tunnel
181
- tunnel_created = self.create_tunnel_via_cli()
182
-
183
- if tunnel_created:
184
- # Add DNS record
185
- self.create_dns_record()
186
-
187
- # Start services
188
- self.start_jupyter()
189
- self.start_tunnel()
190
-
191
- print("\n" + "="*50)
192
- print("🎉 SUCCESS!")
193
- if tunnel_created:
194
- print("📍 Your permanent URL: {}".format(self.jupyter_url))
195
- else:
196
- print("📍 Using quick tunnel - check output for URL")
197
- print("="*50)
198
-
199
- return True
200
-
201
- except Exception as e:
202
- print("❌ Error: {}".format(e))
203
- self.stop()
204
- return False
205
-
206
- def stop(self):
207
- """Stop everything"""
208
- if self.jupyter_process:
209
- self.jupyter_process.terminate()
210
- if self.tunnel_process:
211
- self.tunnel_process.terminate()
212
-
213
- def run(self):
214
- """Run and keep alive"""
215
- try:
216
- if self.start():
217
- print("\nPress Ctrl+C to stop...")
218
- while True:
219
- time.sleep(1)
220
- except KeyboardInterrupt:
221
- print("\n⏹️ Shutting down...")
222
- self.stop()
223
-
224
-
225
- def main():
226
- """Test the API tunnel"""
227
- import platform
228
- import uuid
229
-
230
- hostname = platform.node().replace('.', '-')[:20]
231
- device_id = "{}-{}".format(hostname, str(uuid.uuid4())[:8])
232
-
233
- tunnel = APITunnel(device_id=device_id)
234
- tunnel.run()
235
-
236
-
237
- if __name__ == "__main__":
238
- main()