accli 2.0.6__tar.gz → 2.0.12__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.6 → accli-2.0.12}/PKG-INFO +1 -1
- {accli-2.0.6 → accli-2.0.12}/accli/AcceleratorTaskDispatcher.py +100 -19
- {accli-2.0.6 → accli-2.0.12}/accli/_version.py +1 -1
- {accli-2.0.6 → accli-2.0.12}/accli/gui.py +42 -15
- {accli-2.0.6 → accli-2.0.12}/accli/token.py +1 -0
- {accli-2.0.6 → accli-2.0.12}/accli.egg-info/PKG-INFO +1 -1
- {accli-2.0.6 → accli-2.0.12}/LICENSE +0 -0
- {accli-2.0.6 → accli-2.0.12}/README.md +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli/AcceleratorJobProjectService.py +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli/AcceleratorTerminalCliProjectService.py +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli/CsvRegionalTimeseriesValidator.py +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli/__init__.py +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli/__main__.py +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli/cleanup-nfs.ps1 +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli/cli.py +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli/common.py +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli/mount_downloader.py +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli/nfs_watcher.py +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli/refresh_explorer.ps1 +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli/setup-nfs.ps1 +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli.egg-info/SOURCES.txt +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli.egg-info/dependency_links.txt +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli.egg-info/entry_points.txt +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli.egg-info/requires.txt +0 -0
- {accli-2.0.6 → accli-2.0.12}/accli.egg-info/top_level.txt +0 -0
- {accli-2.0.6 → accli-2.0.12}/pyproject.toml +0 -0
- {accli-2.0.6 → accli-2.0.12}/setup.cfg +0 -0
|
@@ -3,11 +3,15 @@ import zipfile
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import shutil
|
|
5
5
|
import tempfile
|
|
6
|
+
import fnmatch
|
|
7
|
+
import concurrent.futures
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
6
10
|
from typing import Optional, Dict, List
|
|
7
11
|
|
|
8
12
|
import requests
|
|
9
13
|
from functools import lru_cache
|
|
10
|
-
from pydantic import BaseModel, model_validator
|
|
14
|
+
from pydantic import BaseModel, model_validator, Field
|
|
11
15
|
from accli.AcceleratorTerminalCliProjectService import AcceleratorTerminalCliProjectService
|
|
12
16
|
from accli.token import (
|
|
13
17
|
get_token, get_server_url,
|
|
@@ -48,23 +52,72 @@ def get_file_sha1(file_path):
|
|
|
48
52
|
return sha1_hash.hexdigest()
|
|
49
53
|
|
|
50
54
|
|
|
51
|
-
def copy_tree(src, dst):
|
|
52
|
-
"""Recursively copy from src to dst, excluding .git folders."""
|
|
55
|
+
def copy_tree(src, dst, include=None, exclude=None, root_src=None):
|
|
56
|
+
"""Recursively copy from src to dst, excluding .git folders. Includes override excludes."""
|
|
57
|
+
if root_src is None:
|
|
58
|
+
root_src = src
|
|
59
|
+
|
|
60
|
+
excludes = [e.strip() for e in exclude.split(',') if e.strip()] if exclude else []
|
|
61
|
+
includes = [i.strip() for i in include.split(',') if i.strip()] if include else []
|
|
62
|
+
|
|
53
63
|
for item in os.listdir(src):
|
|
54
64
|
src_path = os.path.join(src, item)
|
|
55
65
|
dst_path = os.path.join(dst, item)
|
|
66
|
+
rel_path = os.path.relpath(src_path, root_src)
|
|
56
67
|
|
|
57
68
|
if os.path.isdir(src_path):
|
|
58
69
|
if item == '.git':
|
|
59
70
|
continue
|
|
71
|
+
|
|
72
|
+
is_excluded = any(fnmatch.fnmatch(rel_path, p) or fnmatch.fnmatch(item, p) for p in excludes)
|
|
73
|
+
# Traverse even if excluded as long as includes exist, to find exceptions
|
|
74
|
+
if is_excluded and not includes:
|
|
75
|
+
continue
|
|
76
|
+
|
|
60
77
|
os.makedirs(dst_path, exist_ok=True)
|
|
61
|
-
copy_tree(src_path, dst_path)
|
|
78
|
+
copy_tree(src_path, dst_path, include, exclude, root_src)
|
|
62
79
|
else:
|
|
80
|
+
is_excluded = any(fnmatch.fnmatch(rel_path, p) or fnmatch.fnmatch(item, p) for p in excludes)
|
|
81
|
+
is_included = any(fnmatch.fnmatch(rel_path, p) or fnmatch.fnmatch(item, p) for p in includes)
|
|
82
|
+
|
|
83
|
+
if is_included:
|
|
84
|
+
pass # Included files override exclude
|
|
85
|
+
elif is_excluded:
|
|
86
|
+
continue # Skipped by exclude
|
|
87
|
+
elif includes and not excludes:
|
|
88
|
+
continue # If ONLY includes list is provided, act as whitelist
|
|
89
|
+
|
|
63
90
|
shutil.copy2(src_path, dst_path)
|
|
64
91
|
|
|
65
92
|
|
|
93
|
+
def _upload_cas_file_worker(args):
|
|
94
|
+
rel_path, file_path, term_cli_project_service, project_slug = args
|
|
95
|
+
file_hash = get_file_sha1(file_path)
|
|
96
|
+
blob_name = f"cas_{file_hash}.blob"
|
|
97
|
+
|
|
98
|
+
print(f"Checking {rel_path} (hash: {file_hash[:8]})...")
|
|
99
|
+
presigned_push_url = term_cli_project_service.get_jobstore_push_url(
|
|
100
|
+
project_slug, blob_name
|
|
101
|
+
)
|
|
102
|
+
if presigned_push_url:
|
|
103
|
+
print(f"Uploading {rel_path}...")
|
|
104
|
+
with open(file_path, 'rb') as f:
|
|
105
|
+
res = requests.put(
|
|
106
|
+
presigned_push_url,
|
|
107
|
+
data=f,
|
|
108
|
+
verify=False,
|
|
109
|
+
)
|
|
110
|
+
res.raise_for_status()
|
|
111
|
+
print(f"Uploaded {rel_path}.")
|
|
112
|
+
else:
|
|
113
|
+
print(f"Skipped {rel_path} (already in jobstore).")
|
|
114
|
+
|
|
115
|
+
mode = os.stat(file_path).st_mode
|
|
116
|
+
return {"path": rel_path, "hash": file_hash, "mode": mode}
|
|
117
|
+
|
|
118
|
+
|
|
66
119
|
@lru_cache(maxsize=None)
|
|
67
|
-
def push_folder_job(directory):
|
|
120
|
+
def push_folder_job(directory, include=None, exclude=None):
|
|
68
121
|
server_url = get_server_url()
|
|
69
122
|
project_slug = get_project_slug()
|
|
70
123
|
_, access_token, _ = exchange_refresh_token(project_slug)
|
|
@@ -76,24 +129,37 @@ def push_folder_job(directory):
|
|
|
76
129
|
)
|
|
77
130
|
|
|
78
131
|
repo_dir = tempfile.mkdtemp()
|
|
79
|
-
copy_tree(directory, repo_dir)
|
|
132
|
+
copy_tree(directory, repo_dir, include, exclude)
|
|
80
133
|
|
|
81
134
|
if os.path.isfile(f'{repo_dir}/wkube.py'):
|
|
82
135
|
os.remove(f'{repo_dir}/wkube.py')
|
|
83
136
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
137
|
+
manifest = {"type": "cas_manifest", "files": []}
|
|
138
|
+
|
|
139
|
+
upload_args = []
|
|
140
|
+
for root, dirs, files in os.walk(repo_dir):
|
|
141
|
+
for file in files:
|
|
142
|
+
file_path = os.path.join(root, file)
|
|
143
|
+
rel_path = Path(file_path).relative_to(repo_dir).as_posix()
|
|
144
|
+
upload_args.append((rel_path, file_path, term_cli_project_service, project_slug))
|
|
145
|
+
|
|
146
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor:
|
|
147
|
+
for result in executor.map(_upload_cas_file_worker, upload_args):
|
|
148
|
+
manifest["files"].append(result)
|
|
149
|
+
|
|
150
|
+
manifest_path = os.path.join(repo_dir, "manifest.json")
|
|
151
|
+
with open(manifest_path, "w") as f:
|
|
152
|
+
json.dump(manifest, f)
|
|
153
|
+
|
|
154
|
+
manifest_hash = get_file_sha1(manifest_path)
|
|
155
|
+
manifest_name = f"manifest_{manifest_hash}.json"
|
|
156
|
+
|
|
91
157
|
presigned_push_url = term_cli_project_service.get_jobstore_push_url(
|
|
92
|
-
project_slug,
|
|
158
|
+
project_slug, manifest_name
|
|
93
159
|
)
|
|
94
|
-
|
|
160
|
+
|
|
95
161
|
if presigned_push_url:
|
|
96
|
-
with open(
|
|
162
|
+
with open(manifest_path, 'rb') as f:
|
|
97
163
|
res = requests.put(
|
|
98
164
|
presigned_push_url,
|
|
99
165
|
data=f,
|
|
@@ -102,7 +168,7 @@ def push_folder_job(directory):
|
|
|
102
168
|
res.raise_for_status()
|
|
103
169
|
|
|
104
170
|
shutil.rmtree(repo_dir)
|
|
105
|
-
return f"s3accjobstore://{
|
|
171
|
+
return f"s3accjobstore://{manifest_name}", manifest_hash
|
|
106
172
|
|
|
107
173
|
|
|
108
174
|
class JobDispatchModel(BaseModel):
|
|
@@ -152,7 +218,9 @@ class WKubeTaskMeta(BaseModel):
|
|
|
152
218
|
class WKubeTaskKwargs(BaseModel):
|
|
153
219
|
docker_image: Optional[str] = None
|
|
154
220
|
|
|
155
|
-
job_folder: str = './'
|
|
221
|
+
job_folder: str = Field('./', description="Path to the directory containing job files. Must be a valid existing directory.")
|
|
222
|
+
include: Optional[str] = Field(None, description="Comma-separated wildcard patterns to include (e.g. '*.py, data/*')")
|
|
223
|
+
exclude: Optional[str] = Field(None, description="Comma-separated wildcard patterns to exclude (e.g. '*.txt, .venv/*')")
|
|
156
224
|
|
|
157
225
|
repo_url: Optional[str] = None # required when docker image is not present
|
|
158
226
|
repo_branch: Optional[str] = None # required when docker image is not present
|
|
@@ -172,6 +240,10 @@ class WKubeTaskKwargs(BaseModel):
|
|
|
172
240
|
result = super().model_dump(*args, **kwargs)
|
|
173
241
|
if 'job_folder' in result:
|
|
174
242
|
del result['job_folder']
|
|
243
|
+
if 'include' in result:
|
|
244
|
+
del result['include']
|
|
245
|
+
if 'exclude' in result:
|
|
246
|
+
del result['exclude']
|
|
175
247
|
return result
|
|
176
248
|
|
|
177
249
|
@model_validator(mode="before")
|
|
@@ -179,10 +251,19 @@ class WKubeTaskKwargs(BaseModel):
|
|
|
179
251
|
def validate_root(cls, values):
|
|
180
252
|
if not values.get('docker_image'):
|
|
181
253
|
job_folder = values.get('job_folder', './')
|
|
254
|
+
include = values.get('include')
|
|
255
|
+
exclude = values.get('exclude')
|
|
256
|
+
|
|
257
|
+
if not os.path.isdir(job_folder):
|
|
258
|
+
raise ValueError(
|
|
259
|
+
f"Invalid job_folder: '{job_folder}' does not exist or is not a directory. "
|
|
260
|
+
f"Help: job_folder must be a valid path. You can also use 'include' and 'exclude' "
|
|
261
|
+
f"with comma-separated wildcards (e.g., include='*.py, data/*', exclude='*.txt, .venv/*')."
|
|
262
|
+
)
|
|
182
263
|
|
|
183
264
|
if not (values.get('repo_url') and values.get('repo_branch')):
|
|
184
265
|
remote_url, branch_name = push_folder_job(
|
|
185
|
-
os.path.abspath(job_folder)
|
|
266
|
+
os.path.abspath(job_folder), include, exclude
|
|
186
267
|
)
|
|
187
268
|
values['repo_url'] = remote_url
|
|
188
269
|
values['repo_branch'] = branch_name
|
|
@@ -88,7 +88,11 @@ class AccliGuiApp(tk.Tk):
|
|
|
88
88
|
|
|
89
89
|
# Checkboxes
|
|
90
90
|
self.style.configure("TCheckbutton", background="#1e1e1e", foreground="#ffffff")
|
|
91
|
-
self.style.map("TCheckbutton",
|
|
91
|
+
self.style.map("TCheckbutton",
|
|
92
|
+
background=[("active", "#1e1e1e")],
|
|
93
|
+
indicatorbackground=[("selected", "#007acc"), ("!selected", "#2d2d2d")],
|
|
94
|
+
indicatorforeground=[("selected", "#ffffff"), ("!selected", "#2d2d2d")]
|
|
95
|
+
)
|
|
92
96
|
|
|
93
97
|
def create_widgets(self):
|
|
94
98
|
# Top banner
|
|
@@ -122,7 +126,7 @@ class AccliGuiApp(tk.Tk):
|
|
|
122
126
|
cmd = [sys.executable] + args
|
|
123
127
|
else:
|
|
124
128
|
# Script mode
|
|
125
|
-
cmd = [sys.executable, "-m", "accli
|
|
129
|
+
cmd = [sys.executable, "-m", "accli"] + args
|
|
126
130
|
|
|
127
131
|
if elevate and platform.system() != "Windows":
|
|
128
132
|
import shutil
|
|
@@ -162,11 +166,14 @@ class AccliGuiApp(tk.Tk):
|
|
|
162
166
|
process.wait()
|
|
163
167
|
|
|
164
168
|
if on_done:
|
|
165
|
-
|
|
169
|
+
ret_code = process.returncode
|
|
170
|
+
out_text = "".join(output_lines)
|
|
171
|
+
self.after(0, lambda: on_done(ret_code, out_text))
|
|
166
172
|
except Exception as e:
|
|
167
|
-
|
|
173
|
+
err_text = str(e)
|
|
174
|
+
self.append_log(f"ERROR executing command: {err_text}\n")
|
|
168
175
|
if on_done:
|
|
169
|
-
self.after(0, lambda: on_done(-1,
|
|
176
|
+
self.after(0, lambda: on_done(-1, err_text))
|
|
170
177
|
|
|
171
178
|
threading.Thread(target=worker, daemon=True).start()
|
|
172
179
|
|
|
@@ -196,10 +203,26 @@ class AccliGuiApp(tk.Tk):
|
|
|
196
203
|
self.login_btn = ttk.Button(card, text="Authenticate / Login", command=self.action_login)
|
|
197
204
|
self.login_btn.grid(row=0, column=2, padx=10, pady=5)
|
|
198
205
|
|
|
206
|
+
self.debug_mode_val = tk.BooleanVar(value=bool(os.environ.get("ACCLI_DEBUG")))
|
|
207
|
+
self.debug_check = ttk.Checkbutton(
|
|
208
|
+
card,
|
|
209
|
+
text="Debug mode",
|
|
210
|
+
variable=self.debug_mode_val,
|
|
211
|
+
command=self.on_debug_toggle
|
|
212
|
+
)
|
|
213
|
+
self.debug_check.grid(row=1, column=0, columnspan=3, sticky=tk.W, pady=(5, 0))
|
|
214
|
+
|
|
199
215
|
# Connection status details
|
|
200
216
|
self.conn_details_txt = tk.Text(auth_tab, height=12, bg="#2d2d2d", fg="#ffffff", insertbackground="white", font=("Courier New", 10), state=tk.DISABLED)
|
|
201
217
|
self.conn_details_txt.pack(fill=tk.BOTH, expand=True, pady=10)
|
|
202
218
|
|
|
219
|
+
def on_debug_toggle(self):
|
|
220
|
+
if self.debug_mode_val.get():
|
|
221
|
+
os.environ["ACCLI_DEBUG"] = "1"
|
|
222
|
+
else:
|
|
223
|
+
os.environ.pop("ACCLI_DEBUG", None)
|
|
224
|
+
self.refresh_login_status()
|
|
225
|
+
|
|
203
226
|
def refresh_login_status(self):
|
|
204
227
|
from accli.token import get_db_path
|
|
205
228
|
from tinydb import TinyDB
|
|
@@ -229,13 +252,16 @@ class AccliGuiApp(tk.Tk):
|
|
|
229
252
|
self.server_entry.configure(state=tk.NORMAL)
|
|
230
253
|
self.status_bar_val.set("Session Status: Disconnected")
|
|
231
254
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
self.
|
|
255
|
+
details = [
|
|
256
|
+
f"Server URL: {server_url}",
|
|
257
|
+
f"Session Status: {'Connected / Logged In' if logged_in else 'Not Logged In'}",
|
|
258
|
+
f"Token DB Path: {db_path}",
|
|
259
|
+
f"Debug Mode: {'Enabled ($env:ACCLI_DEBUG=\"1\")' if os.environ.get('ACCLI_DEBUG') else 'Disabled'}"
|
|
260
|
+
]
|
|
261
|
+
self.conn_details_txt.configure(state=tk.NORMAL)
|
|
262
|
+
self.conn_details_txt.delete("1.0", tk.END)
|
|
263
|
+
self.conn_details_txt.insert(tk.END, "\n".join(details) + "\n")
|
|
264
|
+
self.conn_details_txt.configure(state=tk.DISABLED)
|
|
239
265
|
|
|
240
266
|
def action_login(self):
|
|
241
267
|
if hasattr(self, "is_logged_in_state") and self.is_logged_in_state:
|
|
@@ -288,7 +314,7 @@ class AccliGuiApp(tk.Tk):
|
|
|
288
314
|
response = requests.post(
|
|
289
315
|
token_endpoint,
|
|
290
316
|
json={"device_authorization_code": auth_code.strip()},
|
|
291
|
-
verify=
|
|
317
|
+
verify=(not bool(os.environ.get("ACCLI_DEBUG")))
|
|
292
318
|
)
|
|
293
319
|
|
|
294
320
|
if response.status_code == 400:
|
|
@@ -306,8 +332,9 @@ class AccliGuiApp(tk.Tk):
|
|
|
306
332
|
self.after(0, lambda: messagebox.showinfo("Success", "Successfully logged in!"))
|
|
307
333
|
self.after(0, self.refresh_login_status)
|
|
308
334
|
except Exception as e:
|
|
309
|
-
|
|
310
|
-
self.
|
|
335
|
+
err_text = str(e)
|
|
336
|
+
self.append_log(f"Authentication Failed: {err_text}\n")
|
|
337
|
+
self.after(0, lambda msg=err_text: messagebox.showerror("Error", f"Authentication failed: {msg}"))
|
|
311
338
|
self.after(0, self.refresh_login_status)
|
|
312
339
|
|
|
313
340
|
threading.Thread(target=perform_auth, daemon=True).start()
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|