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.
Files changed (28) hide show
  1. aplos_nca_saas_sdk/__init__.py +0 -0
  2. aplos_nca_saas_sdk/aws_resources/aws_cognito.py +188 -0
  3. aplos_nca_saas_sdk/aws_resources/aws_s3_presigned_payload.py +49 -0
  4. aplos_nca_saas_sdk/aws_resources/aws_s3_presigned_upload.py +116 -0
  5. aplos_nca_saas_sdk/files/analysis_files/single_ev/configuration_single_ev.json +51 -0
  6. aplos_nca_saas_sdk/files/analysis_files/single_ev/meta_data.json +17 -0
  7. aplos_nca_saas_sdk/files/analysis_files/single_ev/single_ev.csv +121 -0
  8. aplos_nca_saas_sdk/integration_testing/integration_test_base.py +34 -0
  9. aplos_nca_saas_sdk/integration_testing/integration_test_factory.py +62 -0
  10. aplos_nca_saas_sdk/integration_testing/integration_test_suite.py +84 -0
  11. aplos_nca_saas_sdk/integration_testing/main.py +28 -0
  12. aplos_nca_saas_sdk/integration_testing/readme.md +18 -0
  13. aplos_nca_saas_sdk/integration_testing/tests/app_configuration_test.py +30 -0
  14. aplos_nca_saas_sdk/integration_testing/tests/app_execution_test.py +5 -0
  15. aplos_nca_saas_sdk/integration_testing/tests/app_login_test.py +32 -0
  16. aplos_nca_saas_sdk/integration_testing/tests/app_validation_test.py +5 -0
  17. aplos_nca_saas_sdk/nca_resources/nca_app_configuration.py +69 -0
  18. aplos_nca_saas_sdk/nca_resources/nca_endpoints.py +54 -0
  19. aplos_nca_saas_sdk/nca_resources/nca_executions.py +378 -0
  20. aplos_nca_saas_sdk/nca_resources/nca_login.py +104 -0
  21. aplos_nca_saas_sdk/utilities/commandline_args.py +332 -0
  22. aplos_nca_saas_sdk/utilities/environment_services.py +81 -0
  23. aplos_nca_saas_sdk/utilities/environment_vars.py +23 -0
  24. aplos_nca_saas_sdk/utilities/http_utility.py +30 -0
  25. aplos_nca_saas_sdk/version.py +4 -0
  26. aplos_nca_saas_sdk-1.0.0.dist-info/METADATA +195 -0
  27. aplos_nca_saas_sdk-1.0.0.dist-info/RECORD +28 -0
  28. 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