unitlab 2.3.33__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
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: unitlab
3
- Version: 2.3.33
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()
unitlab/auto_tunnel.py DELETED
@@ -1,174 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Automatic Tunnel Creation - Simplest approach using cloudflared's built-in quick tunnel
4
- No API tokens needed!
5
- """
6
-
7
- import subprocess
8
- import time
9
- import re
10
- import os
11
-
12
- class AutoTunnel:
13
- def __init__(self, device_id=None):
14
- """
15
- Initialize auto tunnel - no credentials needed!
16
- """
17
- self.device_id = device_id or "device"
18
- self.jupyter_process = None
19
- self.tunnel_process = None
20
- self.tunnel_url = None
21
-
22
- def get_cloudflared_path(self):
23
- """Get or download cloudflared binary"""
24
- import platform
25
-
26
- # Check if exists in system
27
- import shutil
28
- if shutil.which("cloudflared"):
29
- return "cloudflared"
30
-
31
- # Check local
32
- local_bin = os.path.expanduser("~/.local/bin/cloudflared")
33
- if os.path.exists(local_bin):
34
- return local_bin
35
-
36
- # Download it
37
- print("📦 Downloading cloudflared...")
38
- system = platform.system().lower()
39
- if system == "linux":
40
- arch = "amd64" if "x86" in platform.machine() else "arm64"
41
- url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
42
-
43
- os.makedirs(os.path.expanduser("~/.local/bin"), exist_ok=True)
44
- subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
45
- subprocess.run("chmod +x {}".format(local_bin), shell=True)
46
- print("✅ cloudflared downloaded")
47
- return local_bin
48
-
49
- return "cloudflared"
50
-
51
- def start_jupyter(self):
52
- """Start Jupyter notebook"""
53
- print("🚀 Starting Jupyter on port 8888...")
54
-
55
- cmd = [
56
- "jupyter", "notebook",
57
- "--port", "8888",
58
- "--no-browser",
59
- "--ip", "0.0.0.0",
60
- "--NotebookApp.token=''",
61
- "--NotebookApp.password=''",
62
- "--NotebookApp.allow_origin='*'"
63
- ]
64
-
65
- self.jupyter_process = subprocess.Popen(
66
- cmd,
67
- stdout=subprocess.PIPE,
68
- stderr=subprocess.PIPE
69
- )
70
-
71
- time.sleep(3)
72
- print("✅ Jupyter started")
73
- return True
74
-
75
- def start_tunnel(self):
76
- """Start tunnel using cloudflared quick tunnel - no auth needed!"""
77
- print("🔧 Starting automatic tunnel (no credentials needed)...")
78
-
79
- cloudflared = self.get_cloudflared_path()
80
-
81
- # Use cloudflared's quick tunnel feature - generates random URL
82
- cmd = [
83
- cloudflared,
84
- "tunnel",
85
- "--url", "http://localhost:8888"
86
- ]
87
-
88
- self.tunnel_process = subprocess.Popen(
89
- cmd,
90
- stdout=subprocess.PIPE,
91
- stderr=subprocess.STDOUT,
92
- text=True,
93
- bufsize=1
94
- )
95
-
96
- # Read output to get the tunnel URL
97
- print("⏳ Waiting for tunnel URL...")
98
- for _ in range(30): # Wait up to 30 seconds
99
- line = self.tunnel_process.stdout.readline()
100
- if line:
101
- # Look for the tunnel URL in output
102
- match = re.search(r'https://[a-zA-Z0-9-]+\.trycloudflare\.com', line)
103
- if match:
104
- self.tunnel_url = match.group(0)
105
- print("✅ Tunnel created: {}".format(self.tunnel_url))
106
- return True
107
- time.sleep(1)
108
-
109
- print("❌ Failed to get tunnel URL")
110
- return False
111
-
112
- def start(self):
113
- """Start everything - super simple!"""
114
- try:
115
- print("="*50)
116
- print("🌐 Automatic Cloudflare Tunnel (No Auth Needed!)")
117
- print("Device: {}".format(self.device_id))
118
- print("="*50)
119
-
120
- # 1. Start Jupyter
121
- if not self.start_jupyter():
122
- raise Exception("Failed to start Jupyter")
123
-
124
- # 2. Start tunnel (automatic, no credentials)
125
- if not self.start_tunnel():
126
- raise Exception("Failed to start tunnel")
127
-
128
- print("\n" + "="*50)
129
- print("🎉 SUCCESS! Your Jupyter is accessible at:")
130
- print(" {}".format(self.tunnel_url))
131
- print("="*50)
132
- print("\n⚠️ Note: This URL is temporary and random")
133
- print("For persistent URLs, use Cloudflare API approach")
134
-
135
- return True
136
-
137
- except Exception as e:
138
- print("❌ Error: {}".format(e))
139
- self.stop()
140
- return False
141
-
142
- def stop(self):
143
- """Stop everything"""
144
- if self.jupyter_process:
145
- self.jupyter_process.terminate()
146
- if self.tunnel_process:
147
- self.tunnel_process.terminate()
148
-
149
- def run(self):
150
- """Run and keep alive"""
151
- try:
152
- if self.start():
153
- print("\nPress Ctrl+C to stop...")
154
- while True:
155
- time.sleep(1)
156
- except KeyboardInterrupt:
157
- print("\n⏹️ Shutting down...")
158
- self.stop()
159
- print("👋 Goodbye!")
160
-
161
-
162
- def main():
163
- """Test automatic tunnel"""
164
- import platform
165
- device_id = platform.node()
166
-
167
- print("Starting auto tunnel for: {}".format(device_id))
168
-
169
- tunnel = AutoTunnel(device_id=device_id)
170
- tunnel.run()
171
-
172
-
173
- if __name__ == "__main__":
174
- main()
unitlab/binary_manager.py DELETED
@@ -1,154 +0,0 @@
1
- import os
2
- import platform
3
- import hashlib
4
- import urllib.request
5
- from pathlib import Path
6
- import stat
7
- import json
8
-
9
- class CloudflaredBinaryManager:
10
- """
11
- Manages cloudflared binary automatically
12
- - Downloads on first use
13
- - Caches for future use
14
- - Verifies integrity
15
- - Zero user configuration
16
- """
17
-
18
- # Binary URLs and checksums
19
- BINARIES = {
20
- 'linux-amd64': {
21
- 'url': 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64',
22
- 'checksum': 'sha256:...', # Add real checksums
23
- 'filename': 'cloudflared'
24
- },
25
- 'linux-arm64': {
26
- 'url': 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64',
27
- 'checksum': 'sha256:...',
28
- 'filename': 'cloudflared'
29
- },
30
- 'darwin-amd64': {
31
- 'url': 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz',
32
- 'checksum': 'sha256:...',
33
- 'filename': 'cloudflared',
34
- 'compressed': True
35
- },
36
- 'windows-amd64': {
37
- 'url': 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe',
38
- 'checksum': 'sha256:...',
39
- 'filename': 'cloudflared.exe'
40
- }
41
- }
42
-
43
- def __init__(self):
44
- # User's home directory - works on all platforms
45
- self.cache_dir = Path.home() / '.unitlab' / 'bin'
46
- self.cache_dir.mkdir(parents=True, exist_ok=True)
47
-
48
- # Detect platform once
49
- self.platform_key = self._detect_platform()
50
-
51
- def _detect_platform(self):
52
- """Detect OS and architecture"""
53
- system = platform.system().lower()
54
- machine = platform.machine().lower()
55
-
56
- if system == 'linux':
57
- if machine in ['x86_64', 'amd64']:
58
- return 'linux-amd64'
59
- elif machine in ['aarch64', 'arm64']:
60
- return 'linux-arm64'
61
-
62
- elif system == 'darwin': # macOS
63
- # Check if ARM (M1/M2) or Intel
64
- if machine == 'arm64':
65
- return 'darwin-arm64'
66
- return 'darwin-amd64'
67
-
68
- elif system == 'windows':
69
- return 'windows-amd64'
70
-
71
- raise RuntimeError(f"Unsupported platform: {system} {machine}")
72
-
73
- def get_binary_path(self):
74
- """Get path to cloudflared binary, downloading if needed"""
75
-
76
- binary_info = self.BINARIES[self.platform_key]
77
- binary_path = self.cache_dir / binary_info['filename']
78
-
79
- # Check if already downloaded
80
- if binary_path.exists():
81
- print("✓ Using cached cloudflared")
82
- return str(binary_path)
83
-
84
- # Download for first time
85
- print("🔄 First time setup - downloading cloudflared...")
86
- self._download_binary(binary_info, binary_path)
87
-
88
- return str(binary_path)
89
-
90
- def _download_binary(self, info, target_path):
91
- """Download and verify binary"""
92
-
93
- # Create SSL context to handle certificate issues
94
- import ssl
95
- ssl_context = ssl.create_default_context()
96
- ssl_context.check_hostname = False
97
- ssl_context.verify_mode = ssl.CERT_NONE
98
-
99
- # Download with progress bar
100
- def download_progress(block_num, block_size, total_size):
101
- downloaded = block_num * block_size
102
- if total_size > 0:
103
- percent = min(downloaded * 100 / total_size, 100)
104
- print(f"Downloading: {percent:.0f}%", end='\r')
105
- else:
106
- print(f"Downloading: {downloaded} bytes", end='\r')
107
-
108
- temp_file = target_path.with_suffix('.tmp')
109
-
110
- try:
111
- # Download file with SSL context
112
- req = urllib.request.Request(info['url'], headers={'User-Agent': 'Mozilla/5.0'})
113
- with urllib.request.urlopen(req, context=ssl_context) as response:
114
- total_size = int(response.headers.get('Content-Length', 0))
115
-
116
- with open(temp_file, 'wb') as f:
117
- downloaded = 0
118
- block_size = 8192
119
- while True:
120
- chunk = response.read(block_size)
121
- if not chunk:
122
- break
123
- f.write(chunk)
124
- downloaded += len(chunk)
125
- if total_size > 0:
126
- percent = min(downloaded * 100 / total_size, 100)
127
- print(f"Downloading: {percent:.0f}%", end='\r')
128
- print("\n✓ Download complete")
129
-
130
- # Handle compressed files (macOS .tgz)
131
- if info.get('compressed'):
132
- import tarfile
133
- with tarfile.open(temp_file, 'r:gz') as tar:
134
- # Extract just the cloudflared binary
135
- tar.extract('cloudflared', self.cache_dir)
136
- temp_file.unlink()
137
- else:
138
- # Move to final location
139
- temp_file.rename(target_path)
140
-
141
- # Make executable on Unix systems
142
- if platform.system() != 'Windows':
143
- target_path.chmod(target_path.stat().st_mode | stat.S_IEXEC)
144
-
145
- print("✓ Cloudflared ready!")
146
-
147
- except Exception as e:
148
- print(f"❌ Download failed: {e}")
149
- if temp_file.exists():
150
- temp_file.unlink()
151
- raise
152
-
153
-
154
-