unitlab 2.3.43__py3-none-any.whl → 2.3.44__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.
@@ -38,25 +38,37 @@ class PersistentTunnel:
38
38
 
39
39
  # Clean device ID for subdomain
40
40
  if device_id:
41
- self.device_id = device_id.replace('_', '').replace('.', '').lower()[:30]
41
+ self.device_id = device_id.replace('-', '').replace('_', '').replace('.', '').lower()[:20]
42
42
  else:
43
43
  import uuid
44
44
  self.device_id = str(uuid.uuid4())[:8]
45
45
 
46
- self.tunnel_name = "agent-{}".format(self.device_id)
46
+ # Main tunnel for Jupyter/API
47
+ self.main_tunnel_name = "agent-{}".format(self.device_id)
48
+ self.main_tunnel_id = None
49
+ self.main_tunnel_process = None
50
+ self.main_tunnel_credentials = None
51
+
52
+ # SSH tunnel
53
+ self.ssh_tunnel_name = "ssh-{}".format(self.device_id)
54
+ self.ssh_tunnel_id = None
55
+ self.ssh_tunnel_process = None
56
+ self.ssh_tunnel_credentials = None
57
+
58
+ # URLs
47
59
  self.subdomain = self.device_id
48
60
  self.domain = "unitlab-ai.com"
49
61
  self.jupyter_url = "https://{}.{}".format(self.subdomain, self.domain)
50
62
  self.api_expose_url = "https://{}.{}/api-agent/".format(self.subdomain, self.domain)
51
- self.ssh_subdomain = "s{}".format(self.device_id) # Shorter SSH subdomain to avoid length issues
52
- self.ssh_url = "{}.{}".format(self.ssh_subdomain, self.domain) # SSH on s{deviceid}.unitlab-ai.com
63
+ self.ssh_subdomain = "ssh{}".format(self.device_id)
64
+ self.ssh_url = "{}.{}".format(self.ssh_subdomain, self.domain)
53
65
 
54
- self.tunnel_id = None
55
- self.tunnel_credentials = None
56
66
  self.jupyter_process = None
57
- self.tunnel_process = None
58
-
59
67
 
68
+ @property
69
+ def tunnel_process(self):
70
+ """Compatibility property for backward compatibility"""
71
+ return self.main_tunnel_process
60
72
 
61
73
  def _get_headers(self):
62
74
  """Get API headers for Global API Key"""
@@ -79,9 +91,49 @@ class PersistentTunnel:
79
91
  # return self.create_new_tunnel()
80
92
 
81
93
 
82
- def create_new_tunnel(self):
83
- """Create a new tunnel via Cloudflare API"""
84
- print("🔧 Creating new tunnel: {}...".format(self.tunnel_name))
94
+ def create_main_tunnel(self):
95
+ """Create main tunnel for Jupyter/API"""
96
+ print("🔧 Creating main tunnel: {}...".format(self.main_tunnel_name))
97
+
98
+ # Generate random tunnel secret (32 bytes)
99
+ tunnel_secret = base64.b64encode(secrets.token_bytes(32)).decode()
100
+
101
+ url = "https://api.cloudflare.com/client/v4/accounts/{}/cfd_tunnel".format(self.cf_account_id)
102
+ headers = self._get_headers()
103
+
104
+ data = {
105
+ "name": self.main_tunnel_name,
106
+ "tunnel_secret": tunnel_secret
107
+ }
108
+
109
+ response = requests.post(url, headers=headers, json=data)
110
+
111
+ if response.status_code in [200, 201]:
112
+ result = response.json()["result"]
113
+ self.main_tunnel_id = result["id"]
114
+
115
+ # Create credentials JSON
116
+ self.main_tunnel_credentials = {
117
+ "AccountTag": self.cf_account_id,
118
+ "TunnelSecret": tunnel_secret,
119
+ "TunnelID": self.main_tunnel_id
120
+ }
121
+
122
+ # Save credentials to file
123
+ cred_file = "/tmp/tunnel-{}.json".format(self.main_tunnel_name)
124
+ with open(cred_file, 'w') as f:
125
+ json.dump(self.main_tunnel_credentials, f)
126
+
127
+ print("✅ Main tunnel created: {}".format(self.main_tunnel_id))
128
+ print("✅ Credentials saved to: {}".format(cred_file))
129
+ return cred_file
130
+ else:
131
+ print("❌ Failed to create main tunnel: {}".format(response.text))
132
+ return None
133
+
134
+ def create_ssh_tunnel(self):
135
+ """Create SSH tunnel"""
136
+ print("🔧 Creating SSH tunnel: {}...".format(self.ssh_tunnel_name))
85
137
 
86
138
  # Generate random tunnel secret (32 bytes)
87
139
  tunnel_secret = base64.b64encode(secrets.token_bytes(32)).decode()
@@ -90,7 +142,7 @@ class PersistentTunnel:
90
142
  headers = self._get_headers()
91
143
 
92
144
  data = {
93
- "name": self.tunnel_name,
145
+ "name": self.ssh_tunnel_name,
94
146
  "tunnel_secret": tunnel_secret
95
147
  }
96
148
 
@@ -98,36 +150,34 @@ class PersistentTunnel:
98
150
 
99
151
  if response.status_code in [200, 201]:
100
152
  result = response.json()["result"]
101
- self.tunnel_id = result["id"]
153
+ self.ssh_tunnel_id = result["id"]
102
154
 
103
155
  # Create credentials JSON
104
- self.tunnel_credentials = {
156
+ self.ssh_tunnel_credentials = {
105
157
  "AccountTag": self.cf_account_id,
106
158
  "TunnelSecret": tunnel_secret,
107
- "TunnelID": self.tunnel_id
159
+ "TunnelID": self.ssh_tunnel_id
108
160
  }
109
161
 
110
162
  # Save credentials to file
111
- cred_file = "/tmp/tunnel-{}.json".format(self.tunnel_name)
163
+ cred_file = "/tmp/tunnel-{}.json".format(self.ssh_tunnel_name)
112
164
  with open(cred_file, 'w') as f:
113
- json.dump(self.tunnel_credentials, f)
165
+ json.dump(self.ssh_tunnel_credentials, f)
114
166
 
115
- print("✅ Tunnel created: {}".format(self.tunnel_id))
167
+ print("✅ SSH tunnel created: {}".format(self.ssh_tunnel_id))
116
168
  print("✅ Credentials saved to: {}".format(cred_file))
117
169
  return cred_file
118
170
  else:
119
- print("❌ Failed to create tunnel: {}".format(response.text))
171
+ print("❌ Failed to create SSH tunnel: {}".format(response.text))
120
172
  return None
121
173
 
122
- def create_dns_record(self):
123
- """Create DNS CNAME records for main domain and SSH subdomain"""
124
- if not self.tunnel_id:
174
+ def create_dns_records(self):
175
+ """Create DNS CNAME records for both tunnels"""
176
+ if not self.main_tunnel_id or not self.ssh_tunnel_id:
125
177
  return False
126
178
 
127
179
  print("🔧 Creating DNS records...")
128
180
 
129
- # self.get_zone_id()
130
-
131
181
  url = "https://api.cloudflare.com/client/v4/zones/{}/dns_records".format(self.cf_zone_id)
132
182
  headers = self._get_headers()
133
183
 
@@ -135,7 +185,7 @@ class PersistentTunnel:
135
185
  data = {
136
186
  "type": "CNAME",
137
187
  "name": self.subdomain,
138
- "content": "{}.cfargotunnel.com".format(self.tunnel_id),
188
+ "content": "{}.cfargotunnel.com".format(self.main_tunnel_id),
139
189
  "proxied": True,
140
190
  "ttl": 1
141
191
  }
@@ -150,58 +200,31 @@ class PersistentTunnel:
150
200
  print("❌ Failed to create main DNS: {}".format(response.text[:200]))
151
201
  return False
152
202
 
153
- # First, check if SSH DNS record exists and delete it
154
- # print("🔍 Checking for existing SSH DNS record: {}.{}".format(self.ssh_subdomain, self.domain))
155
- # list_url = "{}?name={}.{}".format(url, self.ssh_subdomain, self.domain)
156
- # list_response = requests.get(list_url, headers=headers)
157
-
158
- # if list_response.status_code == 200:
159
- # records = list_response.json().get("result", [])
160
- # print("Found {} existing DNS records".format(len(records)))
161
- # print('this is new version')
162
- # for record in records:
163
- # if record["name"] == "{}.{}".format(self.ssh_subdomain, self.domain):
164
- # record_id = record["id"]
165
- # print("🗑️ Deleting old SSH DNS record: {}".format(record_id))
166
- # delete_url = "{}/{}".format(url, record_id)
167
- # delete_response = requests.delete(delete_url, headers=headers)
168
- # if delete_response.status_code in [200, 204]:
169
- # print("✅ Deleted old SSH DNS record")
170
- # else:
171
- # print("⚠️ Could not delete old SSH DNS record: {}".format(delete_response.text[:200]))
172
- # else:
173
- # print("⚠️ Could not list DNS records: {}".format(list_response.text[:200]))
174
-
175
- # Wait a moment for DNS deletion to propagate
203
+ # Wait a moment for DNS propagation
176
204
  time.sleep(2)
177
205
 
178
- # Create new SSH subdomain record pointing to new tunnel
206
+ # Create SSH subdomain record pointing to SSH tunnel
179
207
  ssh_data = {
180
208
  "type": "CNAME",
181
209
  "name": self.ssh_subdomain,
182
- "content": "{}.cfargotunnel.com".format(self.tunnel_id),
210
+ "content": "{}.cfargotunnel.com".format(self.ssh_tunnel_id),
183
211
  "proxied": True,
184
212
  "ttl": 1
185
213
  }
186
214
 
187
- print("📝 Creating SSH DNS record: {} -> {}".format(self.ssh_subdomain, self.tunnel_id))
215
+ print("📝 Creating SSH DNS record: {} -> {}".format(self.ssh_subdomain, self.ssh_tunnel_id))
188
216
  ssh_response = requests.post(url, headers=headers, json=ssh_data)
189
217
 
190
218
  if ssh_response.status_code in [200, 201]:
191
219
  print("✅ SSH DNS record created: {}.{}".format(self.ssh_subdomain, self.domain))
192
- print(" Points to: {}.cfargotunnel.com".format(self.tunnel_id))
220
+ return True
221
+ elif "already exists" in ssh_response.text:
222
+ print("⚠️ SSH DNS record already exists")
223
+ return True
193
224
  else:
194
- print("❌ Failed to create SSH DNS: Status {} - {}".format(ssh_response.status_code, ssh_response.text))
195
- # Try to parse error
196
- try:
197
- error_data = ssh_response.json()
198
- if "errors" in error_data:
199
- for error in error_data["errors"]:
200
- print(" Error: {}".format(error.get("message", error)))
201
- except:
202
- pass
203
-
204
- return True
225
+ print("❌ Failed to create SSH DNS: {}".format(ssh_response.text[:200]))
226
+ return False
227
+
205
228
 
206
229
  def create_access_application(self):
207
230
  """Create Cloudflare Access application for SSH with bypass policy"""
@@ -255,34 +278,45 @@ class PersistentTunnel:
255
278
  print("⚠️ Could not create Access application: {}".format(app_response.text[:200]))
256
279
  return False
257
280
 
258
- def create_tunnel_config(self, cred_file):
259
- """Create tunnel config file"""
260
- config_file = "/tmp/tunnel-config-{}.yml".format(self.tunnel_name)
281
+ def create_main_tunnel_config(self, cred_file):
282
+ """Create main tunnel config file for Jupyter/API"""
283
+ config_file = "/tmp/tunnel-config-{}.yml".format(self.main_tunnel_name)
261
284
  with open(config_file, 'w') as f:
262
- f.write("tunnel: {}\n".format(self.tunnel_id))
285
+ f.write("tunnel: {}\n".format(self.main_tunnel_id))
263
286
  f.write("credentials-file: {}\n\n".format(cred_file))
264
287
  f.write("ingress:\n")
265
288
 
266
- # SSH service on dedicated subdomain (s{deviceid}.unitlab-ai.com)
267
- f.write(" - hostname: {}.{}\n".format(self.ssh_subdomain, self.domain))
268
- f.write(" service: ssh://localhost:22\n")
269
-
270
-
271
-
272
-
273
289
  # API (more specific path goes first)
274
290
  f.write(" - hostname: {}.{}\n".format(self.subdomain, self.domain))
275
- f.write(" path: /api-agent/*\n")
291
+ f.write(" path: /api-agent\n")
276
292
  f.write(" service: http://localhost:8001\n")
277
293
 
278
-
279
-
280
294
  # Jupyter (general hostname for HTTP)
281
295
  f.write(" - hostname: {}.{}\n".format(self.subdomain, self.domain))
282
296
  f.write(" service: http://localhost:8888\n")
283
297
 
284
298
  # Catch-all 404 (MUST be last!)
285
299
  f.write(" - service: http_status:404\n")
300
+
301
+ print("✅ Main tunnel config created: {}".format(config_file))
302
+ return config_file
303
+
304
+ def create_ssh_tunnel_config(self, cred_file):
305
+ """Create SSH tunnel config file"""
306
+ config_file = "/tmp/tunnel-config-{}.yml".format(self.ssh_tunnel_name)
307
+ with open(config_file, 'w') as f:
308
+ f.write("tunnel: {}\n".format(self.ssh_tunnel_id))
309
+ f.write("credentials-file: {}\n\n".format(cred_file))
310
+ f.write("ingress:\n")
311
+
312
+ # SSH service
313
+ f.write(" - hostname: {}.{}\n".format(self.ssh_subdomain, self.domain))
314
+ f.write(" service: ssh://localhost:22\n")
315
+
316
+ # Catch-all 404 (MUST be last!)
317
+ f.write(" - service: http_status:404\n")
318
+
319
+ print("✅ SSH tunnel config created: {}".format(config_file))
286
320
  return config_file
287
321
 
288
322
 
@@ -357,10 +391,7 @@ class PersistentTunnel:
357
391
 
358
392
  print("✅ cloudflared downloaded successfully")
359
393
  return local_bin
360
-
361
-
362
-
363
-
394
+
364
395
  def start_jupyter(self):
365
396
  """Start Jupyter"""
366
397
  print("🚀 Starting Jupyter...")
@@ -373,8 +404,6 @@ class PersistentTunnel:
373
404
  "--NotebookApp.token=''",
374
405
  "--NotebookApp.password=''",
375
406
  "--NotebookApp.allow_origin='*'"
376
-
377
-
378
407
  ]
379
408
 
380
409
  self.jupyter_process = subprocess.Popen(
@@ -394,77 +423,126 @@ class PersistentTunnel:
394
423
 
395
424
  api_thread = threading.Thread(target=run_api, daemon=True)
396
425
  api_thread.start()
397
- print('API is started')
426
+ print('API started')
398
427
 
399
- def start_tunnel(self, config_file):
400
- """Start tunnel with config"""
401
- print("🔧 Starting tunnel...")
402
-
403
- cloudflared = self.get_cloudflared_path()
404
-
405
- cmd = [
406
- cloudflared,
407
- "tunnel",
408
- "--config", config_file,
409
- "run"
410
- ]
411
-
412
- self.tunnel_process = subprocess.Popen(
413
- cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
414
- )
415
-
416
- time.sleep(2)
417
-
418
- # Check if process is still running
419
- if self.tunnel_process.poll() is not None:
420
- print("❌ Tunnel process died!")
421
- # Try to read error output
422
- try:
423
- stdout, stderr = self.tunnel_process.communicate(timeout=1)
424
- if stderr:
425
- print(f"Error: {stderr.decode()}")
426
- if stdout:
427
- print(f"Output: {stdout.decode()}")
428
- except:
429
- pass
430
- return False
431
-
432
- print("✅ Tunnel running at {}".format(self.jupyter_url))
428
+ def start_main_tunnel(self, config_file):
429
+ """Start main tunnel for Jupyter/API"""
430
+ print("🔧 Starting main tunnel...")
431
+
432
+ cloudflared = self.get_cloudflared_path()
433
+
434
+ cmd = [
435
+ cloudflared,
436
+ "tunnel",
437
+ "--config", config_file,
438
+ "run"
439
+ ]
440
+
441
+ self.main_tunnel_process = subprocess.Popen(
442
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
443
+ )
444
+
445
+ time.sleep(2)
446
+
447
+ # Check if process is still running
448
+ if self.main_tunnel_process.poll() is not None:
449
+ print("❌ Main tunnel process died!")
450
+ # Try to read error output
451
+ try:
452
+ stdout, stderr = self.main_tunnel_process.communicate(timeout=1)
453
+ if stderr:
454
+ print(f"Error: {stderr.decode()}")
455
+ if stdout:
456
+ print(f"Output: {stdout.decode()}")
457
+ except:
458
+ pass
459
+ return False
460
+
461
+ print("✅ Main tunnel running at {}".format(self.jupyter_url))
433
462
  print("✅ API running at {}".format(self.api_expose_url))
434
- print("✅ SSH available at {}".format(self.ssh_url))
463
+ return True
464
+
465
+ def start_ssh_tunnel(self, config_file):
466
+ """Start SSH tunnel"""
467
+ print("🔧 Starting SSH tunnel...")
468
+
469
+ cloudflared = self.get_cloudflared_path()
470
+
471
+ cmd = [
472
+ cloudflared,
473
+ "tunnel",
474
+ "--config", config_file,
475
+ "run"
476
+ ]
477
+
478
+ self.ssh_tunnel_process = subprocess.Popen(
479
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
480
+ )
481
+
482
+ time.sleep(2)
483
+
484
+ # Check if process is still running
485
+ if self.ssh_tunnel_process.poll() is not None:
486
+ print("❌ SSH tunnel process died!")
487
+ # Try to read error output
488
+ try:
489
+ stdout, stderr = self.ssh_tunnel_process.communicate(timeout=1)
490
+ if stderr:
491
+ print(f"Error: {stderr.decode()}")
492
+ if stdout:
493
+ print(f"Output: {stdout.decode()}")
494
+ except:
495
+ pass
496
+ return False
497
+
498
+ print("✅ SSH tunnel running at {}".format(self.ssh_url))
435
499
  return True
436
500
 
437
501
  def start(self):
438
502
  """Main entry point"""
439
503
  try:
440
504
  print("="*50)
441
- print("🌐 Persistent Tunnel with API")
505
+ print("🌐 Persistent Tunnel with Separate SSH")
442
506
  print("Device: {}".format(self.device_id))
443
- print("Target: {}.{}".format(self.subdomain, self.domain))
507
+ print("Main: {}.{}".format(self.subdomain, self.domain))
508
+ print("SSH: {}.{}".format(self.ssh_subdomain, self.domain))
444
509
  print("="*50)
445
510
 
446
- # API credentials are hardcoded, so we're ready to go
511
+ # 1. Create main tunnel for Jupyter/API
512
+ main_cred_file = self.create_main_tunnel()
513
+ if not main_cred_file:
514
+ print("❌ Failed to create main tunnel")
515
+ return False
447
516
 
448
- # 1. Get existing or create new tunnel via API
449
- cred_file = self.create_new_tunnel()
450
-
517
+ # 2. Create SSH tunnel
518
+ ssh_cred_file = self.create_ssh_tunnel()
519
+ if not ssh_cred_file:
520
+ print("❌ Failed to create SSH tunnel")
521
+ return False
451
522
 
452
- # 2. Create DNS record
453
- self.create_dns_record()
523
+ # 3. Create DNS records for both tunnels
524
+ self.create_dns_records()
454
525
 
455
- # 3. Create Access application for SSH
526
+ # 4. Create Access application for SSH
456
527
  self.create_access_application()
457
528
 
458
- # 4. Create config
459
- config_file = self.create_tunnel_config(cred_file)
529
+ # 5. Create config files
530
+ main_config_file = self.create_main_tunnel_config(main_cred_file)
531
+ ssh_config_file = self.create_ssh_tunnel_config(ssh_cred_file)
460
532
 
461
- # 5. Start services
533
+ # 6. Start services (Jupyter and API)
462
534
  self.start_jupyter()
463
535
  self.start_api()
464
- self.start_tunnel(config_file)
536
+
537
+ # 7. Start both tunnels
538
+ if not self.start_main_tunnel(main_config_file):
539
+ return False
540
+
541
+ if not self.start_ssh_tunnel(ssh_config_file):
542
+ return False
465
543
 
466
544
  print("\n" + "="*50)
467
- print("🎉 SUCCESS! Persistent URLs created:")
545
+ print("🎉 SUCCESS! All services running:")
468
546
  print("📔 Jupyter: {}".format(self.jupyter_url))
469
547
  print("🔧 API: {}".format(self.api_expose_url))
470
548
  print("🔐 SSH: {}".format(self.ssh_url))
@@ -475,7 +553,8 @@ class PersistentTunnel:
475
553
  print("ssh -o ProxyCommand='cloudflared access ssh --hostname {}' {}@{}".format(
476
554
  self.ssh_url, current_user, self.ssh_url))
477
555
  print("")
478
- print("Tunnel ID: {}".format(self.tunnel_id))
556
+ print("Main Tunnel ID: {}".format(self.main_tunnel_id))
557
+ print("SSH Tunnel ID: {}".format(self.ssh_tunnel_id))
479
558
  print("="*50)
480
559
 
481
560
 
@@ -494,14 +573,30 @@ class PersistentTunnel:
494
573
  """Stop everything"""
495
574
  if self.jupyter_process:
496
575
  self.jupyter_process.terminate()
497
- if self.tunnel_process:
498
- self.tunnel_process.terminate()
499
576
  try:
500
- self.tunnel_process.wait(timeout=5)
577
+ self.jupyter_process.wait(timeout=5)
578
+ except subprocess.TimeoutExpired:
579
+ self.jupyter_process.kill()
580
+ self.jupyter_process.wait()
581
+ print("✅ Jupyter stopped")
582
+
583
+ if self.main_tunnel_process:
584
+ self.main_tunnel_process.terminate()
585
+ try:
586
+ self.main_tunnel_process.wait(timeout=5)
587
+ except subprocess.TimeoutExpired:
588
+ self.main_tunnel_process.kill()
589
+ self.main_tunnel_process.wait()
590
+ print("✅ Main tunnel stopped")
591
+
592
+ if self.ssh_tunnel_process:
593
+ self.ssh_tunnel_process.terminate()
594
+ try:
595
+ self.ssh_tunnel_process.wait(timeout=5)
501
596
  except subprocess.TimeoutExpired:
502
- self.tunnel_process.kill()
503
- self.tunnel_process.wait()
504
- print("✅ Tunnel stopped")
597
+ self.ssh_tunnel_process.kill()
598
+ self.ssh_tunnel_process.wait()
599
+ print("✅ SSH tunnel stopped")
505
600
 
506
601
  # # Optionally delete tunnel when stopping
507
602
  # if self.tunnel_id:
@@ -521,6 +616,13 @@ class PersistentTunnel:
521
616
  print("\nPress Ctrl+C to stop...")
522
617
  while True:
523
618
  time.sleep(1)
619
+ # Check if processes are still running
620
+ if self.main_tunnel_process and self.main_tunnel_process.poll() is not None:
621
+ print("❌ Main tunnel process died!")
622
+ break
623
+ if self.ssh_tunnel_process and self.ssh_tunnel_process.poll() is not None:
624
+ print("❌ SSH tunnel process died!")
625
+ break
524
626
  except KeyboardInterrupt:
525
627
  print("\n⏹️ Shutting down...")
526
628
  self.stop()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitlab
3
- Version: 2.3.43
3
+ Version: 2.3.44
4
4
  Home-page: https://github.com/teamunitlab/unitlab-sdk
5
5
  Author: Unitlab Inc.
6
6
  Author-email: team@unitlab.ai
@@ -3,11 +3,11 @@ unitlab/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
3
3
  unitlab/client.py,sha256=wqREtDuYc5ixeloPEGm0hp1sdUtB59sB1bIJjBcO1y0,25983
4
4
  unitlab/exceptions.py,sha256=68Tr6LreEzjQ3Vns8HAaWdtewtkNUJOvPazbf6NSnXU,950
5
5
  unitlab/main.py,sha256=QA1xv1D-sA4Igga2knulJ99g66aaKFyjQCR2pyyo70w,4431
6
- unitlab/persistent_tunnel.py,sha256=pBdmHrgXS-7CgQjq429W2DzflW8OfkeW9qQwu4r6z4g,22640
6
+ unitlab/persistent_tunnel.py,sha256=sHE94TKkCW7wJId7rJ7tf9h1InH3YldhtFfDCcCCIbE,24161
7
7
  unitlab/utils.py,sha256=9gPRu-d6pbhSoVdll1GXe4eoz_uFYOSbYArFDQdlUZs,1922
8
- unitlab-2.3.43.dist-info/licenses/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
9
- unitlab-2.3.43.dist-info/METADATA,sha256=hxTxTVIZHEBfUztzpzggGcK0MF5Vb22GI6-Jv4H-Zkw,1046
10
- unitlab-2.3.43.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- unitlab-2.3.43.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
12
- unitlab-2.3.43.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
13
- unitlab-2.3.43.dist-info/RECORD,,
8
+ unitlab-2.3.44.dist-info/licenses/LICENSE.md,sha256=Gn7RRvByorAcAaM-WbyUpsgi5ED1-bKFFshbWfYYz2Y,1069
9
+ unitlab-2.3.44.dist-info/METADATA,sha256=QcXJjrLxYrQBDpbeHeetT1UGz5noCTgUaxwgojeUjLE,1046
10
+ unitlab-2.3.44.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ unitlab-2.3.44.dist-info/entry_points.txt,sha256=ig-PjKEqSCj3UTdyANgEi4tsAU84DyXdaOJ02NHX4bY,45
12
+ unitlab-2.3.44.dist-info/top_level.txt,sha256=Al4ZlTYE3fTJK2o6YLCDMH5_DjuQkffRBMxgmWbKaqQ,8
13
+ unitlab-2.3.44.dist-info/RECORD,,