apache-airflow-providers-sftp 5.3.4rc1__tar.gz → 5.4.0rc1__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.
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/PKG-INFO +7 -8
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/README.rst +4 -5
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/docs/changelog.rst +24 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/docs/index.rst +4 -6
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/provider.yaml +2 -1
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/pyproject.toml +4 -4
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/sftp/__init__.py +1 -1
- apache_airflow_providers_sftp-5.4.0rc1/src/airflow/providers/sftp/exceptions.py +23 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/sftp/hooks/sftp.py +124 -108
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/sftp/sensors/sftp.py +23 -8
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/unit/sftp/hooks/test_sftp.py +2 -1
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/unit/sftp/sensors/test_sftp.py +15 -4
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/docs/.latest-doc-only-change.txt +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/docs/commits.rst +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/docs/conf.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/docs/connections/sftp.rst +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/docs/installing-providers-from-sources.rst +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/docs/integration-logos/SFTP.png +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/docs/security.rst +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/docs/sensors/sftp_sensor.rst +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/sftp/LICENSE +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/sftp/decorators/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/sftp/decorators/sensors/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/sftp/decorators/sensors/sftp.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/sftp/get_provider_info.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/sftp/hooks/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/sftp/operators/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/sftp/operators/sftp.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/sftp/sensors/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/sftp/triggers/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/sftp/triggers/sftp.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/src/airflow/providers/sftp/version_compat.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/conftest.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/system/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/system/sftp/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/system/sftp/example_sftp_sensor.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/unit/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/unit/sftp/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/unit/sftp/decorators/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/unit/sftp/decorators/sensors/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/unit/sftp/decorators/sensors/test_sftp.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/unit/sftp/hooks/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/unit/sftp/operators/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/unit/sftp/operators/test_sftp.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/unit/sftp/sensors/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/unit/sftp/triggers/__init__.py +0 -0
- {apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/unit/sftp/triggers/test_sftp.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: apache-airflow-providers-sftp
|
3
|
-
Version: 5.
|
3
|
+
Version: 5.4.0rc1
|
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.staged.apache.org/docs/apache-airflow-providers-sftp/5.
|
31
|
-
Project-URL: Documentation, https://airflow.staged.apache.org/docs/apache-airflow-providers-sftp/5.
|
30
|
+
Project-URL: Changelog, https://airflow.staged.apache.org/docs/apache-airflow-providers-sftp/5.4.0/changelog.html
|
31
|
+
Project-URL: Documentation, https://airflow.staged.apache.org/docs/apache-airflow-providers-sftp/5.4.0
|
32
32
|
Project-URL: Mastodon, https://fosstodon.org/@airflow
|
33
33
|
Project-URL: Slack Chat, https://s.apache.org/airflow-slack
|
34
34
|
Project-URL: Source Code, https://github.com/apache/airflow
|
@@ -61,9 +61,8 @@ Provides-Extra: openlineage
|
|
61
61
|
|
62
62
|
Package ``apache-airflow-providers-sftp``
|
63
63
|
|
64
|
-
Release: ``5.
|
64
|
+
Release: ``5.4.0``
|
65
65
|
|
66
|
-
Release Date: ``|PypiReleaseDate|``
|
67
66
|
|
68
67
|
`SSH File Transfer Protocol (SFTP) <https://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/>`__
|
69
68
|
|
@@ -75,12 +74,12 @@ 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.
|
77
|
+
in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.4.0/>`_.
|
79
78
|
|
80
79
|
Installation
|
81
80
|
------------
|
82
81
|
|
83
|
-
You can install this package on top of an existing Airflow
|
82
|
+
You can install this package on top of an existing Airflow installation (see ``Requirements`` below
|
84
83
|
for the minimum Airflow version supported) via
|
85
84
|
``pip install apache-airflow-providers-sftp``
|
86
85
|
|
@@ -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.
|
122
|
+
`changelog <https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.4.0/changelog.html>`_.
|
124
123
|
|
{apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/README.rst
RENAMED
@@ -23,9 +23,8 @@
|
|
23
23
|
|
24
24
|
Package ``apache-airflow-providers-sftp``
|
25
25
|
|
26
|
-
Release: ``5.
|
26
|
+
Release: ``5.4.0``
|
27
27
|
|
28
|
-
Release Date: ``|PypiReleaseDate|``
|
29
28
|
|
30
29
|
`SSH File Transfer Protocol (SFTP) <https://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/>`__
|
31
30
|
|
@@ -37,12 +36,12 @@ This is a provider package for ``sftp`` provider. All classes for this provider
|
|
37
36
|
are in ``airflow.providers.sftp`` python package.
|
38
37
|
|
39
38
|
You can find package information and changelog for the provider
|
40
|
-
in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.
|
39
|
+
in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.4.0/>`_.
|
41
40
|
|
42
41
|
Installation
|
43
42
|
------------
|
44
43
|
|
45
|
-
You can install this package on top of an existing Airflow
|
44
|
+
You can install this package on top of an existing Airflow installation (see ``Requirements`` below
|
46
45
|
for the minimum Airflow version supported) via
|
47
46
|
``pip install apache-airflow-providers-sftp``
|
48
47
|
|
@@ -82,4 +81,4 @@ Dependent package
|
|
82
81
|
================================================================================================================== =================
|
83
82
|
|
84
83
|
The changelog for the provider package can be found in the
|
85
|
-
`changelog <https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.
|
84
|
+
`changelog <https://airflow.apache.org/docs/apache-airflow-providers-sftp/5.4.0/changelog.html>`_.
|
{apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/docs/changelog.rst
RENAMED
@@ -27,6 +27,30 @@
|
|
27
27
|
Changelog
|
28
28
|
---------
|
29
29
|
|
30
|
+
5.4.0
|
31
|
+
.....
|
32
|
+
|
33
|
+
|
34
|
+
Release Date: ``|PypiReleaseDate|``
|
35
|
+
|
36
|
+
Features
|
37
|
+
~~~~~~~~
|
38
|
+
|
39
|
+
* ``Feature: add optional managed connection (#52700)``
|
40
|
+
* ``Add file_pattern to template fields (#54562)``
|
41
|
+
|
42
|
+
Bug Fixes
|
43
|
+
~~~~~~~~~
|
44
|
+
|
45
|
+
* ``Fix sftp async hoook (#54763)``
|
46
|
+
|
47
|
+
.. Below changes are excluded from the changelog. Move them to
|
48
|
+
appropriate section above if needed. Do not delete the lines(!):
|
49
|
+
* ``Switch pre-commit to prek (#54258)``
|
50
|
+
|
51
|
+
.. Review and move the new changes to one of the sections above:
|
52
|
+
* ``Fix Airflow 2 reference in README/index of providers (#55240)``
|
53
|
+
|
30
54
|
5.3.4
|
31
55
|
.....
|
32
56
|
|
{apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/docs/index.rst
RENAMED
@@ -70,9 +70,7 @@ apache-airflow-providers-sftp package
|
|
70
70
|
`SSH File Transfer Protocol (SFTP) <https://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/>`__
|
71
71
|
|
72
72
|
|
73
|
-
Release: 5.
|
74
|
-
|
75
|
-
Release Date: ``|PypiReleaseDate|``
|
73
|
+
Release: 5.4.0
|
76
74
|
|
77
75
|
Provider package
|
78
76
|
----------------
|
@@ -83,7 +81,7 @@ All classes for this package are included in the ``airflow.providers.sftp`` pyth
|
|
83
81
|
Installation
|
84
82
|
------------
|
85
83
|
|
86
|
-
You can install this package on top of an existing Airflow
|
84
|
+
You can install this package on top of an existing Airflow installation via
|
87
85
|
``pip install apache-airflow-providers-sftp``.
|
88
86
|
For the minimum Airflow version supported, see ``Requirements`` below.
|
89
87
|
|
@@ -128,5 +126,5 @@ Downloading official packages
|
|
128
126
|
You can download officially released packages and verify their checksums and signatures from the
|
129
127
|
`Official Apache Download site <https://downloads.apache.org/airflow/providers/>`_
|
130
128
|
|
131
|
-
* `The apache-airflow-providers-sftp 5.
|
132
|
-
* `The apache-airflow-providers-sftp 5.
|
129
|
+
* `The apache-airflow-providers-sftp 5.4.0 sdist package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_sftp-5.4.0.tar.gz>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_sftp-5.4.0.tar.gz.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_sftp-5.4.0.tar.gz.sha512>`__)
|
130
|
+
* `The apache-airflow-providers-sftp 5.4.0 wheel package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_sftp-5.4.0-py3-none-any.whl>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_sftp-5.4.0-py3-none-any.whl.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_sftp-5.4.0-py3-none-any.whl.sha512>`__)
|
{apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/provider.yaml
RENAMED
@@ -22,12 +22,13 @@ description: |
|
|
22
22
|
`SSH File Transfer Protocol (SFTP) <https://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/>`__
|
23
23
|
|
24
24
|
state: ready
|
25
|
-
source-date-epoch:
|
25
|
+
source-date-epoch: 1756877505
|
26
26
|
# Note that those versions are maintained by release manager - do not update them manually
|
27
27
|
# with the exception of case where other provider in sources has >= new provider version.
|
28
28
|
# In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have
|
29
29
|
# to be done in the same PR
|
30
30
|
versions:
|
31
|
+
- 5.4.0
|
31
32
|
- 5.3.4
|
32
33
|
- 5.3.3
|
33
34
|
- 5.3.2
|
{apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/pyproject.toml
RENAMED
@@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi"
|
|
25
25
|
|
26
26
|
[project]
|
27
27
|
name = "apache-airflow-providers-sftp"
|
28
|
-
version = "5.
|
28
|
+
version = "5.4.0rc1"
|
29
29
|
description = "Provider package apache-airflow-providers-sftp for Apache Airflow"
|
30
30
|
readme = "README.rst"
|
31
31
|
authors = [
|
@@ -54,7 +54,7 @@ requires-python = ">=3.10"
|
|
54
54
|
|
55
55
|
# The dependencies should be modified in place in the generated file.
|
56
56
|
# Any change in the dependencies is preserved when the file is regenerated
|
57
|
-
# Make sure to run ``
|
57
|
+
# Make sure to run ``prek update-providers-dependencies --all-files``
|
58
58
|
# After you modify the dependencies, and rebuild your Breeze CI image with ``breeze ci-image build``
|
59
59
|
dependencies = [
|
60
60
|
"apache-airflow>=2.10.0rc1",
|
@@ -111,8 +111,8 @@ apache-airflow-providers-common-sql = {workspace = true}
|
|
111
111
|
apache-airflow-providers-standard = {workspace = true}
|
112
112
|
|
113
113
|
[project.urls]
|
114
|
-
"Documentation" = "https://airflow.staged.apache.org/docs/apache-airflow-providers-sftp/5.
|
115
|
-
"Changelog" = "https://airflow.staged.apache.org/docs/apache-airflow-providers-sftp/5.
|
114
|
+
"Documentation" = "https://airflow.staged.apache.org/docs/apache-airflow-providers-sftp/5.4.0"
|
115
|
+
"Changelog" = "https://airflow.staged.apache.org/docs/apache-airflow-providers-sftp/5.4.0/changelog.html"
|
116
116
|
"Bug Tracker" = "https://github.com/apache/airflow/issues"
|
117
117
|
"Source Code" = "https://github.com/apache/airflow"
|
118
118
|
"Slack Chat" = "https://s.apache.org/airflow-slack"
|
@@ -29,7 +29,7 @@ from airflow import __version__ as airflow_version
|
|
29
29
|
|
30
30
|
__all__ = ["__version__"]
|
31
31
|
|
32
|
-
__version__ = "5.
|
32
|
+
__version__ = "5.4.0"
|
33
33
|
|
34
34
|
if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
|
35
35
|
"2.10.0"
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
3
|
+
# distributed with this work for additional information
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
6
|
+
# "License"); you may not use this file except in compliance
|
7
|
+
# with the License. You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
12
|
+
# software distributed under the License is distributed on an
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
14
|
+
# KIND, either express or implied. See the License for the
|
15
|
+
# specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
from __future__ import annotations
|
18
|
+
|
19
|
+
from airflow.exceptions import AirflowException
|
20
|
+
|
21
|
+
|
22
|
+
class ConnectionNotOpenedException(AirflowException):
|
23
|
+
"""Thrown when a connection has not been opened and has been tried to be used."""
|
@@ -21,6 +21,7 @@ from __future__ import annotations
|
|
21
21
|
|
22
22
|
import concurrent.futures
|
23
23
|
import datetime
|
24
|
+
import functools
|
24
25
|
import os
|
25
26
|
import stat
|
26
27
|
import warnings
|
@@ -34,7 +35,11 @@ from typing import IO, TYPE_CHECKING, Any, cast
|
|
34
35
|
import asyncssh
|
35
36
|
from asgiref.sync import sync_to_async
|
36
37
|
|
37
|
-
from airflow.exceptions import
|
38
|
+
from airflow.exceptions import (
|
39
|
+
AirflowException,
|
40
|
+
AirflowProviderDeprecationWarning,
|
41
|
+
)
|
42
|
+
from airflow.providers.sftp.exceptions import ConnectionNotOpenedException
|
38
43
|
from airflow.providers.sftp.version_compat import BaseHook
|
39
44
|
from airflow.providers.ssh.hooks.ssh import SSHHook
|
40
45
|
|
@@ -46,6 +51,25 @@ if TYPE_CHECKING:
|
|
46
51
|
from airflow.models.connection import Connection
|
47
52
|
|
48
53
|
|
54
|
+
def handle_connection_management(func: Callable) -> Callable:
|
55
|
+
@functools.wraps(func)
|
56
|
+
def handle_connection_management_wrapper(self, *args: Any, **kwargs: dict[str, Any]) -> Any:
|
57
|
+
if not self.use_managed_conn:
|
58
|
+
if self.conn is None:
|
59
|
+
raise ConnectionNotOpenedException(
|
60
|
+
"Connection not open, use with hook.get_managed_conn() Managed Connection in order to create and open the connection"
|
61
|
+
)
|
62
|
+
|
63
|
+
return func(self, *args, **kwargs)
|
64
|
+
|
65
|
+
with self.get_managed_conn() as conn:
|
66
|
+
self.conn = conn
|
67
|
+
result = func(self, *args, **kwargs)
|
68
|
+
return result
|
69
|
+
|
70
|
+
return handle_connection_management_wrapper
|
71
|
+
|
72
|
+
|
49
73
|
class SFTPHook(SSHHook):
|
50
74
|
"""
|
51
75
|
Interact with SFTP.
|
@@ -86,10 +110,12 @@ class SFTPHook(SSHHook):
|
|
86
110
|
self,
|
87
111
|
ssh_conn_id: str | None = "sftp_default",
|
88
112
|
host_proxy_cmd: str | None = None,
|
113
|
+
use_managed_conn: bool = True,
|
89
114
|
*args,
|
90
115
|
**kwargs,
|
91
116
|
) -> None:
|
92
117
|
self.conn: SFTPClient | None = None
|
118
|
+
self.use_managed_conn = use_managed_conn
|
93
119
|
|
94
120
|
# TODO: remove support for ssh_hook when it is removed from SFTPOperator
|
95
121
|
if kwargs.get("ssh_hook") is not None:
|
@@ -155,6 +181,7 @@ class SFTPHook(SSHHook):
|
|
155
181
|
"""Get the number of open connections."""
|
156
182
|
return self._conn_count
|
157
183
|
|
184
|
+
@handle_connection_management
|
158
185
|
def describe_directory(self, path: str) -> dict[str, dict[str, str | int | None]]:
|
159
186
|
"""
|
160
187
|
Get file information in a directory on the remote system.
|
@@ -164,36 +191,34 @@ class SFTPHook(SSHHook):
|
|
164
191
|
|
165
192
|
:param path: full path to the remote directory
|
166
193
|
"""
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
modify
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
"modify": modify,
|
176
|
-
}
|
177
|
-
return files
|
194
|
+
return {
|
195
|
+
f.filename: {
|
196
|
+
"size": f.st_size,
|
197
|
+
"type": "dir" if stat.S_ISDIR(f.st_mode) else "file", # type: ignore[union-attr]
|
198
|
+
"modify": datetime.datetime.fromtimestamp(f.st_mtime or 0).strftime("%Y%m%d%H%M%S"),
|
199
|
+
}
|
200
|
+
for f in sorted(self.conn.listdir_attr(path), key=lambda f: f.filename) # type: ignore[union-attr]
|
201
|
+
}
|
178
202
|
|
203
|
+
@handle_connection_management
|
179
204
|
def list_directory(self, path: str) -> list[str]:
|
180
205
|
"""
|
181
206
|
List files in a directory on the remote system.
|
182
207
|
|
183
208
|
:param path: full path to the remote directory to list
|
184
209
|
"""
|
185
|
-
|
186
|
-
return sorted(conn.listdir(path))
|
210
|
+
return sorted(self.conn.listdir(path)) # type: ignore[union-attr]
|
187
211
|
|
212
|
+
@handle_connection_management
|
188
213
|
def list_directory_with_attr(self, path: str) -> list[SFTPAttributes]:
|
189
214
|
"""
|
190
215
|
List files in a directory on the remote system including their SFTPAttributes.
|
191
216
|
|
192
217
|
:param path: full path to the remote directory to list
|
193
218
|
"""
|
194
|
-
|
195
|
-
return [file for file in conn.listdir_attr(path)]
|
219
|
+
return [file for file in self.conn.listdir_attr(path)] # type: ignore[union-attr]
|
196
220
|
|
221
|
+
@handle_connection_management
|
197
222
|
def mkdir(self, path: str, mode: int = 0o777) -> None:
|
198
223
|
"""
|
199
224
|
Create a directory on the remote system.
|
@@ -204,33 +229,33 @@ class SFTPHook(SSHHook):
|
|
204
229
|
:param path: full path to the remote directory to create
|
205
230
|
:param mode: int permissions of octal mode for directory
|
206
231
|
"""
|
207
|
-
|
208
|
-
conn.mkdir(path, mode=mode)
|
232
|
+
return self.conn.mkdir(path, mode) # type: ignore[union-attr,return-value]
|
209
233
|
|
234
|
+
@handle_connection_management
|
210
235
|
def isdir(self, path: str) -> bool:
|
211
236
|
"""
|
212
237
|
Check if the path provided is a directory.
|
213
238
|
|
214
239
|
:param path: full path to the remote directory to check
|
215
240
|
"""
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
return False
|
241
|
+
try:
|
242
|
+
return stat.S_ISDIR(self.conn.stat(path).st_mode) # type: ignore[union-attr,arg-type]
|
243
|
+
except OSError:
|
244
|
+
return False
|
221
245
|
|
246
|
+
@handle_connection_management
|
222
247
|
def isfile(self, path: str) -> bool:
|
223
248
|
"""
|
224
249
|
Check if the path provided is a file.
|
225
250
|
|
226
251
|
:param path: full path to the remote file to check
|
227
252
|
"""
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
return False
|
253
|
+
try:
|
254
|
+
return stat.S_ISREG(self.conn.stat(path).st_mode) # type: ignore[arg-type,union-attr]
|
255
|
+
except OSError:
|
256
|
+
return False
|
233
257
|
|
258
|
+
@handle_connection_management
|
234
259
|
def create_directory(self, path: str, mode: int = 0o777) -> None:
|
235
260
|
"""
|
236
261
|
Create a directory on the remote system.
|
@@ -253,9 +278,9 @@ class SFTPHook(SSHHook):
|
|
253
278
|
self.create_directory(dirname, mode)
|
254
279
|
if basename:
|
255
280
|
self.log.info("Creating %s", path)
|
256
|
-
|
257
|
-
conn.mkdir(path, mode=mode)
|
281
|
+
self.conn.mkdir(path, mode=mode) # type: ignore
|
258
282
|
|
283
|
+
@handle_connection_management
|
259
284
|
def delete_directory(self, path: str, include_files: bool = False) -> None:
|
260
285
|
"""
|
261
286
|
Delete a directory on the remote system.
|
@@ -269,13 +294,13 @@ class SFTPHook(SSHHook):
|
|
269
294
|
files, dirs, _ = self.get_tree_map(path)
|
270
295
|
dirs = dirs[::-1] # reverse the order for deleting deepest directories first
|
271
296
|
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
conn.rmdir(path)
|
297
|
+
for file_path in files:
|
298
|
+
self.conn.remove(file_path) # type: ignore
|
299
|
+
for dir_path in dirs:
|
300
|
+
self.conn.rmdir(dir_path) # type: ignore
|
301
|
+
self.conn.rmdir(path) # type: ignore
|
278
302
|
|
303
|
+
@handle_connection_management
|
279
304
|
def retrieve_file(self, remote_full_path: str, local_full_path: str, prefetch: bool = True) -> None:
|
280
305
|
"""
|
281
306
|
Transfer the remote file to a local location.
|
@@ -287,28 +312,28 @@ class SFTPHook(SSHHook):
|
|
287
312
|
:param local_full_path: full path to the local file or a file-like buffer
|
288
313
|
:param prefetch: controls whether prefetch is performed (default: True)
|
289
314
|
"""
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
)
|
315
|
+
if isinstance(local_full_path, BytesIO):
|
316
|
+
# It's a file-like object ( BytesIO), so use getfo().
|
317
|
+
self.log.info("Using streaming download for %s", remote_full_path)
|
318
|
+
self.conn.getfo(remote_full_path, local_full_path, prefetch=prefetch)
|
319
|
+
# We use hasattr checking for 'write' for cases like google.cloud.storage.fileio.BlobWriter
|
320
|
+
elif hasattr(local_full_path, "write"):
|
321
|
+
self.log.info("Using streaming download for %s", remote_full_path)
|
322
|
+
# We need to cast to pass prek hook checks
|
323
|
+
stream_full_path = cast("IO[bytes]", local_full_path)
|
324
|
+
self.conn.getfo(remote_full_path, stream_full_path, prefetch=prefetch) # type: ignore[union-attr]
|
325
|
+
elif isinstance(local_full_path, (str, bytes, os.PathLike)):
|
326
|
+
# It's a string path, so use get().
|
327
|
+
self.log.info("Using standard file download for %s", remote_full_path)
|
328
|
+
self.conn.get(remote_full_path, local_full_path, prefetch=prefetch) # type: ignore[union-attr]
|
329
|
+
# If it's neither, it's an unsupported type.
|
330
|
+
else:
|
331
|
+
raise TypeError(
|
332
|
+
f"Unsupported type for local_full_path: {type(local_full_path)}. "
|
333
|
+
"Expected a stream-like object or a path-like object."
|
334
|
+
)
|
311
335
|
|
336
|
+
@handle_connection_management
|
312
337
|
def store_file(self, remote_full_path: str, local_full_path: str, confirm: bool = True) -> None:
|
313
338
|
"""
|
314
339
|
Transfer a local file to the remote location.
|
@@ -319,20 +344,19 @@ class SFTPHook(SSHHook):
|
|
319
344
|
:param remote_full_path: full path to the remote file
|
320
345
|
:param local_full_path: full path to the local file or a file-like buffer
|
321
346
|
"""
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
conn.put(local_full_path, remote_full_path, confirm=confirm)
|
347
|
+
if isinstance(local_full_path, BytesIO):
|
348
|
+
self.conn.putfo(local_full_path, remote_full_path, confirm=confirm) # type: ignore
|
349
|
+
else:
|
350
|
+
self.conn.put(local_full_path, remote_full_path, confirm=confirm) # type: ignore
|
327
351
|
|
352
|
+
@handle_connection_management
|
328
353
|
def delete_file(self, path: str) -> None:
|
329
354
|
"""
|
330
355
|
Remove a file on the server.
|
331
356
|
|
332
357
|
:param path: full path to the remote file
|
333
358
|
"""
|
334
|
-
|
335
|
-
conn.remove(path)
|
359
|
+
self.conn.remove(path) # type: ignore[arg-type, union-attr]
|
336
360
|
|
337
361
|
def retrieve_directory(self, remote_full_path: str, local_full_path: str, prefetch: bool = True) -> None:
|
338
362
|
"""
|
@@ -348,14 +372,13 @@ class SFTPHook(SSHHook):
|
|
348
372
|
if Path(local_full_path).exists():
|
349
373
|
raise AirflowException(f"{local_full_path} already exists")
|
350
374
|
Path(local_full_path).mkdir(parents=True)
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
self.retrieve_file(file_path, new_local_path, prefetch)
|
375
|
+
files, dirs, _ = self.get_tree_map(remote_full_path)
|
376
|
+
for dir_path in dirs:
|
377
|
+
new_local_path = os.path.join(local_full_path, os.path.relpath(dir_path, remote_full_path))
|
378
|
+
Path(new_local_path).mkdir(parents=True, exist_ok=True)
|
379
|
+
for file_path in files:
|
380
|
+
new_local_path = os.path.join(local_full_path, os.path.relpath(file_path, remote_full_path))
|
381
|
+
self.retrieve_file(file_path, new_local_path, prefetch)
|
359
382
|
|
360
383
|
def retrieve_directory_concurrently(
|
361
384
|
self,
|
@@ -419,6 +442,7 @@ class SFTPHook(SSHHook):
|
|
419
442
|
for conn in conns:
|
420
443
|
conn.close()
|
421
444
|
|
445
|
+
@handle_connection_management
|
422
446
|
def store_directory(self, remote_full_path: str, local_full_path: str, confirm: bool = True) -> None:
|
423
447
|
"""
|
424
448
|
Transfer a local directory to the remote location.
|
@@ -431,21 +455,16 @@ class SFTPHook(SSHHook):
|
|
431
455
|
"""
|
432
456
|
if self.path_exists(remote_full_path):
|
433
457
|
raise AirflowException(f"{remote_full_path} already exists")
|
434
|
-
|
435
|
-
|
436
|
-
for
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
file_path = os.path.join(root, file_name)
|
445
|
-
new_remote_path = os.path.join(
|
446
|
-
remote_full_path, os.path.relpath(file_path, local_full_path)
|
447
|
-
)
|
448
|
-
self.store_file(new_remote_path, file_path, confirm)
|
458
|
+
self.create_directory(remote_full_path)
|
459
|
+
for root, dirs, files in os.walk(local_full_path):
|
460
|
+
for dir_name in dirs:
|
461
|
+
dir_path = os.path.join(root, dir_name)
|
462
|
+
new_remote_path = os.path.join(remote_full_path, os.path.relpath(dir_path, local_full_path))
|
463
|
+
self.create_directory(new_remote_path)
|
464
|
+
for file_name in files:
|
465
|
+
file_path = os.path.join(root, file_name)
|
466
|
+
new_remote_path = os.path.join(remote_full_path, os.path.relpath(file_path, local_full_path))
|
467
|
+
self.store_file(new_remote_path, file_path, confirm)
|
449
468
|
|
450
469
|
def store_directory_concurrently(
|
451
470
|
self,
|
@@ -512,28 +531,28 @@ class SFTPHook(SSHHook):
|
|
512
531
|
for conn in conns:
|
513
532
|
conn.close()
|
514
533
|
|
534
|
+
@handle_connection_management
|
515
535
|
def get_mod_time(self, path: str) -> str:
|
516
536
|
"""
|
517
537
|
Get an entry's modification time.
|
518
538
|
|
519
539
|
:param path: full path to the remote file
|
520
540
|
"""
|
521
|
-
|
522
|
-
|
523
|
-
return datetime.datetime.fromtimestamp(ftp_mdtm).strftime("%Y%m%d%H%M%S") # type: ignore
|
541
|
+
ftp_mdtm = self.conn.stat(path).st_mtime # type: ignore[union-attr]
|
542
|
+
return datetime.datetime.fromtimestamp(ftp_mdtm).strftime("%Y%m%d%H%M%S") # type: ignore
|
524
543
|
|
544
|
+
@handle_connection_management
|
525
545
|
def path_exists(self, path: str) -> bool:
|
526
546
|
"""
|
527
547
|
Whether a remote entity exists.
|
528
548
|
|
529
549
|
:param path: full path to the remote file or directory
|
530
550
|
"""
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
return True
|
551
|
+
try:
|
552
|
+
self.conn.stat(path) # type: ignore[union-attr]
|
553
|
+
except OSError:
|
554
|
+
return False
|
555
|
+
return True
|
537
556
|
|
538
557
|
@staticmethod
|
539
558
|
def _is_path_match(path: str, prefix: str | None = None, delimiter: str | None = None) -> bool:
|
@@ -718,19 +737,16 @@ class SFTPHookAsync(BaseHook):
|
|
718
737
|
self.private_key = extra_options["private_key"]
|
719
738
|
|
720
739
|
host_key = extra_options.get("host_key")
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
raise ValueError("Host key check was skipped, but `host_key` value was given")
|
727
|
-
if no_host_key_check:
|
728
|
-
self.log.warning(
|
729
|
-
"No Host Key Verification. This won't protect against Man-In-The-Middle attacks"
|
730
|
-
)
|
731
|
-
self.known_hosts = "none"
|
740
|
+
nhkc_raw = extra_options.get("no_host_key_check")
|
741
|
+
no_host_key_check = True if nhkc_raw is None else (str(nhkc_raw).lower() == "true")
|
742
|
+
|
743
|
+
if host_key is not None and no_host_key_check:
|
744
|
+
raise ValueError("Host key check was skipped, but `host_key` value was given")
|
732
745
|
|
733
|
-
if
|
746
|
+
if no_host_key_check:
|
747
|
+
self.log.warning("No Host Key Verification. This won't protect against Man-In-The-Middle attacks")
|
748
|
+
self.known_hosts = "none"
|
749
|
+
elif host_key is not None:
|
734
750
|
self.known_hosts = f"{conn.host} {host_key}".encode()
|
735
751
|
|
736
752
|
async def _get_conn(self) -> asyncssh.SSHClientConnection:
|
@@ -54,6 +54,7 @@ class SFTPSensor(BaseSensorOperator):
|
|
54
54
|
|
55
55
|
template_fields: Sequence[str] = (
|
56
56
|
"path",
|
57
|
+
"file_pattern",
|
57
58
|
"newer_than",
|
58
59
|
)
|
59
60
|
|
@@ -67,6 +68,7 @@ class SFTPSensor(BaseSensorOperator):
|
|
67
68
|
python_callable: Callable | None = None,
|
68
69
|
op_args: list | None = None,
|
69
70
|
op_kwargs: dict[str, Any] | None = None,
|
71
|
+
use_managed_conn: bool = True,
|
70
72
|
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
|
71
73
|
**kwargs,
|
72
74
|
) -> None:
|
@@ -76,37 +78,37 @@ class SFTPSensor(BaseSensorOperator):
|
|
76
78
|
self.hook: SFTPHook | None = None
|
77
79
|
self.sftp_conn_id = sftp_conn_id
|
78
80
|
self.newer_than: datetime | str | None = newer_than
|
81
|
+
self.use_managed_conn = use_managed_conn
|
79
82
|
self.python_callable: Callable | None = python_callable
|
80
83
|
self.op_args = op_args or []
|
81
84
|
self.op_kwargs = op_kwargs or {}
|
82
85
|
self.deferrable = deferrable
|
83
86
|
|
84
|
-
def
|
85
|
-
|
86
|
-
|
87
|
-
files_found = []
|
87
|
+
def _get_files(self) -> list[str]:
|
88
|
+
files_from_pattern: list[str] = []
|
89
|
+
files_found: list[str] = []
|
88
90
|
|
89
91
|
if self.file_pattern:
|
90
|
-
files_from_pattern = self.hook.get_files_by_pattern(self.path, self.file_pattern)
|
92
|
+
files_from_pattern = self.hook.get_files_by_pattern(self.path, self.file_pattern) # type: ignore[union-attr]
|
91
93
|
if files_from_pattern:
|
92
94
|
actual_files_present = [
|
93
95
|
os.path.join(self.path, file_from_pattern) for file_from_pattern in files_from_pattern
|
94
96
|
]
|
95
97
|
else:
|
96
|
-
return
|
98
|
+
return files_found
|
97
99
|
else:
|
98
100
|
try:
|
99
101
|
# If a file is present, it is the single element added to the actual_files_present list to be
|
100
102
|
# processed. If the file is a directory, actual_file_present will be assigned an empty list,
|
101
103
|
# since SFTPHook.isfile(...) returns False
|
102
|
-
actual_files_present = [self.path] if self.hook.isfile(self.path) else []
|
104
|
+
actual_files_present = [self.path] if self.hook.isfile(self.path) else [] # type: ignore[union-attr]
|
103
105
|
except Exception as e:
|
104
106
|
raise AirflowException from e
|
105
107
|
|
106
108
|
if self.newer_than:
|
107
109
|
for actual_file_present in actual_files_present:
|
108
110
|
try:
|
109
|
-
mod_time = self.hook.get_mod_time(actual_file_present)
|
111
|
+
mod_time = self.hook.get_mod_time(actual_file_present) # type: ignore[union-attr]
|
110
112
|
self.log.info("Found File %s last modified: %s", actual_file_present, mod_time)
|
111
113
|
except OSError as e:
|
112
114
|
if e.errno != SFTP_NO_SUCH_FILE:
|
@@ -135,6 +137,19 @@ class SFTPSensor(BaseSensorOperator):
|
|
135
137
|
else:
|
136
138
|
files_found = actual_files_present
|
137
139
|
|
140
|
+
return files_found
|
141
|
+
|
142
|
+
def poke(self, context: Context) -> PokeReturnValue | bool:
|
143
|
+
self.hook = SFTPHook(self.sftp_conn_id, use_managed_conn=self.use_managed_conn)
|
144
|
+
|
145
|
+
self.log.info("Poking for %s, with pattern %s", self.path, self.file_pattern)
|
146
|
+
|
147
|
+
if self.use_managed_conn:
|
148
|
+
files_found = self._get_files()
|
149
|
+
else:
|
150
|
+
with self.hook.get_managed_conn():
|
151
|
+
files_found = self._get_files()
|
152
|
+
|
138
153
|
if not len(files_found):
|
139
154
|
return False
|
140
155
|
|
@@ -854,7 +854,7 @@ class TestSFTPHookAsync:
|
|
854
854
|
"username": "username",
|
855
855
|
"password": "password",
|
856
856
|
"client_keys": "~/keys/my_key",
|
857
|
-
"known_hosts":
|
857
|
+
"known_hosts": None,
|
858
858
|
"passphrase": "mypassphrase",
|
859
859
|
}
|
860
860
|
|
@@ -882,6 +882,7 @@ class TestSFTPHookAsync:
|
|
882
882
|
"username": "username",
|
883
883
|
"password": "password",
|
884
884
|
"client_keys": ["test"],
|
885
|
+
"known_hosts": None,
|
885
886
|
"passphrase": "mypassphrase",
|
886
887
|
}
|
887
888
|
|
@@ -97,6 +97,14 @@ class TestSFTPSensor:
|
|
97
97
|
sftp_hook_mock.return_value.close_conn.assert_not_called()
|
98
98
|
assert output
|
99
99
|
|
100
|
+
@patch("airflow.providers.sftp.sensors.sftp.SFTPHook")
|
101
|
+
def test_only_creating_one_connection_with_unmanaged_conn(self, sftp_hook_mock):
|
102
|
+
sftp_hook_mock.return_value.isfile.return_value = True
|
103
|
+
sftp_hook_mock.return_value.get_mod_time.return_value = "19700101000000"
|
104
|
+
sftp_sensor = SFTPSensor(task_id="test", path="path/to/whatever/test", use_managed_conn=False)
|
105
|
+
sftp_sensor.poke({})
|
106
|
+
assert sftp_hook_mock.return_value.get_managed_conn.called is True
|
107
|
+
|
100
108
|
@patch("airflow.providers.sftp.sensors.sftp.SFTPHook")
|
101
109
|
def test_file_not_new_enough(self, sftp_hook_mock):
|
102
110
|
sftp_hook_mock.return_value.isfile.return_value = True
|
@@ -193,9 +201,12 @@ class TestSFTPSensor:
|
|
193
201
|
)
|
194
202
|
context = {"ds": "1970-01-00"}
|
195
203
|
output = sftp_sensor.poke(context)
|
196
|
-
|
197
|
-
[
|
198
|
-
|
204
|
+
assert (
|
205
|
+
[
|
206
|
+
mock.call("/path/to/file/text_file1.txt"),
|
207
|
+
mock.call("/path/to/file/text_file2.txt"),
|
208
|
+
]
|
209
|
+
) in sftp_hook_mock.return_value.get_mod_time.mock_calls
|
199
210
|
sftp_hook_mock.return_value.close_conn.assert_not_called()
|
200
211
|
assert output
|
201
212
|
|
@@ -220,7 +231,7 @@ class TestSFTPSensor:
|
|
220
231
|
)
|
221
232
|
context = {"ds": "1970-01-00"}
|
222
233
|
output = sftp_sensor.poke(context)
|
223
|
-
sftp_hook_mock.return_value.get_mod_time.
|
234
|
+
sftp_hook_mock.return_value.get_mod_time.mock_call(
|
224
235
|
[
|
225
236
|
mock.call("/path/to/file/text_file1.txt"),
|
226
237
|
mock.call("/path/to/file/text_file2.txt"),
|
File without changes
|
{apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/docs/commits.rst
RENAMED
File without changes
|
{apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/docs/conf.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/docs/security.rst
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{apache_airflow_providers_sftp-5.3.4rc1 → apache_airflow_providers_sftp-5.4.0rc1}/tests/conftest.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|