accli 2.0.0rc4__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.
Files changed (26) hide show
  1. {accli-2.0.0rc4 → accli-2.0.1}/PKG-INFO +1 -1
  2. {accli-2.0.0rc4 → accli-2.0.1}/accli/AcceleratorTaskDispatcher.py +2 -2
  3. {accli-2.0.0rc4 → accli-2.0.1}/accli/_version.py +1 -1
  4. {accli-2.0.0rc4 → accli-2.0.1}/accli/cli.py +15 -84
  5. {accli-2.0.0rc4 → accli-2.0.1}/accli/setup-nfs.ps1 +18 -0
  6. accli-2.0.1/accli/token.py +194 -0
  7. {accli-2.0.0rc4 → accli-2.0.1}/accli.egg-info/PKG-INFO +1 -1
  8. accli-2.0.0rc4/accli/token.py +0 -115
  9. {accli-2.0.0rc4 → accli-2.0.1}/LICENSE +0 -0
  10. {accli-2.0.0rc4 → accli-2.0.1}/README.md +0 -0
  11. {accli-2.0.0rc4 → accli-2.0.1}/accli/AcceleratorJobProjectService.py +0 -0
  12. {accli-2.0.0rc4 → accli-2.0.1}/accli/AcceleratorTerminalCliProjectService.py +0 -0
  13. {accli-2.0.0rc4 → accli-2.0.1}/accli/CsvRegionalTimeseriesValidator.py +0 -0
  14. {accli-2.0.0rc4 → accli-2.0.1}/accli/__init__.py +0 -0
  15. {accli-2.0.0rc4 → accli-2.0.1}/accli/__main__.py +0 -0
  16. {accli-2.0.0rc4 → accli-2.0.1}/accli/cleanup-nfs.ps1 +0 -0
  17. {accli-2.0.0rc4 → accli-2.0.1}/accli/common.py +0 -0
  18. {accli-2.0.0rc4 → accli-2.0.1}/accli/gui.py +0 -0
  19. {accli-2.0.0rc4 → accli-2.0.1}/accli/mount_downloader.py +0 -0
  20. {accli-2.0.0rc4 → accli-2.0.1}/accli.egg-info/SOURCES.txt +0 -0
  21. {accli-2.0.0rc4 → accli-2.0.1}/accli.egg-info/dependency_links.txt +0 -0
  22. {accli-2.0.0rc4 → accli-2.0.1}/accli.egg-info/entry_points.txt +0 -0
  23. {accli-2.0.0rc4 → accli-2.0.1}/accli.egg-info/requires.txt +0 -0
  24. {accli-2.0.0rc4 → accli-2.0.1}/accli.egg-info/top_level.txt +0 -0
  25. {accli-2.0.0rc4 → accli-2.0.1}/pyproject.toml +0 -0
  26. {accli-2.0.0rc4 → 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.0rc4
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)
@@ -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,
@@ -1,3 +1,3 @@
1
1
  # Please strictly put double quote to use this info for git tag
2
2
  # Read a developer guide for git tag command with regex
3
- VERSION = "v2.0.0-c4"
3
+ VERSION = "v2.0.1"
@@ -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()
@@ -409,12 +332,16 @@ def copy(
409
332
  refresh_url = f"{server_url.rstrip('/')}/api/v1/oauth/device/cas-token/"
410
333
  refresh_headers = {"Authorization": f"Bearer {cas_token}"}
411
334
 
412
- with Progress(
335
+ import sys
336
+ columns = [
413
337
  TextColumn("[progress.description]{task.description}"),
414
- BarColumn(),
415
338
  TaskProgressColumn(),
416
339
  TimeElapsedColumn(),
417
- ) as progress:
340
+ ]
341
+ if sys.stdout.isatty():
342
+ columns.insert(1, BarColumn())
343
+
344
+ with Progress(*columns) as progress:
418
345
  total_size = sum(item[2] for item in files_to_download)
419
346
  overall_task = progress.add_task("[green]Total Download Progress...", total=total_size)
420
347
 
@@ -548,12 +475,16 @@ def copy(
548
475
  refresh_url = f"{server_url.rstrip('/')}/api/v1/oauth/device/cas-token/"
549
476
  refresh_headers = {"Authorization": f"Bearer {cas_token}"}
550
477
 
551
- with Progress(
478
+ import sys
479
+ columns = [
552
480
  TextColumn("[progress.description]{task.description}"),
553
- BarColumn(),
554
481
  TaskProgressColumn(),
555
482
  TimeElapsedColumn(),
556
- ) as progress:
483
+ ]
484
+ if sys.stdout.isatty():
485
+ columns.insert(1, BarColumn())
486
+
487
+ with Progress(*columns) as progress:
557
488
  total_size = sum(p.stat().st_size for p in local_paths)
558
489
  overall_task = progress.add_task("[green]Total Upload Progress...", total=total_size)
559
490
 
@@ -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.0rc4
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)
@@ -1,115 +0,0 @@
1
- import os
2
-
3
- from tinydb import TinyDB
4
-
5
-
6
- def get_db_path():
7
- sudo_user = os.environ.get("SUDO_USER")
8
- pkexec_uid = os.environ.get("PKEXEC_UID")
9
-
10
- if (sudo_user or pkexec_uid) and os.name != "nt":
11
- try:
12
- import pwd
13
- if pkexec_uid:
14
- home = pwd.getpwuid(int(pkexec_uid)).pw_dir
15
- else:
16
- home = pwd.getpwnam(sudo_user).pw_dir
17
- except Exception:
18
- home = os.path.expanduser("~")
19
- else:
20
- home = os.path.expanduser("~")
21
-
22
- token_directory = f"{home}/.accli"
23
-
24
- if not os.path.exists(token_directory):
25
- os.makedirs(token_directory)
26
-
27
- return f"{token_directory}/data.json"
28
-
29
-
30
- def save_token_details(token, server_url, webcli_url):
31
- db_path = get_db_path()
32
-
33
- try:
34
- os.remove(db_path)
35
- except OSError:
36
- pass
37
-
38
- db = TinyDB(db_path)
39
- db.insert({
40
- 'token': token,
41
- 'server_url': server_url,
42
- 'webcli_url': webcli_url
43
- })
44
-
45
-
46
- def get_token():
47
- db_path = get_db_path()
48
-
49
- db = TinyDB(db_path)
50
-
51
- for item in db:
52
- token = item.get('token')
53
- if token:
54
- break
55
-
56
- if not token:
57
- print("Token does not exists. Please login.")
58
- return token
59
-
60
-
61
- def get_github_app_token():
62
- db_path = get_db_path()
63
-
64
- db = TinyDB(db_path)
65
-
66
- for item in db:
67
- token = item.get('github_app_token')
68
- if token:
69
- break
70
-
71
- if not token:
72
- print("Github app token does not exists.")
73
- return token
74
-
75
-
76
- def set_github_app_token(github_app_token):
77
- db_path = get_db_path()
78
- db = TinyDB(db_path)
79
- db.update({'github_app_token': github_app_token}, doc_ids=[1])
80
-
81
-
82
- def set_project_slug(project_slug):
83
- db_path = get_db_path()
84
- db = TinyDB(db_path)
85
- db.update({'project_slug': project_slug}, doc_ids=[1])
86
-
87
-
88
- def get_project_slug():
89
- db_path = get_db_path()
90
-
91
- db = TinyDB(db_path)
92
-
93
- for item in db:
94
- project_slug = item.get('project_slug')
95
- if project_slug:
96
- break
97
-
98
- if not project_slug:
99
- print("project slug was not set.")
100
- return project_slug
101
-
102
-
103
- def get_server_url():
104
- db_path = get_db_path()
105
-
106
- db = TinyDB(db_path)
107
-
108
- for item in db:
109
- server_url = item.get('server_url')
110
- if server_url:
111
- break
112
-
113
- if not server_url:
114
- print("Server url does not exists. Please login.")
115
- 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