apache-airflow-providers-common-sql 2.3.0__tar.gz → 2.4.0__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_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/PKG-INFO +8 -8
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/README.rst +4 -4
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/changelog.rst +12 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/index.rst +4 -4
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/operators.rst +15 -3
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/provider.yaml +2 -1
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/pyproject.toml +4 -4
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/__init__.py +1 -1
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/config.py +11 -8
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/datafusion/base.py +9 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/datafusion/engine.py +17 -6
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/datafusion/object_storage_provider.py +69 -1
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/datafusion/test_engine.py +92 -8
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/datafusion/test_object_storage_provider.py +153 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/operators/test_analytics.py +1 -1
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/test_config.py +2 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/.gitignore +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/LICENSE +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/NOTICE +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/.latest-doc-only-change.txt +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/commits.rst +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/conf.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/connections.rst +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/dataframes.rst +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/dialects.rst +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/installing-providers-from-sources.rst +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/integration-logos/sql.png +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/security.rst +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/supported-database-types.rst +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/README_API.md +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/datafusion/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/datafusion/exceptions.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/datafusion/format_handlers.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/decorators/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/decorators/analytics.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/decorators/sql.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/dialects/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/dialects/dialect.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/dialects/dialect.pyi +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/doc/adr/0001-record-architecture-decisions.md +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/doc/adr/0002-return-common-data-structure-from-dbapihook-derived-hooks.md +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/doc/adr/0003-introduce-notion-of-dialects-in-dbapihook.md +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/example_dags/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/example_dags/example_analytics.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/get_provider_info.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/get_provider_info.pyi +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/hooks/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/hooks/handlers.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/hooks/handlers.pyi +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/hooks/lineage.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/hooks/lineage.pyi +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/hooks/sql.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/hooks/sql.pyi +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/operators/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/operators/analytics.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/operators/generic_transfer.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/operators/generic_transfer.pyi +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/operators/sql.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/sensors/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/sensors/sql.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/sensors/sql.pyi +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/triggers/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/triggers/sql.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/triggers/sql.pyi +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/src/airflow/providers/common/sql/version_compat.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/conftest.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/example_generic_transfer.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/example_sql_bulk_load.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/example_sql_column_table_check.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/example_sql_execute_query.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/example_sql_insert_rows.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/example_sql_threshold_check.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/system/common/sql/example_sql_value_check.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/datafusion/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/datafusion/test_base.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/datafusion/test_exceptions.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/datafusion/test_format_handlers.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/decorators/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/decorators/test_analytics.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/decorators/test_sql.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/dialects/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/dialects/test_dialect.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/hooks/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/hooks/test_dbapi.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/hooks/test_handlers.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/hooks/test_lineage.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/hooks/test_sql.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/hooks/test_sqlparse.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/operators/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/operators/test_generic_transfer.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/operators/test_sql.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/operators/test_sql_execute.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/sensors/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/sensors/test_sql.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/triggers/__init__.py +0 -0
- {apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/tests/unit/common/sql/triggers/test_sql.py +0 -0
{apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: apache-airflow-providers-common-sql
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Provider package apache-airflow-providers-common-sql for Apache Airflow
|
|
5
5
|
Keywords: airflow-provider,common.sql,airflow,integration
|
|
6
6
|
Author-email: Apache Software Foundation <dev@airflow.apache.org>
|
|
@@ -23,7 +23,7 @@ Classifier: Topic :: System :: Monitoring
|
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
License-File: NOTICE
|
|
25
25
|
Requires-Dist: apache-airflow>=2.11.0
|
|
26
|
-
Requires-Dist: apache-airflow-providers-common-compat>=1.
|
|
26
|
+
Requires-Dist: apache-airflow-providers-common-compat>=1.23.0
|
|
27
27
|
Requires-Dist: sqlparse>=0.5.1
|
|
28
28
|
Requires-Dist: more-itertools>=9.0.0
|
|
29
29
|
Requires-Dist: methodtools>=0.4.7
|
|
@@ -38,8 +38,8 @@ Requires-Dist: polars>=1.26.0 ; extra == "polars"
|
|
|
38
38
|
Requires-Dist: pyiceberg-core>=0.8.0 ; extra == "pyiceberg-core"
|
|
39
39
|
Requires-Dist: sqlalchemy>=1.4.54,!=2.1.0 ; extra == "sqlalchemy"
|
|
40
40
|
Project-URL: Bug Tracker, https://github.com/apache/airflow/issues
|
|
41
|
-
Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.
|
|
42
|
-
Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.
|
|
41
|
+
Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0/changelog.html
|
|
42
|
+
Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0
|
|
43
43
|
Project-URL: Mastodon, https://fosstodon.org/@airflow
|
|
44
44
|
Project-URL: Slack Chat, https://s.apache.org/airflow-slack
|
|
45
45
|
Project-URL: Source Code, https://github.com/apache/airflow
|
|
@@ -82,7 +82,7 @@ Import-Namespace: airflow.providers.common
|
|
|
82
82
|
|
|
83
83
|
Package ``apache-airflow-providers-common-sql``
|
|
84
84
|
|
|
85
|
-
Release: ``2.
|
|
85
|
+
Release: ``2.4.0``
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
`Common SQL Provider <https://en.wikipedia.org/wiki/SQL>`__
|
|
@@ -95,7 +95,7 @@ This is a provider package for ``common.sql`` provider. All classes for this pro
|
|
|
95
95
|
are in ``airflow.providers.common.sql`` python package.
|
|
96
96
|
|
|
97
97
|
You can find package information and changelog for the provider
|
|
98
|
-
in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.
|
|
98
|
+
in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0/>`_.
|
|
99
99
|
|
|
100
100
|
Installation
|
|
101
101
|
------------
|
|
@@ -113,7 +113,7 @@ Requirements
|
|
|
113
113
|
PIP package Version required
|
|
114
114
|
========================================== ==================
|
|
115
115
|
``apache-airflow`` ``>=2.11.0``
|
|
116
|
-
``apache-airflow-providers-common-compat`` ``>=1.
|
|
116
|
+
``apache-airflow-providers-common-compat`` ``>=1.23.0``
|
|
117
117
|
``sqlparse`` ``>=0.5.1``
|
|
118
118
|
``more-itertools`` ``>=9.0.0``
|
|
119
119
|
``methodtools`` ``>=0.4.7``
|
|
@@ -157,5 +157,5 @@ Extra Dependencies
|
|
|
157
157
|
================== =======================================================================================================================================================================
|
|
158
158
|
|
|
159
159
|
The changelog for the provider package can be found in the
|
|
160
|
-
`changelog <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.
|
|
160
|
+
`changelog <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0/changelog.html>`_.
|
|
161
161
|
|
{apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/README.rst
RENAMED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
Package ``apache-airflow-providers-common-sql``
|
|
25
25
|
|
|
26
|
-
Release: ``2.
|
|
26
|
+
Release: ``2.4.0``
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
`Common SQL Provider <https://en.wikipedia.org/wiki/SQL>`__
|
|
@@ -36,7 +36,7 @@ This is a provider package for ``common.sql`` provider. All classes for this pro
|
|
|
36
36
|
are in ``airflow.providers.common.sql`` python package.
|
|
37
37
|
|
|
38
38
|
You can find package information and changelog for the provider
|
|
39
|
-
in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.
|
|
39
|
+
in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0/>`_.
|
|
40
40
|
|
|
41
41
|
Installation
|
|
42
42
|
------------
|
|
@@ -54,7 +54,7 @@ Requirements
|
|
|
54
54
|
PIP package Version required
|
|
55
55
|
========================================== ==================
|
|
56
56
|
``apache-airflow`` ``>=2.11.0``
|
|
57
|
-
``apache-airflow-providers-common-compat`` ``>=1.
|
|
57
|
+
``apache-airflow-providers-common-compat`` ``>=1.23.0``
|
|
58
58
|
``sqlparse`` ``>=0.5.1``
|
|
59
59
|
``more-itertools`` ``>=9.0.0``
|
|
60
60
|
``methodtools`` ``>=0.4.7``
|
|
@@ -98,4 +98,4 @@ Extra Dependencies
|
|
|
98
98
|
================== =======================================================================================================================================================================
|
|
99
99
|
|
|
100
100
|
The changelog for the provider package can be found in the
|
|
101
|
-
`changelog <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.
|
|
101
|
+
`changelog <https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0/changelog.html>`_.
|
|
@@ -25,6 +25,18 @@
|
|
|
25
25
|
Changelog
|
|
26
26
|
---------
|
|
27
27
|
|
|
28
|
+
2.4.0
|
|
29
|
+
.....
|
|
30
|
+
|
|
31
|
+
Features
|
|
32
|
+
~~~~~~~~
|
|
33
|
+
|
|
34
|
+
* ``Support 'abfs://' and 'abfss://' URIs in DataFusion Azure Blob Storage (#74228)``
|
|
35
|
+
|
|
36
|
+
.. Below changes are excluded from the changelog. Move them to
|
|
37
|
+
appropriate section above if needed. Do not delete the lines(!):
|
|
38
|
+
* ``Remove unused mypy suppressions in common providers (#75257)``
|
|
39
|
+
|
|
28
40
|
2.3.0
|
|
29
41
|
.....
|
|
30
42
|
|
|
@@ -79,7 +79,7 @@ apache-airflow-providers-common-sql package
|
|
|
79
79
|
`Common SQL Provider <https://en.wikipedia.org/wiki/SQL>`__
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
Release: 2.
|
|
82
|
+
Release: 2.4.0
|
|
83
83
|
|
|
84
84
|
Provider package
|
|
85
85
|
----------------
|
|
@@ -103,7 +103,7 @@ The minimum Apache Airflow version supported by this provider distribution is ``
|
|
|
103
103
|
PIP package Version required
|
|
104
104
|
========================================== ==================
|
|
105
105
|
``apache-airflow`` ``>=2.11.0``
|
|
106
|
-
``apache-airflow-providers-common-compat`` ``>=1.
|
|
106
|
+
``apache-airflow-providers-common-compat`` ``>=1.23.0``
|
|
107
107
|
``sqlparse`` ``>=0.5.1``
|
|
108
108
|
``more-itertools`` ``>=9.0.0``
|
|
109
109
|
``methodtools`` ``>=0.4.7``
|
|
@@ -160,5 +160,5 @@ Downloading official packages
|
|
|
160
160
|
You can download officially released packages and verify their checksums and signatures from the
|
|
161
161
|
`Official Apache Download site <https://downloads.apache.org/airflow/providers/>`_
|
|
162
162
|
|
|
163
|
-
* `The apache-airflow-providers-common-sql 2.
|
|
164
|
-
* `The apache-airflow-providers-common-sql 2.
|
|
163
|
+
* `The apache-airflow-providers-common-sql 2.4.0 sdist package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.4.0.tar.gz>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.4.0.tar.gz.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.4.0.tar.gz.sha512>`__)
|
|
164
|
+
* `The apache-airflow-providers-common-sql 2.4.0 wheel package <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.4.0-py3-none-any.whl>`_ (`asc <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.4.0-py3-none-any.whl.asc>`__, `sha512 <https://downloads.apache.org/airflow/providers/apache_airflow_providers_common_sql-2.4.0-py3-none-any.whl.sha512>`__)
|
|
@@ -365,10 +365,22 @@ resolved in this order:
|
|
|
365
365
|
|
|
366
366
|
Azure Storage
|
|
367
367
|
-------------
|
|
368
|
-
Use an ``az://`` URI with a ``conn_id`` pointing
|
|
369
|
-
|
|
368
|
+
Use an ``az://``, ``abfs://``, or ``abfss://`` URI with a ``conn_id`` pointing
|
|
369
|
+
to a ``wasb`` connection. ``az://container/path`` takes the storage account
|
|
370
|
+
from the connection; when the connection resolves none (no ``host``, and no ``login``
|
|
371
|
+
or a service principal), the binding falls back to ``AZURE_STORAGE_ACCOUNT_NAME`` on
|
|
372
|
+
the worker.
|
|
373
|
+
``abfs://container@account.dfs.core.windows.net/path`` and ``abfss://...``
|
|
374
|
+
additionally name the account in the URI itself -- if it disagrees with the
|
|
375
|
+
account the connection resolves to, registering the datasource raises rather
|
|
376
|
+
than silently picking one, and when the connection names no account at all, the
|
|
377
|
+
URI's account is used instead of ``AZURE_STORAGE_ACCOUNT_NAME``. The URI host
|
|
378
|
+
follows the same cloud rule as the connection host below: a host outside
|
|
379
|
+
``*.core.windows.net`` raises unless ``AZURE_STORAGE_ENDPOINT``/``AZURE_ENDPOINT``
|
|
380
|
+
is set. The account name
|
|
370
381
|
comes from ``host`` (its first DNS label) when set, falling back to
|
|
371
|
-
``login`` only when ``host`` is empty
|
|
382
|
+
``login`` only when ``host`` is empty and the connection is not a service principal
|
|
383
|
+
(``tenant_id`` set, where ``login`` is the client ID); only the public
|
|
372
384
|
``*.blob.core.windows.net`` cloud is supported, since DataFusion's binding
|
|
373
385
|
otherwise has no endpoint override -- unless the worker sets
|
|
374
386
|
``AZURE_STORAGE_ENDPOINT``/``AZURE_ENDPOINT`` for a real sovereign-cloud
|
|
@@ -23,12 +23,13 @@ description: |
|
|
|
23
23
|
|
|
24
24
|
state: ready
|
|
25
25
|
lifecycle: production
|
|
26
|
-
source-date-epoch:
|
|
26
|
+
source-date-epoch: 1792489528
|
|
27
27
|
# Note that those versions are maintained by release manager - do not update them manually
|
|
28
28
|
# with the exception of case where other provider in sources has >= new provider version.
|
|
29
29
|
# In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have
|
|
30
30
|
# to be done in the same PR
|
|
31
31
|
versions:
|
|
32
|
+
- 2.4.0
|
|
32
33
|
- 2.3.0
|
|
33
34
|
- 2.2.0
|
|
34
35
|
- 2.1.1
|
|
@@ -25,7 +25,7 @@ build-backend = "flit_core.buildapi"
|
|
|
25
25
|
|
|
26
26
|
[project]
|
|
27
27
|
name = "apache-airflow-providers-common-sql"
|
|
28
|
-
version = "2.
|
|
28
|
+
version = "2.4.0"
|
|
29
29
|
description = "Provider package apache-airflow-providers-common-sql for Apache Airflow"
|
|
30
30
|
readme = "README.rst"
|
|
31
31
|
license = "Apache-2.0"
|
|
@@ -59,7 +59,7 @@ requires-python = ">=3.11"
|
|
|
59
59
|
# After you modify the dependencies, and rebuild your Breeze CI image with ``breeze ci-image build``
|
|
60
60
|
dependencies = [
|
|
61
61
|
"apache-airflow>=2.11.0",
|
|
62
|
-
"apache-airflow-providers-common-compat>=1.
|
|
62
|
+
"apache-airflow-providers-common-compat>=1.23.0",
|
|
63
63
|
"sqlparse>=0.5.1",
|
|
64
64
|
"more-itertools>=9.0.0",
|
|
65
65
|
# The methodtools dependency is necessary since the introduction of dialects:
|
|
@@ -147,8 +147,8 @@ apache-airflow-providers-common-sql = {workspace = true}
|
|
|
147
147
|
apache-airflow-providers-standard = {workspace = true}
|
|
148
148
|
|
|
149
149
|
[project.urls]
|
|
150
|
-
"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.
|
|
151
|
-
"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.
|
|
150
|
+
"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0"
|
|
151
|
+
"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-common-sql/2.4.0/changelog.html"
|
|
152
152
|
"Bug Tracker" = "https://github.com/apache/airflow/issues"
|
|
153
153
|
"Source Code" = "https://github.com/apache/airflow"
|
|
154
154
|
"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__ = "2.
|
|
32
|
+
__version__ = "2.4.0"
|
|
33
33
|
|
|
34
34
|
if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
|
|
35
35
|
"2.11.0"
|
|
@@ -52,6 +52,14 @@ class StorageType(str, Enum):
|
|
|
52
52
|
LOCAL = "local"
|
|
53
53
|
|
|
54
54
|
|
|
55
|
+
STORAGE_TYPE_SCHEMES: dict[StorageType, tuple[str, ...]] = {
|
|
56
|
+
StorageType.S3: ("s3://",),
|
|
57
|
+
StorageType.GCS: ("gs://",),
|
|
58
|
+
StorageType.AZURE: ("az://", "abfs://", "abfss://"),
|
|
59
|
+
StorageType.LOCAL: ("file://",),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
55
63
|
@dataclass
|
|
56
64
|
class DataSourceConfig:
|
|
57
65
|
"""
|
|
@@ -114,12 +122,7 @@ class DataSourceConfig:
|
|
|
114
122
|
@property
|
|
115
123
|
def _extract_storage_type(self) -> StorageType | None:
|
|
116
124
|
"""Extract storage type."""
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return StorageType.GCS
|
|
121
|
-
if self.uri.startswith("az://"):
|
|
122
|
-
return StorageType.AZURE
|
|
123
|
-
if self.uri.startswith("file://"):
|
|
124
|
-
return StorageType.LOCAL
|
|
125
|
+
for storage_type, schemes in STORAGE_TYPE_SCHEMES.items():
|
|
126
|
+
if self.uri.startswith(schemes):
|
|
127
|
+
return storage_type
|
|
125
128
|
raise ValueError(f"Unsupported storage type for URI: {self.uri}")
|
|
@@ -50,6 +50,15 @@ class ObjectStorageProvider(LoggingMixin, ABC):
|
|
|
50
50
|
"""Return URL scheme for this storage type (e.g., 's3://', 'gs://')."""
|
|
51
51
|
raise NotImplementedError
|
|
52
52
|
|
|
53
|
+
def normalize_uri(self, uri: str) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Return the canonical ``uri`` to register under.
|
|
56
|
+
|
|
57
|
+
Default is identity; a provider overrides this when its URI shape differs from
|
|
58
|
+
DataFusion's (schema, host) registry key.
|
|
59
|
+
"""
|
|
60
|
+
return uri
|
|
61
|
+
|
|
53
62
|
def get_bucket(self, path: str) -> str | None:
|
|
54
63
|
"""Extract the bucket name from the given path."""
|
|
55
64
|
if path and path.startswith(self.get_scheme()):
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import os
|
|
20
|
+
from dataclasses import replace
|
|
20
21
|
from typing import TYPE_CHECKING, Any
|
|
21
22
|
from urllib.parse import parse_qsl, urlsplit
|
|
22
23
|
|
|
@@ -64,14 +65,15 @@ class DataFusionEngine(LoggingMixin):
|
|
|
64
65
|
else:
|
|
65
66
|
connection_config = self._get_connection_config(datasource_config.conn_id)
|
|
66
67
|
|
|
67
|
-
self._register_object_store(datasource_config, connection_config)
|
|
68
|
+
canonical_uri = self._register_object_store(datasource_config, connection_config)
|
|
69
|
+
datasource_config = replace(datasource_config, uri=canonical_uri)
|
|
68
70
|
|
|
69
71
|
self._register_data_source_format(datasource_config)
|
|
70
72
|
|
|
71
73
|
def _register_object_store(
|
|
72
74
|
self, datasource_config: DataSourceConfig, connection_config: ConnectionConfig | None
|
|
73
|
-
):
|
|
74
|
-
"""Register object
|
|
75
|
+
) -> str:
|
|
76
|
+
"""Register the object store and return the canonical URI to register the table under."""
|
|
75
77
|
if TYPE_CHECKING:
|
|
76
78
|
assert datasource_config.storage_type is not None
|
|
77
79
|
|
|
@@ -80,9 +82,14 @@ class DataFusionEngine(LoggingMixin):
|
|
|
80
82
|
object_store = storage_provider.create_object_store(
|
|
81
83
|
datasource_config.uri, connection_config=connection_config
|
|
82
84
|
)
|
|
85
|
+
canonical_uri = storage_provider.normalize_uri(datasource_config.uri)
|
|
83
86
|
schema = storage_provider.get_scheme()
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
# abfs(s) folds account+container into one host token, so the container-name host
|
|
88
|
+
# DataFusion defaults to when host is omitted would miss the registered store.
|
|
89
|
+
host = urlsplit(canonical_uri).netloc
|
|
90
|
+
self.session_context.register_object_store(schema=schema, store=object_store, host=host)
|
|
91
|
+
self.log.info("Registered object store for schema: %s host: %s", schema, host)
|
|
92
|
+
return canonical_uri
|
|
86
93
|
except Exception as e:
|
|
87
94
|
raise ObjectStoreCreationException(
|
|
88
95
|
f"Error while creating object store for {datasource_config.storage_type}: {e}"
|
|
@@ -240,6 +247,9 @@ class DataFusionEngine(LoggingMixin):
|
|
|
240
247
|
credentials.update(
|
|
241
248
|
{"client_id": conn.login, "client_secret": conn.password, "tenant_id": tenant_id}
|
|
242
249
|
)
|
|
250
|
+
if not conn.host:
|
|
251
|
+
# login holds the client_id here, so it cannot name the storage account.
|
|
252
|
+
credentials["account"] = None
|
|
243
253
|
credential_tier = "client_secret"
|
|
244
254
|
elif shared_access_key := _get_wasb_extra_field(extra_dejson, "shared_access_key"):
|
|
245
255
|
# Checked ahead of sas_token to match WasbHook.get_conn's precedence.
|
|
@@ -353,7 +363,8 @@ class DataFusionEngine(LoggingMixin):
|
|
|
353
363
|
|
|
354
364
|
From ``host`` (its netloc's first label) when set, falling back to ``login`` only when
|
|
355
365
|
``host`` is empty -- login holds the service-principal client_id otherwise, not the
|
|
356
|
-
account name
|
|
366
|
+
account name (the caller drops the login-derived account for service principals).
|
|
367
|
+
Returns ``None`` when neither is set, so the binding falls back to
|
|
357
368
|
``AZURE_STORAGE_ACCOUNT_NAME`` rather than the literal string ``"None"``. Reimplemented
|
|
358
369
|
locally instead of importing
|
|
359
370
|
``airflow.providers.microsoft.azure.utils.parse_blob_account_url``, to avoid pulling in
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
20
22
|
import tempfile
|
|
21
23
|
from pathlib import Path
|
|
22
24
|
|
|
@@ -107,6 +109,15 @@ class GCSObjectStorageProvider(ObjectStorageProvider):
|
|
|
107
109
|
return "gs://"
|
|
108
110
|
|
|
109
111
|
|
|
112
|
+
_ABFS_URI_RE = re.compile(
|
|
113
|
+
r"^abfss?://(?P<container>[^@/]+)@(?P<account>[^./]+)\.(?P<host>[^/]+)(?P<path>/.*)?$"
|
|
114
|
+
)
|
|
115
|
+
# MicrosoftAzure has no endpoint kwarg, so a non-public host only reaches the right cloud when
|
|
116
|
+
# AZURE_STORAGE_ENDPOINT/AZURE_ENDPOINT redirects the binding; same rule as _resolve_wasb_account.
|
|
117
|
+
_AZURE_PUBLIC_HOST_SUFFIX = ".core.windows.net"
|
|
118
|
+
_ABFS_SCHEMES = ("abfs://", "abfss://")
|
|
119
|
+
|
|
120
|
+
|
|
110
121
|
class AzureObjectStorageProvider(ObjectStorageProvider):
|
|
111
122
|
"""Azure Object Storage Provider using DataFusion's MicrosoftAzure."""
|
|
112
123
|
|
|
@@ -115,6 +126,48 @@ class AzureObjectStorageProvider(ObjectStorageProvider):
|
|
|
115
126
|
"""Return the storage type."""
|
|
116
127
|
return StorageType.AZURE
|
|
117
128
|
|
|
129
|
+
def get_bucket(self, path: str) -> str | None:
|
|
130
|
+
"""Extract the container name, from either the ``az://`` or ``abfs(s)://`` URI shape."""
|
|
131
|
+
if match := _ABFS_URI_RE.match(path):
|
|
132
|
+
return match.group("container")
|
|
133
|
+
if path.startswith(_ABFS_SCHEMES):
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"{path!r} does not match the required abfs(s)://<container>@<account>.<host>/<path> shape"
|
|
136
|
+
)
|
|
137
|
+
return super().get_bucket(path)
|
|
138
|
+
|
|
139
|
+
def _validate_uri_host(self, path: str) -> None:
|
|
140
|
+
match = _ABFS_URI_RE.match(path)
|
|
141
|
+
if not match:
|
|
142
|
+
return
|
|
143
|
+
host = match.group("host").lower()
|
|
144
|
+
if host == _AZURE_PUBLIC_HOST_SUFFIX[1:] or host.endswith(_AZURE_PUBLIC_HOST_SUFFIX):
|
|
145
|
+
return
|
|
146
|
+
if os.environ.get("AZURE_STORAGE_ENDPOINT") or os.environ.get("AZURE_ENDPOINT"):
|
|
147
|
+
return
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"URI {path!r} host {match.group('host')!r} is not a public *{_AZURE_PUBLIC_HOST_SUFFIX} "
|
|
150
|
+
"endpoint, and DataFusion's Azure Blob Storage binding has no endpoint override, so the "
|
|
151
|
+
"request would go to the public cloud. A sovereign cloud is supported once the "
|
|
152
|
+
"AZURE_STORAGE_ENDPOINT environment variable is set."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _get_uri_account(self, path: str) -> str | None:
|
|
156
|
+
match = _ABFS_URI_RE.match(path)
|
|
157
|
+
return match.group("account") if match else None
|
|
158
|
+
|
|
159
|
+
def normalize_uri(self, path: str) -> str:
|
|
160
|
+
"""
|
|
161
|
+
Rewrite ``abfs(s)://<container>@<account>.<host>/<path>`` to ``az://<account>.<container>/<path>``.
|
|
162
|
+
|
|
163
|
+
DataFusion's registry keys on (schema, host) alone, so container-only would collide for
|
|
164
|
+
two different accounts sharing a container name. Account and container names never
|
|
165
|
+
contain a dot, so joining on one is unambiguous. ``az://`` URIs pass through unchanged.
|
|
166
|
+
"""
|
|
167
|
+
if match := _ABFS_URI_RE.match(path):
|
|
168
|
+
return f"az://{match.group('account')}.{match.group('container')}{match.group('path') or ''}"
|
|
169
|
+
return path
|
|
170
|
+
|
|
118
171
|
def create_object_store(self, path: str, connection_config: ConnectionConfig | None = None):
|
|
119
172
|
"""Create an Azure object store using DataFusion's MicrosoftAzure."""
|
|
120
173
|
if connection_config is None:
|
|
@@ -124,6 +177,21 @@ class AzureObjectStorageProvider(ObjectStorageProvider):
|
|
|
124
177
|
credentials = connection_config.credentials
|
|
125
178
|
container = self.get_bucket(path)
|
|
126
179
|
|
|
180
|
+
self._validate_uri_host(path)
|
|
181
|
+
uri_account = self._get_uri_account(path)
|
|
182
|
+
resolved_account = credentials.get("account")
|
|
183
|
+
if uri_account and resolved_account and uri_account.lower() != resolved_account.lower():
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"URI {path!r} names storage account {uri_account!r}, but connection "
|
|
186
|
+
f"{connection_config.conn_id!r} resolves to account {resolved_account!r}. Point "
|
|
187
|
+
"the URI and the connection at the same account, or omit the account from one of "
|
|
188
|
+
"them."
|
|
189
|
+
)
|
|
190
|
+
if uri_account and not resolved_account:
|
|
191
|
+
# Pins the URI's account; MicrosoftAzure otherwise falls back to AZURE_STORAGE_ACCOUNT_NAME,
|
|
192
|
+
# which can name a different account.
|
|
193
|
+
credentials = {**credentials, "account": uri_account}
|
|
194
|
+
|
|
127
195
|
azure_store = MicrosoftAzure(container_name=container, **credentials)
|
|
128
196
|
self.log.info("Created Azure object store for container %s", container)
|
|
129
197
|
|
|
@@ -134,7 +202,7 @@ class AzureObjectStorageProvider(ObjectStorageProvider):
|
|
|
134
202
|
raise ObjectStoreCreationException(f"Failed to create Azure object store: {e}")
|
|
135
203
|
|
|
136
204
|
def get_scheme(self) -> str:
|
|
137
|
-
"""Return the scheme for Azure."""
|
|
205
|
+
"""Return the scheme for Azure; ``normalize_uri`` rewrites ``abfs(s)://`` URIs onto it."""
|
|
138
206
|
return "az://"
|
|
139
207
|
|
|
140
208
|
|
|
@@ -18,10 +18,12 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
import os
|
|
20
20
|
import tempfile
|
|
21
|
+
from pathlib import Path
|
|
21
22
|
from unittest.mock import MagicMock, patch
|
|
22
23
|
|
|
23
24
|
import pytest
|
|
24
25
|
from datafusion import SessionContext
|
|
26
|
+
from datafusion.object_store import LocalFileSystem
|
|
25
27
|
|
|
26
28
|
from airflow.models import Connection
|
|
27
29
|
from airflow.providers.common.sql.config import ConnectionConfig, DataSourceConfig
|
|
@@ -96,6 +98,7 @@ class TestDataFusionEngine:
|
|
|
96
98
|
mock_store = MagicMock()
|
|
97
99
|
mock_provider.create_object_store.return_value = mock_store
|
|
98
100
|
mock_provider.get_scheme.return_value = scheme
|
|
101
|
+
mock_provider.normalize_uri.side_effect = lambda uri: uri
|
|
99
102
|
mock_factory.return_value = mock_provider
|
|
100
103
|
|
|
101
104
|
engine = DataFusionEngine()
|
|
@@ -112,7 +115,9 @@ class TestDataFusionEngine:
|
|
|
112
115
|
mock_provider.create_object_store.assert_called_once_with(
|
|
113
116
|
f"{scheme}://bucket/path", connection_config=mock_get_conn.return_value
|
|
114
117
|
)
|
|
115
|
-
engine.df_ctx.register_object_store.assert_called_once_with(
|
|
118
|
+
engine.df_ctx.register_object_store.assert_called_once_with(
|
|
119
|
+
schema=scheme, store=mock_store, host="bucket"
|
|
120
|
+
)
|
|
116
121
|
|
|
117
122
|
if format == "parquet":
|
|
118
123
|
engine.df_ctx.register_parquet.assert_called_once_with("test_table", f"{scheme}://bucket/path")
|
|
@@ -208,8 +213,10 @@ class TestDataFusionEngine:
|
|
|
208
213
|
with pytest.raises(QueryExecutionException, match="Error while executing query"):
|
|
209
214
|
engine.execute_query("SELECT * FROM test_table")
|
|
210
215
|
|
|
216
|
+
@pytest.mark.parametrize("uri_prefix", ["file://", ""], ids=["file-uri", "bare-path"])
|
|
211
217
|
@patch.object(DataFusionEngine, "_get_connection_config")
|
|
212
|
-
def test_execute_query_with_local_csv(self, mock_get_conn):
|
|
218
|
+
def test_execute_query_with_local_csv(self, mock_get_conn, uri_prefix):
|
|
219
|
+
"""A bare path (no file:// prefix) is accepted with an explicit storage_type=StorageType.LOCAL."""
|
|
213
220
|
mock_get_conn.return_value = None
|
|
214
221
|
|
|
215
222
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f:
|
|
@@ -220,7 +227,7 @@ class TestDataFusionEngine:
|
|
|
220
227
|
engine = DataFusionEngine()
|
|
221
228
|
datasource_config = DataSourceConfig(
|
|
222
229
|
table_name="test_csv",
|
|
223
|
-
uri=f"
|
|
230
|
+
uri=f"{uri_prefix}{csv_path}",
|
|
224
231
|
format="csv",
|
|
225
232
|
storage_type="local",
|
|
226
233
|
conn_id="",
|
|
@@ -230,11 +237,87 @@ class TestDataFusionEngine:
|
|
|
230
237
|
|
|
231
238
|
result = engine.execute_query("SELECT * FROM test_csv ORDER BY name")
|
|
232
239
|
|
|
233
|
-
|
|
234
|
-
|
|
240
|
+
assert result == {"name": ["Alice", "Bob"], "age": [30, 25]}
|
|
241
|
+
finally:
|
|
242
|
+
os.unlink(csv_path)
|
|
243
|
+
|
|
244
|
+
@patch.object(DataFusionEngine, "_get_connection_config")
|
|
245
|
+
def test_execute_query_with_abfs_uri_matches_real_registry(self, mock_get_conn):
|
|
246
|
+
"""Only object-store construction is mocked; normalize_uri and DataFusion's real
|
|
247
|
+
registry run live, so a missing store surfaces as "No suitable object store found"."""
|
|
248
|
+
mock_get_conn.return_value = None
|
|
249
|
+
|
|
250
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f:
|
|
251
|
+
f.write("name,age\nAlice,30\nBob,25\n")
|
|
252
|
+
csv_path = f.name
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
engine = DataFusionEngine()
|
|
256
|
+
datasource_config = DataSourceConfig(
|
|
257
|
+
table_name="abfs_csv",
|
|
258
|
+
uri=f"abfss://demo-container@myaccount.dfs.core.windows.net{csv_path}",
|
|
259
|
+
format="csv",
|
|
260
|
+
conn_id="wasb_default",
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
with patch(
|
|
264
|
+
"airflow.providers.common.sql.datafusion.object_storage_provider"
|
|
265
|
+
".AzureObjectStorageProvider.create_object_store",
|
|
266
|
+
autospec=True,
|
|
267
|
+
return_value=LocalFileSystem(),
|
|
268
|
+
):
|
|
269
|
+
engine.register_datasource(datasource_config)
|
|
270
|
+
|
|
271
|
+
result = engine.execute_query("SELECT * FROM abfs_csv ORDER BY name")
|
|
272
|
+
|
|
273
|
+
assert result == {"name": ["Alice", "Bob"], "age": [30, 25]}
|
|
274
|
+
assert datasource_config.uri == f"abfss://demo-container@myaccount.dfs.core.windows.net{csv_path}"
|
|
275
|
+
assert engine.registered_tables["abfs_csv"] == f"az://myaccount.demo-container{csv_path}"
|
|
235
276
|
finally:
|
|
236
277
|
os.unlink(csv_path)
|
|
237
278
|
|
|
279
|
+
@pytest.mark.parametrize(
|
|
280
|
+
("uri_a", "uri_b"),
|
|
281
|
+
[
|
|
282
|
+
pytest.param(
|
|
283
|
+
"abfss://bronze@myaccount.dfs.core.windows.net/data.csv",
|
|
284
|
+
"abfss://silver@myaccount.dfs.core.windows.net/data.csv",
|
|
285
|
+
id="same-account-different-containers",
|
|
286
|
+
),
|
|
287
|
+
pytest.param(
|
|
288
|
+
"abfss://data@devacct.dfs.core.windows.net/data.csv",
|
|
289
|
+
"abfss://data@prodacct.dfs.core.windows.net/data.csv",
|
|
290
|
+
id="same-container-different-accounts",
|
|
291
|
+
),
|
|
292
|
+
],
|
|
293
|
+
)
|
|
294
|
+
@patch.object(DataFusionEngine, "_get_connection_config")
|
|
295
|
+
def test_execute_query_with_shared_abfs_identifiers_reads_each_store(self, mock_get_conn, uri_a, uri_b):
|
|
296
|
+
mock_get_conn.return_value = None
|
|
297
|
+
|
|
298
|
+
with (
|
|
299
|
+
tempfile.TemporaryDirectory() as dir_a,
|
|
300
|
+
tempfile.TemporaryDirectory() as dir_b,
|
|
301
|
+
):
|
|
302
|
+
(Path(dir_a) / "data.csv").write_text("name,age\nAlice,30\n")
|
|
303
|
+
(Path(dir_b) / "data.csv").write_text("name,age\nBob,25\n")
|
|
304
|
+
|
|
305
|
+
engine = DataFusionEngine()
|
|
306
|
+
config_a = DataSourceConfig(table_name="table_a", uri=uri_a, format="csv", conn_id="wasb_a")
|
|
307
|
+
config_b = DataSourceConfig(table_name="table_b", uri=uri_b, format="csv", conn_id="wasb_b")
|
|
308
|
+
|
|
309
|
+
with patch(
|
|
310
|
+
"airflow.providers.common.sql.datafusion.object_storage_provider"
|
|
311
|
+
".AzureObjectStorageProvider.create_object_store",
|
|
312
|
+
autospec=True,
|
|
313
|
+
side_effect=[LocalFileSystem(prefix=dir_a), LocalFileSystem(prefix=dir_b)],
|
|
314
|
+
):
|
|
315
|
+
engine.register_datasource(config_a)
|
|
316
|
+
engine.register_datasource(config_b)
|
|
317
|
+
|
|
318
|
+
assert engine.execute_query("SELECT * FROM table_a") == {"name": ["Alice"], "age": [30]}
|
|
319
|
+
assert engine.execute_query("SELECT * FROM table_b") == {"name": ["Bob"], "age": [25]}
|
|
320
|
+
|
|
238
321
|
@patch("airflow.providers.common.sql.datafusion.engine.get_object_storage_provider", autospec=True)
|
|
239
322
|
@patch.object(DataFusionEngine, "_get_connection_config")
|
|
240
323
|
def test_register_datasource_with_options(self, mock_get_conn, mock_factory):
|
|
@@ -243,6 +326,7 @@ class TestDataFusionEngine:
|
|
|
243
326
|
mock_store = MagicMock()
|
|
244
327
|
mock_provider.create_object_store.return_value = mock_store
|
|
245
328
|
mock_provider.get_scheme.return_value = "s3"
|
|
329
|
+
mock_provider.normalize_uri.side_effect = lambda uri: uri
|
|
246
330
|
mock_factory.return_value = mock_provider
|
|
247
331
|
|
|
248
332
|
engine = DataFusionEngine()
|
|
@@ -263,7 +347,9 @@ class TestDataFusionEngine:
|
|
|
263
347
|
mock_provider.create_object_store.assert_called_once_with(
|
|
264
348
|
"s3://bucket/path/", connection_config=mock_get_conn.return_value
|
|
265
349
|
)
|
|
266
|
-
engine.df_ctx.register_object_store.assert_called_once_with(
|
|
350
|
+
engine.df_ctx.register_object_store.assert_called_once_with(
|
|
351
|
+
schema="s3", store=mock_store, host="bucket"
|
|
352
|
+
)
|
|
267
353
|
|
|
268
354
|
engine.df_ctx.register_parquet.assert_called_once_with(
|
|
269
355
|
"test_table",
|
|
@@ -430,7 +516,6 @@ class TestDataFusionEngine:
|
|
|
430
516
|
credentials, extra_config = engine._get_credentials(mock_conn)
|
|
431
517
|
|
|
432
518
|
assert credentials == {
|
|
433
|
-
"account": "client-id",
|
|
434
519
|
"client_id": "client-id",
|
|
435
520
|
"client_secret": "client-secret",
|
|
436
521
|
"tenant_id": "tenant-id",
|
|
@@ -748,7 +833,6 @@ class TestDataFusionEngine:
|
|
|
748
833
|
else:
|
|
749
834
|
credentials, extra_config = engine._get_credentials(mock_conn)
|
|
750
835
|
assert credentials == {
|
|
751
|
-
"account": "client-id",
|
|
752
836
|
"client_id": "client-id",
|
|
753
837
|
"client_secret": "client-secret",
|
|
754
838
|
"tenant_id": "tenant-id",
|
|
@@ -159,6 +159,136 @@ class TestObjectStorageProvider:
|
|
|
159
159
|
assert provider.get_storage_type == StorageType.AZURE
|
|
160
160
|
assert provider.get_scheme() == "az://"
|
|
161
161
|
|
|
162
|
+
@pytest.mark.parametrize(
|
|
163
|
+
"uri",
|
|
164
|
+
[
|
|
165
|
+
"abfs://demo-container@myaccount.dfs.core.windows.net/path",
|
|
166
|
+
"abfss://demo-container@myaccount.dfs.core.windows.net/path",
|
|
167
|
+
"abfss://demo-container@myaccount.blob.core.windows.net/path",
|
|
168
|
+
"abfss://demo-container@myaccount.DFS.Core.Windows.Net/path",
|
|
169
|
+
],
|
|
170
|
+
)
|
|
171
|
+
@patch("airflow.providers.common.sql.datafusion.object_storage_provider.MicrosoftAzure")
|
|
172
|
+
def test_azure_provider_success_with_abfs_uri(self, mock_azure, uri, monkeypatch):
|
|
173
|
+
# Without these, a public-cloud host check that is too strict would still pass.
|
|
174
|
+
monkeypatch.delenv("AZURE_STORAGE_ENDPOINT", raising=False)
|
|
175
|
+
monkeypatch.delenv("AZURE_ENDPOINT", raising=False)
|
|
176
|
+
provider = AzureObjectStorageProvider()
|
|
177
|
+
connection_config = ConnectionConfig(
|
|
178
|
+
conn_id="wasb_default",
|
|
179
|
+
credentials={"account": "myaccount", "access_key": "fake_key"},
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
store = provider.create_object_store(uri, connection_config)
|
|
183
|
+
|
|
184
|
+
mock_azure.assert_called_once_with(
|
|
185
|
+
container_name="demo-container", account="myaccount", access_key="fake_key"
|
|
186
|
+
)
|
|
187
|
+
assert store == mock_azure.return_value
|
|
188
|
+
assert provider.get_bucket(uri) == "demo-container"
|
|
189
|
+
|
|
190
|
+
@patch("airflow.providers.common.sql.datafusion.object_storage_provider.MicrosoftAzure")
|
|
191
|
+
def test_azure_provider_abfs_uri_account_mismatch_raises(self, mock_azure):
|
|
192
|
+
provider = AzureObjectStorageProvider()
|
|
193
|
+
connection_config = ConnectionConfig(
|
|
194
|
+
conn_id="wasb_default",
|
|
195
|
+
credentials={"account": "connection-account", "access_key": "fake_key"},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
with pytest.raises(ObjectStoreCreationException, match="names storage account 'uri-account'"):
|
|
199
|
+
provider.create_object_store(
|
|
200
|
+
"abfss://demo-container@uri-account.dfs.core.windows.net/path", connection_config
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
mock_azure.assert_not_called()
|
|
204
|
+
|
|
205
|
+
@pytest.mark.parametrize(
|
|
206
|
+
"uri",
|
|
207
|
+
[
|
|
208
|
+
"abfs://demo-container/path", # missing @account entirely
|
|
209
|
+
"abfss://demo-container@/path", # empty account
|
|
210
|
+
],
|
|
211
|
+
)
|
|
212
|
+
@patch("airflow.providers.common.sql.datafusion.object_storage_provider.MicrosoftAzure")
|
|
213
|
+
def test_azure_provider_malformed_abfs_uri_raises_clear_error(self, mock_azure, uri):
|
|
214
|
+
provider = AzureObjectStorageProvider()
|
|
215
|
+
connection_config = ConnectionConfig(
|
|
216
|
+
conn_id="wasb_default",
|
|
217
|
+
credentials={"account": "myaccount", "access_key": "fake_key"},
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
with pytest.raises(ObjectStoreCreationException, match="does not match the required"):
|
|
221
|
+
provider.create_object_store(uri, connection_config)
|
|
222
|
+
|
|
223
|
+
mock_azure.assert_not_called()
|
|
224
|
+
|
|
225
|
+
@patch("airflow.providers.common.sql.datafusion.object_storage_provider.MicrosoftAzure")
|
|
226
|
+
def test_azure_provider_abfs_uri_account_used_when_connection_resolves_none(self, mock_azure):
|
|
227
|
+
provider = AzureObjectStorageProvider()
|
|
228
|
+
connection_config = ConnectionConfig(
|
|
229
|
+
conn_id="wasb_default",
|
|
230
|
+
credentials={"access_key": "fake_key"},
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
provider.create_object_store(
|
|
234
|
+
"abfss://demo-container@uri-account.dfs.core.windows.net/path", connection_config
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
mock_azure.assert_called_once_with(
|
|
238
|
+
container_name="demo-container", account="uri-account", access_key="fake_key"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
@pytest.mark.parametrize("account", [None, "usgovacct"])
|
|
242
|
+
@patch("airflow.providers.common.sql.datafusion.object_storage_provider.MicrosoftAzure")
|
|
243
|
+
def test_azure_provider_abfs_uri_sovereign_host_without_endpoint_raises(
|
|
244
|
+
self, mock_azure, account, monkeypatch
|
|
245
|
+
):
|
|
246
|
+
monkeypatch.delenv("AZURE_STORAGE_ENDPOINT", raising=False)
|
|
247
|
+
monkeypatch.delenv("AZURE_ENDPOINT", raising=False)
|
|
248
|
+
credentials = {"access_key": "fake_key"}
|
|
249
|
+
if account:
|
|
250
|
+
credentials["account"] = account
|
|
251
|
+
connection_config = ConnectionConfig(conn_id="wasb_default", credentials=credentials)
|
|
252
|
+
|
|
253
|
+
with pytest.raises(ObjectStoreCreationException, match="no endpoint override"):
|
|
254
|
+
AzureObjectStorageProvider().create_object_store(
|
|
255
|
+
"abfss://demo-container@usgovacct.dfs.core.usgovcloudapi.net/path", connection_config
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
mock_azure.assert_not_called()
|
|
259
|
+
|
|
260
|
+
@pytest.mark.parametrize("env_var", ["AZURE_STORAGE_ENDPOINT", "AZURE_ENDPOINT"])
|
|
261
|
+
@patch("airflow.providers.common.sql.datafusion.object_storage_provider.MicrosoftAzure")
|
|
262
|
+
def test_azure_provider_abfs_uri_sovereign_host_with_endpoint_env_passes(
|
|
263
|
+
self, mock_azure, env_var, monkeypatch
|
|
264
|
+
):
|
|
265
|
+
monkeypatch.setenv(env_var, "https://usgovacct.blob.core.usgovcloudapi.net")
|
|
266
|
+
connection_config = ConnectionConfig(conn_id="wasb_default", credentials={"access_key": "fake_key"})
|
|
267
|
+
|
|
268
|
+
AzureObjectStorageProvider().create_object_store(
|
|
269
|
+
"abfss://demo-container@usgovacct.dfs.core.usgovcloudapi.net/path", connection_config
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
mock_azure.assert_called_once_with(
|
|
273
|
+
container_name="demo-container", account="usgovacct", access_key="fake_key"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
@patch("airflow.providers.common.sql.datafusion.object_storage_provider.MicrosoftAzure")
|
|
277
|
+
def test_azure_provider_abfs_uri_account_matches_case_insensitively(self, mock_azure):
|
|
278
|
+
provider = AzureObjectStorageProvider()
|
|
279
|
+
connection_config = ConnectionConfig(
|
|
280
|
+
conn_id="wasb_default",
|
|
281
|
+
credentials={"account": "MyAccount", "access_key": "fake_key"},
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
provider.create_object_store(
|
|
285
|
+
"abfss://demo-container@myaccount.dfs.core.windows.net/path", connection_config
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
mock_azure.assert_called_once_with(
|
|
289
|
+
container_name="demo-container", account="MyAccount", access_key="fake_key"
|
|
290
|
+
)
|
|
291
|
+
|
|
162
292
|
def test_azure_provider_failure(self):
|
|
163
293
|
provider = AzureObjectStorageProvider()
|
|
164
294
|
connection_config = ConnectionConfig(conn_id="wasb_default")
|
|
@@ -176,6 +306,29 @@ class TestObjectStorageProvider:
|
|
|
176
306
|
with pytest.raises(ValueError, match="connection_config must be provided for azure"):
|
|
177
307
|
provider.create_object_store("az://demo-container/path")
|
|
178
308
|
|
|
309
|
+
@pytest.mark.parametrize(
|
|
310
|
+
("uri", "expected"),
|
|
311
|
+
[
|
|
312
|
+
(
|
|
313
|
+
"abfs://demo-container@myaccount.dfs.core.windows.net/path/to/data.parquet",
|
|
314
|
+
"az://myaccount.demo-container/path/to/data.parquet",
|
|
315
|
+
),
|
|
316
|
+
(
|
|
317
|
+
"abfss://demo-container@myaccount.dfs.core.windows.net/path/to/data.parquet",
|
|
318
|
+
"az://myaccount.demo-container/path/to/data.parquet",
|
|
319
|
+
),
|
|
320
|
+
("abfss://demo-container@myaccount.dfs.core.windows.net", "az://myaccount.demo-container"),
|
|
321
|
+
("az://demo-container/path", "az://demo-container/path"),
|
|
322
|
+
# Same container name on different accounts maps to distinct hosts.
|
|
323
|
+
("abfss://data@devacct.dfs.core.windows.net/data.csv", "az://devacct.data/data.csv"),
|
|
324
|
+
("abfss://data@prodacct.dfs.core.windows.net/data.csv", "az://prodacct.data/data.csv"),
|
|
325
|
+
],
|
|
326
|
+
)
|
|
327
|
+
def test_azure_provider_normalize_uri(self, uri, expected):
|
|
328
|
+
provider = AzureObjectStorageProvider()
|
|
329
|
+
|
|
330
|
+
assert provider.normalize_uri(uri) == expected
|
|
331
|
+
|
|
179
332
|
def test_azure_provider_partial_service_principal_raises_clear_error(self):
|
|
180
333
|
"""Uses the real MicrosoftAzure binding, not a mock, since it's the one that panics
|
|
181
334
|
on a partial client_id/client_secret/tenant_id combination."""
|
|
@@ -126,7 +126,7 @@ class TestAnalyticsOperator:
|
|
|
126
126
|
datasource_configs=[datasource_config],
|
|
127
127
|
queries=["SELECT * FROM users_data"],
|
|
128
128
|
engine=mock_engine,
|
|
129
|
-
result_output_format=["invalid"],
|
|
129
|
+
result_output_format=["invalid"],
|
|
130
130
|
)
|
|
131
131
|
|
|
132
132
|
with pytest.raises(ValueError, match="Unsupported output format"):
|
|
@@ -35,6 +35,8 @@ class TestDataSourceConfig:
|
|
|
35
35
|
("s3://bucket/path", StorageType.S3),
|
|
36
36
|
("gs://bucket/path", StorageType.GCS),
|
|
37
37
|
("az://container/path", StorageType.AZURE),
|
|
38
|
+
("abfs://container@account.dfs.core.windows.net/path", StorageType.AZURE),
|
|
39
|
+
("abfss://container@account.dfs.core.windows.net/path", StorageType.AZURE),
|
|
38
40
|
("file:///path/to/file", StorageType.LOCAL),
|
|
39
41
|
],
|
|
40
42
|
)
|
{apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/.gitignore
RENAMED
|
File without changes
|
{apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/LICENSE
RENAMED
|
File without changes
|
{apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/NOTICE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{apache_airflow_providers_common_sql-2.3.0 → apache_airflow_providers_common_sql-2.4.0}/docs/conf.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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|