wcp-library 1.1.2__tar.gz → 1.1.4__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.
Files changed (24) hide show
  1. {wcp_library-1.1.2 → wcp_library-1.1.4}/PKG-INFO +3 -1
  2. {wcp_library-1.1.2 → wcp_library-1.1.4}/pyproject.toml +3 -1
  3. wcp_library-1.1.4/wcp_library/credentials/ftp.py +124 -0
  4. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/credentials/oracle.py +1 -1
  5. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/credentials/postgres.py +1 -1
  6. wcp_library-1.1.4/wcp_library/ftp/ftp.py +122 -0
  7. wcp_library-1.1.4/wcp_library/ftp/sftp.py +126 -0
  8. {wcp_library-1.1.2 → wcp_library-1.1.4}/README.md +0 -0
  9. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/__init__.py +0 -0
  10. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/async_credentials/__init__.py +0 -0
  11. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/async_credentials/api.py +0 -0
  12. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/async_credentials/oracle.py +0 -0
  13. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/async_credentials/postgres.py +0 -0
  14. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/async_sql/__init__.py +0 -0
  15. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/async_sql/oracle.py +0 -0
  16. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/async_sql/postgres.py +0 -0
  17. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/credentials/__init__.py +0 -0
  18. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/emailing.py +0 -0
  19. /wcp_library-1.1.2/wcp_library/credentials/api.py → /wcp_library-1.1.4/wcp_library/ftp/__init__.py +0 -0
  20. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/informatica.py +0 -0
  21. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/logging.py +0 -0
  22. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/sql/__init__.py +0 -0
  23. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/sql/oracle.py +0 -0
  24. {wcp_library-1.1.2 → wcp_library-1.1.4}/wcp_library/sql/postgres.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wcp-library
3
- Version: 1.1.2
3
+ Version: 1.1.4
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)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "wcp-library"
3
- version = "1.1.2"
3
+ version = "1.1.4"
4
4
  description = "Common utilites for internal development at WCP"
5
5
  authors = ["Mitch-Petersen <mitch.petersen@wcap.ca>"]
6
6
  readme = "README.md"
@@ -11,8 +11,10 @@ packages = [{include = "wcp_library"}]
11
11
  [tool.poetry.dependencies]
12
12
  python = "^3.12"
13
13
  aiohttp = "^3.10.10"
14
+ ftputil = "^5.1.0"
14
15
  oracledb = "^2.5.0"
15
16
  pandas = "^2.2.3"
17
+ paramiko = "^3.5.0"
16
18
  pip-system-certs = "^4.0"
17
19
  psycopg = "^3.2.3"
18
20
  psycopg-binary = "^3.2.3"
@@ -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
@@ -122,4 +122,4 @@ class OracleCredentialManager:
122
122
  return True
123
123
  else:
124
124
  logger.error(f"Failed to create new credentials for {credentials_dict['UserName']}")
125
- return False
125
+ return False
@@ -121,4 +121,4 @@ class PostgresCredentialManager:
121
121
  return True
122
122
  else:
123
123
  logger.error(f"Failed to create new credentials for {credentials_dict['UserName']}")
124
- return False
124
+ return False
@@ -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
@@ -0,0 +1,126 @@
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: 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.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
+ if not (self._username and self._password):
24
+ self.sftp_connection: Optional[paramiko.SFTP] = None
25
+ else:
26
+ self.ssh.connect(self.host, self.port, self._username, self._password)
27
+ self.sftp_connection = self.ssh.open_sftp()
28
+
29
+ def login(self, username: str, password: str) -> None:
30
+ """
31
+ Login to the SFTP server
32
+
33
+ :param username:
34
+ :param password:
35
+ :return:
36
+ """
37
+
38
+ logger.debug(f"Logging into {self.host} with username {username}")
39
+ self.ssh.connect(self.host, self.port, username, password)
40
+ self.sftp_connection = self.ssh.open_sftp()
41
+
42
+ def download(self, remote_file: Path, local_file: Path) -> None:
43
+ """
44
+ Download a file from the SFTP server
45
+
46
+ :param remote_file:
47
+ :param local_file:
48
+ :return:
49
+ """
50
+
51
+ logger.debug(f"Downloading {remote_file} to {local_file}")
52
+ self.sftp_connection.get(str(remote_file), local_file)
53
+
54
+ def download_files(self, local_dir: Path, regex_pattern: str='*') -> None:
55
+ """
56
+ Download files from the SFTP server matching the regex pattern
57
+
58
+ :param local_dir:
59
+ :param regex_pattern:
60
+ :return:
61
+ """
62
+
63
+ logger.debug(f"Downloading files from FTP server matching {regex_pattern} to {local_dir}")
64
+ files = self.list_files()
65
+ for file in files:
66
+ if re.match(regex_pattern, file.name):
67
+ logger.debug(f"Downloading {file} to {local_dir / file.name}")
68
+ self.sftp_connection.get(str(file), local_dir / file.name)
69
+
70
+ def list_files(self) -> list[Path]:
71
+ """
72
+ List files on the SFTP server
73
+
74
+ Returns a list of file paths
75
+
76
+ :return:
77
+ """
78
+
79
+ logger.debug(f"Listing files on {self.sftp_connection.getcwd()}")
80
+ return [Path(x) for x in self.sftp_connection.listdir() if stat.S_ISREG(self.sftp_connection.lstat(x).st_mode)]
81
+
82
+ def list_dirs(self) -> list[Path]:
83
+ """
84
+ List directories on the SFTP server
85
+
86
+ Returns a list of directory paths
87
+
88
+ :return:
89
+ """
90
+
91
+ logger.debug(f"Listing directories on {self.sftp_connection.getcwd()}")
92
+ return [Path(x) for x in self.sftp_connection.listdir() if stat.S_ISDIR(self.sftp_connection.lstat(x).st_mode)]
93
+
94
+ def change_dir(self, remote_dir: Path) -> None:
95
+ """
96
+ Change the directory on the SFTP server
97
+
98
+ :param remote_dir:
99
+ :return:
100
+ """
101
+
102
+ logger.debug(f"Changing FTP directory to {remote_dir}")
103
+ self.sftp_connection.chdir(str(remote_dir))
104
+
105
+ def upload(self, local_file: Path, remote_file: Path) -> None:
106
+ """
107
+ Upload a file to the SFTP server
108
+
109
+ :param local_file:
110
+ :param remote_file:
111
+ :return:
112
+ """
113
+
114
+ logger.debug(f"Uploading {local_file} to {remote_file}")
115
+ self.sftp_connection.put(local_file, str(remote_file))
116
+
117
+ def close(self):
118
+ """
119
+ Close the SFTP connection
120
+
121
+ :return:
122
+ """
123
+
124
+ logger.debug("Closing FTP connection")
125
+ self.sftp_connection.close()
126
+ self.sftp_connection = None
File without changes