apache-airflow-providers-snowflake 5.3.1rc1__py3-none-any.whl → 5.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of apache-airflow-providers-snowflake might be problematic. Click here for more details.

@@ -27,7 +27,7 @@ import packaging.version
27
27
 
28
28
  __all__ = ["__version__"]
29
29
 
30
- __version__ = "5.3.1"
30
+ __version__ = "5.4.0"
31
31
 
32
32
  try:
33
33
  from airflow import __version__ as airflow_version
@@ -28,8 +28,9 @@ def get_provider_info():
28
28
  "name": "Snowflake",
29
29
  "description": "`Snowflake <https://www.snowflake.com/>`__\n",
30
30
  "state": "ready",
31
- "source-date-epoch": 1707636525,
31
+ "source-date-epoch": 1712666514,
32
32
  "versions": [
33
+ "5.4.0",
33
34
  "5.3.1",
34
35
  "5.3.0",
35
36
  "5.2.1",
@@ -256,6 +256,15 @@ class SnowflakeHook(DbApiHook):
256
256
  conn_config["private_key"] = pkb
257
257
  conn_config.pop("password", None)
258
258
 
259
+ refresh_token = self._get_field(extra_dict, "refresh_token") or ""
260
+ if refresh_token:
261
+ conn_config["refresh_token"] = refresh_token
262
+ conn_config["authenticator"] = "oauth"
263
+ conn_config["client_id"] = conn.login
264
+ conn_config["client_secret"] = conn.password
265
+ conn_config.pop("login", None)
266
+ conn_config.pop("password", None)
267
+
259
268
  return conn_config
260
269
 
261
270
  def get_uri(self) -> str:
@@ -312,8 +321,7 @@ class SnowflakeHook(DbApiHook):
312
321
  split_statements: bool = ...,
313
322
  return_last: bool = ...,
314
323
  return_dictionaries: bool = ...,
315
- ) -> None:
316
- ...
324
+ ) -> None: ...
317
325
 
318
326
  @overload
319
327
  def run(
@@ -325,8 +333,7 @@ class SnowflakeHook(DbApiHook):
325
333
  split_statements: bool = ...,
326
334
  return_last: bool = ...,
327
335
  return_dictionaries: bool = ...,
328
- ) -> tuple | list[tuple] | list[list[tuple] | tuple] | None:
329
- ...
336
+ ) -> tuple | list[tuple] | list[list[tuple] | tuple] | None: ...
330
337
 
331
338
  def run(
332
339
  self,
@@ -341,10 +348,8 @@ class SnowflakeHook(DbApiHook):
341
348
  """Run a command or list of commands.
342
349
 
343
350
  Pass a list of SQL statements to the SQL parameter to get them to
344
- execute sequentially. The variable ``execution_info`` is returned so
345
- that it can be used in the Operators to modify the behavior depending on
346
- the result of the query (i.e fail the operator if the copy has processed
347
- 0 files).
351
+ execute sequentially. The result of the queries is returned if the
352
+ ``handler`` callable is set.
348
353
 
349
354
  :param sql: The SQL string to be executed with possibly multiple
350
355
  statements, or a list of sql statements to execute
@@ -25,6 +25,7 @@ import aiohttp
25
25
  import requests
26
26
  from cryptography.hazmat.backends import default_backend
27
27
  from cryptography.hazmat.primitives import serialization
28
+ from requests.auth import HTTPBasicAuth
28
29
 
29
30
  from airflow.exceptions import AirflowException
30
31
  from airflow.providers.snowflake.hooks.snowflake import SnowflakeHook
@@ -39,8 +40,9 @@ class SnowflakeSqlApiHook(SnowflakeHook):
39
40
  poll to check the status of the execution of a statement. Fetch query results asynchronously.
40
41
 
41
42
  This hook requires the snowflake_conn_id connection. This hooks mainly uses account, schema, database,
42
- warehouse, private_key_file or private_key_content field must be setup in the connection. Other inputs
43
- can be defined in the connection or hook instantiation.
43
+ warehouse, and an authentication mechanism from one of below:
44
+ 1. JWT Token generated from private_key_file or private_key_content. Other inputs can be defined in the connection or hook instantiation.
45
+ 2. OAuth Token generated from the refresh_token, client_id and client_secret specified in the connection
44
46
 
45
47
  :param snowflake_conn_id: Reference to
46
48
  :ref:`Snowflake connection id<howto/connection:snowflake>`
@@ -81,6 +83,17 @@ class SnowflakeSqlApiHook(SnowflakeHook):
81
83
  super().__init__(snowflake_conn_id, *args, **kwargs)
82
84
  self.private_key: Any = None
83
85
 
86
+ @property
87
+ def account_identifier(self) -> str:
88
+ """Returns snowflake account identifier."""
89
+ conn_config = self._get_conn_params()
90
+ account_identifier = f"https://{conn_config['account']}"
91
+
92
+ if conn_config["region"]:
93
+ account_identifier += f".{conn_config['region']}"
94
+
95
+ return account_identifier
96
+
84
97
  def get_private_key(self) -> None:
85
98
  """Get the private key from snowflake connection."""
86
99
  conn = self.get_connection(self.snowflake_conn_id)
@@ -137,10 +150,7 @@ class SnowflakeSqlApiHook(SnowflakeHook):
137
150
  conn_config = self._get_conn_params()
138
151
 
139
152
  req_id = uuid.uuid4()
140
- url = (
141
- f"https://{conn_config['account']}.{conn_config['region']}"
142
- f".snowflakecomputing.com/api/v2/statements"
143
- )
153
+ url = f"{self.account_identifier}.snowflakecomputing.com/api/v2/statements"
144
154
  params: dict[str, Any] | None = {"requestId": str(req_id), "async": True, "pageSize": 10}
145
155
  headers = self.get_headers()
146
156
  if bindings is None:
@@ -175,12 +185,27 @@ class SnowflakeSqlApiHook(SnowflakeHook):
175
185
  return self.query_ids
176
186
 
177
187
  def get_headers(self) -> dict[str, Any]:
178
- """Form JWT Token and header based on the private key, and connection details."""
188
+ """Form auth headers based on either OAuth token or JWT token from private key."""
189
+ conn_config = self._get_conn_params()
190
+
191
+ # Use OAuth if refresh_token and client_id and client_secret are provided
192
+ if all(
193
+ [conn_config.get("refresh_token"), conn_config.get("client_id"), conn_config.get("client_secret")]
194
+ ):
195
+ oauth_token = self.get_oauth_token()
196
+ headers = {
197
+ "Content-Type": "application/json",
198
+ "Authorization": f"Bearer {oauth_token}",
199
+ "Accept": "application/json",
200
+ "User-Agent": "snowflakeSQLAPI/1.0",
201
+ "X-Snowflake-Authorization-Token-Type": "OAUTH",
202
+ }
203
+ return headers
204
+
205
+ # Alternatively, get the JWT token from the connection details and the private key
179
206
  if not self.private_key:
180
207
  self.get_private_key()
181
- conn_config = self._get_conn_params()
182
208
 
183
- # Get the JWT token from the connection details and the private key
184
209
  token = JWTGenerator(
185
210
  conn_config["account"], # type: ignore[arg-type]
186
211
  conn_config["user"], # type: ignore[arg-type]
@@ -198,20 +223,41 @@ class SnowflakeSqlApiHook(SnowflakeHook):
198
223
  }
199
224
  return headers
200
225
 
226
+ def get_oauth_token(self) -> str:
227
+ """Generate temporary OAuth access token using refresh token in connection details."""
228
+ conn_config = self._get_conn_params()
229
+ url = f"{self.account_identifier}.snowflakecomputing.com/oauth/token-request"
230
+ data = {
231
+ "grant_type": "refresh_token",
232
+ "refresh_token": conn_config["refresh_token"],
233
+ "redirect_uri": conn_config.get("redirect_uri", "https://localhost.com"),
234
+ }
235
+ response = requests.post(
236
+ url,
237
+ data=data,
238
+ headers={
239
+ "Content-Type": "application/x-www-form-urlencoded",
240
+ },
241
+ auth=HTTPBasicAuth(conn_config["client_id"], conn_config["client_secret"]), # type: ignore[arg-type]
242
+ )
243
+
244
+ try:
245
+ response.raise_for_status()
246
+ except requests.exceptions.HTTPError as e: # pragma: no cover
247
+ msg = f"Response: {e.response.content.decode()} Status Code: {e.response.status_code}"
248
+ raise AirflowException(msg)
249
+ return response.json()["access_token"]
250
+
201
251
  def get_request_url_header_params(self, query_id: str) -> tuple[dict[str, Any], dict[str, Any], str]:
202
252
  """
203
253
  Build the request header Url with account name identifier and query id from the connection params.
204
254
 
205
255
  :param query_id: statement handles query ids for the individual statements.
206
256
  """
207
- conn_config = self._get_conn_params()
208
257
  req_id = uuid.uuid4()
209
258
  header = self.get_headers()
210
259
  params = {"requestId": str(req_id)}
211
- url = (
212
- f"https://{conn_config['account']}.{conn_config['region']}"
213
- f".snowflakecomputing.com/api/v2/statements/{query_id}"
214
- )
260
+ url = f"{self.account_identifier}.snowflakecomputing.com/api/v2/statements/{query_id}"
215
261
  return header, params, url
216
262
 
217
263
  def check_query_output(self, query_ids: list[str]) -> None:
@@ -449,7 +449,7 @@ class SnowflakeSqlApiOperator(SQLExecuteQueryOperator):
449
449
  When executing the statement, Snowflake replaces placeholders (? and :name) in
450
450
  the statement with these specified values.
451
451
  :param deferrable: Run operator in the deferrable mode.
452
- """ # noqa
452
+ """ # noqa: D205, D400
453
453
 
454
454
  LIFETIME = timedelta(minutes=59) # The tokens will have a 59 minutes lifetime
455
455
  RENEWAL_DELTA = timedelta(minutes=54) # Tokens will be renewed after 54 minutes
@@ -16,6 +16,7 @@
16
16
  # specific language governing permissions and limitations
17
17
  # under the License.
18
18
  """Abstract operator that child classes implement ``COPY INTO <TABLE> SQL in Snowflake``."""
19
+
19
20
  from __future__ import annotations
20
21
 
21
22
  from typing import Any, Sequence
@@ -254,8 +255,8 @@ class CopyFromExternalStageToSnowflakeOperator(BaseOperator):
254
255
  run_facets = {}
255
256
  if extraction_error_files:
256
257
  self.log.debug(
257
- f"Unable to extract Dataset namespace and name "
258
- f"for the following files: `{extraction_error_files}`."
258
+ "Unable to extract Dataset namespace and name for the following files: `%s`.",
259
+ extraction_error_files,
259
260
  )
260
261
  run_facets["extractionError"] = ExtractionErrorRunFacet(
261
262
  totalTasks=len(query_results),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: apache-airflow-providers-snowflake
3
- Version: 5.3.1rc1
3
+ Version: 5.4.0
4
4
  Summary: Provider package apache-airflow-providers-snowflake for Apache Airflow
5
5
  Keywords: airflow-provider,snowflake,airflow,integration
6
6
  Author-email: Apache Software Foundation <dev@airflow.apache.org>
@@ -19,16 +19,17 @@ Classifier: Programming Language :: Python :: 3.8
19
19
  Classifier: Programming Language :: Python :: 3.9
20
20
  Classifier: Programming Language :: Python :: 3.10
21
21
  Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
22
23
  Classifier: Topic :: System :: Monitoring
23
- Requires-Dist: apache-airflow-providers-common-sql>=1.10.0.dev0
24
- Requires-Dist: apache-airflow>=2.6.0.dev0
24
+ Requires-Dist: apache-airflow-providers-common-sql>=1.10.0
25
+ Requires-Dist: apache-airflow>=2.6.0
25
26
  Requires-Dist: snowflake-connector-python>=2.7.8
26
27
  Requires-Dist: snowflake-sqlalchemy>=1.1.0
27
28
  Requires-Dist: apache-airflow-providers-common-sql ; extra == "common.sql"
28
29
  Requires-Dist: apache-airflow-providers-openlineage ; extra == "openlineage"
29
30
  Project-URL: Bug Tracker, https://github.com/apache/airflow/issues
30
- Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-snowflake/5.3.1/changelog.html
31
- Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-snowflake/5.3.1
31
+ Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-snowflake/5.4.0/changelog.html
32
+ Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-snowflake/5.4.0
32
33
  Project-URL: Slack Chat, https://s.apache.org/airflow-slack
33
34
  Project-URL: Source Code, https://github.com/apache/airflow
34
35
  Project-URL: Twitter, https://twitter.com/ApacheAirflow
@@ -80,7 +81,7 @@ Provides-Extra: openlineage
80
81
 
81
82
  Package ``apache-airflow-providers-snowflake``
82
83
 
83
- Release: ``5.3.1.rc1``
84
+ Release: ``5.4.0``
84
85
 
85
86
 
86
87
  `Snowflake <https://www.snowflake.com/>`__
@@ -93,7 +94,7 @@ This is a provider package for ``snowflake`` provider. All classes for this prov
93
94
  are in ``airflow.providers.snowflake`` python package.
94
95
 
95
96
  You can find package information and changelog for the provider
96
- in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-snowflake/5.3.1/>`_.
97
+ in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-snowflake/5.4.0/>`_.
97
98
 
98
99
  Installation
99
100
  ------------
@@ -102,7 +103,7 @@ You can install this package on top of an existing Airflow 2 installation (see `
102
103
  for the minimum Airflow version supported) via
103
104
  ``pip install apache-airflow-providers-snowflake``
104
105
 
105
- The package supports the following python versions: 3.8,3.9,3.10,3.11
106
+ The package supports the following python versions: 3.8,3.9,3.10,3.11,3.12
106
107
 
107
108
  Requirements
108
109
  ------------
@@ -137,4 +138,4 @@ Dependent package
137
138
  ============================================================================================================== ===============
138
139
 
139
140
  The changelog for the provider package can be found in the
140
- `changelog <https://airflow.apache.org/docs/apache-airflow-providers-snowflake/5.3.1/changelog.html>`_.
141
+ `changelog <https://airflow.apache.org/docs/apache-airflow-providers-snowflake/5.4.0/changelog.html>`_.
@@ -1,19 +1,19 @@
1
1
  airflow/providers/snowflake/LICENSE,sha256=ywUBpKZc7Jb96rVt5I3IDbg7dIJAbUSHkuoDcF3jbH4,13569
2
- airflow/providers/snowflake/__init__.py,sha256=0Ltb9XwXTO8bBn-KCUd84KRldnXPXmVy1JHTie04zmc,1584
3
- airflow/providers/snowflake/get_provider_info.py,sha256=EDKGs3pfZhTomDdR-heB3J6wCUZJRhtF7ws2cIfLysM,4700
2
+ airflow/providers/snowflake/__init__.py,sha256=TaU_oNeaJZa6OMgMaNZR2zEfkbRylW_ZWaJ_UIo78-s,1584
3
+ airflow/providers/snowflake/get_provider_info.py,sha256=sVxscfHltpajNtnDlTjTIOnQ20SZU91TbztOHdKpHG0,4721
4
4
  airflow/providers/snowflake/hooks/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
5
- airflow/providers/snowflake/hooks/snowflake.py,sha256=p8En-0cyi0JFXVv20-z4ZlBESrnpmkd7pY1Ccd4hXJQ,21173
6
- airflow/providers/snowflake/hooks/snowflake_sql_api.py,sha256=b8RNT4cdBaWPO31OB6J0VDj-bEYl4TDgjq-Py3B9n7U,12701
5
+ airflow/providers/snowflake/hooks/snowflake.py,sha256=h0581Pn8Hc16CXKYOxg4EsnAkuJQrEA-Zx_MkVLdfSU,21418
6
+ airflow/providers/snowflake/hooks/snowflake_sql_api.py,sha256=sviTTshDIzqlCQCpw_J3OPQJjO8wa1_6JX8-U_goAw8,14773
7
7
  airflow/providers/snowflake/operators/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
8
- airflow/providers/snowflake/operators/snowflake.py,sha256=fX9rot7gXxAZ05h0dfvGcJt256_KtfnJLOLEOO4nTnU,26371
8
+ airflow/providers/snowflake/operators/snowflake.py,sha256=Evn8RfBTjLaSqrj7Dkhvu2-ewJoY6miTUFMiTTVd_dA,26383
9
9
  airflow/providers/snowflake/transfers/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
10
- airflow/providers/snowflake/transfers/copy_into_snowflake.py,sha256=-luuj72W3R7SzPXEjrX6waFGExenvma_GsrQ9KWezMA,12632
10
+ airflow/providers/snowflake/transfers/copy_into_snowflake.py,sha256=3ll6N0UhtjnwneSKiBRb9DVUDMX-uODftYccM8b5Bhs,12631
11
11
  airflow/providers/snowflake/triggers/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
12
12
  airflow/providers/snowflake/triggers/snowflake_trigger.py,sha256=YfMA7IXq3T7voUishTi171-8nIotPFzhYeFboSrHHAg,4223
13
13
  airflow/providers/snowflake/utils/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
14
14
  airflow/providers/snowflake/utils/common.py,sha256=DG-KLy2KpZWAqZqm_XIECm8lmdoUlzwkXv9onmkQThc,1644
15
15
  airflow/providers/snowflake/utils/sql_api_generate_jwt.py,sha256=9mR-vHIquv60tfAni87f6FAjKsiRHUDDrsVhzw4M9vM,6762
16
- apache_airflow_providers_snowflake-5.3.1rc1.dist-info/entry_points.txt,sha256=bCrl5J1PXUMzbgnrKYho61rkbL2gHRT4I6f_1jlxAX4,105
17
- apache_airflow_providers_snowflake-5.3.1rc1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
18
- apache_airflow_providers_snowflake-5.3.1rc1.dist-info/METADATA,sha256=cOymtTavLMwzJ_sHvCQZO4S95ZCd4x_xE2iCUjFQP68,6469
19
- apache_airflow_providers_snowflake-5.3.1rc1.dist-info/RECORD,,
16
+ apache_airflow_providers_snowflake-5.4.0.dist-info/entry_points.txt,sha256=bCrl5J1PXUMzbgnrKYho61rkbL2gHRT4I6f_1jlxAX4,105
17
+ apache_airflow_providers_snowflake-5.4.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
18
+ apache_airflow_providers_snowflake-5.4.0.dist-info/METADATA,sha256=QF3LKAo1GQ7Bp-aDGafZJv2dve-tsQp_h8-TTFXMDSc,6508
19
+ apache_airflow_providers_snowflake-5.4.0.dist-info/RECORD,,