wcp-library 1.0.0__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.
- wcp_library-1.0.0/PKG-INFO +46 -0
- wcp_library-1.0.0/README.md +19 -0
- wcp_library-1.0.0/pyproject.toml +26 -0
- wcp_library-1.0.0/wcp_library/__init__.py +14 -0
- wcp_library-1.0.0/wcp_library/async_credentials/__init__.py +49 -0
- wcp_library-1.0.0/wcp_library/async_credentials/api.py +0 -0
- wcp_library-1.0.0/wcp_library/async_credentials/oracle.py +131 -0
- wcp_library-1.0.0/wcp_library/async_credentials/postgres.py +130 -0
- wcp_library-1.0.0/wcp_library/async_sql/__init__.py +35 -0
- wcp_library-1.0.0/wcp_library/async_sql/oracle.py +242 -0
- wcp_library-1.0.0/wcp_library/async_sql/postgres.py +217 -0
- wcp_library-1.0.0/wcp_library/credentials/__init__.py +49 -0
- wcp_library-1.0.0/wcp_library/credentials/api.py +0 -0
- wcp_library-1.0.0/wcp_library/credentials/oracle.py +125 -0
- wcp_library-1.0.0/wcp_library/credentials/postgres.py +124 -0
- wcp_library-1.0.0/wcp_library/emailing.py +90 -0
- wcp_library-1.0.0/wcp_library/informatica.py +112 -0
- wcp_library-1.0.0/wcp_library/logging.py +51 -0
- wcp_library-1.0.0/wcp_library/sql/__init__.py +35 -0
- wcp_library-1.0.0/wcp_library/sql/oracle.py +249 -0
- wcp_library-1.0.0/wcp_library/sql/postgres.py +226 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: wcp-library
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: Common utilites for internal development at WCP
|
5
|
+
Home-page: https://github.com/Whitecap-DNA/WCP-Library
|
6
|
+
Author: Mitch-Petersen
|
7
|
+
Author-email: mitch.petersen@wcap.ca
|
8
|
+
Requires-Python: >=3.11,<4.0
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
13
|
+
Requires-Dist: aiohttp (>=3.10.10,<4.0.0)
|
14
|
+
Requires-Dist: oracledb (>=2.5.0,<3.0.0)
|
15
|
+
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
16
|
+
Requires-Dist: pip-system-certs (>=4.0,<5.0)
|
17
|
+
Requires-Dist: psycopg (>=3.2.3,<4.0.0)
|
18
|
+
Requires-Dist: psycopg-binary (>=3.2.3,<4.0.0)
|
19
|
+
Requires-Dist: psycopg-pool (>=3.2.3,<4.0.0)
|
20
|
+
Requires-Dist: pycryptodome (>=3.21.0,<4.0.0)
|
21
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
22
|
+
Requires-Dist: yarl (>=1.17.1,<2.0.0)
|
23
|
+
Project-URL: Documentation, https://github.com/Whitecap-DNA/WCP-Library/wiki
|
24
|
+
Project-URL: Repository, https://github.com/Whitecap-DNA/WCP-Library
|
25
|
+
Description-Content-Type: text/markdown
|
26
|
+
|
27
|
+
# WCP-Library
|
28
|
+
|
29
|
+
## Description
|
30
|
+
Commonly used functions for the D&A team.
|
31
|
+
|
32
|
+
## Installation
|
33
|
+
`pip install WCP-Library`
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
See Wiki for more information.
|
37
|
+
|
38
|
+
## Authors and acknowledgment
|
39
|
+
Mitch Petersen
|
40
|
+
|
41
|
+
## Stakeholders
|
42
|
+
D&A Developers
|
43
|
+
|
44
|
+
## Project status
|
45
|
+
In Development
|
46
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# WCP-Library
|
2
|
+
|
3
|
+
## Description
|
4
|
+
Commonly used functions for the D&A team.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
`pip install WCP-Library`
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
See Wiki for more information.
|
11
|
+
|
12
|
+
## Authors and acknowledgment
|
13
|
+
Mitch Petersen
|
14
|
+
|
15
|
+
## Stakeholders
|
16
|
+
D&A Developers
|
17
|
+
|
18
|
+
## Project status
|
19
|
+
In Development
|
@@ -0,0 +1,26 @@
|
|
1
|
+
[tool.poetry]
|
2
|
+
name = "wcp-library"
|
3
|
+
version = "1.0.0"
|
4
|
+
description = "Common utilites for internal development at WCP"
|
5
|
+
authors = ["Mitch-Petersen <mitch.petersen@wcap.ca>"]
|
6
|
+
readme = "README.md"
|
7
|
+
repository = "https://github.com/Whitecap-DNA/WCP-Library"
|
8
|
+
documentation = "https://github.com/Whitecap-DNA/WCP-Library/wiki"
|
9
|
+
packages = [{include = "wcp_library"}]
|
10
|
+
|
11
|
+
[tool.poetry.dependencies]
|
12
|
+
python = "^3.11"
|
13
|
+
aiohttp = "^3.10.10"
|
14
|
+
oracledb = "^2.5.0"
|
15
|
+
pandas = "^2.2.3"
|
16
|
+
pip-system-certs = "^4.0"
|
17
|
+
psycopg = "^3.2.3"
|
18
|
+
psycopg-binary = "^3.2.3"
|
19
|
+
psycopg-pool = "^3.2.3"
|
20
|
+
pycryptodome = "^3.21.0"
|
21
|
+
requests = "^2.32.3"
|
22
|
+
yarl = "^1.17.1"
|
23
|
+
|
24
|
+
[build-system]
|
25
|
+
requires = ["poetry-core"]
|
26
|
+
build-backend = "poetry.core.masonry.api"
|
@@ -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
|