aplos-nca-saas-sdk 1.0.0__py3-none-any.whl
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.
- aplos_nca_saas_sdk/__init__.py +0 -0
- aplos_nca_saas_sdk/aws_resources/aws_cognito.py +188 -0
- aplos_nca_saas_sdk/aws_resources/aws_s3_presigned_payload.py +49 -0
- aplos_nca_saas_sdk/aws_resources/aws_s3_presigned_upload.py +116 -0
- aplos_nca_saas_sdk/files/analysis_files/single_ev/configuration_single_ev.json +51 -0
- aplos_nca_saas_sdk/files/analysis_files/single_ev/meta_data.json +17 -0
- aplos_nca_saas_sdk/files/analysis_files/single_ev/single_ev.csv +121 -0
- aplos_nca_saas_sdk/integration_testing/integration_test_base.py +34 -0
- aplos_nca_saas_sdk/integration_testing/integration_test_factory.py +62 -0
- aplos_nca_saas_sdk/integration_testing/integration_test_suite.py +84 -0
- aplos_nca_saas_sdk/integration_testing/main.py +28 -0
- aplos_nca_saas_sdk/integration_testing/readme.md +18 -0
- aplos_nca_saas_sdk/integration_testing/tests/app_configuration_test.py +30 -0
- aplos_nca_saas_sdk/integration_testing/tests/app_execution_test.py +5 -0
- aplos_nca_saas_sdk/integration_testing/tests/app_login_test.py +32 -0
- aplos_nca_saas_sdk/integration_testing/tests/app_validation_test.py +5 -0
- aplos_nca_saas_sdk/nca_resources/nca_app_configuration.py +69 -0
- aplos_nca_saas_sdk/nca_resources/nca_endpoints.py +54 -0
- aplos_nca_saas_sdk/nca_resources/nca_executions.py +378 -0
- aplos_nca_saas_sdk/nca_resources/nca_login.py +104 -0
- aplos_nca_saas_sdk/utilities/commandline_args.py +332 -0
- aplos_nca_saas_sdk/utilities/environment_services.py +81 -0
- aplos_nca_saas_sdk/utilities/environment_vars.py +23 -0
- aplos_nca_saas_sdk/utilities/http_utility.py +30 -0
- aplos_nca_saas_sdk/version.py +4 -0
- aplos_nca_saas_sdk-1.0.0.dist-info/METADATA +195 -0
- aplos_nca_saas_sdk-1.0.0.dist-info/RECORD +28 -0
- aplos_nca_saas_sdk-1.0.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,378 @@
|
|
1
|
+
"""
|
2
|
+
Copyright 2024 Aplos Analytics
|
3
|
+
All Rights Reserved. www.aplosanalytics.com LICENSED MATERIALS
|
4
|
+
Property of Aplos Analytics, Utah, USA
|
5
|
+
"""
|
6
|
+
|
7
|
+
import json
|
8
|
+
import os
|
9
|
+
import time
|
10
|
+
import zipfile
|
11
|
+
from datetime import datetime, timedelta
|
12
|
+
from pathlib import Path
|
13
|
+
|
14
|
+
import requests
|
15
|
+
from aplos_nca_saas_sdk.aws_resources.aws_cognito import CognitoAuthenication
|
16
|
+
from aplos_nca_saas_sdk.aws_resources.aws_s3_presigned_payload import (
|
17
|
+
S3PresignedPayload,
|
18
|
+
)
|
19
|
+
from aplos_nca_saas_sdk.aws_resources.aws_s3_presigned_upload import (
|
20
|
+
S3PresignedUpload,
|
21
|
+
)
|
22
|
+
from aplos_nca_saas_sdk.utilities.commandline_args import CommandlineArgs
|
23
|
+
from aplos_nca_saas_sdk.utilities.http_utility import HttpUtilities, Routes
|
24
|
+
|
25
|
+
|
26
|
+
class NCAEngine:
|
27
|
+
"""NCA Engine Access"""
|
28
|
+
|
29
|
+
def __init__(
|
30
|
+
self, api_domain: str | None, cognito_client_id: str | None, region: str | None
|
31
|
+
) -> None:
|
32
|
+
self.jwt: str
|
33
|
+
self.access_token: str | None = None
|
34
|
+
self.refresh_token: str | None = None
|
35
|
+
self.__api_domain: str | None = api_domain
|
36
|
+
self.verbose: bool = False
|
37
|
+
|
38
|
+
self.cognito: CognitoAuthenication = CognitoAuthenication(
|
39
|
+
client_id=cognito_client_id, region=region
|
40
|
+
)
|
41
|
+
|
42
|
+
if not self.__api_domain:
|
43
|
+
raise RuntimeError(
|
44
|
+
"Missing Aplos Api Domain. "
|
45
|
+
"Pass in the api_domain as a command arg or set the APLOS_API_DOMAIN environment var."
|
46
|
+
)
|
47
|
+
|
48
|
+
@property
|
49
|
+
def api_root(self) -> str:
|
50
|
+
"""Gets the base url"""
|
51
|
+
url = HttpUtilities.build_url(self.__api_domain)
|
52
|
+
if isinstance(url, str):
|
53
|
+
return (
|
54
|
+
f"{url}/tenants/{self.cognito.tenant_id}/users/{self.cognito.user_id}"
|
55
|
+
)
|
56
|
+
|
57
|
+
raise RuntimeError("Missing Aplos Api Domain")
|
58
|
+
|
59
|
+
def execute(
|
60
|
+
self,
|
61
|
+
username: str,
|
62
|
+
password: str,
|
63
|
+
input_file_path: str,
|
64
|
+
config_data: dict,
|
65
|
+
*,
|
66
|
+
meta_data: str | dict | None = None,
|
67
|
+
wait_for_results: bool = True,
|
68
|
+
output_directory: str | None = None,
|
69
|
+
unzip_after_download: bool = False,
|
70
|
+
) -> None:
|
71
|
+
"""_summary_
|
72
|
+
|
73
|
+
Args:
|
74
|
+
username (str): the username
|
75
|
+
password (str): the users password
|
76
|
+
input_file_path (str): the path to the input (anlysis) file
|
77
|
+
config_data (dict): analysis configuration infomration
|
78
|
+
meta_data (str | dict | None, optional): meta data attached to the execution. Defaults to None.
|
79
|
+
wait_for_results (bool, optional): should the program wait for results. Defaults to True.
|
80
|
+
output_directory (str, optional): the output directory. Defaults to None (the local directy is used)
|
81
|
+
unzip_after_download (bool): Results are downloaded as a zip file, this option will unzip them automatically. Defaults to False
|
82
|
+
"""
|
83
|
+
if self.verbose:
|
84
|
+
print("\tLogging in.")
|
85
|
+
self.jwt = self.cognito.login(username=username, password=password)
|
86
|
+
|
87
|
+
if self.verbose:
|
88
|
+
print("\tUploading the analysis file.")
|
89
|
+
uploader: S3PresignedUpload = S3PresignedUpload(self.jwt, str(self.api_root))
|
90
|
+
presign_payload: S3PresignedPayload = uploader.upload_file(input_file_path)
|
91
|
+
|
92
|
+
if self.verbose:
|
93
|
+
print("\tStarting the execution.")
|
94
|
+
execution_id = self.run_analysis(
|
95
|
+
file_id=str(presign_payload.file_id),
|
96
|
+
config_data=config_data,
|
97
|
+
meta_data=meta_data,
|
98
|
+
)
|
99
|
+
|
100
|
+
if execution_id and wait_for_results:
|
101
|
+
# wait for it
|
102
|
+
download_url = self.wait_for_results(execution_id=execution_id)
|
103
|
+
# download the files
|
104
|
+
if download_url:
|
105
|
+
if self.verbose:
|
106
|
+
print("\tDownloading the results.")
|
107
|
+
self.download_file(
|
108
|
+
download_url,
|
109
|
+
output_directory=output_directory,
|
110
|
+
do_unzip=unzip_after_download,
|
111
|
+
)
|
112
|
+
|
113
|
+
def run_analysis(
|
114
|
+
self,
|
115
|
+
file_id: str,
|
116
|
+
config_data: dict,
|
117
|
+
meta_data: str | dict | None = None,
|
118
|
+
) -> str:
|
119
|
+
"""
|
120
|
+
Run the analysis
|
121
|
+
|
122
|
+
Args:
|
123
|
+
bucket_name (str): s3 bucket name for your organization. this is returned to you
|
124
|
+
object_key (str): 3s object key for the file you are running an analysis on.
|
125
|
+
config_data (dict): the config_data for the analysis file
|
126
|
+
meta_data (str | dict): Optional. Any meta data you'd like attached to this execution
|
127
|
+
Returns:
|
128
|
+
str: the execution id
|
129
|
+
"""
|
130
|
+
|
131
|
+
headers = HttpUtilities.get_headers(self.jwt)
|
132
|
+
# to start a new execution we need the location of the file (s3 bucket and object key)
|
133
|
+
# you basic configuration
|
134
|
+
# optional meta data
|
135
|
+
|
136
|
+
submission = {
|
137
|
+
"file": {"id": file_id},
|
138
|
+
"configuration": config_data,
|
139
|
+
"meta_data": meta_data,
|
140
|
+
}
|
141
|
+
url = f"{str(self.api_root)}/{Routes.NCA_EXECUTIONS}"
|
142
|
+
response: requests.Response = requests.post(
|
143
|
+
url, headers=headers, data=json.dumps(submission), timeout=30
|
144
|
+
)
|
145
|
+
json_response: dict = response.json()
|
146
|
+
|
147
|
+
if response.status_code == 403:
|
148
|
+
raise PermissionError(
|
149
|
+
"Failed to execute. A 403 response occured. "
|
150
|
+
"This could a token issue or a url path issue "
|
151
|
+
"By default unknown gateway calls return 403 errors. "
|
152
|
+
)
|
153
|
+
elif response.status_code != 200:
|
154
|
+
raise RuntimeError(
|
155
|
+
f"Unknown Error occured during executions: {response.status_code}. "
|
156
|
+
f"Reason: {response.reason}"
|
157
|
+
)
|
158
|
+
|
159
|
+
execution_id = str(json_response.get("execution_id"))
|
160
|
+
if self.verbose:
|
161
|
+
print(f"\tExecution {execution_id} started.")
|
162
|
+
|
163
|
+
return execution_id
|
164
|
+
|
165
|
+
def wait_for_results(
|
166
|
+
self, execution_id: str, max_wait_in_minutes: int = 15
|
167
|
+
) -> str | None:
|
168
|
+
"""
|
169
|
+
Wait for results
|
170
|
+
Args:
|
171
|
+
execution_id (str): the analysis execution id
|
172
|
+
|
173
|
+
Returns:
|
174
|
+
str | None: on success: a url for download, on failure: None
|
175
|
+
"""
|
176
|
+
|
177
|
+
url = f"{self.api_root}/{Routes.NCA_EXECUTIONS}/{execution_id}"
|
178
|
+
|
179
|
+
headers = HttpUtilities.get_headers(self.jwt)
|
180
|
+
current_time = datetime.now()
|
181
|
+
# Create a timedelta object representing 15 minutes
|
182
|
+
time_delta = timedelta(minutes=max_wait_in_minutes)
|
183
|
+
|
184
|
+
# Add the timedelta to the current time
|
185
|
+
max_time = current_time + time_delta
|
186
|
+
|
187
|
+
complete = False
|
188
|
+
while not complete:
|
189
|
+
response = requests.get(url, headers=headers, timeout=30)
|
190
|
+
json_response: dict = response.json()
|
191
|
+
status = json_response.get("status")
|
192
|
+
complete = status == "complete"
|
193
|
+
elapsed = (
|
194
|
+
json_response.get("times", {}).get("elapsed", "0:00:00") or "--:--"
|
195
|
+
)
|
196
|
+
if status == "failed" or complete:
|
197
|
+
break
|
198
|
+
if not complete:
|
199
|
+
if self.verbose:
|
200
|
+
print(f"\t\twaiting for results.... {status}: {elapsed}")
|
201
|
+
time.sleep(5)
|
202
|
+
if datetime.now() > max_time:
|
203
|
+
status = "timeout"
|
204
|
+
break
|
205
|
+
if status is None and elapsed is None:
|
206
|
+
# we have a problem
|
207
|
+
status = "unknown issue"
|
208
|
+
break
|
209
|
+
|
210
|
+
if status == "complete":
|
211
|
+
if self.verbose:
|
212
|
+
print("\tExecution complete.")
|
213
|
+
print(f"\tExecution duration = {elapsed}.")
|
214
|
+
return json_response["presigned"]["url"]
|
215
|
+
else:
|
216
|
+
if self.verbose:
|
217
|
+
print(
|
218
|
+
f"\tExecution failed. Execution ID = {execution_id}. reason: {json_response.get('errors')}"
|
219
|
+
)
|
220
|
+
return None
|
221
|
+
|
222
|
+
def download_file(
|
223
|
+
self,
|
224
|
+
presigned_download_url: str,
|
225
|
+
output_directory: str | None = None,
|
226
|
+
do_unzip: bool = False,
|
227
|
+
) -> str | None:
|
228
|
+
"""
|
229
|
+
# Step 5
|
230
|
+
Download completed analysis files
|
231
|
+
|
232
|
+
Args:
|
233
|
+
presigned_download_url (str): presigned download url
|
234
|
+
output_directory (str | None): optional output directory
|
235
|
+
|
236
|
+
Returns:
|
237
|
+
str: file path to results or None
|
238
|
+
"""
|
239
|
+
if output_directory is None:
|
240
|
+
output_directory = str(Path(__file__).parent.parent)
|
241
|
+
output_directory = os.path.join(output_directory, ".aplos-nca-output")
|
242
|
+
|
243
|
+
if presigned_download_url:
|
244
|
+
output_file = f"results-{time.strftime('%Y-%m-%d-%Hh%Mm%Ss')}.zip"
|
245
|
+
|
246
|
+
output_file = os.path.join(output_directory, output_file)
|
247
|
+
os.makedirs(output_directory, exist_ok=True)
|
248
|
+
|
249
|
+
response = requests.get(presigned_download_url, timeout=60)
|
250
|
+
# write the zip to a file
|
251
|
+
with open(output_file, "wb") as f:
|
252
|
+
f.write(response.content)
|
253
|
+
|
254
|
+
# optionally, extract all the files from the zip
|
255
|
+
if do_unzip:
|
256
|
+
with zipfile.ZipFile(output_file, "r") as zip_ref:
|
257
|
+
zip_ref.extractall(output_file.replace(".zip", ""))
|
258
|
+
|
259
|
+
unzipped_state = "and unzipped" if do_unzip else "in zip format"
|
260
|
+
|
261
|
+
if self.verbose:
|
262
|
+
print(f"\tResults file downloaded {unzipped_state}.")
|
263
|
+
print(f"\t\tResults are available in: {output_directory}")
|
264
|
+
|
265
|
+
return output_directory
|
266
|
+
else:
|
267
|
+
return None
|
268
|
+
|
269
|
+
|
270
|
+
def main():
|
271
|
+
try:
|
272
|
+
print("Welcome to the NCA Engine Upload & Execution Demo")
|
273
|
+
args = CommandlineArgs()
|
274
|
+
files_path = os.path.join(Path(__file__).parent, "files")
|
275
|
+
|
276
|
+
args.analysis_file_default = os.path.join(files_path, "single_ev.csv")
|
277
|
+
args.config_file_default = os.path.join(
|
278
|
+
files_path, "configuration_single_ev.json"
|
279
|
+
)
|
280
|
+
args.metadata_file_default = os.path.join(files_path, "meta_data.json")
|
281
|
+
if not args.is_valid():
|
282
|
+
print("\n\n")
|
283
|
+
print("Missing some arguments.")
|
284
|
+
exit()
|
285
|
+
|
286
|
+
engine = NCAEngine(
|
287
|
+
api_domain=args.api_domain,
|
288
|
+
cognito_client_id=args.cognito_client_id,
|
289
|
+
region=args.aws_region,
|
290
|
+
)
|
291
|
+
|
292
|
+
print("\tLoading analysis configurations")
|
293
|
+
print(f"\t\t{args.config_file}")
|
294
|
+
config_data: dict = read_json_file(str(args.config_file))
|
295
|
+
|
296
|
+
print("\tLoading analysis meta data")
|
297
|
+
print(f"\t\t{args.metadata_file}")
|
298
|
+
meta_data = optional_json_loads(read_text_file(str(args.metadata_file)))
|
299
|
+
|
300
|
+
engine.execute(
|
301
|
+
username=str(args.username),
|
302
|
+
password=str(args.password),
|
303
|
+
input_file_path=str(args.analysis_file),
|
304
|
+
config_data=config_data,
|
305
|
+
meta_data=meta_data,
|
306
|
+
output_directory=str(args.output_directory),
|
307
|
+
)
|
308
|
+
print("Thank you for using the NCA Engine Upload and Execution Demo")
|
309
|
+
except Exception as e: # pylint: disable=w0718
|
310
|
+
print("An error occured ... exiting with an error")
|
311
|
+
print(str(e))
|
312
|
+
|
313
|
+
|
314
|
+
def optional_json_loads(data: str | dict) -> str | dict:
|
315
|
+
"""
|
316
|
+
Attempts to load the data as json, fails gracefull and retuns the data is if it fails
|
317
|
+
Args:
|
318
|
+
data (str): data as string
|
319
|
+
|
320
|
+
Returns:
|
321
|
+
str | dict: either the data as is or a converted dictionary/json object
|
322
|
+
"""
|
323
|
+
if isinstance(data, dict):
|
324
|
+
return data
|
325
|
+
|
326
|
+
try:
|
327
|
+
data = json.loads(str(data))
|
328
|
+
finally:
|
329
|
+
pass
|
330
|
+
return data
|
331
|
+
|
332
|
+
|
333
|
+
def read_json_file(file_path: str) -> dict:
|
334
|
+
"""
|
335
|
+
Reads a file and returns the json
|
336
|
+
Args:
|
337
|
+
file_path (str): _description_
|
338
|
+
|
339
|
+
Raises:
|
340
|
+
FileNotFoundError: _description_
|
341
|
+
|
342
|
+
Returns:
|
343
|
+
dict: _description_
|
344
|
+
"""
|
345
|
+
if not os.path.exists(file_path):
|
346
|
+
raise FileNotFoundError(f"File Not Found: {file_path}")
|
347
|
+
|
348
|
+
data = None
|
349
|
+
with open(file_path, mode="r", encoding="utf8") as file:
|
350
|
+
data = json.load(file)
|
351
|
+
|
352
|
+
return data
|
353
|
+
|
354
|
+
|
355
|
+
def read_text_file(file_path: str) -> str:
|
356
|
+
"""
|
357
|
+
Read files contents
|
358
|
+
Args:
|
359
|
+
file_path (str): path to the file
|
360
|
+
|
361
|
+
Raises:
|
362
|
+
FileNotFoundError: if the file is not found
|
363
|
+
|
364
|
+
Returns:
|
365
|
+
str: the files data
|
366
|
+
"""
|
367
|
+
if not os.path.exists(file_path):
|
368
|
+
raise FileNotFoundError(f"File Not Found: {file_path}")
|
369
|
+
|
370
|
+
data = None
|
371
|
+
with open(file_path, mode="r", encoding="utf8") as file:
|
372
|
+
data = file.read()
|
373
|
+
|
374
|
+
return data
|
375
|
+
|
376
|
+
|
377
|
+
if __name__ == "__main__":
|
378
|
+
main()
|
@@ -0,0 +1,104 @@
|
|
1
|
+
"""
|
2
|
+
Copyright 2024 Aplos Analytics
|
3
|
+
All Rights Reserved. www.aplosanalytics.com LICENSED MATERIALS
|
4
|
+
Property of Aplos Analytics, Utah, USA
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Optional
|
8
|
+
from aplos_nca_saas_sdk.aws_resources.aws_cognito import CognitoAuthenication
|
9
|
+
from aplos_nca_saas_sdk.nca_resources.nca_app_configuration import (
|
10
|
+
NCAAppConfiguration,
|
11
|
+
)
|
12
|
+
|
13
|
+
|
14
|
+
class NCALogin:
|
15
|
+
"""NCA Login"""
|
16
|
+
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
*,
|
20
|
+
cognito_client_id: Optional[str] = None,
|
21
|
+
cognito_region: Optional[str] = None,
|
22
|
+
aplos_saas_domain: Optional[str] = None,
|
23
|
+
) -> None:
|
24
|
+
"""
|
25
|
+
NCA SaaS Login
|
26
|
+
|
27
|
+
Args:
|
28
|
+
cognito_client_id (Optional[str], optional): Cognito Client Id. Defaults to None.
|
29
|
+
cognito_region (Optional[str], optional): Cognito Region. Defaults to None.
|
30
|
+
aplos_saas_domain (Optional[str], optional): Aplos NCA SaaS domain. Defaults to None.
|
31
|
+
|
32
|
+
Requirements:
|
33
|
+
Either pass in the cognito_client_id and cognito_region.
|
34
|
+
or set the aplos_saas_domain to automatically get the client_id and region.
|
35
|
+
"""
|
36
|
+
self.jwt: str
|
37
|
+
self.access_token: Optional[str] = None
|
38
|
+
self.refresh_token: Optional[str] = None
|
39
|
+
self.__cognito_client_id = cognito_client_id
|
40
|
+
self.__region = cognito_region
|
41
|
+
self.__domain: Optional[str] = aplos_saas_domain
|
42
|
+
self.__cognito: Optional[CognitoAuthenication] = None
|
43
|
+
self.__config: Optional[NCAAppConfiguration] = None
|
44
|
+
|
45
|
+
@property
|
46
|
+
def cognito(self) -> CognitoAuthenication:
|
47
|
+
"""
|
48
|
+
Cognito Authentication
|
49
|
+
Returns:
|
50
|
+
CognitoAuthenication: object to handle cognito authentication
|
51
|
+
"""
|
52
|
+
if self.__cognito is None:
|
53
|
+
self.__cognito = CognitoAuthenication(
|
54
|
+
client_id=self.__cognito_client_id,
|
55
|
+
region=self.__region,
|
56
|
+
aplos_domain=self.__domain,
|
57
|
+
)
|
58
|
+
|
59
|
+
return self.__cognito
|
60
|
+
|
61
|
+
@property
|
62
|
+
def domain(self) -> str:
|
63
|
+
"""
|
64
|
+
Domain
|
65
|
+
Returns:
|
66
|
+
str: the domain
|
67
|
+
"""
|
68
|
+
return self.__domain
|
69
|
+
|
70
|
+
@property
|
71
|
+
def config(self) -> NCAAppConfiguration:
|
72
|
+
"""
|
73
|
+
NCA App Configuration
|
74
|
+
Returns:
|
75
|
+
NCAAppConfiguration: object to handle the NCA App Configuration
|
76
|
+
"""
|
77
|
+
if self.__config is None:
|
78
|
+
if self.__domain is None:
|
79
|
+
raise RuntimeError(
|
80
|
+
"Failed to get Aplos Configuration. The Domain is not set."
|
81
|
+
)
|
82
|
+
|
83
|
+
self.__config = NCAAppConfiguration(
|
84
|
+
aplos_saas_domain=self.__domain,
|
85
|
+
)
|
86
|
+
|
87
|
+
return self.__config
|
88
|
+
|
89
|
+
def authenticate(
|
90
|
+
self,
|
91
|
+
username: str,
|
92
|
+
password: str,
|
93
|
+
) -> str:
|
94
|
+
"""_summary_
|
95
|
+
|
96
|
+
Args:
|
97
|
+
username (str): the username
|
98
|
+
password (str): the users password
|
99
|
+
|
100
|
+
"""
|
101
|
+
|
102
|
+
self.jwt = self.cognito.login(username=username, password=password)
|
103
|
+
|
104
|
+
return self.jwt
|