unitlab 2.3.32__py3-none-any.whl → 2.3.34__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"""
@@ -84,18 +102,14 @@ class PersistentTunnel:
84
102
  print("✅ Found existing tunnel: {}".format(tunnel["id"]))
85
103
  self.tunnel_id = tunnel["id"]
86
104
 
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
105
+ # Tunnel exists, create a new one with unique name
106
+ print("⚠️ Tunnel with this name already exists")
107
+ import uuid
108
+ unique_suffix = str(uuid.uuid4())[:8]
109
+ self.tunnel_name = "agent-{}-{}".format(self.device_id, unique_suffix)
110
+ print("🔄 Creating new tunnel with unique name: {}".format(self.tunnel_name))
111
+ # Don't break, let it continue to create new tunnel
112
+ return self.create_new_tunnel()
99
113
 
100
114
  # Create new tunnel
101
115
  return self.create_new_tunnel()
@@ -137,15 +151,15 @@ class PersistentTunnel:
137
151
  print("✅ Tunnel created: {}".format(self.tunnel_id))
138
152
  return cred_file
139
153
  else:
140
- print("❌ Failed to create tunnel: {}".format(response.text[:200]))
154
+ print("❌ Failed to create tunnel: {}".format(response.text))
141
155
  return None
142
156
 
143
157
  def create_dns_record(self):
144
- """Create DNS CNAME record"""
158
+ """Create DNS CNAME records for main domain and SSH subdomain"""
145
159
  if not self.tunnel_id:
146
160
  return False
147
161
 
148
- print("🔧 Creating DNS record: {}.{}...".format(self.subdomain, self.domain))
162
+ print("🔧 Creating DNS records...")
149
163
 
150
164
  # Get zone ID if we don't have it
151
165
  if self.cf_zone_id == "NEED_ZONE_ID_FOR_1SCAN_UZ":
@@ -154,6 +168,7 @@ class PersistentTunnel:
154
168
  url = "https://api.cloudflare.com/client/v4/zones/{}/dns_records".format(self.cf_zone_id)
155
169
  headers = self._get_headers()
156
170
 
171
+ # Create main subdomain record for Jupyter and API
157
172
  data = {
158
173
  "type": "CNAME",
159
174
  "name": self.subdomain,
@@ -165,14 +180,33 @@ class PersistentTunnel:
165
180
  response = requests.post(url, headers=headers, json=data)
166
181
 
167
182
  if response.status_code in [200, 201]:
168
- print("✅ DNS record created")
169
- return True
183
+ print("✅ Main DNS record created: {}.{}".format(self.subdomain, self.domain))
170
184
  elif "already exists" in response.text:
171
- print("⚠️ DNS record already exists")
172
- return True
185
+ print("⚠️ Main DNS record already exists: {}.{}".format(self.subdomain, self.domain))
173
186
  else:
174
- print("❌ Failed to create DNS: {}".format(response.text[:200]))
187
+ print("❌ Failed to create main DNS: {}".format(response.text[:200]))
175
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
176
210
 
177
211
  def create_tunnel_config(self, cred_file):
178
212
  """Create tunnel config file"""
@@ -181,11 +215,24 @@ class PersistentTunnel:
181
215
  f.write("tunnel: {}\n".format(self.tunnel_id))
182
216
  f.write("credentials-file: {}\n\n".format(cred_file))
183
217
  f.write("ingress:\n")
218
+
219
+ # SSH service on dedicated subdomain (s{deviceid}.unitlab-ai.com)
220
+ f.write(" - hostname: {}.{}\n".format(self.ssh_subdomain, self.domain))
221
+ f.write(" service: ssh://localhost:22\n")
222
+
223
+ # API (more specific path goes first)
224
+ f.write(" - hostname: {}.{}\n".format(self.subdomain, self.domain))
225
+ f.write(" path: /api-agent/*\n")
226
+ f.write(" service: http://localhost:8001\n")
227
+
228
+ # Jupyter (general hostname for HTTP)
184
229
  f.write(" - hostname: {}.{}\n".format(self.subdomain, self.domain))
185
230
  f.write(" service: http://localhost:8888\n")
231
+
232
+ # Catch-all 404 (MUST be last!)
186
233
  f.write(" - service: http_status:404\n")
187
-
188
- return config_file
234
+ return config_file
235
+
189
236
 
190
237
  def get_cloudflared_path(self):
191
238
  """Get or download cloudflared for any platform"""
@@ -259,6 +306,9 @@ class PersistentTunnel:
259
306
  print("✅ cloudflared downloaded successfully")
260
307
  return local_bin
261
308
 
309
+
310
+
311
+
262
312
  def start_jupyter(self):
263
313
  """Start Jupyter"""
264
314
  print("🚀 Starting Jupyter...")
@@ -278,31 +328,59 @@ class PersistentTunnel:
278
328
  self.jupyter_process = subprocess.Popen(
279
329
  cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
280
330
  )
281
-
331
+
282
332
  time.sleep(3)
283
333
  print("✅ Jupyter started")
284
334
  return True
285
335
 
286
- def start_tunnel(self, config_file):
287
- """Start tunnel with config"""
288
- print("🔧 Starting tunnel...")
289
-
290
- cloudflared = self.get_cloudflared_path()
291
-
292
- cmd = [
293
- cloudflared,
294
- "tunnel",
295
- "--config", config_file,
296
- "run"
297
- ]
298
-
299
- self.tunnel_process = subprocess.Popen(
300
- cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
301
- )
302
-
303
- time.sleep(5)
304
- print("✅ Tunnel running at {}".format(self.jupyter_url))
305
- return True
336
+ def start_api(self):
337
+ def run_api():
338
+ uvicorn.run(
339
+ api,
340
+ port=8001
341
+ )
342
+
343
+ api_thread = threading.Thread(target=run_api, daemon=True)
344
+ api_thread.start()
345
+ print('API is started')
346
+
347
+ def start_tunnel(self, config_file):
348
+ """Start tunnel with config"""
349
+ print("🔧 Starting tunnel...")
350
+
351
+ cloudflared = self.get_cloudflared_path()
352
+
353
+ cmd = [
354
+ cloudflared,
355
+ "tunnel",
356
+ "--config", config_file,
357
+ "run"
358
+ ]
359
+
360
+ self.tunnel_process = subprocess.Popen(
361
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
362
+ )
363
+
364
+ time.sleep(2)
365
+
366
+ # Check if process is still running
367
+ if self.tunnel_process.poll() is not None:
368
+ print("❌ Tunnel process died!")
369
+ # Try to read error output
370
+ try:
371
+ stdout, stderr = self.tunnel_process.communicate(timeout=1)
372
+ if stderr:
373
+ print(f"Error: {stderr.decode()}")
374
+ if stdout:
375
+ print(f"Output: {stdout.decode()}")
376
+ except:
377
+ pass
378
+ return False
379
+
380
+ print("✅ Tunnel running at {}".format(self.jupyter_url))
381
+ print("✅ API running at {}".format(self.api_expose_url))
382
+ print("✅ SSH available at {}".format(self.ssh_url))
383
+ return True
306
384
 
307
385
  def start(self):
308
386
  """Main entry point"""
@@ -317,9 +395,7 @@ class PersistentTunnel:
317
395
 
318
396
  # 1. Get existing or create new tunnel via API
319
397
  cred_file = self.get_or_create_tunnel()
320
- if not cred_file:
321
- print("⚠️ Falling back to quick tunnel")
322
- return self.start_quick_tunnel()
398
+
323
399
 
324
400
  # 2. Create DNS record
325
401
  self.create_dns_record()
@@ -329,13 +405,24 @@ class PersistentTunnel:
329
405
 
330
406
  # 4. Start services
331
407
  self.start_jupyter()
408
+ self.start_api()
332
409
  self.start_tunnel(config_file)
333
410
 
334
411
  print("\n" + "="*50)
335
- print("🎉 SUCCESS! Persistent URL created:")
336
- print(" {}".format(self.jupyter_url))
337
- print(" Tunnel ID: {}".format(self.tunnel_id))
412
+ print("🎉 SUCCESS! Persistent URLs created:")
413
+ print("📔 Jupyter: {}".format(self.jupyter_url))
414
+ print("🔧 API: {}".format(self.api_expose_url))
415
+ print("🔐 SSH: {}".format(self.ssh_url))
416
+ print("")
417
+ print("SSH Connection Command:")
418
+ import getpass
419
+ current_user = getpass.getuser()
420
+ print("ssh -o ProxyCommand='cloudflared access ssh --hostname {}' {}@{}".format(
421
+ self.ssh_url, current_user, self.ssh_url))
422
+ print("")
423
+ print("Tunnel ID: {}".format(self.tunnel_id))
338
424
  print("="*50)
425
+
339
426
 
340
427
  return True
341
428
 
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.32
3
+ Version: 2.3.34
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=LMZ7HFjRxzPV2IrCXlP2GETlY0vWvAxP0RrjSXOn_Jk,26015
4
+ unitlab/exceptions.py,sha256=68Tr6LreEzjQ3Vns8HAaWdtewtkNUJOvPazbf6NSnXU,950
5
+ unitlab/main.py,sha256=EbQNO-Z5drNQjDXJp_sIs5a3WgPoqxaXxpdFGzMWm6k,4416
6
+ unitlab/persistent_tunnel.py,sha256=XDJo2PPq4EjEtI4vT68LIGUUq7WV4m0bnXYKPfY51cY,21180
7
+ unitlab/utils.py,sha256=9gPRu-d6pbhSoVdll1GXe4eoz_uFYOSbYArFDQdlUZs,1922
8
+ unitlab-2.3.34.dist-info/licenses/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
9
+ unitlab-2.3.34.dist-info/METADATA,sha256=ctnyV_ZT0vuOzGMoH0N09v0nalsR79WQCGdi_D-ncw0,1046
10
+ unitlab-2.3.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ unitlab-2.3.34.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
12
+ unitlab-2.3.34.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
13
+ unitlab-2.3.34.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()