brynq-sdk-ftp 3.0.4__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,2 @@
1
+ from .ftps import FTPS
2
+ from .sftp import SFTP
brynq_sdk_ftp/ftps.py ADDED
@@ -0,0 +1,236 @@
1
+ from brynq_sdk_brynq import BrynQ
2
+ import os
3
+ import ftplib
4
+ import time
5
+ from ftplib import FTP
6
+ from typing import Union, List, Optional, Literal
7
+ from tenacity import retry, stop_after_attempt, wait_exponential_jitter, retry_if_exception
8
+
9
+
10
+ def is_ftp_exception(e: BaseException) -> bool:
11
+ if isinstance(e, ftplib.all_errors):
12
+ error = str(e)[:400].replace('\'', '').replace('\"', '')
13
+ print(f"{error}, retrying")
14
+ return True
15
+ else:
16
+ return False
17
+
18
+
19
+ class FTPS(BrynQ):
20
+ def __init__(self, system_type: Optional[Literal['source', 'target']] = None, debug=False):
21
+ super().__init__()
22
+ try:
23
+ credentials = self.interfaces.credentials.get(system="ftps", system_type=system_type)
24
+ credentials = credentials.get('data')
25
+ self.host = credentials['host']
26
+ self.username = credentials['username']
27
+ self.password = credentials['password']
28
+ except ValueError:
29
+ if debug:
30
+ print("No FTPS credentials found in the platform, falling back to environment variables")
31
+ self.host = None
32
+ # if no credentials are retrieved from the platform, fallback to Qlik FTP (can only be target, never source)
33
+ if self.host is None:
34
+ if system_type == 'target' and os.getenv("QLIK_HOST") is not None and os.getenv("QLIK_USER") is not None and os.getenv("QLIK_PASSWORD") is not None:
35
+ if debug:
36
+ print("Connecting to Qlik FTPs server")
37
+ self.host = os.getenv("QLIK_HOST")
38
+ self.username = os.getenv("QLIK_USER")
39
+ self.password = os.getenv("QLIK_PASSWORD")
40
+ else:
41
+ raise ValueError("Set the environment variables QLIK_HOST, QLIK_USER and QLIK_PASSWORD and pass system_type=target to use the Qlik FTPS server, otherwise connect an FTPS authorization to your interface in BrynQ")
42
+ self.debug = debug
43
+
44
+ @retry(stop=stop_after_attempt(5), wait=wait_exponential_jitter(initial=60, max=900), retry=retry_if_exception(is_ftp_exception), reraise=True)
45
+ def upload_file(self, local_path, filename, remote_path, remove_after_upload=False):
46
+ """
47
+ Upload a file from the client to another client or server
48
+ :param local_path: the path where the upload file is located
49
+ :param filename: The file which should be uploaded
50
+ :param remote_path: The path on the destination client where the file should be saved
51
+ :param remove_after_upload: If true, remove the file after a succesfull upload
52
+ :return: a status if the upload is succesfull or not
53
+ """
54
+ with FTP(host=self.host, user=self.username, passwd=self.password) as ftp:
55
+ ftp.cwd(remote_path)
56
+ with open(local_path + filename, 'rb') as fp:
57
+ # This runs until upload is successful, then breaks
58
+ while True:
59
+ try:
60
+ ftp.storbinary("STOR " + filename, fp)
61
+ # If the file should be removed after upload, remove it from the local path
62
+ if remove_after_upload:
63
+ os.remove(f'{local_path}{filename}')
64
+ except ftplib.error_temp as e:
65
+ # this catches 421 errors (socket timeout), sleeps 10 seconds and tries again. If any other exception is encountered, breaks.
66
+ if str(e).split()[0] == '421':
67
+ time.sleep(10)
68
+ continue
69
+ else:
70
+ raise
71
+ break
72
+ ftp.close()
73
+ return 'File is transferred'
74
+
75
+ @retry(stop=stop_after_attempt(5), wait=wait_exponential_jitter(initial=60, max=900), retry=retry_if_exception(is_ftp_exception), reraise=True)
76
+ def upload_multiple_files(self, local_path, remote_path, remove_after_upload=False):
77
+ """
78
+ Upload all files in a directory from the client to another client or server
79
+ :param local_path: the path from where all the files should be uploaded
80
+ :param remote_path: The path on the destination client where the file should be saved
81
+ :param remove_after_upload: If true, remove the file after a succesfull upload
82
+ :return: a status if the upload is succesfull or not
83
+ """
84
+ ftp = FTP(host=self.host, user=self.username, passwd=self.password)
85
+ ftp.cwd(remote_path)
86
+ for filename in os.listdir(local_path):
87
+ file = local_path + filename
88
+ if self.debug:
89
+ print(f"Remote path: {remote_path}, local file: {file}")
90
+ if os.path.isfile(file):
91
+ with open(file, 'rb') as fp:
92
+ # This runs until upload is successful, then breaks
93
+ while True:
94
+ try:
95
+ ftp.storbinary("STOR " + filename, fp)
96
+ # If the file should be removed after upload, remove it from the local path
97
+ if remove_after_upload:
98
+ os.remove(file)
99
+ except ftplib.error_temp as e:
100
+ # this catches 421 errors (socket timeout), sleeps 10 seconds and tries again. If any other exception is encountered, breaks.
101
+ if str(e).split()[0] == '421':
102
+ time.sleep(10)
103
+ continue
104
+ else:
105
+ raise
106
+ break
107
+ ftp.close()
108
+ return 'All files are transferred'
109
+
110
+ @retry(stop=stop_after_attempt(5), wait=wait_exponential_jitter(initial=60, max=900), retry=retry_if_exception(is_ftp_exception), reraise=True)
111
+ def download_file(self, local_path, remote_path, filename, remove_after_download=False):
112
+ """
113
+ Returns a single file from a given remote path
114
+ :param local_path: the folder where the downloaded file should be stored
115
+ :param remote_path: the folder on the server where the file should be downloaded from
116
+ :param filename: the filename itself
117
+ :param remove_after_download: Should the file be removed on the server after the download or not
118
+ :return: a status
119
+ """
120
+ if self.debug:
121
+ print(f"Remote path: {remote_path}, local file path: {filename}")
122
+ with FTP(host=self.host, user=self.username, passwd=self.password) as ftp:
123
+ with open('{}/{}'.format(local_path, filename), 'wb') as fp:
124
+ res = ftp.retrbinary('RETR {}/{}'.format(remote_path, filename), fp.write)
125
+ if not res.startswith('226 Successfully transferred'):
126
+ # Remove the created file on the local client if the download is failed
127
+ if os.path.isfile(f'{local_path}/{filename}'):
128
+ os.remove(f'{local_path}/{filename}')
129
+ else:
130
+ if remove_after_download:
131
+ ftp.delete(filename)
132
+
133
+ return res
134
+
135
+ @retry(stop=stop_after_attempt(5), wait=wait_exponential_jitter(initial=60, max=900), retry=retry_if_exception(is_ftp_exception), reraise=True)
136
+ def make_dir(self, dir_name):
137
+ """
138
+ Create a directory on a remote machine
139
+ :param dir_name: give the path name which should be created
140
+ :return: the status if the creation is successfull or not
141
+ """
142
+ with FTP(host=self.host, user=self.username, passwd=self.password) as ftp:
143
+ status = ftp.mkd(dir_name)
144
+ return status
145
+
146
+ @retry(stop=stop_after_attempt(5), wait=wait_exponential_jitter(initial=60, max=900), retry=retry_if_exception(is_ftp_exception), reraise=True)
147
+ def list_directories(self, remote_path=''):
148
+ """
149
+ Give a NoneType of directories and files in a given directory. This one is only for information. The Nonetype
150
+ can't be iterated or something like that
151
+ :param remote_path: give the folder where to start in
152
+ :return: a NoneType with folders and files
153
+ """
154
+ with FTP(host=self.host, user=self.username, passwd=self.password) as ftp:
155
+ ftp.cwd(remote_path)
156
+ if self.debug:
157
+ print(ftp.dir())
158
+ return ftp.dir()
159
+
160
+ @retry(stop=stop_after_attempt(5), wait=wait_exponential_jitter(initial=60, max=900), retry=retry_if_exception(is_ftp_exception), reraise=True)
161
+ def make_dirs(self, filepath: str):
162
+ """
163
+ shadows os.makedirs but for ftp
164
+ :param filepath: filepath that you want to create
165
+ :return: nothing
166
+ """
167
+ filepath = filepath.split('/')
168
+ with FTP(host=self.host, user=self.username, passwd=self.password) as ftp:
169
+ ftp.cwd('/')
170
+ for subpath in filepath:
171
+ if subpath != '':
172
+ file_list = []
173
+ ftp.retrlines('NLST', file_list.append)
174
+ if subpath in file_list:
175
+ ftp.cwd(subpath)
176
+ else:
177
+ ftp.mkd(subpath)
178
+ ftp.cwd(subpath)
179
+
180
+ @retry(stop=stop_after_attempt(5), wait=wait_exponential_jitter(initial=60, max=900), retry=retry_if_exception(is_ftp_exception), reraise=True)
181
+ def list_files(self, remote_path=''):
182
+ """
183
+ Give a list with files in a certain folder
184
+ :param remote_path: give the folder where to look in
185
+ :return: a list with files
186
+ """
187
+ with FTP(host=self.host, user=self.username, passwd=self.password) as ftp:
188
+ ftp.cwd(remote_path)
189
+ if self.debug:
190
+ print(ftp.nlst())
191
+ return ftp.nlst()
192
+
193
+ @retry(stop=stop_after_attempt(5), wait=wait_exponential_jitter(initial=60, max=900), retry=retry_if_exception(is_ftp_exception), reraise=True)
194
+ def remove_file(self, remote_filepath):
195
+ """
196
+ Remove a file on a remote location
197
+ :param remote_file: the full path of the file that needs to be removed
198
+ """
199
+ with FTP(host=self.host, user=self.username, passwd=self.password) as ftp:
200
+ ftp.delete(remote_filepath)
201
+
202
+ @retry(stop=stop_after_attempt(5), wait=wait_exponential_jitter(initial=60, max=900), retry=retry_if_exception(is_ftp_exception), reraise=True)
203
+ def remove_directory(self, remote_directory, recursive=False):
204
+ """
205
+ Remove a file on a remote location
206
+ :param remote_directory: the directory you want to remove
207
+ :param recursive: if you want to remove the directory recursively (including all files and subdirectories)
208
+ """
209
+ ftp = FTP(host=self.host, user=self.username, passwd=self.password)
210
+ if recursive:
211
+ ftp.cwd(remote_directory)
212
+ files = ftp.nlst() # List directory contents
213
+ for file_name in files:
214
+ if file_name in ('.', '..'):
215
+ continue
216
+ try:
217
+ ftp.delete(file_name) # Delete file
218
+ except:
219
+ self.remove_directory(f"{remote_directory}/{file_name}", recursive=True) # Recursive call for subdirectories
220
+ ftp.cwd('..') # Move back to the parent directory
221
+ ftp.rmd(remote_directory.split('/')[-1]) # Remove the empty directory
222
+ else:
223
+ ftp.rmd(remote_directory)
224
+
225
+
226
+ @retry(stop=stop_after_attempt(5), wait=wait_exponential_jitter(initial=60, max=900), retry=retry_if_exception(is_ftp_exception), reraise=True)
227
+ def send_command(self, command):
228
+ """
229
+ Send a command to the ftp server
230
+ :param command: the command that needs to be send
231
+ :return: the response of the server
232
+ """
233
+ with FTP(host=self.host, user=self.username, passwd=self.password) as ftp:
234
+ response = ftp.sendcmd(command)
235
+ return response
236
+
brynq_sdk_ftp/sftp.py ADDED
@@ -0,0 +1,149 @@
1
+ from brynq_sdk_brynq import BrynQ
2
+ from io import StringIO
3
+ from paramiko.client import SSHClient, AutoAddPolicy
4
+ from paramiko import RSAKey
5
+ from paramiko.sftp_attr import SFTPAttributes
6
+ from typing import Union, List, Literal, Optional
7
+ from stat import S_ISREG
8
+ import os
9
+
10
+
11
+ class SFTP(BrynQ):
12
+ def __init__(self, system_type: Optional[Literal['source', 'target']] = None, debug=False):
13
+ """
14
+ Init the SFTP class
15
+ :param label: The label of the connector
16
+ :param debug: If you want to see debug messages
17
+ """
18
+ super().__init__()
19
+ self.debug = debug
20
+
21
+ self.client = SSHClient()
22
+ self.client.set_missing_host_key_policy(AutoAddPolicy())
23
+
24
+ # Try to fetch credentials from BrynQ; if not present, attributes remain unset
25
+ try:
26
+ credentials = self.interfaces.credentials.get(system="sftp", system_type=system_type)
27
+ credentials = credentials.get('data')
28
+ if credentials:
29
+ if self.debug:
30
+ print(credentials)
31
+ self.host = credentials['host']
32
+ self.port = 22 if credentials.get('port') is None else credentials.get('port')
33
+ self.username = credentials.get('username')
34
+ self.password = credentials.get('password')
35
+ self.private_key_path = credentials.get('private_key_password', None)
36
+ self.private_key_passphrase = credentials.get('private_key_password', None)
37
+ self.private_key = None
38
+ if credentials.get('private_key'):
39
+ self.private_key = RSAKey(file_obj=StringIO(credentials.get('private_key')), password=self.private_key_passphrase)
40
+ except ValueError:
41
+ print("No credentials found for SFTP. If this was intended, use _set_credentials() to set the credentials to pass variables ['host', 'port', 'username', 'password', 'private_key', 'private_key_password']")
42
+
43
+ def _set_credentials(self, credentials: dict):
44
+ """
45
+ When a child class(ex:Meta4) needs to set the credentials, use this method.
46
+ Set SFTP connection credentials programmatically.
47
+
48
+ Expected keys in credentials dict:
49
+ - host (str)
50
+ - port (int, optional; defaults to 22)
51
+ - username (str)
52
+ - password (str, optional)
53
+ - private_key (str, optional; PEM string)
54
+ - private_key_password (str, optional; passphrase for private_key)
55
+ """
56
+ # Core connection parameters
57
+ self.host = credentials['host']
58
+ self.port = 22 if credentials.get('port') is None else credentials.get('port')
59
+ self.username = credentials.get('username')
60
+ self.password = credentials.get('password')
61
+
62
+ # Key options
63
+ self.private_key_path = credentials.get('private_key_path')
64
+ self.private_key_passphrase = credentials.get('private_key_password')
65
+ self.private_key = None
66
+ if credentials.get('private_key'):
67
+ self.private_key = RSAKey(file_obj=StringIO(credentials.get('private_key')), password=self.private_key_passphrase)
68
+
69
+ def upload_file(self, local_filepath, remote_filepath, confirm=True) -> SFTPAttributes:
70
+ """
71
+ Upload a single file to a remote location. If there is no Private key
72
+ :param local_filepath: The file and the full path on your local machine
73
+ :param remote_filepath: The path and filename on the remote location
74
+ :param confirm: If you want to confirm the upload
75
+ :return: status
76
+ """
77
+ self.client.connect(hostname=self.host, port=self.port, username=self.username, password=self.password, pkey=self.private_key, passphrase=self.private_key_passphrase)
78
+ sftp = self.client.open_sftp()
79
+ response = sftp.put(local_filepath, remote_filepath, confirm=confirm)
80
+ self.client.close()
81
+
82
+ return response
83
+
84
+ def list_dir(self, remote_filepath, get_folders: bool = False) -> List[str]:
85
+ """
86
+ Read the files and folders an a certain location
87
+ :param remote_filepath: The full path where you want to get the content from
88
+ :return: a list with files and folders in the given location
89
+ """
90
+ self.client.connect(hostname=self.host, port=self.port, username=self.username, pkey=self.private_key, password=self.password)
91
+ sftp = self.client.open_sftp()
92
+ sftp.chdir(remote_filepath)
93
+ list_files = sftp.listdir_attr()
94
+ list_files = [file.filename for file in list_files if S_ISREG(file.st_mode) or get_folders]
95
+ self.client.close()
96
+
97
+ return list_files
98
+
99
+ def download_file(self, remote_path, remote_file, local_path):
100
+ """
101
+ Download a single file
102
+ :param remote_path: the path where the remote file exists
103
+ :param remote_file: the remote file itself
104
+ :param local_path: the path where the file needs to be downloaded to
105
+ :return: a file object
106
+ """
107
+ self.client.connect(hostname=self.host, port=self.port, username=self.username, pkey=self.private_key, password=self.password)
108
+ sftp = self.client.open_sftp()
109
+ sftp.get(remotepath=f'{remote_path}{remote_file}', localpath=f'{local_path}/{remote_file}')
110
+ self.client.close()
111
+
112
+ def make_dir(self, remote_path, new_dir_name):
113
+ """
114
+ Create a new folder on a remote location
115
+ :param remote_path: The location where you want to create the new folder
116
+ :param new_dir_name: The name of the new folder
117
+ :return: a status if creating succeeded or not
118
+ """
119
+ self.client.connect(hostname=self.host, port=self.port, username=self.username, pkey=self.private_key, password=self.password)
120
+ sftp = self.client.open_sftp()
121
+ sftp.chdir(remote_path)
122
+ sftp.mkdir(new_dir_name)
123
+ self.client.close()
124
+
125
+ def remove_file(self, remote_file):
126
+ """
127
+ Remove a file on a remote location
128
+ :param remote_file: the full path of the file that needs to be removed
129
+ :return: a status if deleting succeeded or not
130
+ """
131
+ self.client.connect(hostname=self.host, port=self.port, username=self.username, pkey=self.private_key, password=self.password)
132
+ sftp = self.client.open_sftp()
133
+ sftp.remove(remote_file)
134
+ self.client.close()
135
+
136
+ def move_file(self, old_file_path: str, new_file_path: str):
137
+ """
138
+ Move or rename a file on a remote location
139
+ :param old_file_path: the full path of the file that needs to be moved or renamed
140
+ :param new_file_path: the full path of the new location of the file
141
+ :return:
142
+ """
143
+ self.client.connect(hostname=self.host, port=self.port, username=self.username, pkey=self.private_key, password=self.password)
144
+ sftp = self.client.open_sftp()
145
+ sftp.rename(oldpath=old_file_path, newpath=new_file_path)
146
+ self.client.close()
147
+
148
+ def rename_file(self, old_file_path: str, new_file_path: str):
149
+ self.move_file(old_file_path=old_file_path, new_file_path=new_file_path)
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: brynq_sdk_ftp
3
+ Version: 3.0.4
4
+ Summary: FTP wrapper from BrynQ
5
+ Author: BrynQ
6
+ Author-email: support@brynq.com
7
+ License: BrynQ License
8
+ Requires-Dist: brynq-sdk-brynq<5,>=4
9
+ Requires-Dist: requests<=3,>=2
10
+ Requires-Dist: paramiko<=4,>=2
11
+ Requires-Dist: pysftp<1,>0.2
12
+ Requires-Dist: tenacity<9,>=8
13
+ Dynamic: author
14
+ Dynamic: author-email
15
+ Dynamic: description
16
+ Dynamic: license
17
+ Dynamic: requires-dist
18
+ Dynamic: summary
19
+
20
+ FTP wrapper from Brynq
@@ -0,0 +1,7 @@
1
+ brynq_sdk_ftp/__init__.py,sha256=iAKN9HUpIb5qfnatYo6uFinfv97Nf_Q6vNpIMOrhgSY,45
2
+ brynq_sdk_ftp/ftps.py,sha256=uXYc2ny6H5ihWbqOgajys98YBSeFYy28btRyVD6RTE0,11954
3
+ brynq_sdk_ftp/sftp.py,sha256=WIYdpJo4Vcsve-DDFIouMye_xo0ZimK9L3-ay5Aoi-o,7065
4
+ brynq_sdk_ftp-3.0.4.dist-info/METADATA,sha256=VOTm__doP_SZ0Vw8eN8fW_OtmgeLFXz4UxasKPfVabM,456
5
+ brynq_sdk_ftp-3.0.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
+ brynq_sdk_ftp-3.0.4.dist-info/top_level.txt,sha256=GuAKZGWkVuRr4MtdiQfEfxZzWE4hwHIFmXa_hwUqxJg,14
7
+ brynq_sdk_ftp-3.0.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ brynq_sdk_ftp