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,217 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ import pandas as pd
5
+ from psycopg.sql import SQL
6
+ from psycopg_pool 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
+ url = f"postgres://{username}:{password}@{hostname}:{port}/{database}"
26
+
27
+ session_pool = AsyncConnectionPool(
28
+ conninfo=url,
29
+ min_size=2,
30
+ max_size=5,
31
+ )
32
+ await session_pool.open()
33
+ return session_pool
34
+
35
+
36
+ class SQLConnection(object):
37
+ """
38
+ SQL Connection Class
39
+
40
+ :return: None
41
+ """
42
+
43
+ def __init__(self):
44
+ self._username: Optional[str] = None
45
+ self._password: Optional[str] = None
46
+ self._hostname: Optional[str] = None
47
+ self._port: Optional[int] = None
48
+ self._database: Optional[str] = None
49
+ self._session_pool: Optional[AsyncConnectionPool] = None
50
+
51
+ self._retry_count = 0
52
+ self.retry_limit = 50
53
+ self.retry_error_codes = ['08001', '08004']
54
+
55
+ @retry
56
+ async def _connect(self) -> None:
57
+ """
58
+ Connect to the warehouse
59
+
60
+ :return: None
61
+ """
62
+
63
+ self._session_pool = await connect_warehouse(self._username, self._password, self._hostname, self._port, self._database)
64
+
65
+ async def set_user(self, credentials_dict: dict) -> None:
66
+ """
67
+ Set the user credentials and connect
68
+
69
+ :param credentials_dict: dictionary of connection details
70
+ :return: None
71
+ """
72
+
73
+ self._username: Optional[str] = credentials_dict['UserName']
74
+ self._password: Optional[str] = credentials_dict['Password']
75
+ self._hostname: Optional[str] = credentials_dict['Host']
76
+ self._port: Optional[int] = int(credentials_dict['Port'])
77
+ self._database: Optional[str] = credentials_dict['Database']
78
+
79
+ await self._connect()
80
+
81
+ async def close_connection(self) -> None:
82
+ """
83
+ Close the connection
84
+
85
+ :return: None
86
+ """
87
+
88
+ await self._session_pool.close()
89
+
90
+ @retry
91
+ async def execute(self, query: SQL | str) -> None:
92
+ """
93
+ Execute the query
94
+
95
+ :param query: query
96
+ :return: None
97
+ """
98
+
99
+ async with self._session_pool.connection() as connection:
100
+ await connection.execute(query)
101
+
102
+ @retry
103
+ async def safe_execute(self, query: SQL | str, packed_values: dict) -> None:
104
+ """
105
+ Execute the query without SQL Injection possibility, to be used with external facing projects.
106
+
107
+ :param query: query
108
+ :param packed_values: dictionary of values
109
+ :return: None
110
+ """
111
+
112
+ async with self._session_pool.connection() as connection:
113
+ await connection.execute(query, packed_values)
114
+
115
+ @retry
116
+ async def execute_multiple(self, queries: list[list[SQL | str, dict]]) -> None:
117
+ """
118
+ Execute multiple queries
119
+
120
+ :param queries: list of queries
121
+ :return: None
122
+ """
123
+
124
+ async with self._session_pool.connection() as connection:
125
+ for item in queries:
126
+ query = item[0]
127
+ packed_values = item[1]
128
+ if packed_values:
129
+ await connection.execute(query, packed_values)
130
+ else:
131
+ await connection.execute(query)
132
+
133
+ @retry
134
+ async def execute_many(self, query: SQL | str, dictionary: list[dict]) -> None:
135
+ """
136
+ Execute many queries
137
+
138
+ :param query: query
139
+ :param dictionary: dictionary of values
140
+ :return: None
141
+ """
142
+
143
+ async with self._session_pool.connection() as connection:
144
+ await connection.executemany(query, dictionary)
145
+
146
+ @retry
147
+ async def fetch_data(self, query: SQL | str, packed_data=None):
148
+ """
149
+ Fetch the data from the query
150
+
151
+ :param query: query
152
+ :param packed_data: packed data
153
+ :return: rows
154
+ """
155
+
156
+ async with self._session_pool.connection() as connection:
157
+ cursor = connection.cursor()
158
+ if packed_data:
159
+ await cursor.execute(query, packed_data)
160
+ else:
161
+ await cursor.execute(query)
162
+ rows = await cursor.fetchall()
163
+ return rows
164
+
165
+ @retry
166
+ async def export_DF_to_warehouse(self, dfObj: pd.DataFrame, outputTableName: str, columns: list, remove_nan=False) -> None:
167
+ """
168
+ Export the DataFrame to the warehouse
169
+
170
+ :param dfObj: DataFrame
171
+ :param outputTableName: output table name
172
+ :param columns: list of columns
173
+ :param remove_nan: remove NaN values
174
+ :return: None
175
+ """
176
+
177
+ col = ', '.join(columns)
178
+ param_list = []
179
+ for column in columns:
180
+ param_list.append(f"%({column})s")
181
+ params = ', '.join(param_list)
182
+
183
+ main_dict = dfObj.to_dict('records')
184
+ if remove_nan:
185
+ for val, item in enumerate(main_dict):
186
+ for sub_item, value in item.items():
187
+ if pd.isna(value):
188
+ main_dict[val][sub_item] = None
189
+ else:
190
+ main_dict[val][sub_item] = value
191
+
192
+ query = """INSERT INTO {} ({}) VALUES ({})""".format(outputTableName, col, params)
193
+ await self.execute_many(query, main_dict)
194
+
195
+ @retry
196
+ async def truncate_table(self, tableName: str) -> None:
197
+ """
198
+ Truncate the table
199
+
200
+ :param tableName: table name
201
+ :return: None
202
+ """
203
+
204
+ truncateQuery = """TRUNCATE TABLE {}""".format(tableName)
205
+ await self.execute(truncateQuery)
206
+
207
+ @retry
208
+ async def empty_table(self, tableName: str) -> None:
209
+ """
210
+ Empty the table
211
+
212
+ :param tableName: table name
213
+ :return: None
214
+ """
215
+
216
+ deleteQuery = """DELETE FROM {}""".format(tableName)
217
+ await self.execute(deleteQuery)
@@ -0,0 +1,49 @@
1
+ import secrets
2
+ import string
3
+
4
+
5
+ class MissingCredentialsError(KeyError):
6
+ pass
7
+
8
+
9
+ 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 = 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 = 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 = generate_password(length, use_nums, use_special, special_chars_override, force_num, force_spec)
48
+
49
+ return pwd
File without changes
@@ -0,0 +1,125 @@
1
+ import logging
2
+
3
+ import requests
4
+ from yarl import URL
5
+
6
+ from WCP_Library.credentials import MissingCredentialsError
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class OracleCredentialManager:
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
+ def _get_credentials(self) -> dict:
19
+ """
20
+ Get all credentials from the password list
21
+
22
+ :return: Dictionary of credentials
23
+ """
24
+
25
+ logger.debug("Getting credentials from PasswordState")
26
+ url = (self.password_url / str(self._password_list_id)).with_query("QueryAll")
27
+ passwords = requests.get(str(url), headers=self.headers).json()
28
+
29
+ if not passwords:
30
+ raise MissingCredentialsError("No credentials found in this Password List")
31
+
32
+ password_dict = {}
33
+ for password in passwords:
34
+ password_info = {'PasswordID': password['PasswordID'], 'UserName': password['UserName'], 'Password': password['Password']}
35
+ for field in password['GenericFieldInfo']:
36
+ password_info[field['DisplayName']] = field['Value'].lower() if field['DisplayName'].lower() == 'username' else field['Value']
37
+ password_dict[password['UserName'].lower()] = password_info
38
+ logger.debug("Credentials retrieved")
39
+ return password_dict
40
+
41
+ def get_credentials(self, username: str) -> dict:
42
+ """
43
+ Get the credentials for a specific username
44
+
45
+ :param username:
46
+ :return: Dictionary of credentials
47
+ """
48
+
49
+ logger.debug(f"Getting credentials for {username}")
50
+ credentials = self._get_credentials()
51
+
52
+ try:
53
+ return_credential = credentials[username.lower()]
54
+ except KeyError:
55
+ raise MissingCredentialsError(f"Credentials for {username} not found in this Password List")
56
+ logger.debug(f"Credentials for {username} retrieved")
57
+ return return_credential
58
+
59
+ def update_credential(self, credentials_dict: dict) -> bool:
60
+ """
61
+ Update the credentials for a specific username
62
+
63
+ Credentials dictionary must have the following keys:
64
+ - PasswordID
65
+ - UserName
66
+ - Password
67
+
68
+ The dictionary should be obtained from the get_credentials method and modified accordingly
69
+
70
+ :param credentials_dict:
71
+ :return: True if successful, False otherwise
72
+ """
73
+
74
+ logger.debug(f"Updating credentials for {credentials_dict['UserName']}")
75
+ url = (self.password_url / str(self._password_list_id)).with_query("QueryAll")
76
+ passwords = requests.get(str(url), headers=self.headers).json()
77
+
78
+ relevant_credential_entry = [x for x in passwords if x['UserName'] == credentials_dict['UserName']][0]
79
+ for field in relevant_credential_entry['GenericFieldInfo']:
80
+ if field['DisplayName'] in credentials_dict:
81
+ credentials_dict[field['GenericFieldID']] = credentials_dict[field['DisplayName']]
82
+ credentials_dict.pop(field['DisplayName'])
83
+
84
+ response = requests.put(str(self.password_url), json=credentials_dict, headers=self.headers)
85
+ if response.status_code == 200:
86
+ logger.debug(f"Credentials for {credentials_dict['UserName']} updated")
87
+ return True
88
+ else:
89
+ logger.error(f"Failed to update credentials for {credentials_dict['UserName']}")
90
+ return False
91
+
92
+ def new_credentials(self, credentials_dict: dict) -> bool:
93
+ """
94
+ Create a new credential entry
95
+
96
+ Credentials dictionary must have the following keys:
97
+ - UserName
98
+ - Password
99
+ - Host
100
+ - Port
101
+ - Service or SID
102
+
103
+ :param credentials_dict:
104
+ :return: True if successful, False otherwise
105
+ """
106
+
107
+ data = {
108
+ "PasswordListID": self._password_list_id,
109
+ "Title": credentials_dict['UserName'].upper() if "Title" not in credentials_dict else credentials_dict['Title'].upper(),
110
+ "Notes": credentials_dict['Notes'] if 'Notes' in credentials_dict else None,
111
+ "UserName": credentials_dict['UserName'].lower(),
112
+ "Password": credentials_dict['Password'],
113
+ "GenericField1": credentials_dict['Host'],
114
+ "GenericField2": credentials_dict['Port'],
115
+ "GenericField3": credentials_dict['Service'] if 'Service' in credentials_dict else None,
116
+ "GenericField4": credentials_dict['SID'] if 'SID' in credentials_dict else None
117
+ }
118
+
119
+ response = requests.post(str(self.password_url), json=data, headers=self.headers)
120
+ if response.status_code == 201:
121
+ logger.debug(f"New credentials for {credentials_dict['UserName']} created")
122
+ return True
123
+ else:
124
+ logger.error(f"Failed to create new credentials for {credentials_dict['UserName']}")
125
+ return False
@@ -0,0 +1,124 @@
1
+ import logging
2
+
3
+ import requests
4
+ from yarl import URL
5
+
6
+ from WCP_Library.credentials import MissingCredentialsError
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class PostgresCredentialManager:
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
+ def _get_credentials(self) -> dict:
19
+ """
20
+ Get all credentials from the password list
21
+
22
+ :return: Dictionary of credentials
23
+ """
24
+
25
+ logger.debug("Getting credentials from PasswordState")
26
+ url = (self.password_url / str(self._password_list_id)).with_query("QueryAll")
27
+ passwords = requests.get(str(url), headers=self.headers).json()
28
+
29
+ if not passwords:
30
+ raise MissingCredentialsError("No credentials found in this Password List")
31
+
32
+ password_dict = {}
33
+ for password in passwords:
34
+ password_info = {'PasswordID': password['PasswordID'], 'UserName': password['UserName'], 'Password': password['Password']}
35
+ for field in password['GenericFieldInfo']:
36
+ password_info[field['DisplayName']] = field['Value'].lower() if field['DisplayName'].lower() == 'username' else field['Value']
37
+ password_dict[password['UserName'].lower()] = password_info
38
+ logger.debug("Credentials retrieved")
39
+ return password_dict
40
+
41
+ def get_credentials(self, username: str) -> dict:
42
+ """
43
+ Get the credentials for a specific username
44
+
45
+ :param username:
46
+ :return: Dictionary of credentials
47
+ """
48
+
49
+ logger.debug(f"Getting credentials for {username}")
50
+ credentials = self._get_credentials()
51
+
52
+ try:
53
+ return_credential = credentials[username.lower()]
54
+ except KeyError:
55
+ raise MissingCredentialsError(f"Credentials for {username} not found in this Password List")
56
+ logger.debug(f"Credentials for {username} retrieved")
57
+ return return_credential
58
+
59
+ def update_credential(self, credentials_dict: dict) -> bool:
60
+ """
61
+ Update the credentials for a specific username
62
+
63
+ Credentials dictionary must have the following keys:
64
+ - PasswordID
65
+ - UserName
66
+ - Password
67
+
68
+ The dictionary should be obtained from the get_credentials method and modified accordingly
69
+
70
+ :param credentials_dict:
71
+ :return: True if successful, False otherwise
72
+ """
73
+
74
+ logger.debug(f"Updating credentials for {credentials_dict['UserName']}")
75
+ url = (self.password_url / str(self._password_list_id)).with_query("QueryAll")
76
+ passwords = requests.get(str(url), headers=self.headers).json()
77
+
78
+ relevant_credential_entry = [x for x in passwords if x['UserName'] == credentials_dict['UserName']][0]
79
+ for field in relevant_credential_entry['GenericFieldInfo']:
80
+ if field['DisplayName'] in credentials_dict:
81
+ credentials_dict[field['GenericFieldID']] = credentials_dict[field['DisplayName']]
82
+ credentials_dict.pop(field['DisplayName'])
83
+
84
+ response = requests.put(str(self.password_url), json=credentials_dict, headers=self.headers)
85
+ if response.status_code == 200:
86
+ logger.debug(f"Credentials for {credentials_dict['UserName']} updated")
87
+ return True
88
+ else:
89
+ logger.error(f"Failed to update credentials for {credentials_dict['UserName']}")
90
+ return False
91
+
92
+ def new_credentials(self, credentials_dict: dict) -> bool:
93
+ """
94
+ Create a new credential entry
95
+
96
+ Credentials dictionary must have the following keys:
97
+ - UserName
98
+ - Password
99
+ - Host
100
+ - Port
101
+ - Database
102
+
103
+ :param credentials_dict:
104
+ :return: True if successful, False otherwise
105
+ """
106
+
107
+ data = {
108
+ "PasswordListID": self._password_list_id,
109
+ "Title": credentials_dict['UserName'].upper() if "Title" not in credentials_dict else credentials_dict['Title'].upper(),
110
+ "Notes": credentials_dict['Notes'] if 'Notes' in credentials_dict else None,
111
+ "UserName": credentials_dict['UserName'].lower(),
112
+ "Password": credentials_dict['Password'],
113
+ "GenericField1": credentials_dict['Host'],
114
+ "GenericField2": credentials_dict['Port'],
115
+ "GenericField3": credentials_dict['Database']
116
+ }
117
+
118
+ response = requests.post(str(self.password_url), json=data, headers=self.headers)
119
+ if response.status_code == 201:
120
+ logger.debug(f"New credentials for {credentials_dict['UserName']} created")
121
+ return True
122
+ else:
123
+ logger.error(f"Failed to create new credentials for {credentials_dict['UserName']}")
124
+ return False
@@ -0,0 +1,90 @@
1
+ import smtplib
2
+ from email import encoders
3
+ from email.mime.base import MIMEBase
4
+ from email.mime.multipart import MIMEMultipart
5
+ from email.utils import formatdate
6
+ from pathlib import Path
7
+
8
+
9
+ def send_email(sender: str, recipients: list, subject: str, message=None):
10
+ """
11
+ Function to send an email
12
+
13
+ :param sender:
14
+ :param recipients:
15
+ :param subject:
16
+ :param message:
17
+ :return:
18
+ """
19
+
20
+ msg = MIMEMultipart()
21
+ msg['From'] = sender
22
+ msg['To'] = ", ".join(recipients)
23
+ msg['Date'] = formatdate(localtime=True)
24
+ msg['Subject'] = subject
25
+ msg.attach(message)
26
+
27
+ smtpServer = 'mail.wcap.ca'
28
+ server = smtplib.SMTP(smtpServer, 25)
29
+ server.ehlo()
30
+ server.sendmail(sender, recipients, msg.as_string())
31
+ server.quit()
32
+
33
+
34
+ def email_reporting(subject: str, message: str):
35
+ """
36
+ Function to email the reporting team from the Python email
37
+
38
+ :param subject:
39
+ :param message:
40
+ :return:
41
+ """
42
+
43
+ msg = MIMEMultipart()
44
+ msg['From'] = "Python@wcap.ca"
45
+ msg['To'] = "Reporting@wcap.ca"
46
+ msg['Date'] = formatdate(localtime=True)
47
+ msg['Subject'] = subject
48
+ msg.attach(message)
49
+
50
+ smtpServer = 'mail.wcap.ca'
51
+ server = smtplib.SMTP(smtpServer, 25)
52
+ server.ehlo()
53
+ server.sendmail("Python@wcap.ca", 'Reporting@wcap.ca', msg.as_string())
54
+ server.quit()
55
+
56
+
57
+ def email_with_attachments(sender: str, recipients: list, subject: str, message=None, attachments: list[Path]=None):
58
+ """
59
+ Function to send an email with attachments
60
+
61
+ File paths must be passed as a list of Path (pathlib.Path) objects
62
+
63
+ :param sender:
64
+ :param recipients:
65
+ :param subject:
66
+ :param message:
67
+ :param attachments:
68
+ :return:
69
+ """
70
+
71
+ msg = MIMEMultipart()
72
+ msg['From'] = sender
73
+ msg['To'] = ", ".join(recipients)
74
+ msg['Date'] = formatdate(localtime=True)
75
+ msg['Subject'] = subject
76
+ msg.attach(message)
77
+
78
+ for attachment in attachments:
79
+ part = MIMEBase('application', "octet-stream")
80
+ with open(attachment, 'rb') as file:
81
+ part.set_payload(file.read())
82
+ encoders.encode_base64(part)
83
+ part.add_header('Content-Disposition', 'attachment; filename={}'.format(attachment.name))
84
+ msg.attach(part)
85
+
86
+ smtpServer = 'mail.wcap.ca'
87
+ server = smtplib.SMTP(smtpServer, 25)
88
+ server.ehlo()
89
+ server.sendmail(sender, recipients, msg.as_string())
90
+ server.quit()