accli 0.3__tar.gz → 0.4__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-0.3/accli.egg-info → accli-0.4}/PKG-INFO +3 -2
- {accli-0.3 → accli-0.4}/README.md +2 -1
- {accli-0.3 → accli-0.4}/accli/AcceleratorJobProjectService.py +80 -3
- accli-0.4/accli/AcceleratorTaskDispatcher.py +218 -0
- {accli-0.3 → accli-0.4}/accli/AcceleratorTerminalCliProjectService.py +24 -0
- {accli-0.3 → accli-0.4}/accli/__init__.py +3 -1
- {accli-0.3 → accli-0.4}/accli/_version.py +1 -1
- {accli-0.3 → accli-0.4}/accli/cli.py +53 -59
- accli-0.4/accli/token.py +77 -0
- {accli-0.3 → accli-0.4/accli.egg-info}/PKG-INFO +3 -2
- {accli-0.3 → accli-0.4}/accli.egg-info/SOURCES.txt +2 -0
- {accli-0.3 → accli-0.4}/accli.egg-info/requires.txt +1 -0
- {accli-0.3 → accli-0.4}/pyproject.toml +1 -1
- accli-0.4/requirements.txt +8 -0
- accli-0.3/requirements.txt +0 -6
- {accli-0.3 → accli-0.4}/.gitignore +0 -0
- {accli-0.3 → accli-0.4}/Dockerfile.dev +0 -0
- {accli-0.3 → accli-0.4}/LICENSE +0 -0
- {accli-0.3 → accli-0.4}/accli/CsvRegionalTimeseriesValidator.py +0 -0
- {accli-0.3 → accli-0.4}/accli/__main__.py +0 -0
- {accli-0.3 → accli-0.4}/accli.egg-info/dependency_links.txt +0 -0
- {accli-0.3 → accli-0.4}/accli.egg-info/entry_points.txt +0 -0
- {accli-0.3 → accli-0.4}/accli.egg-info/top_level.txt +0 -0
- {accli-0.3 → accli-0.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: accli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4
|
|
4
4
|
Summary: IIASA Accelerator Client
|
|
5
5
|
Author-email: Wrufesh S <wrufesh@gmail.com>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -81,6 +81,7 @@ Please follow [this link.](https://packaging.python.org/en/latest/tutorials/pack
|
|
|
81
81
|
**Release process**
|
|
82
82
|
1. Commit with right version on accli/_version.py
|
|
83
83
|
2. Run 'python scripts/tag.py'
|
|
84
|
-
3. python -m build
|
|
84
|
+
3. `python -m build`
|
|
85
|
+
4. `twine upload -r pypi -u __token__ -p <password-or-token> ./dist/*`
|
|
85
86
|
|
|
86
87
|
|
|
@@ -46,6 +46,7 @@ Please follow [this link.](https://packaging.python.org/en/latest/tutorials/pack
|
|
|
46
46
|
**Release process**
|
|
47
47
|
1. Commit with right version on accli/_version.py
|
|
48
48
|
2. Run 'python scripts/tag.py'
|
|
49
|
-
3. python -m build
|
|
49
|
+
3. `python -m build`
|
|
50
|
+
4. `twine upload -r pypi -u __token__ -p <password-or-token> ./dist/*`
|
|
50
51
|
|
|
51
52
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import io
|
|
3
|
+
import typing
|
|
1
4
|
import requests
|
|
2
5
|
import json
|
|
3
6
|
import base64
|
|
4
7
|
import urllib3
|
|
8
|
+
from pathlib import Path
|
|
5
9
|
from typing import List, Tuple
|
|
6
10
|
|
|
7
11
|
class AccAPIError(Exception):
|
|
@@ -16,7 +20,7 @@ class AcceleratorJobProjectService:
|
|
|
16
20
|
def __init__(
|
|
17
21
|
self,
|
|
18
22
|
user_token,
|
|
19
|
-
|
|
23
|
+
server_url='http://accelerator-api.iiasa.ac.at',
|
|
20
24
|
verify_cert=True
|
|
21
25
|
):
|
|
22
26
|
|
|
@@ -29,7 +33,7 @@ class AcceleratorJobProjectService:
|
|
|
29
33
|
else:
|
|
30
34
|
self.http_client = urllib3.poolmanager.PoolManager(cert_reqs="CERT_NONE", num_pools=1, retries=retries)
|
|
31
35
|
|
|
32
|
-
self.cli_base_url =
|
|
36
|
+
self.cli_base_url = f"{server_url}/v1/ajob-cli"
|
|
33
37
|
self.common_request_headers = {
|
|
34
38
|
'x-authorization': user_token
|
|
35
39
|
}
|
|
@@ -92,6 +96,36 @@ class AcceleratorJobProjectService:
|
|
|
92
96
|
if url:
|
|
93
97
|
resp = self.http_client_request("GET", url, preload_content=False)
|
|
94
98
|
return resp
|
|
99
|
+
|
|
100
|
+
def add_log_file(self, data: bytes, filename):
|
|
101
|
+
res = self.http_client_request(
|
|
102
|
+
"GET",
|
|
103
|
+
f"{self.cli_base_url}/presigned-upload-url/?filename={filename}",
|
|
104
|
+
headers=self.common_request_headers
|
|
105
|
+
).json()
|
|
106
|
+
|
|
107
|
+
upload_url = res['upload_url']
|
|
108
|
+
app_bucket_id = res['app_bucket_id']
|
|
109
|
+
res_filename = res['filename']
|
|
110
|
+
|
|
111
|
+
requests.put(
|
|
112
|
+
upload_url,
|
|
113
|
+
data=data,
|
|
114
|
+
# headers=headers,
|
|
115
|
+
verify=False,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
self.http_client_request(
|
|
119
|
+
"POST",
|
|
120
|
+
f"{self.cli_base_url}/register-log-file/",
|
|
121
|
+
json=dict(
|
|
122
|
+
filename=res_filename,
|
|
123
|
+
app_bucket_id=app_bucket_id
|
|
124
|
+
),
|
|
125
|
+
headers=self.common_request_headers
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
95
129
|
|
|
96
130
|
def get_multipart_put_create_signed_url(
|
|
97
131
|
self,
|
|
@@ -475,4 +509,47 @@ class AcceleratorJobProjectService:
|
|
|
475
509
|
)
|
|
476
510
|
),
|
|
477
511
|
headers=headers
|
|
478
|
-
)
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
class Fs:
|
|
515
|
+
@staticmethod
|
|
516
|
+
def write_stream_remote(filestream, dest_filepath, user_token, server_url):
|
|
517
|
+
|
|
518
|
+
accelerator_job_service = AcceleratorJobProjectService(
|
|
519
|
+
user_token,
|
|
520
|
+
server_url=server_url,
|
|
521
|
+
verify_cert=True
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
accelerator_job_service.add_filestream_as_job_output(
|
|
525
|
+
dest_filepath,
|
|
526
|
+
filestream
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
@staticmethod
|
|
530
|
+
def write_stream_local(filestream, dest_filepath):
|
|
531
|
+
with open(dest_filepath, 'wb') as file:
|
|
532
|
+
file.write(filestream.getvalue())
|
|
533
|
+
|
|
534
|
+
@staticmethod
|
|
535
|
+
def write_file(source: typing.Union[str, io.BytesIO], dest_filepath):
|
|
536
|
+
user_token = os.environ.get("ACC_JOB_JOB_TOKEN", None)
|
|
537
|
+
server_url = os.environ.get("ACC_JOB_GATEWAY_SERVER", None)
|
|
538
|
+
|
|
539
|
+
if isinstance(source, io.BytesIO):
|
|
540
|
+
if (user_token and server_url):
|
|
541
|
+
Fs.write_stream_remote(source, dest_filepath, user_token, server_url)
|
|
542
|
+
else:
|
|
543
|
+
Fs.write_stream_local(source, dest_filepath)
|
|
544
|
+
else:
|
|
545
|
+
if type(source) != str:
|
|
546
|
+
raise ValueError("Unknown source argument")
|
|
547
|
+
source_path = Path(source)
|
|
548
|
+
if not source_path.exists():
|
|
549
|
+
raise ValueError("Source path does not exist")
|
|
550
|
+
|
|
551
|
+
with open(source, 'wb') as fstream:
|
|
552
|
+
if (user_token and server_url):
|
|
553
|
+
Fs.write_stream_remote(fstream, dest_filepath, user_token, server_url)
|
|
554
|
+
else:
|
|
555
|
+
Fs.write_stream_local(fstream, dest_filepath)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import git
|
|
2
|
+
import os
|
|
3
|
+
import hashlib
|
|
4
|
+
import shutil
|
|
5
|
+
import tempfile
|
|
6
|
+
from typing import Optional, List, Dict
|
|
7
|
+
from pydantic import BaseModel, root_validator
|
|
8
|
+
|
|
9
|
+
from accli.token import get_github_app_token
|
|
10
|
+
|
|
11
|
+
FOLDER_JOB_REPO_URL = 'https://github.com/IIASA-Accelerator/wkube-job.git'
|
|
12
|
+
|
|
13
|
+
def hash_folder(folder_path):
|
|
14
|
+
md5_hash = hashlib.md5()
|
|
15
|
+
sha256_hash = hashlib.sha256()
|
|
16
|
+
|
|
17
|
+
for root, _, files in os.walk(folder_path):
|
|
18
|
+
for file_name in files:
|
|
19
|
+
file_path = os.path.join(root, file_name)
|
|
20
|
+
try:
|
|
21
|
+
with open(file_path, 'rb') as f:
|
|
22
|
+
# Read and update MD5 and SHA256 hash object with file content
|
|
23
|
+
for chunk in iter(lambda: f.read(4096), b""):
|
|
24
|
+
md5_hash.update(chunk)
|
|
25
|
+
sha256_hash.update(chunk)
|
|
26
|
+
except IOError:
|
|
27
|
+
# Handle read error (if any)
|
|
28
|
+
print(f"Error reading file: {file_path}")
|
|
29
|
+
|
|
30
|
+
# Get hexadecimal digest of hashes
|
|
31
|
+
md5_digest = md5_hash.hexdigest()
|
|
32
|
+
# sha256_digest = sha256_hash.hexdigest()
|
|
33
|
+
|
|
34
|
+
return md5_digest
|
|
35
|
+
|
|
36
|
+
def copy_tree(src, dst):
|
|
37
|
+
"""Recursively copy from src to dst, excluding .git folders."""
|
|
38
|
+
for item in os.listdir(src):
|
|
39
|
+
src_path = os.path.join(src, item)
|
|
40
|
+
dst_path = os.path.join(dst, item)
|
|
41
|
+
|
|
42
|
+
if os.path.isdir(src_path):
|
|
43
|
+
if item == '.git':
|
|
44
|
+
continue
|
|
45
|
+
os.makedirs(dst_path, exist_ok=True)
|
|
46
|
+
copy_tree(src_path, dst_path)
|
|
47
|
+
else:
|
|
48
|
+
shutil.copy2(src_path, dst_path)
|
|
49
|
+
|
|
50
|
+
def push_folder_job(dir):
|
|
51
|
+
|
|
52
|
+
repo_dir = tempfile.mkdtemp()
|
|
53
|
+
|
|
54
|
+
copy_tree(dir, repo_dir)
|
|
55
|
+
|
|
56
|
+
branch_name = hash_folder(repo_dir)
|
|
57
|
+
|
|
58
|
+
repo = git.Repo.init(repo_dir)
|
|
59
|
+
|
|
60
|
+
git_token = get_github_app_token()
|
|
61
|
+
|
|
62
|
+
remote_url = f'https://x-access-token:{git_token}@{FOLDER_JOB_REPO_URL.split("https://")[1]}'
|
|
63
|
+
remote = repo.create_remote("accelerator_job_repo", remote_url)
|
|
64
|
+
|
|
65
|
+
remote.pull('master')
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
repo.git.checkout(branch_name)
|
|
69
|
+
except git.exc.GitCommandError:
|
|
70
|
+
repo.git.checkout('-b', branch_name)
|
|
71
|
+
|
|
72
|
+
repo.git.add('.')
|
|
73
|
+
|
|
74
|
+
repo.index.commit(branch_name)
|
|
75
|
+
|
|
76
|
+
# TODO see what happens with the same checksum
|
|
77
|
+
remote.push(branch_name)
|
|
78
|
+
shutil.rmtree(repo_dir)
|
|
79
|
+
|
|
80
|
+
return FOLDER_JOB_REPO_URL, branch_name
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class JobDispatchModel(BaseModel):
|
|
84
|
+
is_holder_job: bool = True
|
|
85
|
+
|
|
86
|
+
execute_cluster: str
|
|
87
|
+
job_location: str
|
|
88
|
+
job_args: List
|
|
89
|
+
job_kwargs: Dict
|
|
90
|
+
|
|
91
|
+
required_cores: Optional[float]
|
|
92
|
+
required_ram: Optional[float]
|
|
93
|
+
required_storage_local: Optional[float]
|
|
94
|
+
|
|
95
|
+
# is ignored if it is a callback and child of non free node jobs
|
|
96
|
+
required_storage_workflow: Optional[float]
|
|
97
|
+
|
|
98
|
+
job_secrets: Optional[dict] = {}
|
|
99
|
+
|
|
100
|
+
timeout: Optional[int]
|
|
101
|
+
pvc_id: Optional[str]
|
|
102
|
+
node_id: Optional[str]
|
|
103
|
+
|
|
104
|
+
ignore_duplicate_job: bool = False
|
|
105
|
+
free_node: bool = False # Only applies to immediate children jobs
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
children: List['JobDispatchModel'] = []
|
|
110
|
+
callback: Optional['JobDispatchModel']
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class WKubeTaskMeta(BaseModel):
|
|
114
|
+
required_cores: float
|
|
115
|
+
required_ram: float
|
|
116
|
+
required_storage_local: float
|
|
117
|
+
|
|
118
|
+
# is ignored if it is a callback and child of non free node jobs
|
|
119
|
+
required_storage_workflow: float
|
|
120
|
+
|
|
121
|
+
job_secrets: Optional[dict] = {}
|
|
122
|
+
|
|
123
|
+
timeout:int
|
|
124
|
+
|
|
125
|
+
class WKubeTaskKwargs(BaseModel):
|
|
126
|
+
docker_image: Optional[str]
|
|
127
|
+
|
|
128
|
+
job_folder: Optional[str]
|
|
129
|
+
|
|
130
|
+
repo_url: Optional[str] # required when docker image is not present
|
|
131
|
+
repo_branch: Optional[str] # required when docker image is not present
|
|
132
|
+
|
|
133
|
+
docker_filename: Optional[str] # when not docker image;
|
|
134
|
+
base_stack: Optional[str] # when not github docker file #TODO add enum class of available base stack
|
|
135
|
+
|
|
136
|
+
force_build: bool = False
|
|
137
|
+
|
|
138
|
+
command: str # may not be present with docker_image # TODO wrap a command in custom script to implement timeout or possibly log ingestion if required.
|
|
139
|
+
|
|
140
|
+
conf: Dict[str,str] = {}
|
|
141
|
+
|
|
142
|
+
def dict(self, *args, **kwargs):
|
|
143
|
+
result = super().dict(*args, **kwargs)
|
|
144
|
+
if 'job_folder' in result:
|
|
145
|
+
del result['job_folder']
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
@root_validator(pre=True)
|
|
149
|
+
def validate_root(cls, values):
|
|
150
|
+
if not values.get('docker_image'):
|
|
151
|
+
|
|
152
|
+
job_folder = values.get('job_folder')
|
|
153
|
+
|
|
154
|
+
if not job_folder:
|
|
155
|
+
|
|
156
|
+
if not values.get('repo_url'):
|
|
157
|
+
# TODO if has no repo url just set it
|
|
158
|
+
raise ValueError("repo_url is required")
|
|
159
|
+
|
|
160
|
+
if not values.get('repo_branch'):
|
|
161
|
+
raise ValueError('repo_branch is required')
|
|
162
|
+
else:
|
|
163
|
+
remote_url, branch_name = push_folder_job(job_folder)
|
|
164
|
+
values['repo_url'] = remote_url
|
|
165
|
+
values['repo_branch'] = branch_name
|
|
166
|
+
|
|
167
|
+
if not values.get('docker_filename'):
|
|
168
|
+
if not values.get("base_stack"):
|
|
169
|
+
raise ValueError("base_stack is required when dockerfile is not defined")
|
|
170
|
+
return values
|
|
171
|
+
|
|
172
|
+
class WKubeTaskPydantic(WKubeTaskMeta, WKubeTaskKwargs):
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class GenericTask:
|
|
177
|
+
def __init__(self, *args, **kwargs):
|
|
178
|
+
self.dispatch_model_task: JobDispatchModel
|
|
179
|
+
|
|
180
|
+
def add_child(self, task):
|
|
181
|
+
if self.__class__ != task.__class__:
|
|
182
|
+
raise ValueError(f"task should of {self.__class__} class")
|
|
183
|
+
self.dispatch_model_task.children.append(task.dispatch_model_task)
|
|
184
|
+
|
|
185
|
+
def add_callback(self, task):
|
|
186
|
+
|
|
187
|
+
if self.__class__ != task.__class__:
|
|
188
|
+
raise ValueError(f"task should of {self.__class__} class")
|
|
189
|
+
self.dispatch_model_task.callback = task.dispatch_model_task
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def description(self):
|
|
193
|
+
return self.dispatch_model_task.dict()
|
|
194
|
+
|
|
195
|
+
class WKubeTask(GenericTask):
|
|
196
|
+
def __init__(self, *t_args, **t_kwargs):
|
|
197
|
+
|
|
198
|
+
wkube_task_kwargs = None
|
|
199
|
+
wkube_task_meta = dict()
|
|
200
|
+
|
|
201
|
+
if (t_args or t_kwargs):
|
|
202
|
+
|
|
203
|
+
WKubeTaskPydantic(*t_args, **t_kwargs)
|
|
204
|
+
wkube_task_kwargs = WKubeTaskKwargs(*t_args, **t_kwargs)
|
|
205
|
+
wkube_task_meta.update(WKubeTaskMeta(*t_args, **t_kwargs).dict())
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
self.dispatch_model_task = JobDispatchModel(
|
|
209
|
+
is_holder_job=not (t_args or t_kwargs),
|
|
210
|
+
execute_cluster='WKUBE',
|
|
211
|
+
job_location='acc_native_jobs.dispatch_wkube_task',
|
|
212
|
+
job_args=[],
|
|
213
|
+
job_kwargs= wkube_task_kwargs.dict() if wkube_task_kwargs else dict(),
|
|
214
|
+
**wkube_task_meta
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
@@ -70,6 +70,30 @@ class AcceleratorTerminalCliProjectService:
|
|
|
70
70
|
|
|
71
71
|
return res.json()
|
|
72
72
|
|
|
73
|
+
def get_github_app_token(self, project_slug):
|
|
74
|
+
|
|
75
|
+
res = self.http_client_request(
|
|
76
|
+
"GET",
|
|
77
|
+
f"{self.cli_base_url}/{project_slug}/github-app-token/",
|
|
78
|
+
headers=self.common_request_headers
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return res.json()
|
|
82
|
+
|
|
83
|
+
def dispatch(self, project_slug, job_description):
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
res = self.http_client_request(
|
|
87
|
+
"POST",
|
|
88
|
+
f"{self.cli_base_url}/{project_slug}/jobs/dispatch/",
|
|
89
|
+
json=job_description,
|
|
90
|
+
headers=self.common_request_headers
|
|
91
|
+
)
|
|
92
|
+
except AccAPIError as err:
|
|
93
|
+
raise err
|
|
94
|
+
|
|
95
|
+
return res.json()['job_id']
|
|
96
|
+
|
|
73
97
|
def get_dataset_template_details(self, project_slug, template_slug):
|
|
74
98
|
|
|
75
99
|
try:
|
|
@@ -2,7 +2,9 @@ from accli._version import VERSION
|
|
|
2
2
|
|
|
3
3
|
from accli.cli import app
|
|
4
4
|
|
|
5
|
-
from accli.
|
|
5
|
+
from accli.AcceleratorTaskDispatcher import *
|
|
6
|
+
|
|
7
|
+
from accli.AcceleratorJobProjectService import AcceleratorJobProjectService, Fs
|
|
6
8
|
|
|
7
9
|
AjobCliService = AcceleratorJobProjectService
|
|
8
10
|
|
|
@@ -5,83 +5,27 @@ import os
|
|
|
5
5
|
import typer
|
|
6
6
|
import requests
|
|
7
7
|
import warnings
|
|
8
|
+
import importlib.util
|
|
8
9
|
from rich import print
|
|
9
10
|
from typing_extensions import Annotated
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
from accli.token import save_token_details, get_token, get_server_url, set_github_app_token
|
|
11
13
|
|
|
12
14
|
from accli.CsvRegionalTimeseriesValidator import CsvRegionalTimeseriesValidator
|
|
13
15
|
from ._version import VERSION
|
|
14
16
|
|
|
15
|
-
|
|
16
17
|
from accli.AcceleratorTerminalCliProjectService import AcceleratorTerminalCliProjectService
|
|
17
18
|
|
|
18
19
|
from rich.progress import Progress, SpinnerColumn, TextColumn, ProgressColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn, TimeRemainingColumn
|
|
19
20
|
|
|
20
21
|
warnings.filterwarnings('ignore')
|
|
21
22
|
|
|
22
|
-
|
|
23
23
|
app = typer.Typer(
|
|
24
24
|
add_completion=False,
|
|
25
25
|
pretty_exceptions_show_locals=False,
|
|
26
26
|
no_args_is_help=True
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
-
def get_db_path():
|
|
30
|
-
|
|
31
|
-
home = os.path.expanduser("~")
|
|
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
|
-
|
|
42
|
-
db_path = get_db_path()
|
|
43
|
-
|
|
44
|
-
try:
|
|
45
|
-
os.remove(db_path)
|
|
46
|
-
except OSError:
|
|
47
|
-
pass
|
|
48
|
-
|
|
49
|
-
db = TinyDB(db_path)
|
|
50
|
-
db.insert({
|
|
51
|
-
'token': token,
|
|
52
|
-
'server_url': server_url,
|
|
53
|
-
'webcli_url': webcli_url
|
|
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 get_server_url():
|
|
72
|
-
db_path = get_db_path()
|
|
73
|
-
|
|
74
|
-
db = TinyDB(db_path)
|
|
75
|
-
|
|
76
|
-
for item in db:
|
|
77
|
-
server_url = item.get('server_url')
|
|
78
|
-
if server_url:
|
|
79
|
-
break
|
|
80
|
-
|
|
81
|
-
if not server_url:
|
|
82
|
-
print("Server url does not exists. Please login.")
|
|
83
|
-
return server_url
|
|
84
|
-
|
|
85
29
|
|
|
86
30
|
def get_size(path):
|
|
87
31
|
size = 0
|
|
@@ -158,6 +102,9 @@ def upload(
|
|
|
158
102
|
folder_name: Annotated[str, typer.Argument(help="Name of the folder to be made in Accelerator project space.")],
|
|
159
103
|
max_workers: Annotated[int, typer.Option(help="Maximum worker pool for multipart upload.")] = os.cpu_count()
|
|
160
104
|
):
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
#TODO make user free to put anywere except for reserved folder
|
|
161
108
|
|
|
162
109
|
if not re.fullmatch(r'[a-zA-Z0-9\-\_]+', folder_name):
|
|
163
110
|
raise ValueError("Folder name is invalid.")
|
|
@@ -226,3 +173,50 @@ def validate(
|
|
|
226
173
|
|
|
227
174
|
validate()
|
|
228
175
|
|
|
176
|
+
@app.command()
|
|
177
|
+
def dispatch(
|
|
178
|
+
project_slug: Annotated[str, typer.Argument(help="Unique Accelerator project slug.")],
|
|
179
|
+
# experiment_name: Annotated[str, typer.Argument(help="Experiment or simulation name. Please put name in quote it includes space(s).")],
|
|
180
|
+
workflow_file: Annotated[str, typer.Argument(help="Python workflow filepath.")],
|
|
181
|
+
root_task_variable: Annotated[str, typer.Argument(help="Root task variable in workflow_file.")],
|
|
182
|
+
server: Annotated[str, typer.Option(help="Accelerator server url.")] = "https://accelerator-api.iiasa.ac.at",
|
|
183
|
+
):
|
|
184
|
+
|
|
185
|
+
access_token = get_token()
|
|
186
|
+
|
|
187
|
+
server_url = get_server_url()
|
|
188
|
+
|
|
189
|
+
term_cli_project_service = AcceleratorTerminalCliProjectService(
|
|
190
|
+
user_token=access_token,
|
|
191
|
+
server_url=server_url,
|
|
192
|
+
verify_cert=False
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
github_app_token = term_cli_project_service.get_github_app_token(project_slug)
|
|
196
|
+
set_github_app_token(github_app_token)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
spec = importlib.util.spec_from_file_location("workflow", workflow_file)
|
|
200
|
+
|
|
201
|
+
module = importlib.util.module_from_spec(spec)
|
|
202
|
+
|
|
203
|
+
spec.loader.exec_module(module)
|
|
204
|
+
|
|
205
|
+
job_to_dispatch = getattr(module, root_task_variable)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if not job_to_dispatch:
|
|
209
|
+
raise ValueError(f"No root task variable found with name {root_task_variable}")
|
|
210
|
+
|
|
211
|
+
print(job_to_dispatch.description)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
root_job_id = term_cli_project_service.dispatch(
|
|
215
|
+
project_slug,
|
|
216
|
+
job_to_dispatch.description
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
print(f"Dispatched root job #ID: {root_job_id}")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
accli-0.4/accli/token.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from tinydb import TinyDB, Query
|
|
3
|
+
|
|
4
|
+
def get_db_path():
|
|
5
|
+
|
|
6
|
+
home = os.path.expanduser("~")
|
|
7
|
+
token_directory = f"{home}/.accli"
|
|
8
|
+
|
|
9
|
+
if not os.path.exists(token_directory):
|
|
10
|
+
os.makedirs(token_directory)
|
|
11
|
+
|
|
12
|
+
return f"{token_directory}/data.json"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def save_token_details(token, server_url, webcli_url):
|
|
16
|
+
|
|
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
|
+
def get_token():
|
|
32
|
+
db_path = get_db_path()
|
|
33
|
+
|
|
34
|
+
db = TinyDB(db_path)
|
|
35
|
+
|
|
36
|
+
for item in db:
|
|
37
|
+
token = item.get('token')
|
|
38
|
+
if token:
|
|
39
|
+
break
|
|
40
|
+
|
|
41
|
+
if not token:
|
|
42
|
+
print("Token does not exists. Please login.")
|
|
43
|
+
return token
|
|
44
|
+
|
|
45
|
+
def get_github_app_token():
|
|
46
|
+
db_path = get_db_path()
|
|
47
|
+
|
|
48
|
+
db = TinyDB(db_path)
|
|
49
|
+
|
|
50
|
+
for item in db:
|
|
51
|
+
token = item.get('github_app_token')
|
|
52
|
+
if token:
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
if not token:
|
|
56
|
+
print("Github app token does not exists.")
|
|
57
|
+
return token
|
|
58
|
+
|
|
59
|
+
def set_github_app_token(github_app_token):
|
|
60
|
+
db_path = get_db_path()
|
|
61
|
+
db = TinyDB(db_path)
|
|
62
|
+
db.update({'github_app_token': github_app_token}, doc_ids=[1])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_server_url():
|
|
66
|
+
db_path = get_db_path()
|
|
67
|
+
|
|
68
|
+
db = TinyDB(db_path)
|
|
69
|
+
|
|
70
|
+
for item in db:
|
|
71
|
+
server_url = item.get('server_url')
|
|
72
|
+
if server_url:
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
if not server_url:
|
|
76
|
+
print("Server url does not exists. Please login.")
|
|
77
|
+
return server_url
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: accli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4
|
|
4
4
|
Summary: IIASA Accelerator Client
|
|
5
5
|
Author-email: Wrufesh S <wrufesh@gmail.com>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -81,6 +81,7 @@ Please follow [this link.](https://packaging.python.org/en/latest/tutorials/pack
|
|
|
81
81
|
**Release process**
|
|
82
82
|
1. Commit with right version on accli/_version.py
|
|
83
83
|
2. Run 'python scripts/tag.py'
|
|
84
|
-
3. python -m build
|
|
84
|
+
3. `python -m build`
|
|
85
|
+
4. `twine upload -r pypi -u __token__ -p <password-or-token> ./dist/*`
|
|
85
86
|
|
|
86
87
|
|
|
@@ -5,12 +5,14 @@ README.md
|
|
|
5
5
|
pyproject.toml
|
|
6
6
|
requirements.txt
|
|
7
7
|
accli/AcceleratorJobProjectService.py
|
|
8
|
+
accli/AcceleratorTaskDispatcher.py
|
|
8
9
|
accli/AcceleratorTerminalCliProjectService.py
|
|
9
10
|
accli/CsvRegionalTimeseriesValidator.py
|
|
10
11
|
accli/__init__.py
|
|
11
12
|
accli/__main__.py
|
|
12
13
|
accli/_version.py
|
|
13
14
|
accli/cli.py
|
|
15
|
+
accli/token.py
|
|
14
16
|
accli.egg-info/PKG-INFO
|
|
15
17
|
accli.egg-info/SOURCES.txt
|
|
16
18
|
accli.egg-info/dependency_links.txt
|
|
@@ -12,7 +12,7 @@ description = "IIASA Accelerator Client"
|
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
license = {file = "LICENSE"}
|
|
14
14
|
requires-python = ">=3.7.17"
|
|
15
|
-
dependencies = ['urllib3', 'typer[all]', 'tinydb', 'requests', 'jsonschema']
|
|
15
|
+
dependencies = ['urllib3', 'typer[all]', 'tinydb', 'requests', 'jsonschema', 'pydantic==1.10.13']
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Programming Language :: Python :: 3",
|
|
18
18
|
"License :: OSI Approved :: MIT License",
|
accli-0.3/requirements.txt
DELETED
|
File without changes
|
|
File without changes
|
{accli-0.3 → accli-0.4}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|