wcp-library 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.
@@ -0,0 +1,14 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ # PyInstaller import
6
+ import pip_system_certs.wrapt_requests
7
+
8
+
9
+ # Application Path
10
+ if getattr(sys, 'frozen', False):
11
+ application_path = sys.executable + '-'
12
+ application_path = Path(application_path).parent
13
+ else:
14
+ application_path = Path(os.path.dirname(os.path.abspath(__file__)))
@@ -0,0 +1,49 @@
1
+ import secrets
2
+ import string
3
+
4
+
5
+ class MissingCredentialsError(KeyError):
6
+ pass
7
+
8
+
9
+ async def generate_password(length: int=12, use_nums: bool=True, use_special: bool=True, special_chars_override: list=None, force_num: bool=True, force_spec: bool=True) -> str:
10
+ """
11
+ Function to generate a random password
12
+
13
+ :param length:
14
+ :param use_nums: Allows the use of numbers
15
+ :param use_special: Allows the use of special characters
16
+ :param special_chars_override: List of special characters to use
17
+ :param force_num: Requires the password to contain at least one number
18
+ :param force_spec: Requires the password to contain at least one special character
19
+ :return: Password
20
+ """
21
+
22
+ letters = string.ascii_letters
23
+ digits = string.digits
24
+ if special_chars_override:
25
+ special_chars = special_chars_override
26
+ else:
27
+ special_chars = string.punctuation
28
+
29
+ alphabet = letters
30
+ if use_nums:
31
+ alphabet += digits
32
+ if use_special:
33
+ alphabet += special_chars
34
+
35
+ pwd = ''
36
+ for i in range(length):
37
+ pwd += ''.join(secrets.choice(alphabet))
38
+
39
+ if (use_nums and force_num) and (use_special and force_spec):
40
+ while pwd[0].isdigit() or not any(char.isdigit() for char in pwd) or not any(char in pwd for char in special_chars):
41
+ pwd = await generate_password(length, use_nums, use_special, special_chars_override, force_num, force_spec)
42
+ elif use_nums and force_num:
43
+ while pwd[0].isdigit() or not any(char.isdigit() for char in pwd):
44
+ pwd = await generate_password(length, use_nums, use_special, special_chars_override, force_num, force_spec)
45
+ elif use_special and force_spec:
46
+ while not any(char in pwd for char in special_chars):
47
+ pwd = await generate_password(length, use_nums, use_special, special_chars_override, force_num, force_spec)
48
+
49
+ return pwd
File without changes
@@ -0,0 +1,131 @@
1
+ import logging
2
+
3
+ import aiohttp
4
+ from yarl import URL
5
+
6
+ from WCP_Library.async_credentials import MissingCredentialsError
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class AsyncOracleCredentialManager:
12
+ def __init__(self, passwordState_api_key: str):
13
+ self.password_url = URL("https://vault.wcap.ca/api/passwords/")
14
+ self.api_key = passwordState_api_key
15
+ self.headers = {"APIKey": self.api_key, 'Reason': 'Python Script Access'}
16
+ self._password_list_id = 207
17
+
18
+ async def _get_credentials(self) -> dict:
19
+ """
20
+ Get all credentials from the password list
21
+
22
+ :return:
23
+ """
24
+
25
+ logger.debug("Getting credentials from PasswordState")
26
+ url = (self.password_url / str(self._password_list_id)).with_query("QueryAll")
27
+ async with aiohttp.ClientSession() as session:
28
+ async with session.get(str(url), headers=self.headers) as response:
29
+ passwords = await response.json()
30
+
31
+ if not passwords:
32
+ raise MissingCredentialsError("No credentials found in this Password List")
33
+
34
+ password_dict = {}
35
+ for password in passwords:
36
+ password_info = {'PasswordID': password['PasswordID'], 'UserName': password['UserName'], 'Password': password['Password']}
37
+ for field in password['GenericFieldInfo']:
38
+ password_info[field['DisplayName']] = field['Value'].lower() if field['DisplayName'].lower() == 'username' else field['Value']
39
+ password_dict[password['UserName'].lower()] = password_info
40
+ logger.debug("Credentials retrieved")
41
+ return password_dict
42
+
43
+ async def get_credentials(self, username: str) -> dict:
44
+ """
45
+ Get the credentials for a specific username
46
+
47
+ :param username:
48
+ :return:
49
+ """
50
+
51
+ logger.debug(f"Getting credentials for {username}")
52
+ credentials = await self._get_credentials()
53
+
54
+ try:
55
+ return_credential = credentials[username.lower()]
56
+ except KeyError:
57
+ raise MissingCredentialsError(f"Credentials for {username} not found in this Password List")
58
+ logger.debug(f"Credentials for {username} retrieved")
59
+ return return_credential
60
+
61
+ async def update_credential(self, credentials_dict: dict) -> bool:
62
+ """
63
+ Update username and password in PasswordState
64
+
65
+ Credentials dictionary must have the following keys:
66
+ - PasswordID
67
+ - UserName
68
+ - Password
69
+
70
+ The dictionary can be obtained from the get_credentials method
71
+
72
+ :param credentials_dict:
73
+ :return:
74
+ """
75
+
76
+ logger.debug(f"Updating credentials for {credentials_dict['UserName']}")
77
+ url = (self.password_url / str(self._password_list_id)).with_query("QueryAll")
78
+ async with aiohttp.ClientSession() as session:
79
+ async with session.get(str(url), headers=self.headers) as response:
80
+ passwords = await response.json()
81
+
82
+ relevant_credential_entry = [x for x in passwords if x['UserName'] == credentials_dict['UserName']][0]
83
+ for field in relevant_credential_entry['GenericFieldInfo']:
84
+ if field['DisplayName'] in credentials_dict:
85
+ credentials_dict[field['GenericFieldID']] = credentials_dict[field['DisplayName']]
86
+ credentials_dict.pop(field['DisplayName'])
87
+
88
+ async with aiohttp.ClientSession() as session:
89
+ async with session.put(str(self.password_url), json=credentials_dict, headers=self.headers) as response:
90
+ if response.status == 200:
91
+ logger.debug(f"Credentials for {credentials_dict['UserName']} updated")
92
+ return True
93
+ else:
94
+ logger.error(f"Failed to update credentials for {credentials_dict['UserName']}")
95
+ return False
96
+
97
+ async def new_credentials(self, credentials_dict: dict) -> bool:
98
+ """
99
+ Create a new credential entry
100
+
101
+ Credentials dictionary must have the following keys:
102
+ - UserName
103
+ - Password
104
+ - Host
105
+ - Port
106
+ - Service or SID
107
+
108
+ :param credentials_dict:
109
+ :return:
110
+ """
111
+
112
+ data = {
113
+ "PasswordListID": self._password_list_id,
114
+ "Title": credentials_dict['UserName'].upper() if "Title" not in credentials_dict else credentials_dict['Title'].upper(),
115
+ "Notes": credentials_dict['Notes'] if 'Notes' in credentials_dict else None,
116
+ "UserName": credentials_dict['UserName'].lower(),
117
+ "Password": credentials_dict['Password'],
118
+ "GenericField1": credentials_dict['Host'],
119
+ "GenericField2": credentials_dict['Port'],
120
+ "GenericField3": credentials_dict['Service'] if 'Service' in credentials_dict else None,
121
+ "GenericField4": credentials_dict['SID'] if 'SID' in credentials_dict else None
122
+ }
123
+
124
+ async with aiohttp.ClientSession() as session:
125
+ async with session.put(str(self.password_url), json=data, headers=self.headers) as response:
126
+ if response.status == 201:
127
+ logger.debug(f"New credentials for {credentials_dict['UserName']} created")
128
+ return True
129
+ else:
130
+ logger.error(f"Failed to create new credentials for {credentials_dict['UserName']}")
131
+ return False
@@ -0,0 +1,130 @@
1
+ import logging
2
+
3
+ import aiohttp
4
+ from yarl import URL
5
+
6
+ from WCP_Library.async_credentials import MissingCredentialsError
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class AsyncPostgresCredentialManager:
12
+ def __init__(self, passwordState_api_key: str):
13
+ self.password_url = URL("https://vault.wcap.ca/api/passwords/")
14
+ self.api_key = passwordState_api_key
15
+ self.headers = {"APIKey": self.api_key, 'Reason': 'Python Script Access'}
16
+ self._password_list_id = 207
17
+
18
+ async def _get_credentials(self) -> dict:
19
+ """
20
+ Get all credentials from the password list
21
+
22
+ :return:
23
+ """
24
+
25
+ logger.debug("Getting credentials from PasswordState")
26
+ url = (self.password_url / str(self._password_list_id)).with_query("QueryAll")
27
+ async with aiohttp.ClientSession() as session:
28
+ async with session.get(str(url), headers=self.headers) as response:
29
+ passwords = await response.json()
30
+
31
+ if not passwords:
32
+ raise MissingCredentialsError("No credentials found in this Password List")
33
+
34
+ password_dict = {}
35
+ for password in passwords:
36
+ password_info = {'PasswordID': password['PasswordID'], 'UserName': password['UserName'], 'Password': password['Password']}
37
+ for field in password['GenericFieldInfo']:
38
+ password_info[field['DisplayName']] = field['Value'].lower() if field['DisplayName'].lower() == 'username' else field['Value']
39
+ password_dict[password['UserName'].lower()] = password_info
40
+ logger.debug("Credentials retrieved")
41
+ return password_dict
42
+
43
+ async def get_credentials(self, username: str) -> dict:
44
+ """
45
+ Get the credentials for a specific username
46
+
47
+ :param username:
48
+ :return:
49
+ """
50
+
51
+ logger.debug(f"Getting credentials for {username}")
52
+ credentials = await self._get_credentials()
53
+
54
+ try:
55
+ return_credential = credentials[username.lower()]
56
+ except KeyError:
57
+ raise MissingCredentialsError(f"Credentials for {username} not found in this Password List")
58
+ logger.debug(f"Credentials for {username} retrieved")
59
+ return return_credential
60
+
61
+ async def update_credential(self, credentials_dict: dict) -> bool:
62
+ """
63
+ Update username and password in PasswordState
64
+
65
+ Credentials dictionary must have the following keys:
66
+ - PasswordID
67
+ - UserName
68
+ - Password
69
+
70
+ The dictionary can be obtained from the get_credentials method
71
+
72
+ :param credentials_dict:
73
+ :return:
74
+ """
75
+
76
+ logger.debug(f"Updating credentials for {credentials_dict['UserName']}")
77
+ url = (self.password_url / str(self._password_list_id)).with_query("QueryAll")
78
+ async with aiohttp.ClientSession() as session:
79
+ async with session.get(str(url), headers=self.headers) as response:
80
+ passwords = await response.json()
81
+
82
+ relevant_credential_entry = [x for x in passwords if x['UserName'] == credentials_dict['UserName']][0]
83
+ for field in relevant_credential_entry['GenericFieldInfo']:
84
+ if field['DisplayName'] in credentials_dict:
85
+ credentials_dict[field['GenericFieldID']] = credentials_dict[field['DisplayName']]
86
+ credentials_dict.pop(field['DisplayName'])
87
+
88
+ async with aiohttp.ClientSession() as session:
89
+ async with session.put(str(self.password_url), json=credentials_dict, headers=self.headers) as response:
90
+ if response.status == 200:
91
+ logger.debug(f"Credentials for {credentials_dict['UserName']} updated")
92
+ return True
93
+ else:
94
+ logger.error(f"Failed to update credentials for {credentials_dict['UserName']}")
95
+ return False
96
+
97
+ async def new_credentials(self, credentials_dict: dict) -> bool:
98
+ """
99
+ Create a new credential entry
100
+
101
+ Credentials dictionary must have the following keys:
102
+ - UserName
103
+ - Password
104
+ - Host
105
+ - Port
106
+ - Database
107
+
108
+ :param credentials_dict:
109
+ :return:
110
+ """
111
+
112
+ data = {
113
+ "PasswordListID": self._password_list_id,
114
+ "Title": credentials_dict['UserName'].upper() if "Title" not in credentials_dict else credentials_dict['Title'].upper(),
115
+ "Notes": credentials_dict['Notes'] if 'Notes' in credentials_dict else None,
116
+ "UserName": credentials_dict['UserName'].lower(),
117
+ "Password": credentials_dict['Password'],
118
+ "GenericField1": credentials_dict['Host'],
119
+ "GenericField2": credentials_dict['Port'],
120
+ "GenericField3": credentials_dict['Database']
121
+ }
122
+
123
+ async with aiohttp.ClientSession() as session:
124
+ async with session.put(str(self.password_url), json=data, headers=self.headers) as response:
125
+ if response.status == 201:
126
+ logger.debug(f"New credentials for {credentials_dict['UserName']} created")
127
+ return True
128
+ else:
129
+ logger.error(f"Failed to create new credentials for {credentials_dict['UserName']}")
130
+ return False
@@ -0,0 +1,35 @@
1
+ import asyncio
2
+ import logging
3
+ from functools import wraps
4
+
5
+ import oracledb
6
+ import psycopg
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def retry(f: callable) -> callable:
12
+ """
13
+ Decorator to retry a function
14
+
15
+ :param f: function
16
+ :return: function
17
+ """
18
+
19
+ @wraps(f)
20
+ async def wrapper(self, *args, **kwargs):
21
+ self._retry_count = 0
22
+ while True:
23
+ try:
24
+ return await f(self, *args, **kwargs)
25
+ except (oracledb.OperationalError, psycopg.OperationalError) as e:
26
+ error_obj, = e.args
27
+ if error_obj.full_code in self.retry_error_codes and self._retry_count < self.retry_limit:
28
+ self._retry_count += 1
29
+ logger.debug(f"{self._db_service} connection error")
30
+ logger.debug(error_obj.message)
31
+ logger.info("Waiting 5 minutes before retrying Oracle connection")
32
+ await asyncio.sleep(300)
33
+ else:
34
+ raise e
35
+ return wrapper
@@ -0,0 +1,242 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ import pandas as pd
5
+ import oracledb
6
+ from oracledb import AsyncConnectionPool
7
+
8
+ from WCP_Library.async_sql import retry
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ async def connect_warehouse(username: str, password: str, hostname: str, port: int, database: str) -> AsyncConnectionPool:
14
+ """
15
+ Create Warehouse Connection
16
+
17
+ :param username: username
18
+ :param password: password
19
+ :param hostname: hostname
20
+ :param port: port
21
+ :param database: database
22
+ :return: session_pool
23
+ """
24
+
25
+ dsn = oracledb.makedsn(hostname, port, sid=database)
26
+ session_pool = oracledb.create_pool_async(
27
+ user=username,
28
+ password=password,
29
+ dsn=dsn,
30
+ min=2,
31
+ max=5,
32
+ increment=1,
33
+ threaded=True,
34
+ encoding="UTF-8"
35
+ )
36
+ return session_pool
37
+
38
+
39
+ class SQLConnection(object):
40
+ """
41
+ SQL Connection Class
42
+
43
+ :return: None
44
+ """
45
+
46
+ def __init__(self):
47
+ self._db_service: str = "Oracle"
48
+ self._username: Optional[str] = None
49
+ self._password: Optional[str] = None
50
+ self._hostname: Optional[str] = None
51
+ self._port: Optional[int] = None
52
+ self._database: Optional[str] = None
53
+ self._sid: Optional[str] = None
54
+ self._session_pool: Optional[AsyncConnectionPool] = None
55
+
56
+ self._retry_count = 0
57
+ self.retry_limit = 50
58
+ self.retry_error_codes = ['ORA-01033', 'DPY-6005', 'DPY-4011']
59
+
60
+ @retry
61
+ async def _connect(self) -> None:
62
+ """
63
+ Connect to the warehouse
64
+
65
+ :return: None
66
+ """
67
+
68
+ sid_or_service = self._database if self._database else self._sid
69
+
70
+ self._session_pool = connect_warehouse(self._username, self._password, self._hostname, self._port,
71
+ sid_or_service)
72
+
73
+ async def set_user(self, credentials_dict: dict) -> None:
74
+ """
75
+ Set the user credentials and connect
76
+
77
+ :param credentials_dict: dictionary of connection details
78
+ :return: None
79
+ """
80
+
81
+ if not any([self._database, self._sid]):
82
+ raise ValueError("Either Service or SID must be provided")
83
+
84
+ self._username: Optional[str] = credentials_dict['UserName']
85
+ self._password: Optional[str] = credentials_dict['Password']
86
+ self._hostname: Optional[str] = credentials_dict['Host']
87
+ self._port: Optional[int] = int(credentials_dict['Port'])
88
+ self._database: Optional[str] = credentials_dict['Service'] if 'Service' in credentials_dict else None
89
+ self._sid: Optional[str] = credentials_dict['SID'] if 'SID' in credentials_dict else None
90
+
91
+ await self._connect()
92
+
93
+ async def close_connection(self) -> None:
94
+ """
95
+ Close the connection
96
+
97
+ :return: None
98
+ """
99
+
100
+ await self._session_pool.close()
101
+
102
+ @retry
103
+ async def execute(self, query: str) -> None:
104
+ """
105
+ Execute the query
106
+
107
+ :param query: query
108
+ :return: None
109
+ """
110
+
111
+ connection = self._session_pool.acquire()
112
+ cursor = connection.cursor()
113
+ await cursor.execute(query)
114
+ await connection.commit()
115
+ await self._session_pool.release(connection)
116
+
117
+ @retry
118
+ async def safe_execute(self, query: str, packed_values: dict) -> None:
119
+ """
120
+ Execute the query without SQL Injection possibility, to be used with external facing projects.
121
+
122
+ :param query: query
123
+ :param packed_values: dictionary of values
124
+ :return: None
125
+ """
126
+
127
+ connection = self._session_pool.acquire()
128
+ cursor = connection.cursor()
129
+ await cursor.execute(query, packed_values)
130
+ await connection.commit()
131
+ await self._session_pool.release(connection)
132
+
133
+ @retry
134
+ async def execute_multiple(self, queries: list[list[str, dict]]) -> None:
135
+ """
136
+ Execute multiple queries
137
+
138
+ :param queries: list of queries
139
+ :return: None
140
+ """
141
+
142
+ connection = self._session_pool.acquire()
143
+ cursor = connection.cursor()
144
+ for item in queries:
145
+ query = item[0]
146
+ packed_values = item[1]
147
+ if packed_values:
148
+ await cursor.execute(query, packed_values)
149
+ else:
150
+ await cursor.execute(query)
151
+ await connection.commit()
152
+ await self._session_pool.release(connection)
153
+
154
+ @retry
155
+ async def execute_many(self, query: str, dictionary: list[dict]) -> None:
156
+ """
157
+ Execute many queries
158
+
159
+ :param query: query
160
+ :param dictionary: dictionary of values
161
+ :return: None
162
+ """
163
+
164
+ connection = self._session_pool.acquire()
165
+ cursor = connection.cursor()
166
+ await cursor.executemany(query, dictionary)
167
+ await connection.commit()
168
+ await self._session_pool.release(connection)
169
+
170
+ @retry
171
+ async def fetch_data(self, query: str, packed_data=None):
172
+ """
173
+ Fetch the data from the query
174
+
175
+ :param query: query
176
+ :param packed_data: packed data
177
+ :return: rows
178
+ """
179
+
180
+ connection = self._session_pool.acquire()
181
+ cursor = connection.cursor()
182
+ if packed_data:
183
+ await cursor.execute(query, packed_data)
184
+ else:
185
+ await cursor.execute(query)
186
+ rows = cursor.fetchall()
187
+ await self._session_pool.release(connection)
188
+ return rows
189
+
190
+ @retry
191
+ async def export_DF_to_warehouse(self, dfObj: pd.DataFrame, outputTableName: str, columns: list, remove_nan=False) -> None:
192
+ """
193
+ Export the DataFrame to the warehouse
194
+
195
+ :param dfObj: DataFrame
196
+ :param outputTableName: output table name
197
+ :param columns: list of columns
198
+ :param remove_nan: remove NaN values
199
+ :return: None
200
+ """
201
+
202
+ col = ', '.join(columns)
203
+ bindList = []
204
+ for column in columns:
205
+ bindList.append(':' + column)
206
+ bind = ', '.join(bindList)
207
+
208
+ main_dict = dfObj.to_dict('records')
209
+ if remove_nan:
210
+ for val, item in enumerate(main_dict):
211
+ for sub_item, value in item.items():
212
+ if pd.isna(value):
213
+ main_dict[val][sub_item] = None
214
+ else:
215
+ main_dict[val][sub_item] = value
216
+
217
+ query = """INSERT INTO {} ({}) VALUES ({})""".format(outputTableName, col, bind)
218
+ await self.execute_many(query, main_dict)
219
+
220
+ @retry
221
+ async def truncate_table(self, tableName: str) -> None:
222
+ """
223
+ Truncate the table
224
+
225
+ :param tableName: table name
226
+ :return: None
227
+ """
228
+
229
+ truncateQuery = """TRUNCATE TABLE {}""".format(tableName)
230
+ await self.execute(truncateQuery)
231
+
232
+ @retry
233
+ async def empty_table(self, tableName: str) -> None:
234
+ """
235
+ Empty the table
236
+
237
+ :param tableName: table name
238
+ :return: None
239
+ """
240
+
241
+ deleteQuery = """DELETE FROM {}""".format(tableName)
242
+ await self.execute(deleteQuery)