apache-airflow-providers-sftp 5.1.0rc1__py3-none-any.whl → 5.1.1rc1__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.
@@ -29,7 +29,7 @@ from airflow import __version__ as airflow_version
29
29
 
30
30
  __all__ = ["__version__"]
31
31
 
32
- __version__ = "5.1.0"
32
+ __version__ = "5.1.1"
33
33
 
34
34
  if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
35
35
  "2.9.0"
@@ -27,8 +27,9 @@ def get_provider_info():
27
27
  "name": "SFTP",
28
28
  "description": "`SSH File Transfer Protocol (SFTP) <https://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/>`__\n",
29
29
  "state": "ready",
30
- "source-date-epoch": 1739964267,
30
+ "source-date-epoch": 1741509612,
31
31
  "versions": [
32
+ "5.1.1",
32
33
  "5.1.0",
33
34
  "5.0.0",
34
35
  "4.11.1",
@@ -126,4 +127,5 @@ def get_provider_info():
126
127
  "common.compat": ["apache-airflow-providers-common-compat"],
127
128
  "openlineage": ["apache-airflow-providers-openlineage"],
128
129
  },
130
+ "devel-dependencies": [],
129
131
  }
@@ -24,7 +24,7 @@ import os
24
24
  import stat
25
25
  import warnings
26
26
  from collections.abc import Generator, Sequence
27
- from contextlib import closing, contextmanager
27
+ from contextlib import contextmanager
28
28
  from fnmatch import fnmatch
29
29
  from io import BytesIO
30
30
  from pathlib import Path
@@ -38,6 +38,7 @@ from airflow.hooks.base import BaseHook
38
38
  from airflow.providers.ssh.hooks.ssh import SSHHook
39
39
 
40
40
  if TYPE_CHECKING:
41
+ from paramiko import SSHClient
41
42
  from paramiko.sftp_attr import SFTPAttributes
42
43
  from paramiko.sftp_client import SFTPClient
43
44
 
@@ -87,6 +88,8 @@ class SFTPHook(SSHHook):
87
88
  *args,
88
89
  **kwargs,
89
90
  ) -> None:
91
+ self.conn: SFTPClient | None = None
92
+
90
93
  # TODO: remove support for ssh_hook when it is removed from SFTPOperator
91
94
  if kwargs.get("ssh_hook") is not None:
92
95
  warnings.warn(
@@ -108,14 +111,46 @@ class SFTPHook(SSHHook):
108
111
  kwargs["host_proxy_cmd"] = host_proxy_cmd
109
112
  self.ssh_conn_id = ssh_conn_id
110
113
 
114
+ self._ssh_conn: SSHClient | None = None
115
+ self._sftp_conn: SFTPClient | None = None
116
+ self._conn_count = 0
117
+
111
118
  super().__init__(*args, **kwargs)
112
119
 
120
+ def get_conn(self) -> SFTPClient: # type: ignore[override]
121
+ """Open an SFTP connection to the remote host."""
122
+ if self.conn is None:
123
+ self.conn = super().get_conn().open_sftp()
124
+ return self.conn
125
+
126
+ def close_conn(self) -> None:
127
+ """Close the SFTP connection."""
128
+ if self.conn is not None:
129
+ self.conn.close()
130
+ self.conn = None
131
+
113
132
  @contextmanager
114
- def get_conn(self) -> Generator[SFTPClient, None, None]:
133
+ def get_managed_conn(self) -> Generator[SFTPClient, None, None]:
115
134
  """Context manager that closes the connection after use."""
116
- with closing(super().get_conn()) as conn:
117
- with closing(conn.open_sftp()) as sftp:
118
- yield sftp
135
+ if self._sftp_conn is None:
136
+ ssh_conn: SSHClient = super().get_conn()
137
+ self._ssh_conn = ssh_conn
138
+ self._sftp_conn = ssh_conn.open_sftp()
139
+ self._conn_count += 1
140
+
141
+ try:
142
+ yield self._sftp_conn
143
+ finally:
144
+ self._conn_count -= 1
145
+ if self._conn_count == 0 and self._ssh_conn is not None and self._sftp_conn is not None:
146
+ self._sftp_conn.close()
147
+ self._sftp_conn = None
148
+ self._ssh_conn.close()
149
+ self._ssh_conn = None
150
+
151
+ def get_conn_count(self) -> int:
152
+ """Get the number of open connections."""
153
+ return self._conn_count
119
154
 
120
155
  def describe_directory(self, path: str) -> dict[str, dict[str, str | int | None]]:
121
156
  """
@@ -126,7 +161,7 @@ class SFTPHook(SSHHook):
126
161
 
127
162
  :param path: full path to the remote directory
128
163
  """
129
- with self.get_conn() as conn: # type: SFTPClient
164
+ with self.get_managed_conn() as conn: # type: SFTPClient
130
165
  flist = sorted(conn.listdir_attr(path), key=lambda x: x.filename)
131
166
  files = {}
132
167
  for f in flist:
@@ -144,7 +179,7 @@ class SFTPHook(SSHHook):
144
179
 
145
180
  :param path: full path to the remote directory to list
146
181
  """
147
- with self.get_conn() as conn:
182
+ with self.get_managed_conn() as conn:
148
183
  return sorted(conn.listdir(path))
149
184
 
150
185
  def list_directory_with_attr(self, path: str) -> list[SFTPAttributes]:
@@ -153,7 +188,7 @@ class SFTPHook(SSHHook):
153
188
 
154
189
  :param path: full path to the remote directory to list
155
190
  """
156
- with self.get_conn() as conn:
191
+ with self.get_managed_conn() as conn:
157
192
  return [file for file in conn.listdir_attr(path)]
158
193
 
159
194
  def mkdir(self, path: str, mode: int = 0o777) -> None:
@@ -166,7 +201,7 @@ class SFTPHook(SSHHook):
166
201
  :param path: full path to the remote directory to create
167
202
  :param mode: int permissions of octal mode for directory
168
203
  """
169
- with self.get_conn() as conn:
204
+ with self.get_managed_conn() as conn:
170
205
  conn.mkdir(path, mode=mode)
171
206
 
172
207
  def isdir(self, path: str) -> bool:
@@ -175,7 +210,7 @@ class SFTPHook(SSHHook):
175
210
 
176
211
  :param path: full path to the remote directory to check
177
212
  """
178
- with self.get_conn() as conn:
213
+ with self.get_managed_conn() as conn:
179
214
  try:
180
215
  return stat.S_ISDIR(conn.stat(path).st_mode) # type: ignore
181
216
  except OSError:
@@ -187,7 +222,7 @@ class SFTPHook(SSHHook):
187
222
 
188
223
  :param path: full path to the remote file to check
189
224
  """
190
- with self.get_conn() as conn:
225
+ with self.get_managed_conn() as conn:
191
226
  try:
192
227
  return stat.S_ISREG(conn.stat(path).st_mode) # type: ignore
193
228
  except OSError:
@@ -216,7 +251,7 @@ class SFTPHook(SSHHook):
216
251
  self.create_directory(dirname, mode)
217
252
  if basename:
218
253
  self.log.info("Creating %s", path)
219
- with self.get_conn() as conn:
254
+ with self.get_managed_conn() as conn:
220
255
  conn.mkdir(path, mode=mode)
221
256
 
222
257
  def delete_directory(self, path: str, include_files: bool = False) -> None:
@@ -232,7 +267,7 @@ class SFTPHook(SSHHook):
232
267
  files, dirs, _ = self.get_tree_map(path)
233
268
  dirs = dirs[::-1] # reverse the order for deleting deepest directories first
234
269
 
235
- with self.get_conn() as conn:
270
+ with self.get_managed_conn() as conn:
236
271
  for file_path in files:
237
272
  conn.remove(file_path)
238
273
  for dir_path in dirs:
@@ -250,7 +285,7 @@ class SFTPHook(SSHHook):
250
285
  :param local_full_path: full path to the local file or a file-like buffer
251
286
  :param prefetch: controls whether prefetch is performed (default: True)
252
287
  """
253
- with self.get_conn() as conn:
288
+ with self.get_managed_conn() as conn:
254
289
  if isinstance(local_full_path, BytesIO):
255
290
  conn.getfo(remote_full_path, local_full_path, prefetch=prefetch)
256
291
  else:
@@ -266,7 +301,7 @@ class SFTPHook(SSHHook):
266
301
  :param remote_full_path: full path to the remote file
267
302
  :param local_full_path: full path to the local file or a file-like buffer
268
303
  """
269
- with self.get_conn() as conn:
304
+ with self.get_managed_conn() as conn:
270
305
  if isinstance(local_full_path, BytesIO):
271
306
  conn.putfo(local_full_path, remote_full_path, confirm=confirm)
272
307
  else:
@@ -278,7 +313,7 @@ class SFTPHook(SSHHook):
278
313
 
279
314
  :param path: full path to the remote file
280
315
  """
281
- with self.get_conn() as conn:
316
+ with self.get_managed_conn() as conn:
282
317
  conn.remove(path)
283
318
 
284
319
  def retrieve_directory(self, remote_full_path: str, local_full_path: str, prefetch: bool = True) -> None:
@@ -295,13 +330,14 @@ class SFTPHook(SSHHook):
295
330
  if Path(local_full_path).exists():
296
331
  raise AirflowException(f"{local_full_path} already exists")
297
332
  Path(local_full_path).mkdir(parents=True)
298
- files, dirs, _ = self.get_tree_map(remote_full_path)
299
- for dir_path in dirs:
300
- new_local_path = os.path.join(local_full_path, os.path.relpath(dir_path, remote_full_path))
301
- Path(new_local_path).mkdir(parents=True, exist_ok=True)
302
- for file_path in files:
303
- new_local_path = os.path.join(local_full_path, os.path.relpath(file_path, remote_full_path))
304
- self.retrieve_file(file_path, new_local_path, prefetch)
333
+ with self.get_managed_conn():
334
+ files, dirs, _ = self.get_tree_map(remote_full_path)
335
+ for dir_path in dirs:
336
+ new_local_path = os.path.join(local_full_path, os.path.relpath(dir_path, remote_full_path))
337
+ Path(new_local_path).mkdir(parents=True, exist_ok=True)
338
+ for file_path in files:
339
+ new_local_path = os.path.join(local_full_path, os.path.relpath(file_path, remote_full_path))
340
+ self.retrieve_file(file_path, new_local_path, prefetch)
305
341
 
306
342
  def store_directory(self, remote_full_path: str, local_full_path: str, confirm: bool = True) -> None:
307
343
  """
@@ -315,16 +351,21 @@ class SFTPHook(SSHHook):
315
351
  """
316
352
  if self.path_exists(remote_full_path):
317
353
  raise AirflowException(f"{remote_full_path} already exists")
318
- self.create_directory(remote_full_path)
319
- for root, dirs, files in os.walk(local_full_path):
320
- for dir_name in dirs:
321
- dir_path = os.path.join(root, dir_name)
322
- new_remote_path = os.path.join(remote_full_path, os.path.relpath(dir_path, local_full_path))
323
- self.create_directory(new_remote_path)
324
- for file_name in files:
325
- file_path = os.path.join(root, file_name)
326
- new_remote_path = os.path.join(remote_full_path, os.path.relpath(file_path, local_full_path))
327
- self.store_file(new_remote_path, file_path, confirm)
354
+ with self.get_managed_conn():
355
+ self.create_directory(remote_full_path)
356
+ for root, dirs, files in os.walk(local_full_path):
357
+ for dir_name in dirs:
358
+ dir_path = os.path.join(root, dir_name)
359
+ new_remote_path = os.path.join(
360
+ remote_full_path, os.path.relpath(dir_path, local_full_path)
361
+ )
362
+ self.create_directory(new_remote_path)
363
+ for file_name in files:
364
+ file_path = os.path.join(root, file_name)
365
+ new_remote_path = os.path.join(
366
+ remote_full_path, os.path.relpath(file_path, local_full_path)
367
+ )
368
+ self.store_file(new_remote_path, file_path, confirm)
328
369
 
329
370
  def get_mod_time(self, path: str) -> str:
330
371
  """
@@ -332,7 +373,7 @@ class SFTPHook(SSHHook):
332
373
 
333
374
  :param path: full path to the remote file
334
375
  """
335
- with self.get_conn() as conn:
376
+ with self.get_managed_conn() as conn:
336
377
  ftp_mdtm = conn.stat(path).st_mtime
337
378
  return datetime.datetime.fromtimestamp(ftp_mdtm).strftime("%Y%m%d%H%M%S") # type: ignore
338
379
 
@@ -342,7 +383,7 @@ class SFTPHook(SSHHook):
342
383
 
343
384
  :param path: full path to the remote file or directory
344
385
  """
345
- with self.get_conn() as conn:
386
+ with self.get_managed_conn() as conn:
346
387
  try:
347
388
  conn.stat(path)
348
389
  except OSError:
@@ -441,7 +482,7 @@ class SFTPHook(SSHHook):
441
482
  def test_connection(self) -> tuple[bool, str]:
442
483
  """Test the SFTP connection by calling path with directory."""
443
484
  try:
444
- with self.get_conn() as conn:
485
+ with self.get_managed_conn() as conn:
445
486
  conn.normalize(".")
446
487
  return True, "Connection successfully tested"
447
488
  except Exception as e:
@@ -129,7 +129,6 @@ class SFTPSensor(BaseSensorOperator):
129
129
  else:
130
130
  files_found.append(actual_file_to_check)
131
131
 
132
- self.hook.close_conn()
133
132
  if not len(files_found):
134
133
  return False
135
134
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: apache-airflow-providers-sftp
3
- Version: 5.1.0rc1
3
+ Version: 5.1.1rc1
4
4
  Summary: Provider package apache-airflow-providers-sftp for Apache Airflow
5
5
  Keywords: airflow-provider,sftp,airflow,integration
6
6
  Author-email: Apache Software Foundation <dev@airflow.apache.org>
@@ -27,8 +27,8 @@ Requires-Dist: asyncssh>=2.12.0
27
27
  Requires-Dist: apache-airflow-providers-common-compat ; extra == "common-compat"
28
28
  Requires-Dist: apache-airflow-providers-openlineage ; extra == "openlineage"
29
29
  Project-URL: Bug Tracker, https://github.com/apache/airflow/issues
30
- Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.1.0/changelog.html
31
- Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.1.0
30
+ Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.1.1/changelog.html
31
+ Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.1.1
32
32
  Project-URL: Slack Chat, https://s.apache.org/airflow-slack
33
33
  Project-URL: Source Code, https://github.com/apache/airflow
34
34
  Project-URL: Twitter, https://x.com/ApacheAirflow
@@ -37,32 +37,31 @@ Provides-Extra: common-compat
37
37
  Provides-Extra: openlineage
38
38
 
39
39
 
40
- .. Licensed to the Apache Software Foundation (ASF) under one
41
- or more contributor license agreements. See the NOTICE file
42
- distributed with this work for additional information
43
- regarding copyright ownership. The ASF licenses this file
44
- to you under the Apache License, Version 2.0 (the
45
- "License"); you may not use this file except in compliance
46
- with the License. You may obtain a copy of the License at
40
+ .. Licensed to the Apache Software Foundation (ASF) under one
41
+ or more contributor license agreements. See the NOTICE file
42
+ distributed with this work for additional information
43
+ regarding copyright ownership. The ASF licenses this file
44
+ to you under the Apache License, Version 2.0 (the
45
+ "License"); you may not use this file except in compliance
46
+ with the License. You may obtain a copy of the License at
47
47
 
48
- .. http://www.apache.org/licenses/LICENSE-2.0
48
+ .. http://www.apache.org/licenses/LICENSE-2.0
49
49
 
50
- .. Unless required by applicable law or agreed to in writing,
51
- software distributed under the License is distributed on an
52
- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
53
- KIND, either express or implied. See the License for the
54
- specific language governing permissions and limitations
55
- under the License.
50
+ .. Unless required by applicable law or agreed to in writing,
51
+ software distributed under the License is distributed on an
52
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
53
+ KIND, either express or implied. See the License for the
54
+ specific language governing permissions and limitations
55
+ under the License.
56
56
 
57
- .. NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE OVERWRITTEN!
58
-
59
- .. IF YOU WANT TO MODIFY TEMPLATE FOR THIS FILE, YOU SHOULD MODIFY THE TEMPLATE
60
- `PROVIDER_README_TEMPLATE.rst.jinja2` IN the `dev/breeze/src/airflow_breeze/templates` DIRECTORY
57
+ .. NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE OVERWRITTEN!
61
58
 
59
+ .. IF YOU WANT TO MODIFY TEMPLATE FOR THIS FILE, YOU SHOULD MODIFY THE TEMPLATE
60
+ ``PROVIDER_README_TEMPLATE.rst.jinja2`` IN the ``dev/breeze/src/airflow_breeze/templates`` DIRECTORY
62
61
 
63
62
  Package ``apache-airflow-providers-sftp``
64
63
 
65
- Release: ``5.1.0``
64
+ Release: ``5.1.1``
66
65
 
67
66
 
68
67
  `SSH File Transfer Protocol (SFTP) <https://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/>`__
@@ -75,7 +74,7 @@ This is a provider package for ``sftp`` provider. All classes for this provider
75
74
  are in ``airflow.providers.sftp`` python package.
76
75
 
77
76
  You can find package information and changelog for the provider
78
- in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.1.0/>`_.
77
+ in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.1.1/>`_.
79
78
 
80
79
  Installation
81
80
  ------------
@@ -120,5 +119,5 @@ Dependent package
120
119
  ================================================================================================================== =================
121
120
 
122
121
  The changelog for the provider package can be found in the
123
- `changelog <https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.1.0/changelog.html>`_.
122
+ `changelog <https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.1.1/changelog.html>`_.
124
123
 
@@ -1,18 +1,18 @@
1
1
  airflow/providers/sftp/LICENSE,sha256=gXPVwptPlW1TJ4HSuG5OMPg-a3h43OGMkZRR1rpwfJA,10850
2
- airflow/providers/sftp/__init__.py,sha256=cozZuS7gnm3FJTNUVcuHiETa115Qw8xdtX6-YMm8Kl8,1491
3
- airflow/providers/sftp/get_provider_info.py,sha256=r-DSixnfFdgYqggP4wv6SGfxh5aVzrvSfGPfTRHo3AQ,4269
2
+ airflow/providers/sftp/__init__.py,sha256=N2tDz6dqlCE4CDTlsZJXq39AESjhHCOXWX6QyKt_ZCo,1491
3
+ airflow/providers/sftp/get_provider_info.py,sha256=YKtWfzGpCEAE0RAeyWbZ5r3W-DAU5rxk7zjGUcH46r0,4324
4
4
  airflow/providers/sftp/decorators/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
5
5
  airflow/providers/sftp/decorators/sensors/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
6
6
  airflow/providers/sftp/decorators/sensors/sftp.py,sha256=deps7xldfdLcO5mwxuZ9SJ7wFNrf2DZWMSt_KYMmGG4,2888
7
7
  airflow/providers/sftp/hooks/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
8
- airflow/providers/sftp/hooks/sftp.py,sha256=aDdBE7yNkh-H0SFc3Uqkx0LP4lWtu3rDqqeYwqxrvN0,24945
8
+ airflow/providers/sftp/hooks/sftp.py,sha256=srBUSfeqxI37T7ytSfAQo8zX-GDWoDdWKFXHD1xwHCs,26457
9
9
  airflow/providers/sftp/operators/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
10
10
  airflow/providers/sftp/operators/sftp.py,sha256=skNYBFzdZUnkU_SpDf_m6HxnUuTVd5KDwJDRJnly7NE,11670
11
11
  airflow/providers/sftp/sensors/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
12
- airflow/providers/sftp/sensors/sftp.py,sha256=2McTofMV0IPM_NmxzH9EwWmmeLWuQSrfF8hq6aMtmGE,8077
12
+ airflow/providers/sftp/sensors/sftp.py,sha256=GF8GGnkuSKrg2kqNMGWLm99SE9rtrtDqWxoOPE1QdY8,8046
13
13
  airflow/providers/sftp/triggers/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
14
14
  airflow/providers/sftp/triggers/sftp.py,sha256=fSi-I5FocNQblHt4GYfGispFgOOl8XQ9Vk9ZFLcv_Sw,6182
15
- apache_airflow_providers_sftp-5.1.0rc1.dist-info/entry_points.txt,sha256=Fa1IkUHV6qnIuwLd0U7tKoklbLXXVrbB2hhG6N7Q-zo,100
16
- apache_airflow_providers_sftp-5.1.0rc1.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
17
- apache_airflow_providers_sftp-5.1.0rc1.dist-info/METADATA,sha256=r3I0JViv1sqlwuHbQok8IAoFfRMsbz7kxOnK0Hwx0II,5704
18
- apache_airflow_providers_sftp-5.1.0rc1.dist-info/RECORD,,
15
+ apache_airflow_providers_sftp-5.1.1rc1.dist-info/entry_points.txt,sha256=Fa1IkUHV6qnIuwLd0U7tKoklbLXXVrbB2hhG6N7Q-zo,100
16
+ apache_airflow_providers_sftp-5.1.1rc1.dist-info/WHEEL,sha256=_2ozNFCLWc93bK4WKHCO-eDUENDlo-dgc9cU3qokYO4,82
17
+ apache_airflow_providers_sftp-5.1.1rc1.dist-info/METADATA,sha256=VV9CC3toyzU5oIqBP4ijawbV5MS1I-lDp-0N6AfJrTo,5690
18
+ apache_airflow_providers_sftp-5.1.1rc1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: flit 3.10.1
2
+ Generator: flit 3.11.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any