unitlab 2.3.26__tar.gz → 2.3.27__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 (23) hide show
  1. {unitlab-2.3.26/src/unitlab.egg-info → unitlab-2.3.27}/PKG-INFO +1 -1
  2. {unitlab-2.3.26 → unitlab-2.3.27}/setup.py +1 -1
  3. {unitlab-2.3.26 → unitlab-2.3.27}/src/unitlab/client.py +48 -56
  4. unitlab-2.3.27/src/unitlab/simple_tunnel.py +158 -0
  5. {unitlab-2.3.26 → unitlab-2.3.27/src/unitlab.egg-info}/PKG-INFO +1 -1
  6. {unitlab-2.3.26 → unitlab-2.3.27}/src/unitlab.egg-info/SOURCES.txt +1 -0
  7. {unitlab-2.3.26 → unitlab-2.3.27}/LICENSE.md +0 -0
  8. {unitlab-2.3.26 → unitlab-2.3.27}/README.md +0 -0
  9. {unitlab-2.3.26 → unitlab-2.3.27}/setup.cfg +0 -0
  10. {unitlab-2.3.26 → unitlab-2.3.27}/src/unitlab/__init__.py +0 -0
  11. {unitlab-2.3.26 → unitlab-2.3.27}/src/unitlab/__main__.py +0 -0
  12. {unitlab-2.3.26 → unitlab-2.3.27}/src/unitlab/binary_manager.py +0 -0
  13. {unitlab-2.3.26 → unitlab-2.3.27}/src/unitlab/cloudflare_api_tunnel.py +0 -0
  14. {unitlab-2.3.26 → unitlab-2.3.27}/src/unitlab/cloudflare_api_tunnel_backup.py +0 -0
  15. {unitlab-2.3.26 → unitlab-2.3.27}/src/unitlab/exceptions.py +0 -0
  16. {unitlab-2.3.26 → unitlab-2.3.27}/src/unitlab/main.py +0 -0
  17. {unitlab-2.3.26 → unitlab-2.3.27}/src/unitlab/tunnel_config.py +0 -0
  18. {unitlab-2.3.26 → unitlab-2.3.27}/src/unitlab/tunnel_service_token.py +0 -0
  19. {unitlab-2.3.26 → unitlab-2.3.27}/src/unitlab/utils.py +0 -0
  20. {unitlab-2.3.26 → unitlab-2.3.27}/src/unitlab.egg-info/dependency_links.txt +0 -0
  21. {unitlab-2.3.26 → unitlab-2.3.27}/src/unitlab.egg-info/entry_points.txt +0 -0
  22. {unitlab-2.3.26 → unitlab-2.3.27}/src/unitlab.egg-info/requires.txt +0 -0
  23. {unitlab-2.3.26 → unitlab-2.3.27}/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.26
3
+ Version: 2.3.27
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.26",
5
+ version="2.3.27",
6
6
  license="MIT",
7
7
  author="Unitlab Inc.",
8
8
  author_email="team@unitlab.ai",
@@ -17,6 +17,7 @@ import psutil
17
17
  from datetime import datetime, timezone
18
18
  from .tunnel_config import CloudflareTunnel
19
19
  from .cloudflare_api_tunnel import CloudflareAPITunnel
20
+ from .simple_tunnel import SimpleTunnel
20
21
  from .utils import get_api_url, handle_exceptions
21
22
  from pathlib import Path
22
23
 
@@ -37,9 +38,6 @@ except ImportError:
37
38
  pass # dotenv not installed, use system env vars only
38
39
 
39
40
 
40
-
41
-
42
-
43
41
  logger = logging.getLogger(__name__)
44
42
 
45
43
  class UnitlabClient:
@@ -283,24 +281,12 @@ class UnitlabClient:
283
281
  self.device_id = device_id
284
282
  self.base_domain = base_domain
285
283
 
286
- # Always use API-based tunnel now that we have hardcoded credentials
287
- # This provides better SSH/Jupyter separation with j/s prefixes
288
- try:
289
- logger.info("Using API-based Cloudflare tunnel management")
290
- self.tunnel_manager = CloudflareAPITunnel(device_id, base_domain)
291
- self.jupyter_url = self.tunnel_manager.jupyter_url
292
- self.ssh_url = self.tunnel_manager.ssh_url
293
- except Exception as e:
294
- logger.warning(f"Failed to initialize API tunnel, falling back: {e}")
295
- if CloudflareTunnel:
296
- logger.info("Using service token Cloudflare tunnel")
297
- self.tunnel_manager = CloudflareTunnel(base_domain, device_id)
298
- self.jupyter_url = self.tunnel_manager.jupyter_url
299
- self.ssh_url = self.tunnel_manager.ssh_url
300
- else:
301
- self.tunnel_manager = None
302
- self.jupyter_url = f"https://jupyter-{device_id}.{base_domain}"
303
- self.ssh_url = f"https://ssh-{device_id}.{base_domain}"
284
+ # Use the simple hardcoded tunnel approach
285
+ logger.info("Using simple hardcoded Cloudflare tunnel")
286
+ self.tunnel_manager = SimpleTunnel(device_id=self.device_id)
287
+ # SimpleTunnel generates the URL internally based on device_id
288
+ self.jupyter_url = self.tunnel_manager.jupyter_url
289
+ self.ssh_url = self.jupyter_url # Same URL for both services
304
290
 
305
291
  # Setup signal handlers
306
292
  signal.signal(signal.SIGINT, self._handle_shutdown)
@@ -400,23 +386,27 @@ class UnitlabClient:
400
386
  def setup_tunnels(self) -> bool:
401
387
  """Setup Cloudflare tunnels"""
402
388
  try:
403
- if not self.jupyter_port:
404
- logger.error("Jupyter port not available")
405
- return False
406
-
407
389
  if not self.tunnel_manager:
408
- logger.warning("CloudflareTunnel not available, skipping tunnel setup")
390
+ logger.warning("Tunnel manager not available, skipping tunnel setup")
409
391
  return True
410
392
 
411
- logger.info("Setting up Cloudflare tunnels...")
412
- self.tunnel_proc = self.tunnel_manager.setup(self.jupyter_port)
393
+ logger.info("Setting up Cloudflare tunnel...")
413
394
 
414
- if self.tunnel_proc:
415
- logger.info("✅ Tunnels established")
395
+ # SimpleTunnel handles both Jupyter and tunnel startup internally
396
+ # It doesn't need the jupyter_port parameter since it starts its own Jupyter
397
+ if self.tunnel_manager.start():
398
+ # Store the processes for monitoring
399
+ self.jupyter_proc = self.tunnel_manager.jupyter_process
400
+ self.tunnel_proc = self.tunnel_manager.tunnel_process
401
+ self.jupyter_port = "8888" # SimpleTunnel uses fixed port
402
+
403
+ # The tunnel is now running
404
+ logger.info("✅ Tunnel and Jupyter established")
416
405
  self.report_services()
417
406
  return True
418
-
419
- return False
407
+ else:
408
+ logger.error("Failed to start tunnel")
409
+ return False
420
410
 
421
411
  except Exception as e:
422
412
  logger.error(f"Tunnel setup failed: {e}")
@@ -603,13 +593,9 @@ class UnitlabClient:
603
593
  # Check SSH
604
594
  self.check_ssh()
605
595
 
606
- # Start Jupyter
607
- if not self.start_jupyter():
608
- logger.error("Failed to start Jupyter")
609
- return
610
-
611
- # Wait a moment for Jupyter to fully initialize
612
- time.sleep(1)
596
+ # SimpleTunnel handles Jupyter internally, so we don't start it separately
597
+ # Just setup the tunnels which will also start Jupyter
598
+ logger.info("Starting integrated Jupyter and tunnel...")
613
599
 
614
600
  # Setup tunnels
615
601
  if not self.setup_tunnels():
@@ -648,22 +634,28 @@ class UnitlabClient:
648
634
 
649
635
  self.running = False
650
636
 
651
- # Stop Jupyter
652
- if self.jupyter_proc:
653
- logger.info("Stopping Jupyter...")
654
- self.jupyter_proc.terminate()
655
- try:
656
- self.jupyter_proc.wait(timeout=5)
657
- except subprocess.TimeoutExpired:
658
- self.jupyter_proc.kill()
659
-
660
- # Stop tunnel
661
- if self.tunnel_proc:
662
- logger.info("Stopping tunnel...")
663
- self.tunnel_proc.terminate()
664
- try:
665
- self.tunnel_proc.wait(timeout=5)
666
- except subprocess.TimeoutExpired:
667
- self.tunnel_proc.kill()
637
+ # With SimpleTunnel, we just call stop() method
638
+ if self.tunnel_manager and hasattr(self.tunnel_manager, 'stop'):
639
+ logger.info("Stopping tunnel and Jupyter...")
640
+ self.tunnel_manager.stop()
641
+ else:
642
+ # Fallback to individual process cleanup
643
+ # Stop Jupyter
644
+ if self.jupyter_proc:
645
+ logger.info("Stopping Jupyter...")
646
+ self.jupyter_proc.terminate()
647
+ try:
648
+ self.jupyter_proc.wait(timeout=5)
649
+ except subprocess.TimeoutExpired:
650
+ self.jupyter_proc.kill()
651
+
652
+ # Stop tunnel
653
+ if self.tunnel_proc:
654
+ logger.info("Stopping tunnel...")
655
+ self.tunnel_proc.terminate()
656
+ try:
657
+ self.tunnel_proc.wait(timeout=5)
658
+ except subprocess.TimeoutExpired:
659
+ self.tunnel_proc.kill()
668
660
 
669
661
  logger.info("Cleanup complete")
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Simple Cloudflare Tunnel for Jupyter - MVP Version
4
+ Uses hardcoded tunnel credentials
5
+ """
6
+
7
+ import subprocess
8
+ from pathlib import Path
9
+ import time
10
+
11
+
12
+ class SimpleTunnel:
13
+ def __init__(self, device_id=None):
14
+ """
15
+ Initialize SimpleTunnel with hardcoded everything
16
+ device_id: Unique device identifier for subdomain generation
17
+ """
18
+ # Everything hardcoded for simplicity
19
+ self.tunnel_uuid = "c6caf64a-7499-4aa5-8702-0d1870388114"
20
+ self.domain = "1scan.uz"
21
+
22
+ # Generate unique subdomain from device_id
23
+ if device_id:
24
+ # Clean device_id: remove special chars, lowercase, limit length
25
+ clean_id = device_id.replace('-', '').replace('_', '').replace('.', '').lower()[:30]
26
+ self.subdomain = clean_id
27
+ else:
28
+ # Fallback to a random subdomain if no device_id
29
+ import uuid
30
+ self.subdomain = str(uuid.uuid4())[:8]
31
+
32
+ # The unique URL for this device
33
+ self.jupyter_url = "https://{}.{}".format(self.subdomain, self.domain)
34
+
35
+ self.jupyter_process = None
36
+ self.tunnel_process = None
37
+
38
+ def start_jupyter(self, port=8888):
39
+ """Start Jupyter notebook server"""
40
+ print("🚀 Starting Jupyter on port {}...".format(port))
41
+
42
+ cmd = [
43
+ "jupyter", "notebook",
44
+ "--port", '8888',
45
+ "--no-browser",
46
+ "--ip", "0.0.0.0",
47
+ "--ServerApp.token=''",
48
+ "--ServerApp.password=''",
49
+ "--ServerApp.allow_origin='*'"
50
+ ]
51
+
52
+ self.jupyter_process = subprocess.Popen(
53
+ cmd,
54
+ stdout=subprocess.PIPE,
55
+ stderr=subprocess.PIPE
56
+ )
57
+
58
+ # Wait for Jupyter to start
59
+ time.sleep(3)
60
+
61
+ print("✅ Jupyter started on port {}".format(port))
62
+ return True
63
+
64
+ def start_tunnel(self, local_port=8888):
65
+ """Start cloudflared tunnel - simple and direct"""
66
+ print("🔧 Starting Cloudflare tunnel...")
67
+
68
+ # Simple command - just run the tunnel
69
+ cmd = [
70
+ "cloudflared", # Use system cloudflared
71
+ "tunnel",
72
+ "run",
73
+ "--url", "http://127.0.0.1:{}".format(local_port),
74
+ self.tunnel_uuid
75
+ ]
76
+
77
+ self.tunnel_process = subprocess.Popen(
78
+ cmd,
79
+ stdout=subprocess.PIPE,
80
+ stderr=subprocess.PIPE
81
+ )
82
+
83
+ # Wait for tunnel to establish
84
+ time.sleep(3)
85
+
86
+ print("✅ Tunnel running at {}".format(self.jupyter_url))
87
+
88
+ def start(self):
89
+ """Start tunnel and Jupyter (non-blocking)"""
90
+ try:
91
+ print("="*50)
92
+ print("🌐 Simple Cloudflare Tunnel for Jupyter - MVP")
93
+ print("="*50)
94
+
95
+ # 1. Cloudflared should be installed on system
96
+
97
+ # 2. Start Jupyter
98
+ if not self.start_jupyter():
99
+ raise Exception("Failed to start Jupyter")
100
+
101
+ # 3. Start tunnel
102
+ self.start_tunnel()
103
+
104
+ # 4. Print access info
105
+ print("\n" + "="*50)
106
+ print("🎉 SUCCESS! Your Jupyter is now accessible at:")
107
+ print(" {}".format(self.jupyter_url))
108
+ print(" Device subdomain: {}".format(self.subdomain))
109
+ print("="*50)
110
+
111
+ return True
112
+
113
+ except Exception as e:
114
+ print("❌ Error: {}".format(e))
115
+ self.stop()
116
+ return False
117
+
118
+ def stop(self):
119
+ """Stop tunnel and Jupyter"""
120
+ if self.jupyter_process:
121
+ self.jupyter_process.terminate()
122
+ self.jupyter_process = None
123
+ if self.tunnel_process:
124
+ self.tunnel_process.terminate()
125
+ self.tunnel_process = None
126
+
127
+ def run(self):
128
+ """Main entry point for standalone use - sets up everything and blocks"""
129
+ try:
130
+ if self.start():
131
+ # Keep running
132
+ while True:
133
+ time.sleep(1)
134
+ except KeyboardInterrupt:
135
+ print("\n⏹️ Shutting down...")
136
+ self.stop()
137
+ print("👋 Goodbye!")
138
+
139
+
140
+ def main():
141
+ """Example usage with device ID"""
142
+
143
+ # Generate a unique device ID (in real usage, this comes from main.py)
144
+ import platform
145
+ import uuid
146
+ hostname = platform.node().replace('.', '-').replace(' ', '-')[:20]
147
+ random_suffix = str(uuid.uuid4())[:8]
148
+ device_id = "{}-{}".format(hostname, random_suffix)
149
+
150
+ print("Device ID: {}".format(device_id))
151
+
152
+ # Create tunnel with device ID for unique subdomain
153
+ tunnel = SimpleTunnel(device_id=device_id)
154
+ tunnel.run()
155
+
156
+
157
+ if __name__ == "__main__":
158
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unitlab
3
- Version: 2.3.26
3
+ Version: 2.3.27
4
4
  Home-page: https://github.com/teamunitlab/unitlab-sdk
5
5
  Author: Unitlab Inc.
6
6
  Author-email: team@unitlab.ai
@@ -10,6 +10,7 @@ src/unitlab/cloudflare_api_tunnel.py
10
10
  src/unitlab/cloudflare_api_tunnel_backup.py
11
11
  src/unitlab/exceptions.py
12
12
  src/unitlab/main.py
13
+ src/unitlab/simple_tunnel.py
13
14
  src/unitlab/tunnel_config.py
14
15
  src/unitlab/tunnel_service_token.py
15
16
  src/unitlab/utils.py
File without changes
File without changes
File without changes
File without changes
File without changes