accli 2.0.14__tar.gz → 2.1.0__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 (36) hide show
  1. {accli-2.0.14 → accli-2.1.0}/PKG-INFO +26 -9
  2. {accli-2.0.14 → accli-2.1.0}/README.md +25 -8
  3. accli-2.1.0/accli/AcceleratorJobProjectService.py +397 -0
  4. {accli-2.0.14 → accli-2.1.0}/accli/AcceleratorTaskDispatcher.py +17 -3
  5. accli-2.1.0/accli/AcceleratorTerminalCliProjectService.py +220 -0
  6. {accli-2.0.14 → accli-2.1.0}/accli/_version.py +1 -1
  7. {accli-2.0.14 → accli-2.1.0}/accli/cli.py +238 -8
  8. {accli-2.0.14 → accli-2.1.0}/accli/gui.py +19 -0
  9. accli-2.1.0/accli/job_views.py +233 -0
  10. accli-2.1.0/accli/log_timeline.py +121 -0
  11. accli-2.1.0/accli/version_checker.py +55 -0
  12. {accli-2.0.14 → accli-2.1.0}/accli.egg-info/PKG-INFO +26 -9
  13. {accli-2.0.14 → accli-2.1.0}/accli.egg-info/SOURCES.txt +6 -0
  14. accli-2.1.0/tests/test_jobs_commands.py +140 -0
  15. accli-2.1.0/tests/test_log_timeline.py +28 -0
  16. accli-2.1.0/tests/test_logs_command.py +67 -0
  17. accli-2.0.14/accli/AcceleratorJobProjectService.py +0 -830
  18. accli-2.0.14/accli/AcceleratorTerminalCliProjectService.py +0 -397
  19. {accli-2.0.14 → accli-2.1.0}/LICENSE +0 -0
  20. {accli-2.0.14 → accli-2.1.0}/accli/CsvRegionalTimeseriesValidator.py +0 -0
  21. {accli-2.0.14 → accli-2.1.0}/accli/__init__.py +0 -0
  22. {accli-2.0.14 → accli-2.1.0}/accli/__main__.py +0 -0
  23. {accli-2.0.14 → accli-2.1.0}/accli/cleanup-nfs.ps1 +0 -0
  24. {accli-2.0.14 → accli-2.1.0}/accli/common.py +0 -0
  25. {accli-2.0.14 → accli-2.1.0}/accli/mount_downloader.py +0 -0
  26. {accli-2.0.14 → accli-2.1.0}/accli/nfs_watcher.py +0 -0
  27. {accli-2.0.14 → accli-2.1.0}/accli/refresh_explorer.ps1 +0 -0
  28. {accli-2.0.14 → accli-2.1.0}/accli/setup-nfs.ps1 +0 -0
  29. {accli-2.0.14 → accli-2.1.0}/accli/token.py +0 -0
  30. {accli-2.0.14 → accli-2.1.0}/accli.egg-info/dependency_links.txt +0 -0
  31. {accli-2.0.14 → accli-2.1.0}/accli.egg-info/entry_points.txt +0 -0
  32. {accli-2.0.14 → accli-2.1.0}/accli.egg-info/requires.txt +0 -0
  33. {accli-2.0.14 → accli-2.1.0}/accli.egg-info/top_level.txt +0 -0
  34. {accli-2.0.14 → accli-2.1.0}/pyproject.toml +0 -0
  35. {accli-2.0.14 → accli-2.1.0}/setup.cfg +0 -0
  36. {accli-2.0.14 → accli-2.1.0}/tests/test_mount_lifecycle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: accli
3
- Version: 2.0.14
3
+ Version: 2.1.0
4
4
  Summary: IIASA Accelerator Client
5
5
  Author-email: Wrufesh S <wrufesh@gmail.com>
6
6
  License: The MIT License (MIT)
@@ -132,20 +132,37 @@ accli --help
132
132
  Usage: accli [OPTIONS] COMMAND [ARGS]...
133
133
  ```
134
134
 
135
+ ### 📋 Jobs
136
+
137
+ List root jobs, newest first, with each job's type (`standalone`, `children`, `callback` or `children+callback`), its own status and the status of its whole graph:
138
+
139
+ ```bash
140
+ accli jobs # latest 10 in the last project used
141
+ accli jobs -p my-project -n 50
142
+ accli jobs -s failed # graphs with a failed job
143
+ accli jobs -s processing,error --all
144
+ ```
145
+
146
+ `--status` filters on the graph status: `processing` (or `active`), `error` (or `failed`) and `done`. A standalone job counts by its own status.
147
+
148
+ Show a job's status and details. For a job holding children or callbacks, Graph status covers the jobs below it (the same as GRAPH in `accli jobs`) and each one is listed; any job also links to the pipeline it is part of. Read a job's output with `accli logs <job>`:
149
+
150
+ ```bash
151
+ accli job 1234
152
+ accli job 1234 --json
153
+ ```
154
+
135
155
  ---
136
156
 
137
157
  ## 👩‍💻 Developer Guide
138
158
 
139
159
  ### Handling self-signed certificates
140
160
 
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
- `./certs/dev-minio-ca.crt`.
144
- 2. Run as Administrator
145
- ```
146
- cd scripts
147
- .\import_certs_certutil.ps1
148
- ```
161
+ Import the certificates from `accelerator_service` into the Windows Root store (auto-elevates to Administrator if needed):
162
+ ```powershell
163
+ cd <path to>/accelerator_service/scripts
164
+ .\import_certs_certutil.ps1
165
+ ```
149
166
 
150
167
  ### 🛠 Build & Upload
151
168
 
@@ -88,20 +88,37 @@ accli --help
88
88
  Usage: accli [OPTIONS] COMMAND [ARGS]...
89
89
  ```
90
90
 
91
+ ### 📋 Jobs
92
+
93
+ List root jobs, newest first, with each job's type (`standalone`, `children`, `callback` or `children+callback`), its own status and the status of its whole graph:
94
+
95
+ ```bash
96
+ accli jobs # latest 10 in the last project used
97
+ accli jobs -p my-project -n 50
98
+ accli jobs -s failed # graphs with a failed job
99
+ accli jobs -s processing,error --all
100
+ ```
101
+
102
+ `--status` filters on the graph status: `processing` (or `active`), `error` (or `failed`) and `done`. A standalone job counts by its own status.
103
+
104
+ Show a job's status and details. For a job holding children or callbacks, Graph status covers the jobs below it (the same as GRAPH in `accli jobs`) and each one is listed; any job also links to the pipeline it is part of. Read a job's output with `accli logs <job>`:
105
+
106
+ ```bash
107
+ accli job 1234
108
+ accli job 1234 --json
109
+ ```
110
+
91
111
  ---
92
112
 
93
113
  ## 👩‍💻 Developer Guide
94
114
 
95
115
  ### Handling self-signed certificates
96
116
 
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
- `./certs/dev-minio-ca.crt`.
100
- 2. Run as Administrator
101
- ```
102
- cd scripts
103
- .\import_certs_certutil.ps1
104
- ```
117
+ Import the certificates from `accelerator_service` into the Windows Root store (auto-elevates to Administrator if needed):
118
+ ```powershell
119
+ cd <path to>/accelerator_service/scripts
120
+ .\import_certs_certutil.ps1
121
+ ```
105
122
 
106
123
  ### 🛠 Build & Upload
107
124
 
@@ -0,0 +1,397 @@
1
+ import os
2
+ import io
3
+ from typing import Union, List
4
+
5
+ import requests
6
+ import json
7
+ import base64
8
+ import urllib3
9
+ from pathlib import Path
10
+
11
+ from accli.common import todict
12
+
13
+ ACCLI_DEBUG = os.environ.get('ACCLI_DEBUG', False)
14
+
15
+
16
+ class AccAPIError(Exception):
17
+ pass
18
+
19
+ # def __init__(self, *args, **kwargs):
20
+ # self.status_code = kwargs.pop('status_code')
21
+ # self.response_data = kwargs.pop('response_data')
22
+ # super().__init__(*args, **kwargs)
23
+
24
+
25
+ retries = urllib3.util.Retry(total=10, backoff_factor=1)
26
+
27
+ http_client = urllib3.poolmanager.PoolManager(
28
+ num_pools=20, retries=retries, maxsize=2000, block=True
29
+ )
30
+
31
+ http_client_wo_cert_verification = urllib3.poolmanager.PoolManager(
32
+ cert_reqs="CERT_NONE", num_pools=20, retries=retries, maxsize=2000, block=True
33
+ )
34
+
35
+
36
+ class AcceleratorJobProjectService:
37
+ def __init__(
38
+ self,
39
+ user_token,
40
+ server_url='http://accelerator.iiasa.ac.at',
41
+ verify_cert=(not ACCLI_DEBUG)
42
+ ):
43
+
44
+ self.user_token = user_token
45
+
46
+ if verify_cert:
47
+ self.http_client = http_client
48
+ else:
49
+ self.http_client = http_client_wo_cert_verification
50
+
51
+ self.cli_base_url = f"{server_url}/api/v1/ajob-cli"
52
+ self.common_request_headers = {
53
+ 'x-authorization': user_token
54
+ }
55
+
56
+ def http_client_request(self, *args, **kwargs):
57
+
58
+ if urllib3.__version__.startswith('1.'):
59
+ if 'json' in kwargs:
60
+ json_dict = kwargs.pop('json')
61
+
62
+ assert isinstance(json_dict, dict), (
63
+ f"urllib request: json keyword argument should be dict."
64
+ )
65
+
66
+ encoded_data = json.dumps(json_dict).encode('utf-8')
67
+
68
+ kwargs['body'] = encoded_data
69
+
70
+ res = self.http_client.request(*args, **kwargs)
71
+
72
+ if str(res.status)[0] in ['4', '5']:
73
+ raise AccAPIError(
74
+ f"Accelerator api error:: status_code={res.status} :: response_data={res.data}",
75
+ )
76
+ return res
77
+
78
+ def get_file_stat(self, bucket_object_id):
79
+ res = self.http_client_request(
80
+ "GET",
81
+ f"{self.cli_base_url}/file-stat/{bucket_object_id}/",
82
+ headers=self.common_request_headers
83
+ )
84
+ return todict(res.data)
85
+
86
+ def enumerate_files_by_prefix(self, prefix):
87
+ raise NotImplementedError("filename parameter is not implemented yet")
88
+
89
+ project_slug = filename.split('/')[0]
90
+
91
+ b64_encoded_prefix = base64.b64encode(prefix.encode()).decode()
92
+
93
+ res = self.http_client_request(
94
+ "GET",
95
+ f"{self.cli_base_url}/{project_slug}/enumerate-all-files/{b64_encoded_prefix}/",
96
+ headers=self.common_request_headers
97
+ )
98
+ if res.data:
99
+ return todict(res.data)
100
+
101
+ def get_file_url_from_repo(self, filename):
102
+ project_slug = filename.split('/')[0]
103
+ res = self.http_client_request(
104
+ "GET",
105
+ f"{self.cli_base_url}/{project_slug}/get-file-download-url/?filename={filename}",
106
+ headers=self.common_request_headers
107
+ )
108
+ if res.data:
109
+ return todict(res.data)
110
+ return None
111
+
112
+ def get_dataset_type(self, *args, **kwargs):
113
+ return self.get_bucket_object_validation_type(*args, **kwargs)
114
+
115
+ def get_bucket_object_validation_type(self, bucket_object_id):
116
+ res = self.http_client_request(
117
+ "GET",
118
+ f"{self.cli_base_url}/dataset-type/{bucket_object_id}/",
119
+ headers=self.common_request_headers
120
+ )
121
+ if res.data:
122
+ return todict(res.data)
123
+ return None
124
+
125
+ def get_filename_dataset_type(self, filename):
126
+ res = self.http_client_request(
127
+ "GET",
128
+ f"{self.cli_base_url}/filename-dataset-type/?filename={filename}",
129
+ headers=self.common_request_headers
130
+ )
131
+ if res.data:
132
+ return todict(res.data)
133
+ return None
134
+
135
+ def get_filename_validation_details(self, filename):
136
+ res = self.http_client_request(
137
+ "GET",
138
+ f"{self.cli_base_url}/filename-validation-detail/?filename={filename}",
139
+ headers=self.common_request_headers
140
+ )
141
+ if res.data:
142
+ return todict(res.data)
143
+ return None
144
+
145
+ def get_bucket_object_validation_details(self, bucket_object_id):
146
+ res = self.http_client_request(
147
+ "GET",
148
+ f"{self.cli_base_url}/validation-detail/{bucket_object_id}/",
149
+ headers=self.common_request_headers
150
+ )
151
+ if res.data:
152
+ return todict(res.data)
153
+ return None
154
+
155
+ def get_file_url(self, bucket_object_id):
156
+ res = self.http_client_request(
157
+ "GET",
158
+ f"{self.cli_base_url}/file-url/{bucket_object_id}/",
159
+ headers=self.common_request_headers
160
+ )
161
+
162
+ if res.data:
163
+ return todict(res.data)
164
+ return None
165
+
166
+ def get_file_stream(self, bucket_object_id):
167
+ url = self.get_file_url(bucket_object_id)
168
+ if url:
169
+ resp = self.http_client_request("GET", url, preload_content=False)
170
+ return resp
171
+ return None
172
+
173
+ def check_job_health(self):
174
+
175
+ res = self.http_client_request(
176
+ "GET",
177
+ f"{self.cli_base_url}/is-healthy/",
178
+ headers=self.common_request_headers
179
+ )
180
+
181
+ if res.data:
182
+ res = todict(res.data)
183
+
184
+ is_healthy = res['is_healthy']
185
+ return is_healthy
186
+
187
+ def add_log_file(self, data: bytes, filename):
188
+
189
+ res = self.http_client_request(
190
+ "GET",
191
+ f"{self.cli_base_url}/presigned-log-upload-url/?filename={filename}",
192
+ headers=self.common_request_headers
193
+ )
194
+
195
+ if res.data:
196
+ res = todict(res.data)
197
+ else:
198
+ raise ValueError(f"Unable to get presign url in response: {res.data}")
199
+
200
+ upload_url = res['upload_url']
201
+ app_bucket_id = res['app_bucket_id']
202
+ res_filename = res['filename']
203
+ is_healthy = res['is_healthy']
204
+
205
+ requests.put(
206
+ upload_url,
207
+ data=data,
208
+ # headers=headers,
209
+ verify=False,
210
+ )
211
+
212
+ self.http_client_request(
213
+ "POST",
214
+ f"{self.cli_base_url}/register-log-file/",
215
+ json=dict(
216
+ filename=res_filename,
217
+ app_bucket_id=app_bucket_id
218
+ ),
219
+ headers=self.common_request_headers
220
+ )
221
+
222
+ return is_healthy
223
+
224
+ # # TODO @wrufesh ensure it requires special token
225
+ # def register_iamc_validation(
226
+ # self, validated_bucket_object_id, indexdb_bucket_object_id
227
+ # ):
228
+
229
+ # headers = {"Content-Type": "application/json"}
230
+
231
+ # headers.update(self.common_request_headers)
232
+
233
+ # res = self.http_client_request(
234
+ # "PUT",
235
+ # f"{self.cli_base_url}/register-iamc-validation/",
236
+ # json=dict(
237
+ # validated_bucket_object_id=validated_bucket_object_id,
238
+ # indexdb_bucket_object_id=indexdb_bucket_object_id
239
+ # ),
240
+ # headers=headers
241
+ # )
242
+
243
+ def register_validation(
244
+ self,
245
+ validated_bucket_object_id: int,
246
+ dataset_template_id: int,
247
+ validated_metadata: dict,
248
+ validation_supporting_bucket_object_ids: List[int]
249
+ ):
250
+ headers = {"Content-Type": "application/json"}
251
+
252
+ headers.update(self.common_request_headers)
253
+
254
+ res = self.http_client_request(
255
+ "PUT",
256
+ f"{self.cli_base_url}/register-validation/",
257
+ json=dict(
258
+ validated_bucket_object_id=validated_bucket_object_id,
259
+ dataset_template_id=dataset_template_id,
260
+ validated_metadata=validated_metadata,
261
+ validation_supporting_bucket_object_ids=validation_supporting_bucket_object_ids
262
+ ),
263
+ headers=headers
264
+ )
265
+
266
+ def register_validation_with_filename(
267
+ self,
268
+ validated_filename: str,
269
+ dataset_template_id: int,
270
+ validated_metadata: dict,
271
+ validation_supporting_filenames: List[str]
272
+ ):
273
+ headers = {"Content-Type": "application/json"}
274
+
275
+ headers.update(self.common_request_headers)
276
+
277
+ res = self.http_client_request(
278
+ "PUT",
279
+ f"{self.cli_base_url}/register-validation-with-filename/",
280
+ json=dict(
281
+ validated_filename=validated_filename,
282
+ dataset_template_id=dataset_template_id,
283
+ validated_metadata=validated_metadata,
284
+ validation_supporting_filenames=validation_supporting_filenames
285
+ ),
286
+ headers=headers
287
+ )
288
+
289
+
290
+ def get_dataset_template_details(
291
+ self,
292
+ dataset_template_id
293
+ ):
294
+ headers = {"Content-Type": "application/json"}
295
+
296
+ headers.update(self.common_request_headers)
297
+
298
+ res = self.http_client_request(
299
+ "GET",
300
+ f"{self.cli_base_url}/dataset-template-detail/{dataset_template_id}/",
301
+ headers=headers
302
+ )
303
+
304
+ return todict(res.data)
305
+
306
+ def update_job_status(self, status):
307
+ headers = {
308
+ # "Content-Type": "application/json"
309
+ # 'Content-Type': 'application/x-www-form-urlencoded'
310
+ }
311
+
312
+ headers.update(self.common_request_headers)
313
+
314
+ res = self.http_client_request(
315
+ "POST",
316
+ f"{self.cli_base_url}/webhook-event/",
317
+ json=dict(
318
+ type='STATUS_UPDATE',
319
+ data=dict(
320
+ new_status=status
321
+ )
322
+ ),
323
+ headers=headers
324
+ )
325
+
326
+
327
+ class Fs:
328
+ @staticmethod
329
+ def write_stream_remote(filestream, dest_filepath, user_token, server_url):
330
+ raise NotImplementedError(
331
+ "Remote uploads through accli are no longer supported. "
332
+ "Write job outputs to /mnt/wdrv instead; the job agent uploads them."
333
+ )
334
+
335
+ @staticmethod
336
+ def write_stream_local(filestream, dest_filepath):
337
+ with open(dest_filepath, 'wb') as file:
338
+ file.write(filestream.getvalue())
339
+
340
+ @staticmethod
341
+ def get_file_url(remote_filepath):
342
+ user_token = os.environ.get("ACC_JOB_TOKEN", None)
343
+ server_url = os.environ.get("ACC_JOB_GATEWAY_SERVER", None)
344
+
345
+ if not (user_token and server_url):
346
+ raise ValueError("Remote data repository credentials not found.")
347
+
348
+ accelerator_job_service = AcceleratorJobProjectService(
349
+ user_token,
350
+ server_url=server_url,
351
+ verify_cert=(not ACCLI_DEBUG)
352
+ )
353
+
354
+ return accelerator_job_service.get_file_url_from_repo(
355
+ remote_filepath
356
+ )
357
+
358
+ @staticmethod
359
+ def write_file(source: Union[str, io.BytesIO], dest_filepath):
360
+ user_token = os.environ.get("ACC_JOB_TOKEN", None)
361
+ server_url = os.environ.get("ACC_JOB_GATEWAY_SERVER", None)
362
+
363
+ if isinstance(source, io.BytesIO):
364
+ if user_token and server_url:
365
+ Fs.write_stream_remote(source, dest_filepath, user_token, server_url)
366
+ else:
367
+ Fs.write_stream_local(source, dest_filepath)
368
+ else:
369
+ if type(source) != str:
370
+ raise ValueError("Unknown source argument")
371
+ source_path = Path(source)
372
+ if not source_path.exists():
373
+ raise ValueError("Source path does not exist")
374
+
375
+ with open(source, 'rb') as fstream:
376
+ if user_token and server_url:
377
+ Fs.write_stream_remote(fstream, dest_filepath, user_token, server_url)
378
+ else:
379
+ Fs.write_stream_local(fstream, dest_filepath)
380
+
381
+ @staticmethod
382
+ def enumerate_files_by_prefix(prefix):
383
+ user_token = os.environ.get("ACC_JOB_TOKEN", None)
384
+ server_url = os.environ.get("ACC_JOB_GATEWAY_SERVER", None)
385
+
386
+ if not (user_token and server_url):
387
+ raise ValueError("Remote data repository credentials not found.")
388
+
389
+ accelerator_job_service = AcceleratorJobProjectService(
390
+ user_token,
391
+ server_url=server_url,
392
+ verify_cert=(not ACCLI_DEBUG)
393
+ )
394
+
395
+ return accelerator_job_service.enumerate_files_by_prefix(
396
+ prefix
397
+ )
@@ -102,9 +102,11 @@ def _upload_cas_file_worker(args):
102
102
  if presigned_push_url:
103
103
  print(f"Uploading {rel_path}...")
104
104
  with open(file_path, 'rb') as f:
105
+ data = f.read()
105
106
  res = requests.put(
106
107
  presigned_push_url,
107
- data=f,
108
+ data=data,
109
+ headers={"Content-Length": str(len(data))},
108
110
  verify=False,
109
111
  )
110
112
  res.raise_for_status()
@@ -171,6 +173,16 @@ def push_folder_job(directory, include=None, exclude=None):
171
173
  return f"s3accjobstore://{manifest_name}", manifest_hash
172
174
 
173
175
 
176
+ JOB_SECRETS_DESCRIPTION = (
177
+ "Env var name to secret key, e.g. {'GIT_PAT': 'GIT_PAT'}. Keys name project or personal secrets "
178
+ "stored under Project > Secrets in the web app; never put a secret value here, the gateway rejects it."
179
+ )
180
+ JOB_SECRET_FILES_DESCRIPTION = (
181
+ "File name to secret key. The value is mounted at /var/run/secrets/wkube/<file name> instead of "
182
+ "an env var, which keeps it out of env dumps and child processes."
183
+ )
184
+
185
+
174
186
  class JobDispatchModel(BaseModel):
175
187
  is_holder_job: bool = True
176
188
  build_only_task: bool = False
@@ -189,7 +201,8 @@ class JobDispatchModel(BaseModel):
189
201
  # is ignored if it is a callback and child of non-free node jobs
190
202
  required_storage_workflow: Optional[float] = None
191
203
 
192
- job_secrets: Optional[dict] = {}
204
+ job_secrets: Optional[Dict[str, str]] = Field({}, description=JOB_SECRETS_DESCRIPTION)
205
+ job_secret_files: Optional[Dict[str, str]] = Field({}, description=JOB_SECRET_FILES_DESCRIPTION)
193
206
 
194
207
  timeout: Optional[int] = None
195
208
  pvc_id: Optional[str] = None
@@ -210,7 +223,8 @@ class WKubeTaskMeta(BaseModel):
210
223
  # is ignored if it is a callback and child of non-free node jobs
211
224
  required_storage_workflow: Optional[float] = None
212
225
 
213
- job_secrets: Optional[dict] = {}
226
+ job_secrets: Optional[Dict[str, str]] = Field({}, description=JOB_SECRETS_DESCRIPTION)
227
+ job_secret_files: Optional[Dict[str, str]] = Field({}, description=JOB_SECRET_FILES_DESCRIPTION)
214
228
 
215
229
  timeout: int
216
230