wcp-library 1.1.2__py3-none-any.whl → 1.1.3__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.
- wcp_library/credentials/ftp.py +124 -0
- wcp_library/credentials/oracle.py +1 -1
- wcp_library/credentials/postgres.py +1 -1
- wcp_library/ftp/ftp.py +122 -0
- wcp_library/ftp/sftp.py +122 -0
- {wcp_library-1.1.2.dist-info → wcp_library-1.1.3.dist-info}/METADATA +3 -1
- {wcp_library-1.1.2.dist-info → wcp_library-1.1.3.dist-info}/RECORD +9 -6
- /wcp_library/{credentials/api.py → ftp/__init__.py} +0 -0
- {wcp_library-1.1.2.dist-info → wcp_library-1.1.3.dist-info}/WHEEL +0 -0
@@ -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 FTPCredentialManager:
|
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 = 208
|
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
|
+
- FTP/SFTP (FTP or SFTP)
|
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['FTP/SFTP']
|
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
|
wcp_library/ftp/ftp.py
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
import ftplib
|
2
|
+
import logging
|
3
|
+
import re
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Optional
|
6
|
+
|
7
|
+
import ftputil
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
|
12
|
+
class FTP:
|
13
|
+
def __init__(self, host: Optional[str]=None, port: Optional[int]=21, password_vault_dict: Optional[dict]=None):
|
14
|
+
self.host: str = host if not password_vault_dict else password_vault_dict['Host']
|
15
|
+
self.port: int = port if not password_vault_dict else password_vault_dict['Port']
|
16
|
+
self._username: Optional[str] = None if not password_vault_dict else password_vault_dict['UserName']
|
17
|
+
self._password: Optional[str] = None if not password_vault_dict else password_vault_dict['Password']
|
18
|
+
|
19
|
+
self._ftp_factory: ftputil.session.session_factory = ftputil.session.session_factory(base_class=ftplib.FTP_TLS,
|
20
|
+
port=self.port,
|
21
|
+
encrypt_data_channel=True,
|
22
|
+
encoding="UTF-8")
|
23
|
+
self.ftp_connection: Optional[ftputil.FTPHost] = None if not (self._username and self._password) \
|
24
|
+
else ftputil.FTPHost(self.host, self._username, self._password, session_factory=self._ftp_factory)
|
25
|
+
|
26
|
+
def login(self, username: str, password: str) -> None:
|
27
|
+
"""
|
28
|
+
Login to the FTP server
|
29
|
+
|
30
|
+
:param username:
|
31
|
+
:param password:
|
32
|
+
:return:
|
33
|
+
"""
|
34
|
+
|
35
|
+
logger.debug(f"Logging into {self.host} with username {username}")
|
36
|
+
self.host = ftputil.FTPHost(self.host, username, password, session_factory=self._ftp_factory)
|
37
|
+
|
38
|
+
def download(self, remote_file: Path, local_file: Path) -> None:
|
39
|
+
"""
|
40
|
+
Download a file from the FTP server
|
41
|
+
|
42
|
+
:param remote_file:
|
43
|
+
:param local_file:
|
44
|
+
:return:
|
45
|
+
"""
|
46
|
+
|
47
|
+
logger.debug(f"Downloading {remote_file} to {local_file}")
|
48
|
+
self.ftp_connection.download(remote_file, local_file)
|
49
|
+
|
50
|
+
def download_files(self, local_dir: Path, regex_pattern: str='*') -> None:
|
51
|
+
"""
|
52
|
+
Download files from the FTP server matching the regex pattern
|
53
|
+
|
54
|
+
:param local_dir:
|
55
|
+
:param regex_pattern:
|
56
|
+
:return:
|
57
|
+
"""
|
58
|
+
|
59
|
+
logger.debug(f"Downloading files from FTP server matching {regex_pattern} to {local_dir}")
|
60
|
+
files = self.list_files()
|
61
|
+
for file in files:
|
62
|
+
if re.match(regex_pattern, file.name):
|
63
|
+
logger.debug(f"Downloading {file} to {local_dir / file.name}")
|
64
|
+
self.ftp_connection.download(file, local_dir / file.name)
|
65
|
+
|
66
|
+
def list_files(self) -> list[Path]:
|
67
|
+
"""
|
68
|
+
List files on the FTP server
|
69
|
+
|
70
|
+
Returns a list of file paths
|
71
|
+
|
72
|
+
:return:
|
73
|
+
"""
|
74
|
+
|
75
|
+
logger.debug(f"Listing files on {self.ftp_connection.curdir}")
|
76
|
+
return [Path(x) for x in self.ftp_connection.listdir(self.ftp_connection.curdir) if self.ftp_connection.path.isfile(x)]
|
77
|
+
|
78
|
+
def list_dirs(self) -> list[Path]:
|
79
|
+
"""
|
80
|
+
List directories on the FTP server
|
81
|
+
|
82
|
+
Returns a list of directory paths
|
83
|
+
|
84
|
+
:return:
|
85
|
+
"""
|
86
|
+
|
87
|
+
logger.debug(f"Listing directories on {self.ftp_connection.curdir}")
|
88
|
+
return [Path(x) for x in self.ftp_connection.listdir(self.ftp_connection.curdir) if self.ftp_connection.path.isdir(x)]
|
89
|
+
|
90
|
+
def change_dir(self, remote_dir: Path) -> None:
|
91
|
+
"""
|
92
|
+
Change the directory on the FTP server
|
93
|
+
|
94
|
+
:param remote_dir:
|
95
|
+
:return:
|
96
|
+
"""
|
97
|
+
|
98
|
+
logger.debug(f"Changing FTP directory to {remote_dir}")
|
99
|
+
self.ftp_connection.chdir(remote_dir)
|
100
|
+
|
101
|
+
def upload(self, local_file: Path, remote_file: Path) -> None:
|
102
|
+
"""
|
103
|
+
Upload a file to the FTP server
|
104
|
+
|
105
|
+
:param local_file:
|
106
|
+
:param remote_file:
|
107
|
+
:return:
|
108
|
+
"""
|
109
|
+
|
110
|
+
logger.debug(f"Uploading {local_file} to {remote_file}")
|
111
|
+
self.ftp_connection.upload(local_file, remote_file)
|
112
|
+
|
113
|
+
def close(self) -> None:
|
114
|
+
"""
|
115
|
+
Close the FTP connection
|
116
|
+
|
117
|
+
:return:
|
118
|
+
"""
|
119
|
+
|
120
|
+
logger.debug(f"Closing FTP connection")
|
121
|
+
self.ftp_connection.close()
|
122
|
+
self.ftp_connection = None
|
wcp_library/ftp/sftp.py
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
import logging
|
2
|
+
import re
|
3
|
+
import stat
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Optional
|
6
|
+
|
7
|
+
import paramiko
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
|
12
|
+
class SFTP:
|
13
|
+
def __init__(self, host: str, port: int=22):
|
14
|
+
self.host: str = host
|
15
|
+
self.port: int = port
|
16
|
+
self._username: Optional[str] = None
|
17
|
+
self._password: Optional[str] = None
|
18
|
+
|
19
|
+
self.ssh = paramiko.SSHClient()
|
20
|
+
# AutoAddPolicy automatically adds the hostname and new host key to the local HostKeys object
|
21
|
+
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
22
|
+
|
23
|
+
self.sftp_connection: Optional[paramiko.SFTP] = None
|
24
|
+
|
25
|
+
def login(self, username: str, password: str) -> None:
|
26
|
+
"""
|
27
|
+
Login to the SFTP server
|
28
|
+
|
29
|
+
:param username:
|
30
|
+
:param password:
|
31
|
+
:return:
|
32
|
+
"""
|
33
|
+
|
34
|
+
logger.debug(f"Logging into {self.host} with username {username}")
|
35
|
+
self.ssh.connect(self.host, self.port, username, password)
|
36
|
+
self.sftp_connection = self.ssh.open_sftp()
|
37
|
+
|
38
|
+
def download(self, remote_file: Path, local_file: Path) -> None:
|
39
|
+
"""
|
40
|
+
Download a file from the SFTP server
|
41
|
+
|
42
|
+
:param remote_file:
|
43
|
+
:param local_file:
|
44
|
+
:return:
|
45
|
+
"""
|
46
|
+
|
47
|
+
logger.debug(f"Downloading {remote_file} to {local_file}")
|
48
|
+
self.sftp_connection.get(str(remote_file), local_file)
|
49
|
+
|
50
|
+
def download_files(self, local_dir: Path, regex_pattern: str='*') -> None:
|
51
|
+
"""
|
52
|
+
Download files from the SFTP server matching the regex pattern
|
53
|
+
|
54
|
+
:param local_dir:
|
55
|
+
:param regex_pattern:
|
56
|
+
:return:
|
57
|
+
"""
|
58
|
+
|
59
|
+
logger.debug(f"Downloading files from FTP server matching {regex_pattern} to {local_dir}")
|
60
|
+
files = self.list_files()
|
61
|
+
for file in files:
|
62
|
+
if re.match(regex_pattern, file.name):
|
63
|
+
logger.debug(f"Downloading {file} to {local_dir / file.name}")
|
64
|
+
self.sftp_connection.get(str(file), local_dir / file.name)
|
65
|
+
|
66
|
+
def list_files(self) -> list[Path]:
|
67
|
+
"""
|
68
|
+
List files on the SFTP server
|
69
|
+
|
70
|
+
Returns a list of file paths
|
71
|
+
|
72
|
+
:return:
|
73
|
+
"""
|
74
|
+
|
75
|
+
logger.debug(f"Listing files on {self.sftp_connection.getcwd()}")
|
76
|
+
return [Path(x) for x in self.sftp_connection.listdir() if stat.S_ISREG(self.sftp_connection.lstat(x).st_mode)]
|
77
|
+
|
78
|
+
def list_dirs(self) -> list[Path]:
|
79
|
+
"""
|
80
|
+
List directories on the SFTP server
|
81
|
+
|
82
|
+
Returns a list of directory paths
|
83
|
+
|
84
|
+
:return:
|
85
|
+
"""
|
86
|
+
|
87
|
+
logger.debug(f"Listing directories on {self.sftp_connection.getcwd()}")
|
88
|
+
return [Path(x) for x in self.sftp_connection.listdir() if stat.S_ISDIR(self.sftp_connection.lstat(x).st_mode)]
|
89
|
+
|
90
|
+
def change_dir(self, remote_dir: Path) -> None:
|
91
|
+
"""
|
92
|
+
Change the directory on the SFTP server
|
93
|
+
|
94
|
+
:param remote_dir:
|
95
|
+
:return:
|
96
|
+
"""
|
97
|
+
|
98
|
+
logger.debug(f"Changing FTP directory to {remote_dir}")
|
99
|
+
self.sftp_connection.chdir(str(remote_dir))
|
100
|
+
|
101
|
+
def upload(self, local_file: Path, remote_file: Path) -> None:
|
102
|
+
"""
|
103
|
+
Upload a file to the SFTP server
|
104
|
+
|
105
|
+
:param local_file:
|
106
|
+
:param remote_file:
|
107
|
+
:return:
|
108
|
+
"""
|
109
|
+
|
110
|
+
logger.debug(f"Uploading {local_file} to {remote_file}")
|
111
|
+
self.sftp_connection.put(local_file, str(remote_file))
|
112
|
+
|
113
|
+
def close(self):
|
114
|
+
"""
|
115
|
+
Close the SFTP connection
|
116
|
+
|
117
|
+
:return:
|
118
|
+
"""
|
119
|
+
|
120
|
+
logger.debug("Closing FTP connection")
|
121
|
+
self.sftp_connection.close()
|
122
|
+
self.sftp_connection = None
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: wcp-library
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.3
|
4
4
|
Summary: Common utilites for internal development at WCP
|
5
5
|
Home-page: https://github.com/Whitecap-DNA/WCP-Library
|
6
6
|
Author: Mitch-Petersen
|
@@ -10,8 +10,10 @@ Classifier: Programming Language :: Python :: 3
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.12
|
11
11
|
Classifier: Programming Language :: Python :: 3.13
|
12
12
|
Requires-Dist: aiohttp (>=3.10.10,<4.0.0)
|
13
|
+
Requires-Dist: ftputil (>=5.1.0,<6.0.0)
|
13
14
|
Requires-Dist: oracledb (>=2.5.0,<3.0.0)
|
14
15
|
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
16
|
+
Requires-Dist: paramiko (>=3.5.0,<4.0.0)
|
15
17
|
Requires-Dist: pip-system-certs (>=4.0,<5.0)
|
16
18
|
Requires-Dist: psycopg (>=3.2.3,<4.0.0)
|
17
19
|
Requires-Dist: psycopg-binary (>=3.2.3,<4.0.0)
|
@@ -7,15 +7,18 @@ wcp_library/async_sql/__init__.py,sha256=m4eWmUGYUEDomhhOp1ScB2uSIXFsNUNO589o5Rw
|
|
7
7
|
wcp_library/async_sql/oracle.py,sha256=T4wLdX3sHQQOfDNqb35GM4wHFP5Vs5veB7iQtdCxPVM,7402
|
8
8
|
wcp_library/async_sql/postgres.py,sha256=MGPv1ffUDn7oJX1ojviVD3N7jk8dtQERnbDateuwZJo,6597
|
9
9
|
wcp_library/credentials/__init__.py,sha256=HRmg7mqcATeclIz3lZQjSR4nmK6aY6MK9-QXEYZoFrw,1857
|
10
|
-
wcp_library/credentials/
|
11
|
-
wcp_library/credentials/oracle.py,sha256=
|
12
|
-
wcp_library/credentials/postgres.py,sha256=
|
10
|
+
wcp_library/credentials/ftp.py,sha256=FRxKVWifz7olQIrSjDgkTqk7rmc7Zdwmkfq7b62DQY8,4965
|
11
|
+
wcp_library/credentials/oracle.py,sha256=IJVGd10LH5LUNKUSFAbApi0sjR1AbloMJRHTuR9zFrQ,5095
|
12
|
+
wcp_library/credentials/postgres.py,sha256=psOPIAr6rBxce47hIHBJxgBAdsAEufjsUKZD0JRnZwI,4956
|
13
13
|
wcp_library/emailing.py,sha256=xqNly6Tmj-pvwl5bdys3gauZFDd4SuWCQYzGFNemv2Q,2496
|
14
|
+
wcp_library/ftp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
+
wcp_library/ftp/ftp.py,sha256=0Cv_7iKMrYfqMnax9_8rJ12yiGN117ktwsi1HUNqLd8,4234
|
16
|
+
wcp_library/ftp/sftp.py,sha256=zUL2IJSJ4FAk-OI1SeGVd3njbBfbiP7ZsBWBt8irou4,3536
|
14
17
|
wcp_library/informatica.py,sha256=IXZtk_9X1XLbOEwFrsyOwTgajQKvtXgANBHmuTOP3Kk,4064
|
15
18
|
wcp_library/logging.py,sha256=cFaY7kHEV1DukNiIVIsckrS046vBQ3W9sbFDfP1tcPk,2056
|
16
19
|
wcp_library/sql/__init__.py,sha256=CLlBEBrWVAwE79bUxuQiwikSrYH8m9QRYSJ2l0-ofsY,1003
|
17
20
|
wcp_library/sql/oracle.py,sha256=FeMvqLte26vNQlR1esdZAUICy9IqOuE0zTTrx245bhY,7283
|
18
21
|
wcp_library/sql/postgres.py,sha256=I2iDQ13z_hbEfpFBliozLfsg8xUdSzEVqr12yyPSdh4,6489
|
19
|
-
wcp_library-1.1.
|
20
|
-
wcp_library-1.1.
|
21
|
-
wcp_library-1.1.
|
22
|
+
wcp_library-1.1.3.dist-info/METADATA,sha256=zajA-dJ586d2R7DYhH7Ams4EFUkDpkYJYLSbdR2JBdU,1421
|
23
|
+
wcp_library-1.1.3.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
24
|
+
wcp_library-1.1.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|