accli 0.1.2__tar.gz → 0.1.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: accli
3
- Version: 0.1.2
3
+ Version: 0.1.3.1
4
4
  Summary: IIASA Accelerator Client
5
5
  Author-email: Wrufesh S <wrufesh@gmail.com>
6
6
  License: The MIT License (MIT)
@@ -33,6 +33,9 @@ Requires-Python: >=3.7
33
33
  Description-Content-Type: text/markdown
34
34
  License-File: LICENSE
35
35
  Requires-Dist: urllib3
36
+ Requires-Dist: typer[all]
37
+ Requires-Dist: tinydb
38
+ Requires-Dist: requests
36
39
 
37
40
  ## Accelerator python client
38
41
 
@@ -11,11 +11,11 @@ class AccAPIError(Exception):
11
11
  # self.response_data = kwargs.pop('response_data')
12
12
  # super().__init__(*args, **kwargs)
13
13
 
14
- class AcceleratorProjectService:
14
+ class AcceleratorJobProjectService:
15
15
  def __init__(
16
16
  self,
17
17
  user_token,
18
- cli_base_url='http://accelerator-api/v1/acli',
18
+ job_cli_base_url='http://accelerator-api/v1/ajob-cli',
19
19
  verify_cert=True
20
20
  ):
21
21
 
@@ -26,7 +26,7 @@ class AcceleratorProjectService:
26
26
  else:
27
27
  self.http_client = urllib3.poolmanager.PoolManager(cert_reqs="CERT_NONE", num_pools=1)
28
28
 
29
- self.cli_base_url = cli_base_url
29
+ self.cli_base_url = job_cli_base_url
30
30
  self.common_request_headers = {
31
31
  'x-authorization': user_token
32
32
  }
@@ -63,6 +63,15 @@ class AcceleratorProjectService:
63
63
  if res.data:
64
64
  return res.json()
65
65
 
66
+ def get_bucket_object_validation_details(self, bucket_object_id):
67
+ res = self.http_client_request(
68
+ "GET",
69
+ f"{self.cli_base_url}/validation-detail/{bucket_object_id}",
70
+ headers=self.common_request_headers
71
+ )
72
+ if res.data:
73
+ return res.json()
74
+
66
75
 
67
76
  def get_file_url(self, bucket_object_id):
68
77
  res = self.http_client_request(
@@ -299,7 +308,7 @@ class AcceleratorProjectService:
299
308
  headers = dict()
300
309
  headers["Content-Type"] = "application/octet-stream"
301
310
 
302
- part_size, part_count = 5 * 1024**2, -1
311
+ part_size, part_count = 50 * 1024**2, -1
303
312
 
304
313
  upload_id = None
305
314
  app_bucket_id = None
@@ -376,7 +385,7 @@ class AcceleratorProjectService:
376
385
  headers = dict()
377
386
  headers["Content-Type"] = "application/octet-stream"
378
387
 
379
- part_size, part_count = 5 * 1024**2, -1
388
+ part_size, part_count = 50 * 1024**2, -1
380
389
 
381
390
  upload_id = None
382
391
  app_bucket_id = None
@@ -0,0 +1,272 @@
1
+ import os
2
+ import requests
3
+ import urllib3
4
+ import json
5
+ import base64
6
+ import concurrent.futures
7
+ from rich.progress import Progress
8
+
9
+
10
+ class AccAPIError(Exception):
11
+
12
+ def __init__(self, message, response, status_code):
13
+
14
+ super().__init__(message)
15
+
16
+ self.response = response
17
+ self.status_code = status_code
18
+
19
+ class AcceleratorTerminalCliProjectService:
20
+ def __init__(
21
+ self,
22
+ user_token,
23
+ cli_base_url='http://accelerator-api/v1/aterm-cli',
24
+ verify_cert=True
25
+ ):
26
+
27
+ self.user_token = user_token
28
+
29
+ if verify_cert:
30
+ self.http_client = urllib3.poolmanager.PoolManager(num_pools=1)
31
+ else:
32
+ self.http_client = urllib3.poolmanager.PoolManager(cert_reqs="CERT_NONE", num_pools=1)
33
+
34
+ self.cli_base_url = cli_base_url
35
+ self.common_request_headers = {
36
+ 'x-authorization': user_token
37
+ }
38
+
39
+ def http_client_request(self, *args, **kwargs):
40
+ res = self.http_client.request(*args, **kwargs)
41
+
42
+ if str(res.status)[0] in ['4', '5']:
43
+ raise AccAPIError(
44
+ f"Accelerator api error:: status_code={res.status} :: response_data={res.data}",
45
+ status_code=res.status,
46
+ response=res
47
+ )
48
+
49
+ return res
50
+
51
+ def get_file_stat(self, project_slug, filename):
52
+
53
+ try:
54
+ res = self.http_client_request(
55
+ "POST",
56
+ f"{self.cli_base_url}/{project_slug}/file-stat/",
57
+ json=dict(filename=filename),
58
+ headers=self.common_request_headers
59
+ )
60
+ except AccAPIError as err:
61
+ if err.status_code == 404:
62
+ return None
63
+ else:
64
+ raise err
65
+
66
+ return res.json()
67
+
68
+
69
+ def get_multipart_put_create_signed_url(
70
+ self,
71
+ project_slug,
72
+ app_bucket_id,
73
+ object_name,
74
+ upload_id,
75
+ part_number
76
+ ):
77
+ res = self.http_client_request(
78
+ "GET",
79
+ f"{self.cli_base_url}/{project_slug}/put-multipart-signed-url",
80
+ fields=dict(
81
+ app_bucket_id=app_bucket_id,
82
+ object_name=object_name,
83
+ upload_id=upload_id,
84
+ part_number=part_number
85
+ ),
86
+ headers=self.common_request_headers
87
+ )
88
+
89
+ return res.json()
90
+
91
+
92
+
93
+ def get_put_create_multipart_upload_id(self, project_slug, filename):
94
+
95
+ b64_filename = base64.b64encode(filename.encode()).decode()
96
+
97
+ res = self.http_client_request(
98
+ "GET",
99
+ f"{self.cli_base_url}/{project_slug}/create-multipart-upload-id/{b64_filename}",
100
+ headers=self.common_request_headers
101
+ )
102
+
103
+ data = res.json()
104
+
105
+ return data['upload_id'], data['app_bucket_id'], data['uniqified_filename']
106
+
107
+
108
+ def complete_create_multipart_upload(
109
+ self,
110
+ project_slug,
111
+ app_bucket_id,
112
+ filename,
113
+ upload_id,
114
+ parts: list[tuple[str, str]]
115
+ ):
116
+ headers = {"Content-Type": "application/json"}
117
+
118
+ headers.update(self.common_request_headers)
119
+
120
+ res = self.http_client_request(
121
+ "PUT",
122
+ f"{self.cli_base_url}/{project_slug}/complete-create-multipart-upload",
123
+ json=dict(
124
+ app_bucket_id=app_bucket_id,
125
+ filename=filename,
126
+ upload_id=upload_id,
127
+ parts=base64.b64encode(json.dumps(parts).encode()).decode()
128
+ ),
129
+ headers=headers
130
+ )
131
+
132
+ return res.json()
133
+
134
+
135
+ def abort_create_multipart_upload(self, project_slug, app_bucket_id, filename, upload_id):
136
+
137
+ headers = {"Content-Type": "application/json"}
138
+
139
+ headers.update(self.common_request_headers)
140
+
141
+ res = self.http_client_request(
142
+ "PUT",
143
+ f"{self.cli_base_url}/{project_slug}/abort-create-multipart-upload",
144
+ json=dict(
145
+ app_bucket_id=app_bucket_id,
146
+ filename=filename,
147
+ upload_id=upload_id
148
+ ),
149
+ headers=headers
150
+ )
151
+
152
+
153
+
154
+ def read_part_data(self, stream, size, part_data=b"", progress=None):
155
+ """Read part data of given size from stream."""
156
+ size -= len(part_data)
157
+ while size:
158
+ data = stream.read(size)
159
+ if not data:
160
+ break # EOF reached
161
+ if not isinstance(data, bytes):
162
+ raise ValueError("read() must return 'bytes' object")
163
+ part_data += data
164
+ size -= len(data)
165
+ if progress:
166
+ progress.update(len(data))
167
+ return part_data
168
+
169
+
170
+ def put_part(self, project_slug, app_bucket_id, uniqified_filename, upload_id, part_number, part_data, progress, task):
171
+
172
+ put_presigned_url = self.get_multipart_put_create_signed_url(
173
+ project_slug, app_bucket_id, uniqified_filename, upload_id, part_number
174
+ )
175
+
176
+ part_upload_response = requests.put(
177
+ put_presigned_url,
178
+ data=part_data,
179
+ # headers=headers,
180
+ verify=False,
181
+ )
182
+
183
+ progress.update(task, advance=len(part_data))
184
+
185
+ etag = part_upload_response.headers.get("etag").replace('"', "")
186
+ return part_number, etag
187
+
188
+
189
+ def upload_filestream_to_accelerator(self, project_slug, filename, file_stream, progress, task, max_workers=os.cpu_count()):
190
+ headers = dict()
191
+ headers["Content-Type"] = "application/octet-stream"
192
+
193
+ part_size, part_count = 200 * 1024**2, -1
194
+
195
+ upload_id = None
196
+ app_bucket_id = None
197
+ uniqified_filename = None
198
+
199
+ one_byte = b""
200
+ stop = False
201
+ part_number = 0
202
+ parts = []
203
+ uploaded_size = 0
204
+ put_presigned_url = None
205
+
206
+ futures = []
207
+
208
+ try:
209
+
210
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
211
+
212
+ while not stop:
213
+ part_number += 1
214
+ part_data = self.read_part_data(
215
+ file_stream,
216
+ part_size + 1,
217
+ one_byte,
218
+ progress=None,
219
+ )
220
+
221
+ # If part_data_size is less or equal to part_size,
222
+ # then we have reached last part.
223
+ if len(part_data) <= part_size:
224
+ part_count = part_number
225
+ stop = True
226
+ else:
227
+ one_byte = part_data[-1:]
228
+ part_data = part_data[:-1]
229
+
230
+ uploaded_size += len(part_data)
231
+
232
+ if not upload_id:
233
+ (
234
+ upload_id,
235
+ app_bucket_id,
236
+ uniqified_filename,
237
+ ) = self.get_put_create_multipart_upload_id(
238
+ project_slug,
239
+ filename,
240
+ # headers=headers
241
+ )
242
+
243
+
244
+ future = executor.submit(self.put_part, project_slug, app_bucket_id, uniqified_filename, upload_id, part_number, part_data, progress, task)
245
+
246
+ futures.append(future)
247
+
248
+ futures_results = concurrent.futures.wait(futures, return_when=concurrent.futures.ALL_COMPLETED)
249
+
250
+ parts = [f.result() for f in futures_results.done]
251
+
252
+ parts.sort(key=lambda x: x[0])
253
+
254
+
255
+ created_bucket_object_id = self.complete_create_multipart_upload(
256
+ project_slug, app_bucket_id, uniqified_filename, upload_id, parts
257
+ )
258
+ return created_bucket_object_id
259
+
260
+ except Exception as err:
261
+ # Cancel if any error
262
+ if upload_id:
263
+ self.abort_create_multipart_upload(
264
+ project_slug,
265
+ app_bucket_id,
266
+ uniqified_filename,
267
+ upload_id,
268
+ )
269
+
270
+ raise err
271
+
272
+
@@ -0,0 +1,6 @@
1
+ from .AcceleratorJobProjectService import AcceleratorJobProjectService
2
+
3
+ from .cli import app
4
+
5
+ AjobCliService = AcceleratorJobProjectService
6
+
@@ -0,0 +1,4 @@
1
+ from .cli import app as cli_app
2
+
3
+ if __name__ == "__main__":
4
+ cli_app()
@@ -0,0 +1,179 @@
1
+ import re
2
+ import glob
3
+ import os
4
+ import typer
5
+ import requests
6
+ from rich import print
7
+ from typing_extensions import Annotated
8
+ from tinydb import TinyDB, Query
9
+
10
+ from .AcceleratorTerminalCliProjectService import AcceleratorTerminalCliProjectService
11
+
12
+ from rich.progress import Progress, SpinnerColumn, TextColumn, ProgressColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn, TimeRemainingColumn
13
+
14
+
15
+ app = typer.Typer()
16
+
17
+ def get_db_path():
18
+
19
+ home = os.path.expanduser("~")
20
+ token_directory = f"{home}/.accli"
21
+
22
+ if not os.path.exists(token_directory):
23
+ os.makedirs(token_directory)
24
+
25
+ return f"{token_directory}/data.json"
26
+
27
+
28
+ def save_token_details(token, server_url, webcli_url):
29
+
30
+ db_path = get_db_path()
31
+
32
+ try:
33
+ os.remove(db_path)
34
+ except OSError:
35
+ pass
36
+
37
+ db = TinyDB(db_path)
38
+ db.insert({
39
+ 'token': token,
40
+ 'server_url': server_url,
41
+ 'webcli_url': webcli_url
42
+ })
43
+
44
+ def get_token():
45
+ db_path = get_db_path()
46
+
47
+ db = TinyDB(db_path)
48
+
49
+ for item in db:
50
+ token = item.get('token')
51
+ if token:
52
+ break
53
+
54
+ if not token:
55
+ print("Token does not exists. Please login.")
56
+ return token
57
+
58
+
59
+ def get_server_url():
60
+ db_path = get_db_path()
61
+
62
+ db = TinyDB(db_path)
63
+
64
+ for item in db:
65
+ server_url = item.get('server_url')
66
+ if server_url:
67
+ break
68
+
69
+ if not server_url:
70
+ print("Server url does not exists. Please login.")
71
+ return server_url
72
+
73
+
74
+ def get_size(path):
75
+ size = 0
76
+
77
+ for file in glob.iglob(f"{path}/**/*.*", recursive=True):
78
+
79
+ size += os.path.getsize(file)
80
+
81
+ return size
82
+
83
+
84
+
85
+ @app.command()
86
+ def login(
87
+ server: Annotated[str, typer.Option(help="Accelerator server url.")] = "https://acceleratoapi.iiasa.ac.at",
88
+ webcli: Annotated[str, typer.Option(help="Accelerator web client for authorization.")] = "https://accelerator.iiasa.ac.at"
89
+ ):
90
+ print(
91
+ f"Welcome to Accelerator Terminal Client.\n"
92
+ f"Powered by IIASA\n"
93
+ )
94
+
95
+ print(f"Get authorization code [link={webcli}/acli-auth-code]here[/link].\n")
96
+
97
+ device_authorization_code = typer.prompt("Enter the authorization code?")
98
+
99
+ token_response = requests.post(
100
+ f"{server}/v1/oauth/device/token/",
101
+ json={"device_authorization_code": device_authorization_code}
102
+ )
103
+
104
+ print("")
105
+
106
+ if token_response.status_code == 400:
107
+ print(f"ERROR: {token_response.json().get('detail')}")
108
+ raise typer.Exit(1)
109
+
110
+ save_token_details(token_response.json(), server, webcli)
111
+
112
+ print("Successfully logged in.:rocket: :rocket:")
113
+
114
+ def upload_file(project_slug, accelerator_filename, local_filepath, progress, task, folder_name, max_workers=os.cpu_count()):
115
+
116
+ access_token = get_token()
117
+
118
+ server_url = get_server_url()
119
+
120
+ term_cli_project_service = AcceleratorTerminalCliProjectService(
121
+ user_token=access_token,
122
+ cli_base_url=f"{server_url}/v1/aterm-cli",
123
+ verify_cert=False
124
+ )
125
+
126
+ stat = term_cli_project_service.get_file_stat(project_slug, f"{folder_name}/{accelerator_filename}")
127
+
128
+ if stat:
129
+ progress.update(task, advance=stat.get('size'))
130
+ else:
131
+
132
+ with open(local_filepath, 'rb') as file_stream:
133
+ term_cli_project_service.upload_filestream_to_accelerator(project_slug, f"{folder_name}/{accelerator_filename}", file_stream, progress, task, max_workers=max_workers)
134
+
135
+ @app.command()
136
+ def upload(
137
+ project_slug: Annotated[str, typer.Option(help="Project slug")],
138
+ path: Annotated[str, typer.Option(help="File or folder path.")],
139
+ folder_name: Annotated[str, typer.Option(help="New folder name.")],
140
+ max_workers: Annotated[int, typer.Option(help="Maximum worker pool for multipart upload")] = os.cpu_count()
141
+ ):
142
+
143
+ if not re.fullmatch(r'\w+', folder_name):
144
+ raise ValueError("Folder name is invalid.")
145
+
146
+
147
+ with Progress(
148
+ TextColumn("[progress.description]"),
149
+ BarColumn(),
150
+ TaskProgressColumn(),
151
+ TimeElapsedColumn(),
152
+ TextColumn("{task.description}"),
153
+ transient=True
154
+ ) as progress:
155
+
156
+ if (os.path.isdir(path)):
157
+
158
+ if not path.endswith("/"):
159
+ path = path + ("/")
160
+
161
+ folder_size = get_size(path)
162
+ print('Folder size', folder_size)
163
+ upload_task = progress.add_task("[cyan]Uploading.", total=folder_size)
164
+
165
+ for local_file_path in glob.iglob(f"{path}/**/*.*", recursive=True):
166
+ accelerator_filename = os.path.relpath(local_file_path, path)
167
+
168
+ progress.update(
169
+ upload_task,
170
+ description=f"[cyan]Uploading {local_file_path} \t"
171
+ )
172
+
173
+ upload_file(project_slug, accelerator_filename, local_file_path, progress, upload_task, folder_name, max_workers=max_workers)
174
+ elif (os.path.isfile(path)):
175
+ raise NotImplementedError('Only folder can be uploaded. File upload is not implemented.')
176
+ else:
177
+ print("ERROR: No such file or directory.")
178
+ typer.Exit(1)
179
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: accli
3
- Version: 0.1.2
3
+ Version: 0.1.3.1
4
4
  Summary: IIASA Accelerator Client
5
5
  Author-email: Wrufesh S <wrufesh@gmail.com>
6
6
  License: The MIT License (MIT)
@@ -33,6 +33,9 @@ Requires-Python: >=3.7
33
33
  Description-Content-Type: text/markdown
34
34
  License-File: LICENSE
35
35
  Requires-Dist: urllib3
36
+ Requires-Dist: typer[all]
37
+ Requires-Dist: tinydb
38
+ Requires-Dist: requests
36
39
 
37
40
  ## Accelerator python client
38
41
 
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ accli/AcceleratorJobProjectService.py
5
+ accli/AcceleratorTerminalCliProjectService.py
6
+ accli/__init__.py
7
+ accli/__main__.py
8
+ accli/cli.py
9
+ accli.egg-info/PKG-INFO
10
+ accli.egg-info/SOURCES.txt
11
+ accli.egg-info/dependency_links.txt
12
+ accli.egg-info/entry_points.txt
13
+ accli.egg-info/requires.txt
14
+ accli.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ accli = accli.cli.app
@@ -0,0 +1,4 @@
1
+ urllib3
2
+ typer[all]
3
+ tinydb
4
+ requests
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "accli"
7
- version = "0.1.2"
7
+ version = "0.1.3.1"
8
8
  authors = [
9
9
  { name="Wrufesh S", email="wrufesh@gmail.com" },
10
10
  ]
@@ -12,7 +12,7 @@ description = "IIASA Accelerator Client"
12
12
  readme = "README.md"
13
13
  license = {file = "LICENSE"}
14
14
  requires-python = ">=3.7"
15
- dependencies = ['urllib3']
15
+ dependencies = ['urllib3', 'typer[all]', 'tinydb', 'requests']
16
16
  classifiers = [
17
17
  "Programming Language :: Python :: 3",
18
18
  "License :: OSI Approved :: MIT License",
@@ -24,4 +24,7 @@ packages = ['accli']
24
24
 
25
25
  [project.urls]
26
26
  "Homepage" = "https://github.com/iiasa/accli"
27
- "Bug Tracker" = "https://github.com/iiasa/accli/issues"
27
+ "Bug Tracker" = "https://github.com/iiasa/accli/issues"
28
+
29
+ [project.scripts]
30
+ accli = "accli.cli.app"
@@ -1,3 +0,0 @@
1
- from .AcceleratorProjectService import AcceleratorProjectService
2
-
3
- ACliService = AcceleratorProjectService
@@ -1,10 +0,0 @@
1
- LICENSE
2
- README.md
3
- pyproject.toml
4
- accli/AcceleratorProjectService.py
5
- accli/__init__.py
6
- accli.egg-info/PKG-INFO
7
- accli.egg-info/SOURCES.txt
8
- accli.egg-info/dependency_links.txt
9
- accli.egg-info/requires.txt
10
- accli.egg-info/top_level.txt
@@ -1 +0,0 @@
1
- urllib3
File without changes
File without changes
File without changes