unitlab 2.3.7__tar.gz → 2.3.9__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.
- {unitlab-2.3.7/src/unitlab.egg-info → unitlab-2.3.9}/PKG-INFO +2 -1
- {unitlab-2.3.7 → unitlab-2.3.9}/setup.py +2 -1
- {unitlab-2.3.7 → unitlab-2.3.9}/src/unitlab/client.py +14 -15
- {unitlab-2.3.7 → unitlab-2.3.9}/src/unitlab/cloudflare_api_tunnel.py +88 -12
- {unitlab-2.3.7 → unitlab-2.3.9/src/unitlab.egg-info}/PKG-INFO +2 -1
- {unitlab-2.3.7 → unitlab-2.3.9}/src/unitlab.egg-info/requires.txt +1 -0
- {unitlab-2.3.7 → unitlab-2.3.9}/LICENSE.md +0 -0
- {unitlab-2.3.7 → unitlab-2.3.9}/README.md +0 -0
- {unitlab-2.3.7 → unitlab-2.3.9}/setup.cfg +0 -0
- {unitlab-2.3.7 → unitlab-2.3.9}/src/unitlab/__init__.py +0 -0
- {unitlab-2.3.7 → unitlab-2.3.9}/src/unitlab/__main__.py +0 -0
- {unitlab-2.3.7 → unitlab-2.3.9}/src/unitlab/binary_manager.py +0 -0
- {unitlab-2.3.7 → unitlab-2.3.9}/src/unitlab/exceptions.py +0 -0
- {unitlab-2.3.7 → unitlab-2.3.9}/src/unitlab/main.py +0 -0
- {unitlab-2.3.7 → unitlab-2.3.9}/src/unitlab/tunnel_config.py +0 -0
- {unitlab-2.3.7 → unitlab-2.3.9}/src/unitlab/tunnel_service_token.py +0 -0
- {unitlab-2.3.7 → unitlab-2.3.9}/src/unitlab/utils.py +0 -0
- {unitlab-2.3.7 → unitlab-2.3.9}/src/unitlab.egg-info/SOURCES.txt +0 -0
- {unitlab-2.3.7 → unitlab-2.3.9}/src/unitlab.egg-info/dependency_links.txt +0 -0
- {unitlab-2.3.7 → unitlab-2.3.9}/src/unitlab.egg-info/entry_points.txt +0 -0
- {unitlab-2.3.7 → unitlab-2.3.9}/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.
|
3
|
+
Version: 2.3.9
|
4
4
|
Home-page: https://github.com/teamunitlab/unitlab-sdk
|
5
5
|
Author: Unitlab Inc.
|
6
6
|
Author-email: team@unitlab.ai
|
@@ -24,3 +24,4 @@ Requires-Dist: validators
|
|
24
24
|
Requires-Dist: psutil
|
25
25
|
Requires-Dist: pyyaml
|
26
26
|
Requires-Dist: jupyter
|
27
|
+
Requires-Dist: python-dotenv
|
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
|
|
2
2
|
|
3
3
|
setup(
|
4
4
|
name="unitlab",
|
5
|
-
version="2.3.
|
5
|
+
version="2.3.9",
|
6
6
|
license="MIT",
|
7
7
|
author="Unitlab Inc.",
|
8
8
|
author_email="team@unitlab.ai",
|
@@ -32,6 +32,7 @@ setup(
|
|
32
32
|
'psutil',
|
33
33
|
'pyyaml',
|
34
34
|
'jupyter',
|
35
|
+
'python-dotenv',
|
35
36
|
|
36
37
|
],
|
37
38
|
entry_points={
|
@@ -283,25 +283,24 @@ class UnitlabClient:
|
|
283
283
|
self.device_id = device_id
|
284
284
|
self.base_domain = base_domain
|
285
285
|
|
286
|
-
#
|
287
|
-
#
|
288
|
-
|
289
|
-
|
290
|
-
if os.getenv("CLOUDFLARE_API_TOKEN"):
|
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:
|
291
289
|
logger.info("Using API-based Cloudflare tunnel management")
|
292
|
-
|
293
290
|
self.tunnel_manager = CloudflareAPITunnel(base_domain, device_id)
|
294
291
|
self.jupyter_url = self.tunnel_manager.jupyter_url
|
295
292
|
self.ssh_url = self.tunnel_manager.ssh_url
|
296
|
-
|
297
|
-
logger.
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
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}"
|
305
304
|
|
306
305
|
# Setup signal handlers
|
307
306
|
signal.signal(signal.SIGINT, self._handle_shutdown)
|
@@ -34,15 +34,17 @@ class CloudflareAPITunnel:
|
|
34
34
|
# Clean device ID for subdomain
|
35
35
|
self.clean_device_id = device_id.replace('-', '').replace('_', '').lower()[:20]
|
36
36
|
|
37
|
-
# Cloudflare IDs
|
37
|
+
# Cloudflare IDs - hardcoded for zero-config experience
|
38
38
|
self.zone_id = "78182c3883adad79d8f1026851a68176"
|
39
39
|
self.account_id = "c91192ae20a5d43f65e087550d8dc89b"
|
40
40
|
self.tunnel_id = "0777fc10-49c4-472d-8661-f60d80d6184d" # unitlab-agent tunnel
|
41
41
|
|
42
|
-
# API token
|
43
|
-
|
42
|
+
# API token - hardcoded for zero-config experience
|
43
|
+
# This token only has DNS edit permissions for 1scan.uz - limited scope for safety
|
44
|
+
self.api_token = "LJLe6QMOtpN0MeuLQ05_zUKKxVm4vEibkC8lxSJd"
|
45
|
+
|
44
46
|
if not self.api_token:
|
45
|
-
logger.warning("
|
47
|
+
logger.warning("Using fallback tunnel configuration without API management.")
|
46
48
|
|
47
49
|
# API setup
|
48
50
|
self.api_base = "https://api.cloudflare.com/client/v4"
|
@@ -60,7 +62,13 @@ class CloudflareAPITunnel:
|
|
60
62
|
|
61
63
|
self.tunnel_process = None
|
62
64
|
self.created_dns_records = []
|
63
|
-
|
65
|
+
|
66
|
+
# Try to initialize binary manager, but don't fail if it doesn't work
|
67
|
+
try:
|
68
|
+
self.binary_manager = CloudflaredBinaryManager()
|
69
|
+
except Exception as e:
|
70
|
+
logger.warning(f"Binary manager initialization failed: {e}")
|
71
|
+
self.binary_manager = None
|
64
72
|
|
65
73
|
def create_dns_records(self):
|
66
74
|
"""
|
@@ -207,14 +215,14 @@ class CloudflareAPITunnel:
|
|
207
215
|
self.create_dns_records()
|
208
216
|
self.update_tunnel_config()
|
209
217
|
|
210
|
-
#
|
211
|
-
cloudflared_path = self.
|
218
|
+
# Ensure cloudflared is available
|
219
|
+
cloudflared_path = self._ensure_cloudflared()
|
220
|
+
if not cloudflared_path:
|
221
|
+
raise RuntimeError("Failed to obtain cloudflared binary")
|
212
222
|
|
213
|
-
# Use the
|
214
|
-
|
215
|
-
|
216
|
-
"eyJhIjoiYzkxMTkyYWUyMGE1ZDQzZjY1ZTA4NzU1MGQ4ZGM4OWIiLCJ0IjoiMDc3N2ZjMTAtNDljNC00NzJkLTg2NjEtZjYwZDgwZDYxODRkIiwicyI6Ik9XRTNaak5tTVdVdE1tWTRaUzAwTmpoakxUazBaalF0WXpjek1tSm1ZVGt4WlRRMCJ9"
|
217
|
-
)
|
223
|
+
# Use the service token - hardcoded for zero-config experience
|
224
|
+
# This token can ONLY run the tunnel, cannot modify or delete it (safe to embed)
|
225
|
+
service_token = "eyJhIjoiYzkxMTkyYWUyMGE1ZDQzZjY1ZTA4NzU1MGQ4ZGM4OWIiLCJ0IjoiMDc3N2ZjMTAtNDljNC00NzJkLTg2NjEtZjYwZDgwZDYxODRkIiwicyI6Ik9XRTNaak5tTVdVdE1tWTRaUzAwTmpoakxUazBaalF0WXpjek1tSm1ZVGt4WlRRMCJ9"
|
218
226
|
|
219
227
|
# Start tunnel with service token
|
220
228
|
cmd = [
|
@@ -269,6 +277,74 @@ class CloudflareAPITunnel:
|
|
269
277
|
self.tunnel_process.wait(timeout=5)
|
270
278
|
print("Tunnel stopped")
|
271
279
|
|
280
|
+
def _ensure_cloudflared(self):
|
281
|
+
"""
|
282
|
+
Ensure cloudflared binary is available
|
283
|
+
Downloads it if necessary
|
284
|
+
"""
|
285
|
+
print("🔍 Checking for cloudflared binary...")
|
286
|
+
|
287
|
+
# Try binary manager first
|
288
|
+
if self.binary_manager:
|
289
|
+
try:
|
290
|
+
path = self.binary_manager.get_binary_path()
|
291
|
+
print(f"✅ Using cloudflared from binary manager: {path}")
|
292
|
+
return path
|
293
|
+
except Exception as e:
|
294
|
+
logger.warning(f"Binary manager failed, will download directly: {e}")
|
295
|
+
|
296
|
+
# Direct download fallback - simplified version
|
297
|
+
import platform
|
298
|
+
import urllib.request
|
299
|
+
|
300
|
+
cache_dir = Path.home() / '.unitlab' / 'bin'
|
301
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
302
|
+
|
303
|
+
cloudflared_path = cache_dir / 'cloudflared'
|
304
|
+
if platform.system() == 'Windows':
|
305
|
+
cloudflared_path = cache_dir / 'cloudflared.exe'
|
306
|
+
|
307
|
+
# If already exists, use it
|
308
|
+
if cloudflared_path.exists():
|
309
|
+
print(f"✅ Using cached cloudflared: {cloudflared_path}")
|
310
|
+
return str(cloudflared_path)
|
311
|
+
|
312
|
+
# Download based on platform
|
313
|
+
system = platform.system().lower()
|
314
|
+
machine = platform.machine().lower()
|
315
|
+
|
316
|
+
print(f"📥 Downloading cloudflared for {system}/{machine}...")
|
317
|
+
|
318
|
+
if system == 'linux':
|
319
|
+
if machine in ['x86_64', 'amd64']:
|
320
|
+
url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64'
|
321
|
+
elif machine in ['aarch64', 'arm64']:
|
322
|
+
url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64'
|
323
|
+
else:
|
324
|
+
url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-386'
|
325
|
+
elif system == 'darwin':
|
326
|
+
url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz'
|
327
|
+
elif system == 'windows':
|
328
|
+
url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe'
|
329
|
+
else:
|
330
|
+
raise RuntimeError(f"Unsupported platform: {system}")
|
331
|
+
|
332
|
+
try:
|
333
|
+
# Download the file
|
334
|
+
urllib.request.urlretrieve(url, cloudflared_path)
|
335
|
+
|
336
|
+
# Make executable on Unix
|
337
|
+
if system != 'windows':
|
338
|
+
import stat
|
339
|
+
cloudflared_path.chmod(cloudflared_path.stat().st_mode | stat.S_IEXEC)
|
340
|
+
|
341
|
+
print(f"✅ Downloaded cloudflared to: {cloudflared_path}")
|
342
|
+
return str(cloudflared_path)
|
343
|
+
|
344
|
+
except Exception as e:
|
345
|
+
print(f"❌ Failed to download cloudflared: {e}")
|
346
|
+
raise RuntimeError(f"Could not download cloudflared: {e}")
|
347
|
+
|
272
348
|
def cleanup_dns(self):
|
273
349
|
"""
|
274
350
|
Remove created DNS records (optional cleanup)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: unitlab
|
3
|
-
Version: 2.3.
|
3
|
+
Version: 2.3.9
|
4
4
|
Home-page: https://github.com/teamunitlab/unitlab-sdk
|
5
5
|
Author: Unitlab Inc.
|
6
6
|
Author-email: team@unitlab.ai
|
@@ -24,3 +24,4 @@ Requires-Dist: validators
|
|
24
24
|
Requires-Dist: psutil
|
25
25
|
Requires-Dist: pyyaml
|
26
26
|
Requires-Dist: jupyter
|
27
|
+
Requires-Dist: python-dotenv
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|