accli 0.2.2__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.2.2
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
+
@@ -22,7 +22,7 @@ class AcceleratorTerminalCliProjectService:
22
22
  def __init__(
23
23
  self,
24
24
  user_token,
25
- cli_base_url='http://accelerator-api/v1/aterm-cli',
25
+ server_url,
26
26
  verify_cert=True
27
27
  ):
28
28
 
@@ -35,7 +35,8 @@ class AcceleratorTerminalCliProjectService:
35
35
  else:
36
36
  self.http_client = urllib3.poolmanager.PoolManager(cert_reqs="CERT_NONE", num_pools=1, retries=retries)
37
37
 
38
- self.cli_base_url = cli_base_url
38
+ self.server_url = server_url
39
+ self.cli_base_url = f"{self.server_url}/v1/aterm-cli"
39
40
  self.common_request_headers = {
40
41
  'x-authorization': user_token
41
42
  }
@@ -68,6 +69,45 @@ class AcceleratorTerminalCliProjectService:
68
69
  raise err
69
70
 
70
71
  return res.json()
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
+
97
+ def get_dataset_template_details(self, project_slug, template_slug):
98
+
99
+ try:
100
+ res = self.http_client_request(
101
+ "GET",
102
+ f"{self.server_url}/v1/projects/{project_slug}/dataset-templates/{template_slug}/by-slug/",
103
+ )
104
+ except AccAPIError as err:
105
+ if err.status_code == 404:
106
+ return None
107
+ else:
108
+ raise err
109
+
110
+ return res.json()
71
111
 
72
112
 
73
113
  def get_multipart_put_create_signed_url(
@@ -0,0 +1,230 @@
1
+ import typing
2
+ import typer
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import csv
7
+ import uuid
8
+ import itertools
9
+ from typing import Optional
10
+ from rich.progress import Progress, SpinnerColumn, TextColumn
11
+ from rich import print
12
+
13
+ from jsonschema import validate as jsonschema_validate
14
+ from jsonschema.exceptions import ValidationError, SchemaError
15
+
16
+
17
+ if typing.TYPE_CHECKING:
18
+ from accli.AcceleratorTerminalCliProjectService import AcceleratorTerminalCliProjectService
19
+
20
+ def lower_rows(iterator):
21
+ # return itertools.chain([next(iterator).lower()], iterator)
22
+ for item in iterator:
23
+ yield item.lower()
24
+
25
+ class CsvRegionalTimeseriesValidator():
26
+ def __init__(
27
+ self,
28
+ *,
29
+ project_slug,
30
+ dataset_template_slug,
31
+ input_filepath,
32
+ project_service: 'AcceleratorTerminalCliProjectService',
33
+ csv_fieldnames: Optional[typing.List[str]]=None,
34
+
35
+ ):
36
+ self.project_slug = project_slug
37
+ self.project_service = project_service
38
+
39
+ self.dataset_template_slug = dataset_template_slug
40
+
41
+ self.input_filepath = input_filepath
42
+
43
+ self.csv_fieldnames = csv_fieldnames
44
+
45
+
46
+
47
+ self.errors = dict()
48
+
49
+
50
+ def get_map_documents(self, field_name):
51
+ map_documents = self.rules.get(f'map_{field_name}')
52
+ return map_documents
53
+
54
+
55
+ def init_validation_metadata(self):
56
+ self.validation_metadata = {
57
+ self.time_dimension.lower(): {
58
+ "min_value": float('+inf'),
59
+ "max_value": float('-inf')
60
+ }
61
+ }
62
+
63
+
64
+ def set_csv_regional_validation_rules(self):
65
+
66
+ dataset_template_details = self.project_service.get_dataset_template_details(
67
+ self.project_slug,
68
+ self.dataset_template_slug
69
+ )
70
+
71
+ self.rules = dataset_template_details.get('rules')
72
+
73
+ assert self.rules, \
74
+ f"No dataset template rules found for dataset_template id: \
75
+ {self.dataset_template_id}"
76
+
77
+ self.time_dimension = self.rules['root_schema_declarations']['time_dimension']
78
+ self.value_dimension = self.rules['root_schema_declarations']['value_dimension']
79
+
80
+ self.region_dimension = self.rules['root_schema_declarations']['region_dimension']
81
+
82
+ # specific to regional timeseries
83
+ self.region_layer_dimension = self.rules['root_schema_declarations']['region_layer_map_key']
84
+
85
+ def validate_row_data(self, row):
86
+ try:
87
+ jsonschema_validate(
88
+ self.rules.get('root'),
89
+ row
90
+ )
91
+
92
+ except SchemaError as schema_error:
93
+
94
+ raise ValueError(
95
+ f"Schema itself is not valid with template id. Template id: {self.dataset_template_id}. Original exception: {str(schema_error)}"
96
+ )
97
+ except ValidationError as validation_error:
98
+ raise ValueError(
99
+ f"Invalid data. Template id: {self.dataset_template_id}. Data: {str(validation_error)}. Original exception: {str(validation_error)}"
100
+ )
101
+
102
+
103
+ for key in self.rules['root']['properties']:
104
+
105
+ if key == self.time_dimension.lower():
106
+ if float(row[key]) < self.validation_metadata[
107
+ self.time_dimension.lower()
108
+ ]["min_value"]:
109
+ self.validation_metadata[
110
+ self.time_dimension.lower()
111
+ ]["min_value"] = float(row[key])
112
+
113
+ if float(row[key]) > self.validation_metadata[
114
+ self.time_dimension.lower()
115
+ ]["max_value"]:
116
+ self.validation_metadata[
117
+ self.time_dimension.lower()
118
+ ]["max_value"] = float(row[key])
119
+
120
+ continue
121
+
122
+ if key == self.value_dimension:
123
+ continue
124
+
125
+
126
+ map_documents = self.get_map_documents(key)
127
+
128
+ if map_documents:
129
+ if row[key] not in map_documents:
130
+ raise ValueError(f"'{row[key]}' must be one of {map_documents.keys()}" )
131
+
132
+
133
+ if self.validation_metadata.get(key):
134
+ if len(self.validation_metadata[key]) <= 200: #limit harvest
135
+ self.validation_metadata[key].add(row[key])
136
+
137
+ else:
138
+ self.validation_metadata[key] = set([row[key]])
139
+
140
+
141
+ extra_template_validators = self.rules.get('template_validators')
142
+
143
+ if extra_template_validators and extra_template_validators != 'not defined':
144
+
145
+ for row_key in extra_template_validators.keys():
146
+ lhs = row[row_key]
147
+
148
+ condition_object = extra_template_validators[row_key]
149
+
150
+ for condition in condition_object.keys():
151
+
152
+ rhs_value_pointer = condition_object[condition]
153
+
154
+ rhs = None
155
+ for pointer in rhs_value_pointer:
156
+ if pointer.startswith('&'):
157
+ rhs = self.rules[pointer[1:]]
158
+ elif pointer.startswith('{') and pointer.endswith('}'):
159
+ rhs = rhs[row[pointer[1:-1]]]
160
+
161
+ else:
162
+ rhs = rhs[pointer]
163
+
164
+
165
+ if condition == 'value_equals':
166
+ if lhs.lower() != rhs.lower():
167
+ raise ValueError(
168
+ f'{lhs} in {row_key} column must be equal to {rhs}.'
169
+ )
170
+
171
+ if condition == 'is_subset_of_map':
172
+ if not lhs.lower() in rhs.lower():
173
+ raise ValueError(
174
+ f'{lhs} in {row_key} column must be member of {rhs}.'
175
+ )
176
+
177
+ return row
178
+ def get_validated_rows(self, filepath):
179
+ with open(filepath) as csvfile:
180
+ # with open(self.temp_downloaded_filepath) as csvfile:
181
+ reader = csv.DictReader(
182
+ lower_rows(csvfile),
183
+ fieldnames=self.csv_fieldnames,
184
+ restkey='restkeys',
185
+ restval='restvals'
186
+ )
187
+
188
+ for row in reader:
189
+ row.pop('restkeys', None)
190
+ row.pop('restvals', None)
191
+
192
+ try:
193
+ row = self.validate_row_data(row)
194
+ row[self.region_layer_dimension] = self.rules[f'map_{self.region_dimension}'][row[self.region_dimension]]['region_layer']
195
+ except Exception as err:
196
+ if len(self.errors) <= 50:
197
+ self.errors[str(err)] = str(row)
198
+
199
+ yield row
200
+
201
+
202
+ def __call__(self):
203
+
204
+ self.set_csv_regional_validation_rules()
205
+ self.init_validation_metadata()
206
+
207
+ with Progress(
208
+ SpinnerColumn(),
209
+ TextColumn("[progress.description]{task.description}"),
210
+ transient=True,
211
+ ) as progress:
212
+ progress.add_task(description="[bold cyan]Validating dataset. Please wait...", total=None)
213
+
214
+ for row in self.get_validated_rows(self.input_filepath):
215
+ pass
216
+
217
+
218
+ if self.errors:
219
+ for key in self.errors:
220
+ print(f"[bold red]Invalid data:[/bold red] {self.errors[key]}")
221
+ print(f"[bold red]Error:[/bold red] {key}")
222
+ print("[bold red]Data is not valid against selected template. ⚠️⚠️")
223
+ typer.Exit(1)
224
+ else:
225
+ print("[bold green]Data validated against selected template.[/bold green] 🎉🎉")
226
+ typer.Exit(0)
227
+
228
+
229
+
230
+
@@ -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.2.2"
3
+ VERSION = "v0.4"
@@ -5,11 +5,14 @@ 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
- from ._version import VERSION
12
11
 
12
+ from accli.token import save_token_details, get_token, get_server_url, set_github_app_token
13
+
14
+ from accli.CsvRegionalTimeseriesValidator import CsvRegionalTimeseriesValidator
15
+ from ._version import VERSION
13
16
 
14
17
  from accli.AcceleratorTerminalCliProjectService import AcceleratorTerminalCliProjectService
15
18
 
@@ -17,69 +20,12 @@ from rich.progress import Progress, SpinnerColumn, TextColumn, ProgressColumn, B
17
20
 
18
21
  warnings.filterwarnings('ignore')
19
22
 
20
-
21
23
  app = typer.Typer(
22
24
  add_completion=False,
23
25
  pretty_exceptions_show_locals=False,
24
26
  no_args_is_help=True
25
27
  )
26
28
 
27
- def get_db_path():
28
-
29
- home = os.path.expanduser("~")
30
- token_directory = f"{home}/.accli"
31
-
32
- if not os.path.exists(token_directory):
33
- os.makedirs(token_directory)
34
-
35
- return f"{token_directory}/data.json"
36
-
37
-
38
- def save_token_details(token, server_url, webcli_url):
39
-
40
- db_path = get_db_path()
41
-
42
- try:
43
- os.remove(db_path)
44
- except OSError:
45
- pass
46
-
47
- db = TinyDB(db_path)
48
- db.insert({
49
- 'token': token,
50
- 'server_url': server_url,
51
- 'webcli_url': webcli_url
52
- })
53
-
54
- def get_token():
55
- db_path = get_db_path()
56
-
57
- db = TinyDB(db_path)
58
-
59
- for item in db:
60
- token = item.get('token')
61
- if token:
62
- break
63
-
64
- if not token:
65
- print("Token does not exists. Please login.")
66
- return token
67
-
68
-
69
- def get_server_url():
70
- db_path = get_db_path()
71
-
72
- db = TinyDB(db_path)
73
-
74
- for item in db:
75
- server_url = item.get('server_url')
76
- if server_url:
77
- break
78
-
79
- if not server_url:
80
- print("Server url does not exists. Please login.")
81
- return server_url
82
-
83
29
 
84
30
  def get_size(path):
85
31
  size = 0
@@ -136,7 +82,7 @@ def upload_file(project_slug, accelerator_filename, local_filepath, progress, ta
136
82
 
137
83
  term_cli_project_service = AcceleratorTerminalCliProjectService(
138
84
  user_token=access_token,
139
- cli_base_url=f"{server_url}/v1/aterm-cli",
85
+ server_url=server_url,
140
86
  verify_cert=False
141
87
  )
142
88
 
@@ -156,8 +102,11 @@ def upload(
156
102
  folder_name: Annotated[str, typer.Argument(help="Name of the folder to be made in Accelerator project space.")],
157
103
  max_workers: Annotated[int, typer.Option(help="Maximum worker pool for multipart upload.")] = os.cpu_count()
158
104
  ):
105
+
106
+
107
+ #TODO make user free to put anywere except for reserved folder
159
108
 
160
- if not re.fullmatch(r'\w+', folder_name):
109
+ if not re.fullmatch(r'[a-zA-Z0-9\-\_]+', folder_name):
161
110
  raise ValueError("Folder name is invalid.")
162
111
 
163
112
 
@@ -200,3 +149,74 @@ def upload(
200
149
  print("ERROR: No such file or directory.")
201
150
  typer.Exit(1)
202
151
 
152
+ @app.command()
153
+ def validate(
154
+ project_slug: Annotated[str, typer.Argument(help="Unique Accelerator project slug.")],
155
+ template_slug: Annotated[str, typer.Argument(help="Unique project template slug")],
156
+ filepath: Annotated[str, typer.Argument(help="Path of the file to validate")],
157
+ server: Annotated[str, typer.Option(help="Accelerator server url.")] = "https://accelerator-api.iiasa.ac.at",
158
+ ):
159
+
160
+
161
+ term_cli_project_service = AcceleratorTerminalCliProjectService(
162
+ user_token="",
163
+ server_url=server,
164
+ verify_cert=False
165
+ )
166
+
167
+ validate = CsvRegionalTimeseriesValidator(
168
+ project_slug=project_slug,
169
+ dataset_template_slug=template_slug,
170
+ input_filepath=filepath,
171
+ project_service=term_cli_project_service,
172
+ )
173
+
174
+ validate()
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.2.2
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,11 +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
10
+ accli/CsvRegionalTimeseriesValidator.py
9
11
  accli/__init__.py
10
12
  accli/__main__.py
11
13
  accli/_version.py
12
14
  accli/cli.py
15
+ accli/token.py
13
16
  accli.egg-info/PKG-INFO
14
17
  accli.egg-info/SOURCES.txt
15
18
  accli.egg-info/dependency_links.txt
@@ -2,3 +2,5 @@ urllib3
2
2
  typer[all]
3
3
  tinydb
4
4
  requests
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']
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,5 +0,0 @@
1
- urllib3
2
- requests
3
- typer[all]
4
- tinydb
5
- twine
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes