From 9b002476060a3c484a51d64ab2c88a52cc2b251f Mon Sep 17 00:00:00 2001 From: sanix-darker Date: Tue, 3 Oct 2023 16:39:10 +0200 Subject: [PATCH 01/10] feat: increase timeout values and the retry amount --- peakina/io/ftp/ftp_utils.py | 10 ++++++---- tests/io/ftp/test_ftp_utils.py | 25 +++++++++++++++++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/peakina/io/ftp/ftp_utils.py b/peakina/io/ftp/ftp_utils.py index 8be15c7b..ea83bbc9 100644 --- a/peakina/io/ftp/ftp_utils.py +++ b/peakina/io/ftp/ftp_utils.py @@ -16,6 +16,8 @@ import paramiko FTP_SCHEMES = ["ftp", "ftps", "sftp"] +_DEFAULT_MAX_TIMEOUT = 10 +_DEFAULT_MAX_RETRY = 7 FTPClient = ftplib.FTP | paramiko.SFTPClient @@ -73,7 +75,7 @@ def makepasv(self) -> tuple[str, int]: def ftps_client(params: ParseResult) -> Generator[tuple[FTPS, str], None, None]: ftps = FTPS() try: - ftps.connect(host=params.hostname or "", port=params.port, timeout=3) + ftps.connect(host=params.hostname or "", port=params.port, timeout=_DEFAULT_MAX_TIMEOUT) try: ftps.prot_p() ftps.login(user=params.username or "", passwd=params.password or "") @@ -97,7 +99,7 @@ def ftp_client(params: ParseResult) -> Generator[tuple[ftplib.FTP, str], None, N port = params.port or 21 ftp = ftplib.FTP() try: - ftp.connect(host=params.hostname or "", port=port, timeout=3) + ftp.connect(host=params.hostname or "", port=port, timeout=_DEFAULT_MAX_TIMEOUT) ftp.login(user=params.username or "", passwd=params.password or "") yield ftp, params.path @@ -117,7 +119,7 @@ def sftp_client(params: ParseResult) -> Generator[tuple[paramiko.SFTPClient, str username=params.username, password=params.password, port=port, - timeout=3, + timeout=_DEFAULT_MAX_TIMEOUT, ) sftp = ssh_client.open_sftp() yield sftp, params.path @@ -177,7 +179,7 @@ def _open(url: str) -> IO[bytes]: return ret -def ftp_open(url: str, retry: int = 4) -> IO[bytes]: # type: ignore +def ftp_open(url: str, retry: int = _DEFAULT_MAX_RETRY) -> IO[bytes]: # type: ignore for i in range(1, retry + 1): try: return _open(url) diff --git a/tests/io/ftp/test_ftp_utils.py b/tests/io/ftp/test_ftp_utils.py index 2e10ad5c..39ad7d35 100644 --- a/tests/io/ftp/test_ftp_utils.py +++ b/tests/io/ftp/test_ftp_utils.py @@ -2,10 +2,19 @@ import os import socket import ssl +from urllib.parse import ParseResult from pytest import fixture, raises +from pytest_mock import MockFixture -from peakina.io.ftp.ftp_utils import dir_mtimes, ftp_listdir, ftp_mtime, ftp_open +from peakina.io.ftp.ftp_utils import ( + _DEFAULT_MAX_TIMEOUT, + dir_mtimes, + ftp_listdir, + ftp_mtime, + ftp_open, + sftp_client, +) @fixture @@ -102,7 +111,9 @@ def test_ftp_client(mocker): url = "ftp://sacha@ondine.com:123/picha/chu.csv" ftp_open(url) - mock_ftp_client.connect.assert_called_once_with(host="ondine.com", port=123, timeout=3) + mock_ftp_client.connect.assert_called_once_with( + host="ondine.com", port=123, timeout=_DEFAULT_MAX_TIMEOUT + ) mock_ftp_client.login.assert_called_once_with(passwd="", user="sacha") mock_ftp_client.quit.assert_called_once() @@ -122,7 +133,9 @@ def test_ftps_client(mocker): url = "ftps://sacha@ondine.com:123/picha/chu.csv" ftp_open(url) - mock_ftps_client.connect.assert_called_once_with(host="ondine.com", port=123, timeout=3) + mock_ftps_client.connect.assert_called_once_with( + host="ondine.com", port=123, timeout=_DEFAULT_MAX_TIMEOUT + ) mock_ftps_client.login.assert_called_once_with(passwd="", user="sacha") mock_ftps_client.quit.assert_called_once() @@ -167,7 +180,11 @@ def test_sftp_client(mocker): ftp_open(url) mock_ssh_client.connect.assert_called_once_with( - timeout=3, hostname="atat.com", port=666, username="id#de@me*de", password="randompass" + timeout=_DEFAULT_MAX_TIMEOUT, + hostname="atat.com", + port=666, + username="id#de@me*de", + password="randompass", ) mock_ssh_client.open_sftp.assert_called_once() mock_ssh_client.close.assert_called_once() From 6bf5b586a5cff4139f678525360a24ce56978b7c Mon Sep 17 00:00:00 2001 From: sanix-darker Date: Tue, 3 Oct 2023 16:39:55 +0200 Subject: [PATCH 02/10] fix: suppress the .close error for None type ? --- peakina/io/ftp/ftp_utils.py | 4 +++- tests/io/ftp/test_ftp_utils.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/peakina/io/ftp/ftp_utils.py b/peakina/io/ftp/ftp_utils.py index ea83bbc9..c77101e0 100644 --- a/peakina/io/ftp/ftp_utils.py +++ b/peakina/io/ftp/ftp_utils.py @@ -125,7 +125,9 @@ def sftp_client(params: ParseResult) -> Generator[tuple[paramiko.SFTPClient, str yield sftp, params.path finally: - ssh_client.close() + # In cae of Exception, we don't want to raise it + with suppress(AttributeError): + ssh_client.close() def _urlparse(url: str) -> ParseResult: diff --git a/tests/io/ftp/test_ftp_utils.py b/tests/io/ftp/test_ftp_utils.py index 39ad7d35..97b215aa 100644 --- a/tests/io/ftp/test_ftp_utils.py +++ b/tests/io/ftp/test_ftp_utils.py @@ -198,3 +198,16 @@ def test_sftp_client(mocker): url = "sftp://id#de@me*de:randompass@atat.com:666" ftp_listdir(url) cl_ftp.listdir.assert_called_once_with(".") + + +def test_sftp_client_silent_close(mocker: MockFixture) -> None: + invalid_params = ParseResult(scheme="", netloc="", path="", params="", query="", fragment="") + ssh_client = mocker.patch("paramiko.SSHClient") + ssh_client.return_value.close.side_effect = AttributeError("NoneType doesnt have .close()") + + with sftp_client(invalid_params) as (sftp, _): + # This block should raise an exception due to invalid parameters + # The exception is expected to be suppressed by the context manager in the finally block + # So, the test will pass if no exception propagates beyond this point + + assert sftp.get_channel() From e4e4a5d6f213d35c562901fd361e9a350c522954 Mon Sep 17 00:00:00 2001 From: sanix-darker Date: Tue, 3 Oct 2023 17:25:51 +0200 Subject: [PATCH 03/10] fix: add second in the const name --- peakina/io/ftp/ftp_utils.py | 10 ++++++---- tests/io/ftp/test_ftp_utils.py | 8 ++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/peakina/io/ftp/ftp_utils.py b/peakina/io/ftp/ftp_utils.py index c77101e0..59e9f49d 100644 --- a/peakina/io/ftp/ftp_utils.py +++ b/peakina/io/ftp/ftp_utils.py @@ -16,7 +16,7 @@ import paramiko FTP_SCHEMES = ["ftp", "ftps", "sftp"] -_DEFAULT_MAX_TIMEOUT = 10 +_DEFAULT_MAX_TIMEOUT_SECONDS = 10 _DEFAULT_MAX_RETRY = 7 FTPClient = ftplib.FTP | paramiko.SFTPClient @@ -75,7 +75,9 @@ def makepasv(self) -> tuple[str, int]: def ftps_client(params: ParseResult) -> Generator[tuple[FTPS, str], None, None]: ftps = FTPS() try: - ftps.connect(host=params.hostname or "", port=params.port, timeout=_DEFAULT_MAX_TIMEOUT) + ftps.connect( + host=params.hostname or "", port=params.port, timeout=_DEFAULT_MAX_TIMEOUT_SECONDS + ) try: ftps.prot_p() ftps.login(user=params.username or "", passwd=params.password or "") @@ -99,7 +101,7 @@ def ftp_client(params: ParseResult) -> Generator[tuple[ftplib.FTP, str], None, N port = params.port or 21 ftp = ftplib.FTP() try: - ftp.connect(host=params.hostname or "", port=port, timeout=_DEFAULT_MAX_TIMEOUT) + ftp.connect(host=params.hostname or "", port=port, timeout=_DEFAULT_MAX_TIMEOUT_SECONDS) ftp.login(user=params.username or "", passwd=params.password or "") yield ftp, params.path @@ -119,7 +121,7 @@ def sftp_client(params: ParseResult) -> Generator[tuple[paramiko.SFTPClient, str username=params.username, password=params.password, port=port, - timeout=_DEFAULT_MAX_TIMEOUT, + timeout=_DEFAULT_MAX_TIMEOUT_SECONDS, ) sftp = ssh_client.open_sftp() yield sftp, params.path diff --git a/tests/io/ftp/test_ftp_utils.py b/tests/io/ftp/test_ftp_utils.py index 97b215aa..410bd2aa 100644 --- a/tests/io/ftp/test_ftp_utils.py +++ b/tests/io/ftp/test_ftp_utils.py @@ -8,7 +8,7 @@ from pytest_mock import MockFixture from peakina.io.ftp.ftp_utils import ( - _DEFAULT_MAX_TIMEOUT, + _DEFAULT_MAX_TIMEOUT_SECONDS, dir_mtimes, ftp_listdir, ftp_mtime, @@ -112,7 +112,7 @@ def test_ftp_client(mocker): ftp_open(url) mock_ftp_client.connect.assert_called_once_with( - host="ondine.com", port=123, timeout=_DEFAULT_MAX_TIMEOUT + host="ondine.com", port=123, timeout=_DEFAULT_MAX_TIMEOUT_SECONDS ) mock_ftp_client.login.assert_called_once_with(passwd="", user="sacha") mock_ftp_client.quit.assert_called_once() @@ -134,7 +134,7 @@ def test_ftps_client(mocker): ftp_open(url) mock_ftps_client.connect.assert_called_once_with( - host="ondine.com", port=123, timeout=_DEFAULT_MAX_TIMEOUT + host="ondine.com", port=123, timeout=_DEFAULT_MAX_TIMEOUT_SECONDS ) mock_ftps_client.login.assert_called_once_with(passwd="", user="sacha") mock_ftps_client.quit.assert_called_once() @@ -180,7 +180,7 @@ def test_sftp_client(mocker): ftp_open(url) mock_ssh_client.connect.assert_called_once_with( - timeout=_DEFAULT_MAX_TIMEOUT, + timeout=_DEFAULT_MAX_TIMEOUT_SECONDS, hostname="atat.com", port=666, username="id#de@me*de", From c85f92687aa8d63395e6d50526629cdb3f6126cc Mon Sep 17 00:00:00 2001 From: sanix-darker Date: Fri, 6 Oct 2023 14:27:05 +0200 Subject: [PATCH 04/10] feat: log file list from the directory to see what's inside --- peakina/io/ftp/ftp_utils.py | 33 ++++++++++++++++++++++++++++++--- peakina/readers/excel.py | 1 - tests/conftest.py | 1 - 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/peakina/io/ftp/ftp_utils.py b/peakina/io/ftp/ftp_utils.py index 59e9f49d..04e1b4f3 100644 --- a/peakina/io/ftp/ftp_utils.py +++ b/peakina/io/ftp/ftp_utils.py @@ -4,6 +4,7 @@ import socket import ssl import tempfile +import urllib.parse from contextlib import contextmanager, suppress from datetime import datetime from functools import partial @@ -184,13 +185,39 @@ def _open(url: str) -> IO[bytes]: def ftp_open(url: str, retry: int = _DEFAULT_MAX_RETRY) -> IO[bytes]: # type: ignore + file_listed: bool = False for i in range(1, retry + 1): try: return _open(url) except (AttributeError, OSError, ftplib.error_temp) as e: - sleep_time = 2 * i**2 - logging.getLogger(__name__).warning(f"Retry #{i}: Sleeping {sleep_time}s because {e}") - sleep(sleep_time) + # If this occurs, we need to see what's actually inside that dir + # by listing maxi 15 entries + # TODO: remove this after the debuging is done ! + # FileNotFoundError inerits from OSError + if isinstance(e, FileNotFoundError) and not file_listed: # pragma: no cover + try: + full_path = "/".join(url.split(":")[-1].split("/")[1:]) + logging.getLogger(__name__).warning(f"'{full_path}' not found !") + + parsed_url = urllib.parse.urlsplit(url) + path_without_file = parsed_url.path.rsplit("/", 1)[0] + modified_url = urllib.parse.urlunsplit( + (parsed_url.scheme, parsed_url.netloc, path_without_file, "", "") + ) + files_available = ", ".join(ftp_listdir(modified_url)[:15]) + # we list only 15 as maximum + logging.getLogger(__name__).warning( + f"Listing files availables > ({files_available})" + ) + file_listed = True + except Exception as exp: + logging.getLogger(__name__).error(exp) + else: + sleep_time = 2 * i**2 + logging.getLogger(__name__).warning( + f"Retry #{i}: Sleeping {sleep_time}s because {e}" + ) + sleep(sleep_time) def _get_all_files(c: FTPClient, path: str) -> list[str]: diff --git a/peakina/readers/excel.py b/peakina/readers/excel.py index 99f83d8f..c16cc729 100644 --- a/peakina/readers/excel.py +++ b/peakina/readers/excel.py @@ -17,7 +17,6 @@ def read_excel( preview_offset: int = 0, **kwargs: Any, ) -> pd.DataFrame: - df_or_dict: dict[str, pd.DataFrame] | pd.DataFrame = pd.read_excel(*args, **kwargs) if isinstance(df_or_dict, dict): # multiple sheets diff --git a/tests/conftest.py b/tests/conftest.py index 3259fefc..d3108978 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -143,7 +143,6 @@ def f( skip_exception=None, timeout=None, ): - if docker_pull: print(f"Pulling {image} image") docker.pull(image) From d8118be0e3d92bb8c3c09a54527ee13444e352c3 Mon Sep 17 00:00:00 2001 From: sanix-darker Date: Mon, 9 Oct 2023 08:44:15 +0200 Subject: [PATCH 05/10] chore: adding more details on the logging --- peakina/io/ftp/ftp_utils.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/peakina/io/ftp/ftp_utils.py b/peakina/io/ftp/ftp_utils.py index 04e1b4f3..3e571b78 100644 --- a/peakina/io/ftp/ftp_utils.py +++ b/peakina/io/ftp/ftp_utils.py @@ -190,28 +190,30 @@ def ftp_open(url: str, retry: int = _DEFAULT_MAX_RETRY) -> IO[bytes]: # type: i try: return _open(url) except (AttributeError, OSError, ftplib.error_temp) as e: + log = logging.getLogger(__name__) # If this occurs, we need to see what's actually inside that dir # by listing maxi 15 entries # TODO: remove this after the debuging is done ! - # FileNotFoundError inerits from OSError + # FileNotFoundError inherits from OSError if isinstance(e, FileNotFoundError) and not file_listed: # pragma: no cover try: + file_path = url.split("/")[-1] full_path = "/".join(url.split(":")[-1].split("/")[1:]) - logging.getLogger(__name__).warning(f"'{full_path}' not found !") + dir_path = full_path.replace(full_path.split("/")[-1], "") + log.warning(f"File '{file_path}' not available inside : '{dir_path}' !") parsed_url = urllib.parse.urlsplit(url) path_without_file = parsed_url.path.rsplit("/", 1)[0] modified_url = urllib.parse.urlunsplit( (parsed_url.scheme, parsed_url.netloc, path_without_file, "", "") ) + files_available = ", ".join(ftp_listdir(modified_url)[:15]) # we list only 15 as maximum - logging.getLogger(__name__).warning( - f"Listing files availables > ({files_available})" - ) + log.warning(f"List of files : ({files_available})") file_listed = True except Exception as exp: - logging.getLogger(__name__).error(exp) + logging.getLogger(__name__).error(f"Exception on file listing :: {exp}") else: sleep_time = 2 * i**2 logging.getLogger(__name__).warning( From eac79a3f84eda2365406f674e0ce020e5d95b235 Mon Sep 17 00:00:00 2001 From: sanix-darker Date: Fri, 13 Oct 2023 11:45:38 +0200 Subject: [PATCH 06/10] fix: default timeout was originally at 60s, but for each ftp-client it was defined at 3s, Setting it to 10s is too small when we have a long list of files that could takes too much time to resolve, the value should be increased, in this commit, am tripling the default fomr FTPS class that was at 60s to 180s. Later on, we should find a way to set a FF that will forward that extremly HUGE value to the lib itself. --- peakina/io/ftp/ftp_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peakina/io/ftp/ftp_utils.py b/peakina/io/ftp/ftp_utils.py index 3e571b78..15c14670 100644 --- a/peakina/io/ftp/ftp_utils.py +++ b/peakina/io/ftp/ftp_utils.py @@ -17,7 +17,7 @@ import paramiko FTP_SCHEMES = ["ftp", "ftps", "sftp"] -_DEFAULT_MAX_TIMEOUT_SECONDS = 10 +_DEFAULT_MAX_TIMEOUT_SECONDS = 180 _DEFAULT_MAX_RETRY = 7 FTPClient = ftplib.FTP | paramiko.SFTPClient From e874a8611be16c92104cc9f73e3fef624a2f6fee Mon Sep 17 00:00:00 2001 From: sanix-darker Date: Fri, 13 Oct 2023 14:46:58 +0200 Subject: [PATCH 07/10] fix: catch SSHException to trigger the retry on connection dropped --- peakina/io/ftp/ftp_utils.py | 2 +- tests/io/ftp/test_ftp_utils.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/peakina/io/ftp/ftp_utils.py b/peakina/io/ftp/ftp_utils.py index 15c14670..d40fe198 100644 --- a/peakina/io/ftp/ftp_utils.py +++ b/peakina/io/ftp/ftp_utils.py @@ -189,7 +189,7 @@ def ftp_open(url: str, retry: int = _DEFAULT_MAX_RETRY) -> IO[bytes]: # type: i for i in range(1, retry + 1): try: return _open(url) - except (AttributeError, OSError, ftplib.error_temp) as e: + except (AttributeError, OSError, ftplib.error_temp, paramiko.SSHException) as e: log = logging.getLogger(__name__) # If this occurs, we need to see what's actually inside that dir # by listing maxi 15 entries diff --git a/tests/io/ftp/test_ftp_utils.py b/tests/io/ftp/test_ftp_utils.py index 410bd2aa..0ced13e9 100644 --- a/tests/io/ftp/test_ftp_utils.py +++ b/tests/io/ftp/test_ftp_utils.py @@ -4,6 +4,7 @@ import ssl from urllib.parse import ParseResult +from paramiko.ssh_exception import SSHException from pytest import fixture, raises from pytest_mock import MockFixture @@ -68,6 +69,7 @@ def test_retry_open(mocker): ftplib.error_temp("421 Could not create socket"), AttributeError("'NoneType' object has no attribute 'sendall'"), OSError("Random OSError"), + SSHException("Random connection dropped error"), "ok", ] mock_sleep = mocker.patch("peakina.io.ftp.ftp_utils.sleep") From a72fc9cc266cc81e2c58ddfab17064632069a5d9 Mon Sep 17 00:00:00 2001 From: sanix-darker Date: Fri, 13 Oct 2023 15:14:29 +0200 Subject: [PATCH 08/10] chore: more cleaning, one log with details on the file we're not seeing --- peakina/io/ftp/ftp_utils.py | 42 +++++++++++-------------------------- 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/peakina/io/ftp/ftp_utils.py b/peakina/io/ftp/ftp_utils.py index d40fe198..1ec18b19 100644 --- a/peakina/io/ftp/ftp_utils.py +++ b/peakina/io/ftp/ftp_utils.py @@ -1,10 +1,10 @@ import ftplib import logging +import os import re import socket import ssl import tempfile -import urllib.parse from contextlib import contextmanager, suppress from datetime import datetime from functools import partial @@ -17,7 +17,7 @@ import paramiko FTP_SCHEMES = ["ftp", "ftps", "sftp"] -_DEFAULT_MAX_TIMEOUT_SECONDS = 180 +_DEFAULT_MAX_TIMEOUT_SECONDS = 30 _DEFAULT_MAX_RETRY = 7 FTPClient = ftplib.FTP | paramiko.SFTPClient @@ -185,41 +185,23 @@ def _open(url: str) -> IO[bytes]: def ftp_open(url: str, retry: int = _DEFAULT_MAX_RETRY) -> IO[bytes]: # type: ignore - file_listed: bool = False for i in range(1, retry + 1): try: return _open(url) except (AttributeError, OSError, ftplib.error_temp, paramiko.SSHException) as e: log = logging.getLogger(__name__) - # If this occurs, we need to see what's actually inside that dir - # by listing maxi 15 entries - # TODO: remove this after the debuging is done ! + # FileNotFoundError inherits from OSError - if isinstance(e, FileNotFoundError) and not file_listed: # pragma: no cover - try: - file_path = url.split("/")[-1] - full_path = "/".join(url.split(":")[-1].split("/")[1:]) - dir_path = full_path.replace(full_path.split("/")[-1], "") - log.warning(f"File '{file_path}' not available inside : '{dir_path}' !") - - parsed_url = urllib.parse.urlsplit(url) - path_without_file = parsed_url.path.rsplit("/", 1)[0] - modified_url = urllib.parse.urlunsplit( - (parsed_url.scheme, parsed_url.netloc, path_without_file, "", "") - ) - - files_available = ", ".join(ftp_listdir(modified_url)[:15]) - # we list only 15 as maximum - log.warning(f"List of files : ({files_available})") - file_listed = True - except Exception as exp: - logging.getLogger(__name__).error(f"Exception on file listing :: {exp}") - else: - sleep_time = 2 * i**2 - logging.getLogger(__name__).warning( - f"Retry #{i}: Sleeping {sleep_time}s because {e}" + # We need to log that we're not seeing the specified file + if isinstance(e, FileNotFoundError): # pragma: no cover + log.warning( + f"File '{os.path.basename(url)}' not available inside : " + f"'{os.path.dirname(urlparse(url).path)}' !" ) - sleep(sleep_time) + + sleep_time = 2 * i**2 + logging.getLogger(__name__).warning(f"Retry #{i}: Sleeping {sleep_time}s because {e}") + sleep(sleep_time) def _get_all_files(c: FTPClient, path: str) -> list[str]: From 902b49684800ccb623c429298283a5767909de75 Mon Sep 17 00:00:00 2001 From: sanix-darker Date: Fri, 13 Oct 2023 15:22:44 +0200 Subject: [PATCH 09/10] chore: added the entry on CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cb42e17..a75df455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Fixed + +- FTP: retry connection on `SSHException` while opening a remote url. + ## [0.9.5] - 2023-04-19 ### Added From 006ebab6a7834bd9ec653d5b777d20c710065a13 Mon Sep 17 00:00:00 2001 From: sanix-darker Date: Fri, 13 Oct 2023 15:43:11 +0200 Subject: [PATCH 10/10] chore: add log in AttributeError for the .close() --- peakina/io/ftp/ftp_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/peakina/io/ftp/ftp_utils.py b/peakina/io/ftp/ftp_utils.py index 1ec18b19..9f82d7f2 100644 --- a/peakina/io/ftp/ftp_utils.py +++ b/peakina/io/ftp/ftp_utils.py @@ -130,6 +130,7 @@ def sftp_client(params: ParseResult) -> Generator[tuple[paramiko.SFTPClient, str finally: # In cae of Exception, we don't want to raise it with suppress(AttributeError): + logging.getLogger(__name__).warning("Unable to close the Connection the SSHConnection.") ssh_client.close()