accli 2.0.0rc1__tar.gz → 2.0.1__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.
- {accli-2.0.0rc1 → accli-2.0.1}/PKG-INFO +4 -4
- {accli-2.0.0rc1 → accli-2.0.1}/README.md +2 -2
- {accli-2.0.0rc1 → accli-2.0.1}/accli/AcceleratorTaskDispatcher.py +2 -2
- {accli-2.0.0rc1 → accli-2.0.1}/accli/_version.py +1 -1
- {accli-2.0.0rc1 → accli-2.0.1}/accli/cli.py +258 -176
- {accli-2.0.0rc1 → accli-2.0.1}/accli/gui.py +10 -3
- {accli-2.0.0rc1 → accli-2.0.1}/accli/mount_downloader.py +1 -1
- {accli-2.0.0rc1 → accli-2.0.1}/accli/setup-nfs.ps1 +18 -0
- accli-2.0.1/accli/token.py +194 -0
- {accli-2.0.0rc1 → accli-2.0.1}/accli.egg-info/PKG-INFO +4 -4
- {accli-2.0.0rc1 → accli-2.0.1}/accli.egg-info/requires.txt +1 -1
- {accli-2.0.0rc1 → accli-2.0.1}/pyproject.toml +1 -1
- accli-2.0.0rc1/accli/token.py +0 -101
- {accli-2.0.0rc1 → accli-2.0.1}/LICENSE +0 -0
- {accli-2.0.0rc1 → accli-2.0.1}/accli/AcceleratorJobProjectService.py +0 -0
- {accli-2.0.0rc1 → accli-2.0.1}/accli/AcceleratorTerminalCliProjectService.py +0 -0
- {accli-2.0.0rc1 → accli-2.0.1}/accli/CsvRegionalTimeseriesValidator.py +0 -0
- {accli-2.0.0rc1 → accli-2.0.1}/accli/__init__.py +0 -0
- {accli-2.0.0rc1 → accli-2.0.1}/accli/__main__.py +0 -0
- {accli-2.0.0rc1 → accli-2.0.1}/accli/cleanup-nfs.ps1 +0 -0
- {accli-2.0.0rc1 → accli-2.0.1}/accli/common.py +0 -0
- {accli-2.0.0rc1 → accli-2.0.1}/accli.egg-info/SOURCES.txt +0 -0
- {accli-2.0.0rc1 → accli-2.0.1}/accli.egg-info/dependency_links.txt +0 -0
- {accli-2.0.0rc1 → accli-2.0.1}/accli.egg-info/entry_points.txt +0 -0
- {accli-2.0.0rc1 → accli-2.0.1}/accli.egg-info/top_level.txt +0 -0
- {accli-2.0.0rc1 → accli-2.0.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: accli
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: IIASA Accelerator Client
|
|
5
5
|
Author-email: Wrufesh S <wrufesh@gmail.com>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -39,7 +39,7 @@ Requires-Dist: requests>=2.25.1
|
|
|
39
39
|
Requires-Dist: jsonschema>=4.5.1
|
|
40
40
|
Requires-Dist: pydantic>=2.0
|
|
41
41
|
Requires-Dist: GitPython
|
|
42
|
-
Requires-Dist: hf-xet
|
|
42
|
+
Requires-Dist: hf-xet==1.5.2
|
|
43
43
|
Dynamic: license-file
|
|
44
44
|
|
|
45
45
|
# Accelerator Terminal Client and Python API
|
|
@@ -138,8 +138,8 @@ Usage: accli [OPTIONS] COMMAND [ARGS]...
|
|
|
138
138
|
|
|
139
139
|
### Handling self-signed certificates
|
|
140
140
|
|
|
141
|
-
1. Copy and paste certificates from `<control_services_backend>/certs/
|
|
142
|
-
`./certs/dev-backend.crt` and `<accelerator_service>/minio_certs/
|
|
141
|
+
1. Copy and paste certificates from `<control_services_backend>/certs/ca.crt` as
|
|
142
|
+
`./certs/dev-backend.crt` and `<accelerator_service>/minio_certs/ca.crt` as
|
|
143
143
|
`./certs/dev-minio-ca.crt`.
|
|
144
144
|
2. Run
|
|
145
145
|
```
|
|
@@ -94,8 +94,8 @@ Usage: accli [OPTIONS] COMMAND [ARGS]...
|
|
|
94
94
|
|
|
95
95
|
### Handling self-signed certificates
|
|
96
96
|
|
|
97
|
-
1. Copy and paste certificates from `<control_services_backend>/certs/
|
|
98
|
-
`./certs/dev-backend.crt` and `<accelerator_service>/minio_certs/
|
|
97
|
+
1. Copy and paste certificates from `<control_services_backend>/certs/ca.crt` as
|
|
98
|
+
`./certs/dev-backend.crt` and `<accelerator_service>/minio_certs/ca.crt` as
|
|
99
99
|
`./certs/dev-minio-ca.crt`.
|
|
100
100
|
2. Run
|
|
101
101
|
```
|
|
@@ -11,7 +11,7 @@ from pydantic import BaseModel, model_validator
|
|
|
11
11
|
from accli.AcceleratorTerminalCliProjectService import AcceleratorTerminalCliProjectService
|
|
12
12
|
from accli.token import (
|
|
13
13
|
get_token, get_server_url,
|
|
14
|
-
get_project_slug
|
|
14
|
+
get_project_slug, exchange_refresh_token
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
ACCLI_DEBUG = os.environ.get('ACCLI_DEBUG', False)
|
|
@@ -65,9 +65,9 @@ def copy_tree(src, dst):
|
|
|
65
65
|
|
|
66
66
|
@lru_cache(maxsize=None)
|
|
67
67
|
def push_folder_job(directory):
|
|
68
|
-
access_token = get_token()
|
|
69
68
|
server_url = get_server_url()
|
|
70
69
|
project_slug = get_project_slug()
|
|
70
|
+
_, access_token, _ = exchange_refresh_token(project_slug)
|
|
71
71
|
|
|
72
72
|
term_cli_project_service = AcceleratorTerminalCliProjectService(
|
|
73
73
|
user_token=access_token,
|
|
@@ -15,7 +15,7 @@ from typing import List
|
|
|
15
15
|
|
|
16
16
|
from accli.AcceleratorTerminalCliProjectService import AcceleratorTerminalCliProjectService
|
|
17
17
|
from accli.CsvRegionalTimeseriesValidator import CsvRegionalTimeseriesValidator
|
|
18
|
-
from accli.token import save_token_details, get_token, get_server_url, set_project_slug, get_project_slug, get_db_path
|
|
18
|
+
from accli.token import save_token_details, get_token, get_server_url, set_project_slug, get_project_slug, get_db_path, exchange_refresh_token
|
|
19
19
|
from rich import print
|
|
20
20
|
from rich.progress import Progress, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn
|
|
21
21
|
from typing_extensions import Annotated
|
|
@@ -182,84 +182,7 @@ def compute_sha256(filepath: str) -> str:
|
|
|
182
182
|
h.update(chunk)
|
|
183
183
|
return h.hexdigest()
|
|
184
184
|
|
|
185
|
-
import threading
|
|
186
185
|
|
|
187
|
-
_token_lock = threading.Lock()
|
|
188
|
-
_cached_cas_token = None
|
|
189
|
-
_cached_expires_at = 0
|
|
190
|
-
_cached_access_token = None
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def exchange_refresh_token(project_slug: str) -> tuple[str, str, int]:
|
|
194
|
-
"""
|
|
195
|
-
Exchanges the stored refresh token for a short-lived access token
|
|
196
|
-
and a new rotated refresh token. Updates the local TinyDB token cache.
|
|
197
|
-
Returns (cas_token, access_token, expires_at).
|
|
198
|
-
"""
|
|
199
|
-
import time
|
|
200
|
-
from tinydb import TinyDB
|
|
201
|
-
from accli.token import get_db_path, save_token_details
|
|
202
|
-
|
|
203
|
-
global _cached_cas_token, _cached_expires_at, _cached_access_token
|
|
204
|
-
|
|
205
|
-
# 1. Fast-path check: Reuse valid in-memory cache if still fresh (> 5 mins remaining)
|
|
206
|
-
# and matches the requested project slug prefix.
|
|
207
|
-
now = int(time.time())
|
|
208
|
-
if _cached_cas_token and _cached_access_token and (_cached_expires_at - now > 300):
|
|
209
|
-
expected_prefix = f"xet_session_prj_{project_slug}_"
|
|
210
|
-
if _cached_cas_token.startswith(expected_prefix):
|
|
211
|
-
return _cached_cas_token, _cached_access_token, _cached_expires_at
|
|
212
|
-
|
|
213
|
-
# 2. Block/lock to serialize requests and avoid rotation race conditions (RTR invalidation)
|
|
214
|
-
with _token_lock:
|
|
215
|
-
# Re-check cache inside the lock (double-checked locking pattern)
|
|
216
|
-
now = int(time.time())
|
|
217
|
-
if _cached_cas_token and _cached_access_token and (_cached_expires_at - now > 300):
|
|
218
|
-
expected_prefix = f"xet_session_prj_{project_slug}_"
|
|
219
|
-
if _cached_cas_token.startswith(expected_prefix):
|
|
220
|
-
return _cached_cas_token, _cached_access_token, _cached_expires_at
|
|
221
|
-
|
|
222
|
-
db_path = get_db_path()
|
|
223
|
-
db = TinyDB(db_path)
|
|
224
|
-
item = next(iter(db), {})
|
|
225
|
-
refresh_token = item.get('token')
|
|
226
|
-
server_url = item.get('server_url', "https://accelerator.iiasa.ac.at")
|
|
227
|
-
webcli_url = item.get('webcli_url', "https://accelerator.iiasa.ac.at")
|
|
228
|
-
|
|
229
|
-
if not refresh_token:
|
|
230
|
-
print("[bold red]ERROR: No token found. Please run 'accli login' first.[/bold red]")
|
|
231
|
-
raise typer.Exit(1)
|
|
232
|
-
|
|
233
|
-
refresh_endpoint = f"{server_url.rstrip('/')}/api/v1/oauth/device/access-token/"
|
|
234
|
-
|
|
235
|
-
try:
|
|
236
|
-
response = requests.post(
|
|
237
|
-
refresh_endpoint,
|
|
238
|
-
json={"refresh_token": refresh_token},
|
|
239
|
-
verify=(not ACCLI_DEBUG)
|
|
240
|
-
)
|
|
241
|
-
response.raise_for_status()
|
|
242
|
-
data = response.json()
|
|
243
|
-
|
|
244
|
-
access_token = data["access_token"]
|
|
245
|
-
new_refresh_token = data["refresh_token"]
|
|
246
|
-
|
|
247
|
-
# Save rotated refresh token back to local TinyDB
|
|
248
|
-
save_token_details(new_refresh_token, server_url, webcli_url)
|
|
249
|
-
|
|
250
|
-
cas_token = f"xet_session_prj_{project_slug}_{access_token}"
|
|
251
|
-
# Expire slightly before the 1-hour limit (e.g. 50 minutes)
|
|
252
|
-
expires_at = int(time.time()) + 3000
|
|
253
|
-
|
|
254
|
-
# Populate in-memory cache
|
|
255
|
-
_cached_cas_token = cas_token
|
|
256
|
-
_cached_access_token = access_token
|
|
257
|
-
_cached_expires_at = expires_at
|
|
258
|
-
|
|
259
|
-
return cas_token, access_token, expires_at
|
|
260
|
-
except Exception as e:
|
|
261
|
-
print(f"[bold red]ERROR: Failed to authenticate/exchange refresh token: {e}[/bold red]")
|
|
262
|
-
raise typer.Exit(1)
|
|
263
186
|
|
|
264
187
|
|
|
265
188
|
@app.command()
|
|
@@ -300,12 +223,6 @@ def copy(
|
|
|
300
223
|
verify_cert=(not ACCLI_DEBUG),
|
|
301
224
|
)
|
|
302
225
|
|
|
303
|
-
def make_token_refresher(slug: str):
|
|
304
|
-
def refresher() -> tuple[str, int]:
|
|
305
|
-
token_str, _, exp_time = exchange_refresh_token(slug)
|
|
306
|
-
return token_str, exp_time
|
|
307
|
-
return refresher
|
|
308
|
-
|
|
309
226
|
if is_src_remote:
|
|
310
227
|
# Download flow: acc://[project_slug]/[remote_prefix] -> local destination
|
|
311
228
|
source_parsed = source[len("acc://"):]
|
|
@@ -336,7 +253,7 @@ def copy(
|
|
|
336
253
|
else:
|
|
337
254
|
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
338
255
|
|
|
339
|
-
|
|
256
|
+
files_to_download = []
|
|
340
257
|
for filename in filenames:
|
|
341
258
|
# Filename returned usually starts with project_slug/
|
|
342
259
|
if filename.startswith(f"{project_slug}/"):
|
|
@@ -378,32 +295,102 @@ def copy(
|
|
|
378
295
|
|
|
379
296
|
local_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
380
297
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
destination_path=str(local_dest.resolve()),
|
|
384
|
-
hash=merkle_hash,
|
|
385
|
-
file_size=file_size
|
|
386
|
-
)
|
|
298
|
+
files_to_download.append(
|
|
299
|
+
(str(local_dest.resolve()), merkle_hash, file_size)
|
|
387
300
|
)
|
|
388
301
|
|
|
389
|
-
if not
|
|
302
|
+
if not files_to_download:
|
|
390
303
|
print("[bold red]ERROR: No valid files identified for download.[/bold red]")
|
|
391
304
|
raise typer.Exit(1)
|
|
392
305
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
306
|
+
MAX_FILES_PER_BATCH = 100
|
|
307
|
+
MAX_BYTES_PER_BATCH = 10 * 1024 * 1024 * 1024 # 10 GB
|
|
308
|
+
|
|
309
|
+
batches = []
|
|
310
|
+
current_batch = []
|
|
311
|
+
current_batch_size = 0
|
|
312
|
+
|
|
313
|
+
for item in files_to_download:
|
|
314
|
+
size = item[2]
|
|
315
|
+
if size >= MAX_BYTES_PER_BATCH and not current_batch:
|
|
316
|
+
batches.append([item])
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
if len(current_batch) >= MAX_FILES_PER_BATCH or current_batch_size + size > MAX_BYTES_PER_BATCH:
|
|
320
|
+
batches.append(current_batch)
|
|
321
|
+
current_batch = []
|
|
322
|
+
current_batch_size = 0
|
|
323
|
+
|
|
324
|
+
current_batch.append(item)
|
|
325
|
+
current_batch_size += size
|
|
326
|
+
|
|
327
|
+
if current_batch:
|
|
328
|
+
batches.append(current_batch)
|
|
329
|
+
|
|
330
|
+
print(f"[bold cyan]Downloading {len(files_to_download)} files in {len(batches)} batches using hf-xet...[/bold cyan]")
|
|
331
|
+
|
|
332
|
+
refresh_url = f"{server_url.rstrip('/')}/api/v1/oauth/device/cas-token/"
|
|
333
|
+
refresh_headers = {"Authorization": f"Bearer {cas_token}"}
|
|
334
|
+
|
|
335
|
+
import sys
|
|
336
|
+
columns = [
|
|
337
|
+
TextColumn("[progress.description]{task.description}"),
|
|
338
|
+
TaskProgressColumn(),
|
|
339
|
+
TimeElapsedColumn(),
|
|
340
|
+
]
|
|
341
|
+
if sys.stdout.isatty():
|
|
342
|
+
columns.insert(1, BarColumn())
|
|
343
|
+
|
|
344
|
+
with Progress(*columns) as progress:
|
|
345
|
+
total_size = sum(item[2] for item in files_to_download)
|
|
346
|
+
overall_task = progress.add_task("[green]Total Download Progress...", total=total_size)
|
|
347
|
+
|
|
348
|
+
completed_in_previous_batches = 0
|
|
349
|
+
|
|
350
|
+
session = hf_xet.XetSession()
|
|
351
|
+
for batch_index, batch_items in enumerate(batches, 1):
|
|
352
|
+
progress.print(f"[cyan]Processing batch {batch_index}/{len(batches)}...[/cyan]")
|
|
353
|
+
|
|
354
|
+
# capture current completed_in_previous_batches in a local scope to avoid late binding issues
|
|
355
|
+
file_tasks = {}
|
|
356
|
+
|
|
357
|
+
def make_progress_handler(base_completed):
|
|
358
|
+
def on_progress(group_report, item_reports):
|
|
359
|
+
progress.update(overall_task, completed=base_completed + group_report.total_bytes_completed)
|
|
360
|
+
for item_id, item in item_reports.items():
|
|
361
|
+
if item_id not in file_tasks:
|
|
362
|
+
file_tasks[item_id] = progress.add_task(f"[blue]{item.item_name}", total=item.total_bytes)
|
|
363
|
+
|
|
364
|
+
progress.update(file_tasks[item_id], completed=item.bytes_completed)
|
|
365
|
+
|
|
366
|
+
# Hide completed tasks to keep the UI clean when downloading thousands of files
|
|
367
|
+
if item.bytes_completed >= item.total_bytes:
|
|
368
|
+
progress.update(file_tasks[item_id], visible=False)
|
|
369
|
+
return on_progress
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
with session.new_file_download_group(
|
|
373
|
+
endpoint=cas_endpoint,
|
|
374
|
+
token=cas_token,
|
|
375
|
+
token_expiry_unix_secs=expires_at,
|
|
376
|
+
token_refresh_url=refresh_url,
|
|
377
|
+
token_refresh_headers=refresh_headers,
|
|
378
|
+
progress_callback=make_progress_handler(completed_in_previous_batches),
|
|
379
|
+
progress_interval_ms=500,
|
|
380
|
+
) as group:
|
|
381
|
+
for dest_path, hash_val, size_val in batch_items:
|
|
382
|
+
info = hf_xet.XetFileInfo(hash_val, size_val)
|
|
383
|
+
group.start_download_file(info, dest_path)
|
|
384
|
+
|
|
385
|
+
batch_size = sum(item[2] for item in batch_items)
|
|
386
|
+
completed_in_previous_batches += batch_size
|
|
387
|
+
progress.update(overall_task, completed=completed_in_previous_batches)
|
|
388
|
+
progress.print(f"[bold green][OK] Batch {batch_index} completed successfully![/bold green]")
|
|
389
|
+
except Exception as e:
|
|
390
|
+
progress.print(f"[bold red]ERROR: Download failed for batch {batch_index}: {e}[/bold red]")
|
|
391
|
+
raise typer.Exit(1)
|
|
392
|
+
|
|
393
|
+
print("[bold green][OK] Download completed successfully![/bold green]")
|
|
407
394
|
|
|
408
395
|
else:
|
|
409
396
|
# Upload flow: local source -> acc://[project_slug]/[remote_path]
|
|
@@ -456,65 +443,141 @@ def copy(
|
|
|
456
443
|
print("[bold yellow]No files to upload.[/bold yellow]")
|
|
457
444
|
raise typer.Exit(0)
|
|
458
445
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
446
|
+
MAX_FILES_PER_BATCH = 100
|
|
447
|
+
MAX_BYTES_PER_BATCH = 10 * 1024 * 1024 * 1024 # 10 GB
|
|
448
|
+
|
|
449
|
+
batches = []
|
|
450
|
+
current_batch_paths = []
|
|
451
|
+
current_batch_remote = []
|
|
452
|
+
current_batch_size = 0
|
|
453
|
+
|
|
454
|
+
for local_p, remote_n in zip(local_paths, remote_filenames):
|
|
455
|
+
size = local_p.stat().st_size
|
|
456
|
+
if size >= MAX_BYTES_PER_BATCH and not current_batch_paths:
|
|
457
|
+
batches.append(([local_p], [remote_n]))
|
|
458
|
+
continue
|
|
459
|
+
|
|
460
|
+
if len(current_batch_paths) >= MAX_FILES_PER_BATCH or current_batch_size + size > MAX_BYTES_PER_BATCH:
|
|
461
|
+
batches.append((current_batch_paths, current_batch_remote))
|
|
462
|
+
current_batch_paths = []
|
|
463
|
+
current_batch_remote = []
|
|
464
|
+
current_batch_size = 0
|
|
465
|
+
|
|
466
|
+
current_batch_paths.append(local_p)
|
|
467
|
+
current_batch_remote.append(remote_n)
|
|
468
|
+
current_batch_size += size
|
|
469
|
+
|
|
470
|
+
if current_batch_paths:
|
|
471
|
+
batches.append((current_batch_paths, current_batch_remote))
|
|
475
472
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
"
|
|
493
|
-
|
|
473
|
+
print(f"[bold cyan]Uploading {len(local_paths)} files in {len(batches)} batches using hf-xet...[/bold cyan]")
|
|
474
|
+
|
|
475
|
+
refresh_url = f"{server_url.rstrip('/')}/api/v1/oauth/device/cas-token/"
|
|
476
|
+
refresh_headers = {"Authorization": f"Bearer {cas_token}"}
|
|
477
|
+
|
|
478
|
+
import sys
|
|
479
|
+
columns = [
|
|
480
|
+
TextColumn("[progress.description]{task.description}"),
|
|
481
|
+
TaskProgressColumn(),
|
|
482
|
+
TimeElapsedColumn(),
|
|
483
|
+
]
|
|
484
|
+
if sys.stdout.isatty():
|
|
485
|
+
columns.insert(1, BarColumn())
|
|
486
|
+
|
|
487
|
+
with Progress(*columns) as progress:
|
|
488
|
+
total_size = sum(p.stat().st_size for p in local_paths)
|
|
489
|
+
overall_task = progress.add_task("[green]Total Upload Progress...", total=total_size)
|
|
490
|
+
|
|
491
|
+
completed_in_previous_batches = 0
|
|
492
|
+
|
|
493
|
+
session = hf_xet.XetSession()
|
|
494
|
+
for batch_index, (b_paths, b_remotes) in enumerate(batches, 1):
|
|
495
|
+
progress.print(f"[cyan]Processing batch {batch_index}/{len(batches)}...[/cyan]")
|
|
496
|
+
|
|
497
|
+
file_tasks = {}
|
|
498
|
+
|
|
499
|
+
def make_progress_handler(base_completed):
|
|
500
|
+
def on_progress(group_report, item_reports):
|
|
501
|
+
progress.update(overall_task, completed=base_completed + group_report.total_bytes_completed)
|
|
502
|
+
for item_id, item in item_reports.items():
|
|
503
|
+
if item_id not in file_tasks:
|
|
504
|
+
file_tasks[item_id] = progress.add_task(f"[blue]{item.item_name}", total=item.total_bytes)
|
|
505
|
+
|
|
506
|
+
progress.update(file_tasks[item_id], completed=item.bytes_completed)
|
|
507
|
+
|
|
508
|
+
# Hide completed tasks to keep the UI clean
|
|
509
|
+
if item.bytes_completed >= item.total_bytes:
|
|
510
|
+
progress.update(file_tasks[item_id], visible=False)
|
|
511
|
+
return on_progress
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
with session.new_upload_commit(
|
|
515
|
+
endpoint=cas_endpoint,
|
|
516
|
+
token=cas_token,
|
|
517
|
+
token_expiry_unix_secs=expires_at,
|
|
518
|
+
token_refresh_url=refresh_url,
|
|
519
|
+
token_refresh_headers=refresh_headers,
|
|
520
|
+
progress_callback=make_progress_handler(completed_in_previous_batches),
|
|
521
|
+
progress_interval_ms=500,
|
|
522
|
+
) as commit:
|
|
523
|
+
handles = []
|
|
524
|
+
for p in b_paths:
|
|
525
|
+
sha = compute_sha256(str(p))
|
|
526
|
+
h = commit.start_upload_file(str(p), sha256=sha)
|
|
527
|
+
handles.append((p, sha, h))
|
|
528
|
+
|
|
529
|
+
upload_results = [(p, sha, h.result()) for p, sha, h in handles]
|
|
530
|
+
except Exception as e:
|
|
531
|
+
progress.print(f"[bold red]ERROR: Upload failed for batch {batch_index}: {e}[/bold red]")
|
|
532
|
+
raise typer.Exit(1)
|
|
494
533
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
534
|
+
progress.print("[cyan]Computing SHA-256 hashes and registering metadata for batch...[/cyan]")
|
|
535
|
+
registration_items = []
|
|
536
|
+
for local_path, remote_filename, (p, sha256_hash, upload_info) in zip(b_paths, b_remotes, upload_results):
|
|
537
|
+
registration_items.append({
|
|
538
|
+
"filename": f"{project_slug}/{remote_filename}",
|
|
539
|
+
"merkle_hash": upload_info.xet_info.hash,
|
|
540
|
+
"sha256": sha256_hash,
|
|
541
|
+
"file_size": upload_info.xet_info.file_size,
|
|
542
|
+
"content_type": "application/octet-stream"
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
headers = {
|
|
546
|
+
"Content-Type": "application/json",
|
|
547
|
+
"X-Project-Slug": project_slug,
|
|
548
|
+
"Authorization": f"Bearer {cas_token}"
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
try:
|
|
552
|
+
response = requests.post(
|
|
553
|
+
register_url,
|
|
554
|
+
json={"items": registration_items},
|
|
555
|
+
headers=headers,
|
|
556
|
+
verify=(not ACCLI_DEBUG)
|
|
557
|
+
)
|
|
558
|
+
response.raise_for_status()
|
|
559
|
+
|
|
560
|
+
batch_size = sum(p.stat().st_size for p in b_paths)
|
|
561
|
+
completed_in_previous_batches += batch_size
|
|
562
|
+
progress.update(overall_task, completed=completed_in_previous_batches)
|
|
563
|
+
|
|
564
|
+
progress.print(f"[bold green][OK] Batch {batch_index} registered successfully![/bold green]")
|
|
565
|
+
except requests.exceptions.HTTPError as e:
|
|
566
|
+
detail = None
|
|
567
|
+
try:
|
|
568
|
+
detail = e.response.json().get("detail")
|
|
569
|
+
except Exception:
|
|
570
|
+
pass
|
|
571
|
+
if detail:
|
|
572
|
+
progress.print(f"[bold red]ERROR: Bulk metadata registration failed for batch {batch_index}: {detail}[/bold red]")
|
|
573
|
+
else:
|
|
574
|
+
progress.print(f"[bold red]ERROR: Bulk metadata registration failed for batch {batch_index}: {e}[/bold red]")
|
|
575
|
+
raise typer.Exit(1)
|
|
576
|
+
except Exception as e:
|
|
577
|
+
progress.print(f"[bold red]ERROR: Bulk metadata registration failed for batch {batch_index}: {e}[/bold red]")
|
|
578
|
+
raise typer.Exit(1)
|
|
579
|
+
|
|
580
|
+
print("[bold green][OK] Upload completed successfully![/bold green]")
|
|
518
581
|
|
|
519
582
|
|
|
520
583
|
# --- Mount commands group ---
|
|
@@ -1314,21 +1377,40 @@ def mount_status(
|
|
|
1314
1377
|
args = [str(hf_mount_bin), "status"]
|
|
1315
1378
|
|
|
1316
1379
|
import subprocess
|
|
1380
|
+
active_mounts = []
|
|
1381
|
+
|
|
1382
|
+
# 1. Query hf-mount status directly
|
|
1317
1383
|
try:
|
|
1318
1384
|
result = subprocess.run(args, capture_output=True, text=True)
|
|
1319
1385
|
if result.returncode == 0:
|
|
1320
1386
|
stdout = result.stdout.strip()
|
|
1321
|
-
if
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1387
|
+
if stdout:
|
|
1388
|
+
for line in stdout.splitlines():
|
|
1389
|
+
if line.strip():
|
|
1390
|
+
active_mounts.append(line.strip())
|
|
1391
|
+
except Exception:
|
|
1392
|
+
pass
|
|
1393
|
+
|
|
1394
|
+
# 2. Fallback: parse system mount table on Linux to find localhost NFS or FUSE hf-mounts
|
|
1395
|
+
if not active_mounts and platform.system() == "Linux":
|
|
1396
|
+
try:
|
|
1397
|
+
if os.path.exists("/proc/mounts"):
|
|
1398
|
+
with open("/proc/mounts", "r") as f:
|
|
1399
|
+
for line in f:
|
|
1400
|
+
parts = line.strip().split()
|
|
1401
|
+
if len(parts) >= 3:
|
|
1402
|
+
dev, mnt_point, fstype = parts[0], parts[1], parts[2]
|
|
1403
|
+
is_nfs_localhost = (fstype == "nfs" and dev.startswith("127.0.0.1:"))
|
|
1404
|
+
is_fuse_hf = ("hf-mount" in dev or "hf-mount" in fstype)
|
|
1405
|
+
if is_nfs_localhost or is_fuse_hf:
|
|
1406
|
+
active_mounts.append(f"{mnt_point} ({fstype} via {dev})")
|
|
1407
|
+
except Exception:
|
|
1408
|
+
pass
|
|
1409
|
+
|
|
1410
|
+
if not active_mounts:
|
|
1411
|
+
print("[cyan]No running mounts found.[/cyan]")
|
|
1412
|
+
else:
|
|
1413
|
+
print("[bold cyan]Active Mounts:[/bold cyan]")
|
|
1414
|
+
for m in active_mounts:
|
|
1415
|
+
print(m)
|
|
1334
1416
|
|
|
@@ -113,7 +113,7 @@ class AccliGuiApp(tk.Tk):
|
|
|
113
113
|
self.build_auth_tab()
|
|
114
114
|
self.build_logs_tab()
|
|
115
115
|
|
|
116
|
-
def run_cli_async(self, args, on_done=None, log_to_viewer=True):
|
|
116
|
+
def run_cli_async(self, args, on_done=None, log_to_viewer=True, elevate=False):
|
|
117
117
|
"""Runs the CLI binary in a background thread to prevent UI freezing."""
|
|
118
118
|
def worker():
|
|
119
119
|
# Determine how to invoke accli
|
|
@@ -124,6 +124,13 @@ class AccliGuiApp(tk.Tk):
|
|
|
124
124
|
# Script mode
|
|
125
125
|
cmd = [sys.executable, "-m", "accli.cli"] + args
|
|
126
126
|
|
|
127
|
+
if elevate and platform.system() != "Windows":
|
|
128
|
+
import shutil
|
|
129
|
+
if shutil.which("pkexec"):
|
|
130
|
+
cmd = ["pkexec"] + cmd
|
|
131
|
+
else:
|
|
132
|
+
cmd = ["sudo"] + cmd
|
|
133
|
+
|
|
127
134
|
try:
|
|
128
135
|
if log_to_viewer:
|
|
129
136
|
self.append_log(f"Executing: {' '.join(cmd)}\n")
|
|
@@ -491,7 +498,7 @@ class AccliGuiApp(tk.Tk):
|
|
|
491
498
|
self.status_bar_val.set("Mount Failed")
|
|
492
499
|
messagebox.showerror("Error", f"Failed to mount project '{slug}'. See logs for details.")
|
|
493
500
|
|
|
494
|
-
self.run_cli_async(args, on_done=on_done)
|
|
501
|
+
self.run_cli_async(args, on_done=on_done, elevate=True)
|
|
495
502
|
|
|
496
503
|
def action_mount_stop(self):
|
|
497
504
|
selected = self.mounts_list.get(tk.ACTIVE)
|
|
@@ -521,7 +528,7 @@ class AccliGuiApp(tk.Tk):
|
|
|
521
528
|
self.status_bar_val.set("Stop Failed")
|
|
522
529
|
messagebox.showerror("Error", f"Failed to stop mount at {mount_point}.")
|
|
523
530
|
|
|
524
|
-
self.run_cli_async(args, on_done=on_done)
|
|
531
|
+
self.run_cli_async(args, on_done=on_done, elevate=True)
|
|
525
532
|
|
|
526
533
|
# ------------------ COPY TAB ------------------
|
|
527
534
|
def build_copy_tab(self):
|
|
@@ -9,7 +9,7 @@ import typer
|
|
|
9
9
|
from rich import print
|
|
10
10
|
from rich.progress import Progress, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn
|
|
11
11
|
|
|
12
|
-
DEFAULT_VERSION = "v0.6.5-acc-pr140-
|
|
12
|
+
DEFAULT_VERSION = "v0.6.5-acc-pr140-p5"
|
|
13
13
|
BINARY_DIR = Path.home() / ".accli" / "bin"
|
|
14
14
|
|
|
15
15
|
# Regex to safely validate version strings to prevent URL manipulation or directory traversal
|
|
@@ -35,6 +35,17 @@ if (-not $NfsAlreadyEnabled) {
|
|
|
35
35
|
Write-Host "[OK] Client for NFS features already enabled." -ForegroundColor Green
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
# 1.5 Configure Client for NFS (NfsClnt) service startup type to Manual
|
|
39
|
+
# This prevents NFS Client from starting automatically on Windows boot, avoiding startup delays/hangs.
|
|
40
|
+
# The service will be started on-demand when accli mounts a drive.
|
|
41
|
+
Write-Host "Configuring Client for NFS (NfsClnt) service startup type to Manual..." -ForegroundColor Cyan
|
|
42
|
+
if (Get-Service -Name "NfsClnt" -ErrorAction SilentlyContinue) {
|
|
43
|
+
Set-Service -Name "NfsClnt" -StartupType Manual -ErrorAction SilentlyContinue
|
|
44
|
+
Write-Host "[OK] NfsClnt service startup type configured to Manual." -ForegroundColor Green
|
|
45
|
+
} else {
|
|
46
|
+
Write-Warning "Client for NFS service (NfsClnt) not found. Could not configure startup type."
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
# 2. Add Registry value for Linked Connections
|
|
39
50
|
# Allows mapped network drives to be shared between elevated and standard sessions under the same user log-in.
|
|
40
51
|
if (-not $RegAlreadyEnabled) {
|
|
@@ -108,6 +119,13 @@ if ($cfg.skip_auto_mount) {
|
|
|
108
119
|
$env:HF_MOUNT_SKIP_AUTO_MOUNT = "1"
|
|
109
120
|
}
|
|
110
121
|
|
|
122
|
+
# Ensure NfsClnt service is started before attempting mount
|
|
123
|
+
try {
|
|
124
|
+
if ((Get-Service -Name "NfsClnt" -ErrorAction SilentlyContinue).Status -ne 'Running') {
|
|
125
|
+
Start-Service -Name "NfsClnt" -ErrorAction SilentlyContinue
|
|
126
|
+
}
|
|
127
|
+
} catch {}
|
|
128
|
+
|
|
111
129
|
# Construct arguments
|
|
112
130
|
$mountArgs = @(
|
|
113
131
|
"--token-file", $cfg.db_path,
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
import requests
|
|
5
|
+
import typer
|
|
6
|
+
from tinydb import TinyDB
|
|
7
|
+
|
|
8
|
+
ACCLI_DEBUG = os.environ.get('ACCLI_DEBUG', False)
|
|
9
|
+
|
|
10
|
+
_token_lock = threading.Lock()
|
|
11
|
+
_cached_cas_token = None
|
|
12
|
+
_cached_expires_at = 0
|
|
13
|
+
_cached_access_token = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_db_path():
|
|
17
|
+
sudo_user = os.environ.get("SUDO_USER")
|
|
18
|
+
pkexec_uid = os.environ.get("PKEXEC_UID")
|
|
19
|
+
|
|
20
|
+
if (sudo_user or pkexec_uid) and os.name != "nt":
|
|
21
|
+
try:
|
|
22
|
+
import pwd
|
|
23
|
+
if pkexec_uid:
|
|
24
|
+
home = pwd.getpwuid(int(pkexec_uid)).pw_dir
|
|
25
|
+
else:
|
|
26
|
+
home = pwd.getpwnam(sudo_user).pw_dir
|
|
27
|
+
except Exception:
|
|
28
|
+
home = os.path.expanduser("~")
|
|
29
|
+
else:
|
|
30
|
+
home = os.path.expanduser("~")
|
|
31
|
+
|
|
32
|
+
token_directory = f"{home}/.accli"
|
|
33
|
+
|
|
34
|
+
if not os.path.exists(token_directory):
|
|
35
|
+
os.makedirs(token_directory)
|
|
36
|
+
|
|
37
|
+
return f"{token_directory}/data.json"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def save_token_details(token, server_url, webcli_url):
|
|
41
|
+
db_path = get_db_path()
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
os.remove(db_path)
|
|
45
|
+
except OSError:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
db = TinyDB(db_path)
|
|
49
|
+
db.insert({
|
|
50
|
+
'token': token,
|
|
51
|
+
'server_url': server_url,
|
|
52
|
+
'webcli_url': webcli_url
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_token():
|
|
57
|
+
db_path = get_db_path()
|
|
58
|
+
|
|
59
|
+
db = TinyDB(db_path)
|
|
60
|
+
|
|
61
|
+
for item in db:
|
|
62
|
+
token = item.get('token')
|
|
63
|
+
if token:
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
if not token:
|
|
67
|
+
print("Token does not exists. Please login.")
|
|
68
|
+
return token
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def exchange_refresh_token(project_slug: str) -> tuple[str, str, int]:
|
|
72
|
+
"""
|
|
73
|
+
Exchanges the stored refresh token for a short-lived access token
|
|
74
|
+
and a new rotated refresh token. Updates the local TinyDB token cache.
|
|
75
|
+
Returns (cas_token, access_token, expires_at).
|
|
76
|
+
"""
|
|
77
|
+
global _cached_cas_token, _cached_expires_at, _cached_access_token
|
|
78
|
+
|
|
79
|
+
# 1. Fast-path check: Reuse valid in-memory cache if still fresh (> 5 mins remaining)
|
|
80
|
+
# and matches the requested project slug prefix.
|
|
81
|
+
now = int(time.time())
|
|
82
|
+
if _cached_cas_token and _cached_access_token and (_cached_expires_at - now > 300):
|
|
83
|
+
expected_prefix = f"xet_session_prj_{project_slug}_"
|
|
84
|
+
if _cached_cas_token.startswith(expected_prefix):
|
|
85
|
+
return _cached_cas_token, _cached_access_token, _cached_expires_at
|
|
86
|
+
|
|
87
|
+
# 2. Block/lock to serialize requests and avoid rotation race conditions (RTR invalidation)
|
|
88
|
+
with _token_lock:
|
|
89
|
+
# Re-check cache inside the lock (double-checked locking pattern)
|
|
90
|
+
now = int(time.time())
|
|
91
|
+
if _cached_cas_token and _cached_access_token and (_cached_expires_at - now > 300):
|
|
92
|
+
expected_prefix = f"xet_session_prj_{project_slug}_"
|
|
93
|
+
if _cached_cas_token.startswith(expected_prefix):
|
|
94
|
+
return _cached_cas_token, _cached_access_token, _cached_expires_at
|
|
95
|
+
|
|
96
|
+
db_path = get_db_path()
|
|
97
|
+
db = TinyDB(db_path)
|
|
98
|
+
item = next(iter(db), {})
|
|
99
|
+
refresh_token = item.get('token')
|
|
100
|
+
server_url = item.get('server_url', "https://accelerator.iiasa.ac.at")
|
|
101
|
+
webcli_url = item.get('webcli_url', "https://accelerator.iiasa.ac.at")
|
|
102
|
+
|
|
103
|
+
if not refresh_token:
|
|
104
|
+
print("[bold red]ERROR: No token found. Please run 'accli login' first.[/bold red]")
|
|
105
|
+
raise typer.Exit(1)
|
|
106
|
+
|
|
107
|
+
refresh_endpoint = f"{server_url.rstrip('/')}/api/v1/oauth/device/access-token/"
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
response = requests.post(
|
|
111
|
+
refresh_endpoint,
|
|
112
|
+
json={"refresh_token": refresh_token},
|
|
113
|
+
verify=(not ACCLI_DEBUG)
|
|
114
|
+
)
|
|
115
|
+
response.raise_for_status()
|
|
116
|
+
data = response.json()
|
|
117
|
+
|
|
118
|
+
access_token = data["access_token"]
|
|
119
|
+
new_refresh_token = data["refresh_token"]
|
|
120
|
+
|
|
121
|
+
# Save rotated refresh token back to local TinyDB
|
|
122
|
+
save_token_details(new_refresh_token, server_url, webcli_url)
|
|
123
|
+
|
|
124
|
+
cas_token = f"xet_session_prj_{project_slug}_{access_token}"
|
|
125
|
+
# Expire slightly before the 1-hour limit (e.g. 50 minutes)
|
|
126
|
+
expires_at = int(time.time()) + 3000
|
|
127
|
+
|
|
128
|
+
# Populate in-memory cache
|
|
129
|
+
_cached_cas_token = cas_token
|
|
130
|
+
_cached_access_token = access_token
|
|
131
|
+
_cached_expires_at = expires_at
|
|
132
|
+
|
|
133
|
+
return cas_token, access_token, expires_at
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print(f"[bold red]ERROR: Failed to authenticate/exchange refresh token: {e}[/bold red]")
|
|
136
|
+
raise typer.Exit(1)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_github_app_token():
|
|
140
|
+
db_path = get_db_path()
|
|
141
|
+
|
|
142
|
+
db = TinyDB(db_path)
|
|
143
|
+
|
|
144
|
+
for item in db:
|
|
145
|
+
token = item.get('github_app_token')
|
|
146
|
+
if token:
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
if not token:
|
|
150
|
+
print("Github app token does not exists.")
|
|
151
|
+
return token
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def set_github_app_token(github_app_token):
|
|
155
|
+
db_path = get_db_path()
|
|
156
|
+
db = TinyDB(db_path)
|
|
157
|
+
db.update({'github_app_token': github_app_token}, doc_ids=[1])
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def set_project_slug(project_slug):
|
|
161
|
+
db_path = get_db_path()
|
|
162
|
+
db = TinyDB(db_path)
|
|
163
|
+
db.update({'project_slug': project_slug}, doc_ids=[1])
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_project_slug():
|
|
167
|
+
db_path = get_db_path()
|
|
168
|
+
|
|
169
|
+
db = TinyDB(db_path)
|
|
170
|
+
|
|
171
|
+
for item in db:
|
|
172
|
+
project_slug = item.get('project_slug')
|
|
173
|
+
if project_slug:
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
if not project_slug:
|
|
177
|
+
print("project slug was not set.")
|
|
178
|
+
return project_slug
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def get_server_url():
|
|
182
|
+
db_path = get_db_path()
|
|
183
|
+
|
|
184
|
+
db = TinyDB(db_path)
|
|
185
|
+
|
|
186
|
+
for item in db:
|
|
187
|
+
server_url = item.get('server_url')
|
|
188
|
+
if server_url:
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
if not server_url:
|
|
192
|
+
print("Server url does not exists. Please login.")
|
|
193
|
+
return server_url
|
|
194
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: accli
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: IIASA Accelerator Client
|
|
5
5
|
Author-email: Wrufesh S <wrufesh@gmail.com>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -39,7 +39,7 @@ Requires-Dist: requests>=2.25.1
|
|
|
39
39
|
Requires-Dist: jsonschema>=4.5.1
|
|
40
40
|
Requires-Dist: pydantic>=2.0
|
|
41
41
|
Requires-Dist: GitPython
|
|
42
|
-
Requires-Dist: hf-xet
|
|
42
|
+
Requires-Dist: hf-xet==1.5.2
|
|
43
43
|
Dynamic: license-file
|
|
44
44
|
|
|
45
45
|
# Accelerator Terminal Client and Python API
|
|
@@ -138,8 +138,8 @@ Usage: accli [OPTIONS] COMMAND [ARGS]...
|
|
|
138
138
|
|
|
139
139
|
### Handling self-signed certificates
|
|
140
140
|
|
|
141
|
-
1. Copy and paste certificates from `<control_services_backend>/certs/
|
|
142
|
-
`./certs/dev-backend.crt` and `<accelerator_service>/minio_certs/
|
|
141
|
+
1. Copy and paste certificates from `<control_services_backend>/certs/ca.crt` as
|
|
142
|
+
`./certs/dev-backend.crt` and `<accelerator_service>/minio_certs/ca.crt` as
|
|
143
143
|
`./certs/dev-minio-ca.crt`.
|
|
144
144
|
2. Run
|
|
145
145
|
```
|
|
@@ -20,7 +20,7 @@ dependencies = [
|
|
|
20
20
|
"jsonschema>=4.5.1", # 4.5.1+ is safe across libraries
|
|
21
21
|
"pydantic>=2.0", # Major version 2 required if you're using v2 features
|
|
22
22
|
"GitPython",
|
|
23
|
-
"hf-xet
|
|
23
|
+
"hf-xet==1.5.2",
|
|
24
24
|
]
|
|
25
25
|
classifiers = [
|
|
26
26
|
"Programming Language :: Python :: 3",
|
accli-2.0.0rc1/accli/token.py
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
|
-
from tinydb import TinyDB
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def get_db_path():
|
|
7
|
-
home = os.path.expanduser("~")
|
|
8
|
-
token_directory = f"{home}/.accli"
|
|
9
|
-
|
|
10
|
-
if not os.path.exists(token_directory):
|
|
11
|
-
os.makedirs(token_directory)
|
|
12
|
-
|
|
13
|
-
return f"{token_directory}/data.json"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def save_token_details(token, server_url, webcli_url):
|
|
17
|
-
db_path = get_db_path()
|
|
18
|
-
|
|
19
|
-
try:
|
|
20
|
-
os.remove(db_path)
|
|
21
|
-
except OSError:
|
|
22
|
-
pass
|
|
23
|
-
|
|
24
|
-
db = TinyDB(db_path)
|
|
25
|
-
db.insert({
|
|
26
|
-
'token': token,
|
|
27
|
-
'server_url': server_url,
|
|
28
|
-
'webcli_url': webcli_url
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def get_token():
|
|
33
|
-
db_path = get_db_path()
|
|
34
|
-
|
|
35
|
-
db = TinyDB(db_path)
|
|
36
|
-
|
|
37
|
-
for item in db:
|
|
38
|
-
token = item.get('token')
|
|
39
|
-
if token:
|
|
40
|
-
break
|
|
41
|
-
|
|
42
|
-
if not token:
|
|
43
|
-
print("Token does not exists. Please login.")
|
|
44
|
-
return token
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def get_github_app_token():
|
|
48
|
-
db_path = get_db_path()
|
|
49
|
-
|
|
50
|
-
db = TinyDB(db_path)
|
|
51
|
-
|
|
52
|
-
for item in db:
|
|
53
|
-
token = item.get('github_app_token')
|
|
54
|
-
if token:
|
|
55
|
-
break
|
|
56
|
-
|
|
57
|
-
if not token:
|
|
58
|
-
print("Github app token does not exists.")
|
|
59
|
-
return token
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def set_github_app_token(github_app_token):
|
|
63
|
-
db_path = get_db_path()
|
|
64
|
-
db = TinyDB(db_path)
|
|
65
|
-
db.update({'github_app_token': github_app_token}, doc_ids=[1])
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def set_project_slug(project_slug):
|
|
69
|
-
db_path = get_db_path()
|
|
70
|
-
db = TinyDB(db_path)
|
|
71
|
-
db.update({'project_slug': project_slug}, doc_ids=[1])
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def get_project_slug():
|
|
75
|
-
db_path = get_db_path()
|
|
76
|
-
|
|
77
|
-
db = TinyDB(db_path)
|
|
78
|
-
|
|
79
|
-
for item in db:
|
|
80
|
-
project_slug = item.get('project_slug')
|
|
81
|
-
if project_slug:
|
|
82
|
-
break
|
|
83
|
-
|
|
84
|
-
if not project_slug:
|
|
85
|
-
print("project slug was not set.")
|
|
86
|
-
return project_slug
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def get_server_url():
|
|
90
|
-
db_path = get_db_path()
|
|
91
|
-
|
|
92
|
-
db = TinyDB(db_path)
|
|
93
|
-
|
|
94
|
-
for item in db:
|
|
95
|
-
server_url = item.get('server_url')
|
|
96
|
-
if server_url:
|
|
97
|
-
break
|
|
98
|
-
|
|
99
|
-
if not server_url:
|
|
100
|
-
print("Server url does not exists. Please login.")
|
|
101
|
-
return server_url
|
|
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
|