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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: accli
3
- Version: 0.3
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
- job_cli_base_url='http://accelerator-api/v1/ajob-cli',
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 = job_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.AcceleratorJobProjectService import AcceleratorJobProjectService
5
+ from accli.AcceleratorTaskDispatcher import *
6
+
7
+ from accli.AcceleratorJobProjectService import AcceleratorJobProjectService, Fs
6
8
 
7
9
  AjobCliService = AcceleratorJobProjectService
8
10
 
@@ -1,3 +1,3 @@
1
1
  # Please strictly put double quote to use this info for git tag
2
2
  # Read developer guide for git tag command with regex
3
- VERSION = "v0.3"
3
+ VERSION = "v0.4"
@@ -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
- from tinydb import TinyDB, Query
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
+
@@ -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
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
@@ -3,3 +3,4 @@ typer[all]
3
3
  tinydb
4
4
  requests
5
5
  jsonschema
6
+ pydantic==1.10.13
@@ -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",
@@ -0,0 +1,8 @@
1
+ urllib3
2
+ requests
3
+ typer[all]
4
+ tinydb
5
+ twine
6
+ jsonschema
7
+ pydantic==1.10.13
8
+ gitpython==3.1.43
@@ -1,6 +0,0 @@
1
- urllib3
2
- requests
3
- typer[all]
4
- tinydb
5
- twine
6
- jsonschema
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes