Skip to content

Commit 69629a5

Browse files
TobKedpotiuk
authored andcommitted
[AIRFLOW-5807] Move SFTP from contrib to providers. (#6464)
* [AIRFLOW-5807] Move SFTP from contrib to core
1 parent 25830a0 commit 69629a5

File tree

26 files changed

+1521
-1180
lines changed

26 files changed

+1521
-1180
lines changed

β€Žairflow/contrib/hooks/sftp_hook.py

Lines changed: 11 additions & 268 deletions
Original file line numberDiff line numberDiff line change
@@ -16,274 +16,17 @@
1616
# KIND, either express or implied. See the License for the
1717
# specific language governing permissions and limitations
1818
# under the License.
19+
"""
20+
This module is deprecated. Please use `airflow.providers.sftp.hooks.sftp_hook`.
21+
"""
1922

20-
import datetime
21-
import stat
22-
from typing import Dict, List, Optional, Tuple
23+
import warnings
2324

24-
import pysftp
25+
# pylint: disable=unused-import
26+
from airflow.providers.sftp.hooks.sftp_hook import SFTPHook # noqa
2527

26-
from airflow.contrib.hooks.ssh_hook import SSHHook
27-
28-
29-
class SFTPHook(SSHHook):
30-
"""
31-
This hook is inherited from SSH hook. Please refer to SSH hook for the input
32-
arguments.
33-
34-
Interact with SFTP. Aims to be interchangeable with FTPHook.
35-
36-
:Pitfalls::
37-
38-
- In contrast with FTPHook describe_directory only returns size, type and
39-
modify. It doesn't return unix.owner, unix.mode, perm, unix.group and
40-
unique.
41-
- retrieve_file and store_file only take a local full path and not a
42-
buffer.
43-
- If no mode is passed to create_directory it will be created with 777
44-
permissions.
45-
46-
Errors that may occur throughout but should be handled downstream.
47-
"""
48-
49-
def __init__(self, ftp_conn_id: str = 'sftp_default', *args, **kwargs) -> None:
50-
kwargs['ssh_conn_id'] = ftp_conn_id
51-
super().__init__(*args, **kwargs)
52-
53-
self.conn = None
54-
self.private_key_pass = None
55-
56-
# Fail for unverified hosts, unless this is explicitly allowed
57-
self.no_host_key_check = False
58-
59-
if self.ssh_conn_id is not None:
60-
conn = self.get_connection(self.ssh_conn_id)
61-
if conn.extra is not None:
62-
extra_options = conn.extra_dejson
63-
if 'private_key_pass' in extra_options:
64-
self.private_key_pass = extra_options.get('private_key_pass', None)
65-
66-
# For backward compatibility
67-
# TODO: remove in Airflow 2.1
68-
import warnings
69-
if 'ignore_hostkey_verification' in extra_options:
70-
warnings.warn(
71-
'Extra option `ignore_hostkey_verification` is deprecated.'
72-
'Please use `no_host_key_check` instead.'
73-
'This option will be removed in Airflow 2.1',
74-
DeprecationWarning,
75-
stacklevel=2,
76-
)
77-
self.no_host_key_check = str(
78-
extra_options['ignore_hostkey_verification']
79-
).lower() == 'true'
80-
81-
if 'no_host_key_check' in extra_options:
82-
self.no_host_key_check = str(
83-
extra_options['no_host_key_check']).lower() == 'true'
84-
85-
if 'private_key' in extra_options:
86-
warnings.warn(
87-
'Extra option `private_key` is deprecated.'
88-
'Please use `key_file` instead.'
89-
'This option will be removed in Airflow 2.1',
90-
DeprecationWarning,
91-
stacklevel=2,
92-
)
93-
self.key_file = extra_options.get('private_key')
94-
95-
def get_conn(self) -> pysftp.Connection:
96-
"""
97-
Returns an SFTP connection object
98-
"""
99-
if self.conn is None:
100-
cnopts = pysftp.CnOpts()
101-
if self.no_host_key_check:
102-
cnopts.hostkeys = None
103-
cnopts.compression = self.compress
104-
conn_params = {
105-
'host': self.remote_host,
106-
'port': self.port,
107-
'username': self.username,
108-
'cnopts': cnopts
109-
}
110-
if self.password and self.password.strip():
111-
conn_params['password'] = self.password
112-
if self.key_file:
113-
conn_params['private_key'] = self.key_file
114-
if self.private_key_pass:
115-
conn_params['private_key_pass'] = self.private_key_pass
116-
117-
self.conn = pysftp.Connection(**conn_params)
118-
return self.conn
119-
120-
def close_conn(self) -> None:
121-
"""
122-
Closes the connection. An error will occur if the
123-
connection wasnt ever opened.
124-
"""
125-
conn = self.conn
126-
conn.close() # type: ignore
127-
self.conn = None
128-
129-
def describe_directory(self, path: str) -> Dict[str, Dict[str, str]]:
130-
"""
131-
Returns a dictionary of {filename: {attributes}} for all files
132-
on the remote system (where the MLSD command is supported).
133-
134-
:param path: full path to the remote directory
135-
:type path: str
136-
"""
137-
conn = self.get_conn()
138-
flist = conn.listdir_attr(path)
139-
files = {}
140-
for f in flist:
141-
modify = datetime.datetime.fromtimestamp(
142-
f.st_mtime).strftime('%Y%m%d%H%M%S')
143-
files[f.filename] = {
144-
'size': f.st_size,
145-
'type': 'dir' if stat.S_ISDIR(f.st_mode) else 'file',
146-
'modify': modify}
147-
return files
148-
149-
def list_directory(self, path: str) -> List[str]:
150-
"""
151-
Returns a list of files on the remote system.
152-
153-
:param path: full path to the remote directory to list
154-
:type path: str
155-
"""
156-
conn = self.get_conn()
157-
files = conn.listdir(path)
158-
return files
159-
160-
def create_directory(self, path: str, mode: int = 777) -> None:
161-
"""
162-
Creates a directory on the remote system.
163-
164-
:param path: full path to the remote directory to create
165-
:type path: str
166-
:param mode: int representation of octal mode for directory
167-
"""
168-
conn = self.get_conn()
169-
conn.makedirs(path, mode)
170-
171-
def delete_directory(self, path: str) -> None:
172-
"""
173-
Deletes a directory on the remote system.
174-
175-
:param path: full path to the remote directory to delete
176-
:type path: str
177-
"""
178-
conn = self.get_conn()
179-
conn.rmdir(path)
180-
181-
def retrieve_file(self, remote_full_path: str, local_full_path: str) -> None:
182-
"""
183-
Transfers the remote file to a local location.
184-
If local_full_path is a string path, the file will be put
185-
at that location
186-
187-
:param remote_full_path: full path to the remote file
188-
:type remote_full_path: str
189-
:param local_full_path: full path to the local file
190-
:type local_full_path: str
191-
"""
192-
conn = self.get_conn()
193-
self.log.info('Retrieving file from FTP: %s', remote_full_path)
194-
conn.get(remote_full_path, local_full_path)
195-
self.log.info('Finished retrieving file from FTP: %s', remote_full_path)
196-
197-
def store_file(self, remote_full_path: str, local_full_path: str) -> None:
198-
"""
199-
Transfers a local file to the remote location.
200-
If local_full_path_or_buffer is a string path, the file will be read
201-
from that location
202-
203-
:param remote_full_path: full path to the remote file
204-
:type remote_full_path: str
205-
:param local_full_path: full path to the local file
206-
:type local_full_path: str
207-
"""
208-
conn = self.get_conn()
209-
conn.put(local_full_path, remote_full_path)
210-
211-
def delete_file(self, path: str) -> None:
212-
"""
213-
Removes a file on the FTP Server
214-
215-
:param path: full path to the remote file
216-
:type path: str
217-
"""
218-
conn = self.get_conn()
219-
conn.remove(path)
220-
221-
def get_mod_time(self, path: str) -> str:
222-
conn = self.get_conn()
223-
ftp_mdtm = conn.stat(path).st_mtime
224-
return datetime.datetime.fromtimestamp(ftp_mdtm).strftime('%Y%m%d%H%M%S')
225-
226-
def path_exists(self, path: str) -> bool:
227-
"""
228-
Returns True if a remote entity exists
229-
230-
:param path: full path to the remote file or directory
231-
:type path: str
232-
"""
233-
conn = self.get_conn()
234-
return conn.exists(path)
235-
236-
@staticmethod
237-
def _is_path_match(path: str, prefix: Optional[str] = None, delimiter: Optional[str] = None) -> bool:
238-
"""
239-
Return True if given path starts with prefix (if set) and ends with delimiter (if set).
240-
241-
:param path: path to be checked
242-
:type path: str
243-
:param prefix: if set path will be checked is starting with prefix
244-
:type prefix: str
245-
:param delimiter: if set path will be checked is ending with suffix
246-
:type delimiter: str
247-
:return: bool
248-
"""
249-
if prefix is not None and not path.startswith(prefix):
250-
return False
251-
if delimiter is not None and not path.endswith(delimiter):
252-
return False
253-
return True
254-
255-
def get_tree_map(
256-
self, path: str, prefix: Optional[str] = None, delimiter: Optional[str] = None
257-
) -> Tuple[List[str], List[str], List[str]]:
258-
"""
259-
Return tuple with recursive lists of files, directories and unknown paths from given path.
260-
It is possible to filter results by giving prefix and/or delimiter parameters.
261-
262-
:param path: path from which tree will be built
263-
:type path: str
264-
:param prefix: if set paths will be added if start with prefix
265-
:type prefix: str
266-
:param delimiter: if set paths will be added if end with delimiter
267-
:type delimiter: str
268-
:return: tuple with list of files, dirs and unknown items
269-
:rtype: Tuple[List[str], List[str], List[str]]
270-
"""
271-
conn = self.get_conn()
272-
files, dirs, unknowns = [], [], [] # type: List[str], List[str], List[str]
273-
274-
def append_matching_path_callback(list_):
275-
return (
276-
lambda item: list_.append(item)
277-
if self._is_path_match(item, prefix, delimiter)
278-
else None
279-
)
280-
281-
conn.walktree(
282-
remotepath=path,
283-
fcallback=append_matching_path_callback(files),
284-
dcallback=append_matching_path_callback(dirs),
285-
ucallback=append_matching_path_callback(unknowns),
286-
recurse=True,
287-
)
288-
289-
return files, dirs, unknowns
28+
warnings.warn(
29+
"This module is deprecated. Please use `airflow.providers.sftp.hooks.sftp_hook`.",
30+
DeprecationWarning,
31+
stacklevel=2,
32+
)

0 commit comments

Comments
 (0)