Skip to content

Commit c238333

Browse files
Merge branch 'main' into SNOW-2043523-windows-313-support
# Conflicts: # DESCRIPTION.md
2 parents 3977689 + b866808 commit c238333

30 files changed

+633
-173
lines changed

DESCRIPTION.md

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,41 @@ https://docs.snowflake.com/
77
Source code is also available at: https://github.com/snowflakedb/snowflake-connector-python
88

99
# Release Notes
10-
- v3.14.1(TBD)
10+
- v3.16(TBD)
11+
- Added basic arrow support for Interval types.
12+
13+
- v3.15.0(Apr 29,2025)
14+
- Bumped up min boto and botocore version to 1.24.
15+
- OCSP: terminate certificates chain traversal if a trusted certificate already reached.
16+
- Added new authentication methods support for programmatic access tokens (PATs), OAuth 2.0 Authorization Code Flow, OAuth 2.0 Client Credentials Flow, and OAuth Token caching.
17+
- For OAuth 2.0 Authorization Code Flow:
18+
- Added the `oauth_client_id`, `oauth_client_secret`, `oauth_authorization_url`, `oauth_token_request_url`, `oauth_redirect_uri`, `oauth_scope`, `oauth_disable_pkce`, `oauth_enable_refresh_tokens` and `oauth_enable_single_use_refresh_tokens` parameters.
19+
- Added the `OAUTH_AUTHORIZATION_CODE` value for the parameter authenticator.
20+
- For OAuth 2.0 Client Credentials Flow:
21+
- Added the `oauth_client_id`, `oauth_client_secret`, `oauth_token_request_url`, and `oauth_scope` parameters.
22+
- Added the `OAUTH_CLIENT_CREDENTIALS` value for the parameter authenticator.
23+
- For OAuth Token caching: Passing a username to driver configuration is required, and the `client_store_temporary_credential property` is to be set to `true`.
24+
- Bumped numpy dependency from <2.1.0 to <2.2.4
25+
- Added Windows support for Python 3.13.
26+
27+
- v3.14.1(April 21, 2025)
1128
- Added support for Python 3.13.
29+
- NOTE: Windows 64 support is still experimental and should not yet be used for production environments.
1230
- Dropped support for Python 3.8.
13-
- Basic decimal floating-point type support.
14-
- Added handling of PAT provided in `password` field.
15-
- Added experimental support for OAuth authorization code and client credentials flows.
16-
- Improved error message for client-side query cancellations due to timeouts.
31+
- Added basic decimal floating-point type support.
32+
- Added experimental authentication methods.
1733
- Added support of GCS regional endpoints.
18-
- Fixed a bug that caused driver to fail silently on `TO_DATE` arrow to python conversion when invalid date was followed by the correct one.
34+
- Added support of GCS virtual urls. See more: https://cloud.google.com/storage/docs/request-endpoints#xml-api
35+
- Added `client_fetch_threads` experimental parameter to better utilize threads for fetching query results.
1936
- Added `check_arrow_conversion_error_on_every_column` connection property that can be set to `False` to restore previous behaviour in which driver will ignore errors until it occurs in the last column. This flag's purpose is to unblock workflows that may be impacted by the bugfix and will be removed in later releases.
20-
- Lower log levels from info to debug for some of the messages to make the output easier to follow.
21-
- Allow the connector to inherit a UUID4 generated upstream, provided in statement parameters (field: `requestId`), rather than automatically generate a UUID4 to use for the HTTP Request ID.
37+
- Lowered log levels from info to debug for some of the messages to make the output easier to follow.
38+
- Allowed the connector to inherit a UUID4 generated upstream, provided in statement parameters (field: `requestId`), rather than automatically generate a UUID4 to use for the HTTP Request ID.
2239
- Improved logging in urllib3, boto3, botocore - assured data masking even after migration to the external owned library in the future.
23-
- Fix expired S3 credentials update and increment retry when expired credentials are found.
24-
- Added `client_fetch_threads` experimental parameter to better utilize threads for fetching query results.
25-
- Added support of GCS virtual urls. See more: https://cloud.google.com/storage/docs/request-endpoints#xml-api
26-
- Bumped numpy dependency from <2.1.0 to <2.2.4
40+
- Improved error message for client-side query cancellations due to timeouts.
41+
- Improved security and robustness for the temporary credentials cache storage.
42+
- Fixed a bug that caused driver to fail silently on `TO_DATE` arrow to python conversion when invalid date was followed by the correct one.
43+
- Fixed expired S3 credentials update and increment retry when expired credentials are found.
44+
- Deprecated `insecure_mode` connection property and replaced it with `disable_ocsp_checks` with the same behavior as the former property.
2745

2846
- v3.14.0(March 03, 2025)
2947
- Bumped pyOpenSSL dependency upper boundary from <25.0.0 to <26.0.0.
@@ -35,7 +53,6 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
3553
- Fixed a bug where file permission check happened on Windows.
3654
- Added support for File types.
3755
- Added `unsafe_file_write` connection parameter that restores the previous behaviour of saving files downloaded with GET with 644 permissions.
38-
- Deprecated `insecure_mode` connection property and replaced it with `disable_ocsp_checks` with the same behavior as the former property.
3956

4057
- v3.13.2(January 29, 2025)
4158
- Changed not to use scoped temporary objects.

setup.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ python_requires = >=3.9
4444
packages = find_namespace:
4545
install_requires =
4646
asn1crypto>0.24.0,<2.0.0
47-
boto3>=1.0
48-
botocore>=1.0
47+
boto3>=1.24
48+
botocore>=1.24
4949
cffi>=1.9,<2.0.0
5050
cryptography>=3.1.0
5151
pyOpenSSL>=22.0.0,<26.0.0

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def build_extension(self, ext):
103103
"FixedSizeListConverter.cpp",
104104
"FloatConverter.cpp",
105105
"IntConverter.cpp",
106+
"IntervalConverter.cpp",
106107
"MapConverter.cpp",
107108
"ObjectConverter.cpp",
108109
"SnowflakeType.cpp",

src/snowflake/connector/arrow_context.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .converter import _generate_tzinfo_from_tzoffset
1616

1717
if TYPE_CHECKING:
18-
from numpy import datetime64, float64, int64
18+
from numpy import datetime64, float64, int64, timedelta64
1919

2020

2121
try:
@@ -163,3 +163,41 @@ def DECFLOAT_to_decimal(self, exponent: int, significand: bytes) -> decimal.Deci
163163

164164
def DECFLOAT_to_numpy_float64(self, exponent: int, significand: bytes) -> float64:
165165
return numpy.float64(self.DECFLOAT_to_decimal(exponent, significand))
166+
167+
def INTERVAL_YEAR_MONTH_to_numpy_timedelta(self, months: int) -> timedelta64:
168+
return numpy.timedelta64(months, "M")
169+
170+
def INTERVAL_DAY_TIME_int_to_numpy_timedelta(self, nanos: int) -> timedelta64:
171+
return numpy.timedelta64(nanos, "ns")
172+
173+
def INTERVAL_DAY_TIME_int_to_timedelta(self, nanos: int) -> timedelta:
174+
# Python timedelta only supports microsecond precision. We receive value in
175+
# nanoseconds.
176+
return timedelta(microseconds=nanos // 1000)
177+
178+
def INTERVAL_DAY_TIME_decimal_to_numpy_timedelta(self, value: bytes) -> timedelta64:
179+
# Snowflake supports up to 9 digits leading field precision for the day-time
180+
# interval. That when represented in nanoseconds can not be stored in a 64-bit
181+
# integer. So we send these as Decimal128 from server to client.
182+
# Arrow uses little-endian by default.
183+
# https://arrow.apache.org/docs/format/Columnar.html#byte-order-endianness
184+
nanos = int.from_bytes(value, byteorder="little", signed=True)
185+
# Numpy timedelta only supports up to 64-bit integers, so we need to change the
186+
# unit to milliseconds to avoid overflow.
187+
# Max value received from server
188+
# = 10**9 * NANOS_PER_DAY - 1
189+
# = 86399999999999999999999 nanoseconds
190+
# = 86399999999999999 milliseconds
191+
# math.log2(86399999999999999) = 56.3 < 64
192+
return numpy.timedelta64(nanos // 1_000_000, "ms")
193+
194+
def INTERVAL_DAY_TIME_decimal_to_timedelta(self, value: bytes) -> timedelta:
195+
# Snowflake supports up to 9 digits leading field precision for the day-time
196+
# interval. That when represented in nanoseconds can not be stored in a 64-bit
197+
# integer. So we send these as Decimal128 from server to client.
198+
# Arrow uses little-endian by default.
199+
# https://arrow.apache.org/docs/format/Columnar.html#byte-order-endianness
200+
nanos = int.from_bytes(value, byteorder="little", signed=True)
201+
# Python timedelta only supports microsecond precision. We receive value in
202+
# nanoseconds.
203+
return timedelta(microseconds=nanos // 1000)

src/snowflake/connector/auth/oauth_code.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def __init__(
5858
token_cache: TokenCache | None = None,
5959
refresh_token_enabled: bool = False,
6060
external_browser_timeout: int | None = None,
61+
enable_single_use_refresh_tokens: bool = False,
6162
**kwargs,
6263
) -> None:
6364
super().__init__(
@@ -81,6 +82,7 @@ def __init__(
8182
logger.debug("oauth pkce is going to be used")
8283
self._verifier: str | None = None
8384
self._external_browser_timeout = external_browser_timeout
85+
self._enable_single_use_refresh_tokens = enable_single_use_refresh_tokens
8486

8587
def _get_oauth_type_id(self) -> str:
8688
return OAUTH_TYPE_AUTHORIZATION_CODE
@@ -296,6 +298,8 @@ def _do_token_request(
296298
"code": code,
297299
"redirect_uri": callback_server.url,
298300
}
301+
if self._enable_single_use_refresh_tokens:
302+
fields["enable_single_use_refresh_tokens"] = "true"
299303
if self._pkce_enabled:
300304
assert self._verifier is not None
301305
fields["code_verifier"] = self._verifier

src/snowflake/connector/connection.py

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from __future__ import annotations
33

44
import atexit
5-
import collections.abc
65
import logging
76
import os
87
import pathlib
@@ -94,6 +93,7 @@
9493
ER_INVALID_WIF_SETTINGS,
9594
ER_NO_ACCOUNT_NAME,
9695
ER_NO_CLIENT_ID,
96+
ER_NO_CLIENT_SECRET,
9797
ER_NO_NUMPY,
9898
ER_NO_PASSWORD,
9999
ER_NO_USER,
@@ -346,11 +346,20 @@ def _get_private_bytes_from_file(
346346
str,
347347
# SNOW-1825621: OAUTH implementation
348348
),
349-
"oauth_security_features": (
350-
("pkce",),
351-
collections.abc.Iterable, # of strings
349+
"oauth_disable_pkce": (
350+
False,
351+
bool,
352352
# SNOW-1825621: OAUTH PKCE
353353
),
354+
"oauth_enable_refresh_tokens": (
355+
False,
356+
bool,
357+
),
358+
"oauth_enable_single_use_refresh_tokens": (
359+
False,
360+
bool,
361+
# Client-side opt-in to single-use refresh tokens.
362+
),
354363
"check_arrow_conversion_error_on_every_column": (
355364
True,
356365
bool,
@@ -838,21 +847,6 @@ def unsafe_file_write(self) -> bool:
838847
def unsafe_file_write(self, value: bool) -> None:
839848
self._unsafe_file_write = value
840849

841-
class _OAuthSecurityFeatures(NamedTuple):
842-
pkce_enabled: bool
843-
refresh_token_enabled: bool
844-
845-
@property
846-
def oauth_security_features(self) -> _OAuthSecurityFeatures:
847-
features = self._oauth_security_features
848-
if isinstance(features, str):
849-
features = features.split(" ")
850-
features = [feat.lower() for feat in features]
851-
return self._OAuthSecurityFeatures(
852-
pkce_enabled="pkce" in features,
853-
refresh_token_enabled="refresh_token" in features,
854-
)
855-
856850
@property
857851
def check_arrow_conversion_error_on_every_column(self) -> bool:
858852
return self._check_arrow_conversion_error_on_every_column
@@ -1210,9 +1204,7 @@ def __open_connection(self):
12101204
backoff_generator=self._backoff_generator,
12111205
)
12121206
elif self._authenticator == OAUTH_AUTHORIZATION_CODE:
1213-
self._check_experimental_authentication_flag()
1214-
self._check_oauth_required_parameters()
1215-
features = self.oauth_security_features
1207+
self._check_oauth_parameters()
12161208
if self._role and (self._oauth_scope == ""):
12171209
# if role is known then let's inject it into scope
12181210
self._oauth_scope = _OAUTH_DEFAULT_SCOPE.format(role=self._role)
@@ -1228,19 +1220,18 @@ def __open_connection(self):
12281220
),
12291221
redirect_uri=self._oauth_redirect_uri,
12301222
scope=self._oauth_scope,
1231-
pkce_enabled=features.pkce_enabled,
1223+
pkce_enabled=not self._oauth_disable_pkce,
12321224
token_cache=(
12331225
auth.get_token_cache()
12341226
if self._client_store_temporary_credential
12351227
else None
12361228
),
1237-
refresh_token_enabled=features.refresh_token_enabled,
1229+
refresh_token_enabled=self._oauth_enable_refresh_tokens,
12381230
external_browser_timeout=self._external_browser_timeout,
1231+
enable_single_use_refresh_tokens=self._oauth_enable_single_use_refresh_tokens,
12391232
)
12401233
elif self._authenticator == OAUTH_CLIENT_CREDENTIALS:
1241-
self._check_experimental_authentication_flag()
1242-
self._check_oauth_required_parameters()
1243-
features = self.oauth_security_features
1234+
self._check_oauth_parameters()
12441235
if self._role and (self._oauth_scope == ""):
12451236
# if role is known then let's inject it into scope
12461237
self._oauth_scope = _OAUTH_DEFAULT_SCOPE.format(role=self._role)
@@ -1257,7 +1248,7 @@ def __open_connection(self):
12571248
if self._client_store_temporary_credential
12581249
else None
12591250
),
1260-
refresh_token_enabled=features.refresh_token_enabled,
1251+
refresh_token_enabled=self._oauth_enable_refresh_tokens,
12611252
)
12621253
elif self._authenticator == USR_PWD_MFA_AUTHENTICATOR:
12631254
self._session_parameters[PARAMETER_CLIENT_REQUEST_MFA_TOKEN] = (
@@ -2245,7 +2236,7 @@ def _check_experimental_authentication_flag(self) -> None:
22452236
},
22462237
)
22472238

2248-
def _check_oauth_required_parameters(self) -> None:
2239+
def _check_oauth_parameters(self) -> None:
22492240
if self._oauth_client_id is None:
22502241
Error.errorhandler_wrapper(
22512242
self,
@@ -2263,6 +2254,32 @@ def _check_oauth_required_parameters(self) -> None:
22632254
ProgrammingError,
22642255
{
22652256
"msg": "Oauth code flow requirement 'client_secret' is empty",
2266-
"errno": ER_NO_CLIENT_ID,
2257+
"errno": ER_NO_CLIENT_SECRET,
2258+
},
2259+
)
2260+
if (
2261+
self._oauth_authorization_url
2262+
and not self._oauth_authorization_url.startswith("https://")
2263+
):
2264+
Error.errorhandler_wrapper(
2265+
self,
2266+
None,
2267+
ProgrammingError,
2268+
{
2269+
"msg": "OAuth supports only authorization urls that use 'https' scheme",
2270+
"errno": ER_INVALID_VALUE,
2271+
},
2272+
)
2273+
if self._oauth_redirect_uri and not (
2274+
self._oauth_redirect_uri.startswith("http://")
2275+
or self._oauth_redirect_uri.startswith("https://")
2276+
):
2277+
Error.errorhandler_wrapper(
2278+
self,
2279+
None,
2280+
ProgrammingError,
2281+
{
2282+
"msg": "OAuth supports only authorization urls that use 'http(s)' scheme",
2283+
"errno": ER_INVALID_VALUE,
22672284
},
22682285
)

src/snowflake/connector/connection_diagnostic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,7 @@ def __check_for_proxies(self) -> None:
579579
cert_reqs=cert_reqs,
580580
)
581581
resp = http.request(
582-
"GET", "https://nonexistentdomain.invalidtld", timeout=10.0
582+
"GET", "https://nonexistentdomain.invalid", timeout=10.0
583583
)
584584

585585
# squid does not throw exception. Check HTML

src/snowflake/connector/errorcode.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
ER_INVALID_WIF_SETTINGS = 251017
3535
ER_WIF_CREDENTIALS_NOT_FOUND = 251018
3636
ER_EXPERIMENTAL_AUTHENTICATION_NOT_SUPPORTED = 251019
37+
ER_NO_CLIENT_SECRET = 251020
3738

3839
# cursor
3940
ER_FAILED_TO_REWRITE_MULTI_ROW_INSERT = 252001

src/snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowChunkIterator.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "FixedSizeListConverter.hpp"
1414
#include "FloatConverter.hpp"
1515
#include "IntConverter.hpp"
16+
#include "IntervalConverter.hpp"
1617
#include "MapConverter.hpp"
1718
#include "ObjectConverter.hpp"
1819
#include "StringConverter.hpp"
@@ -479,6 +480,36 @@ std::shared_ptr<sf::IColumnConverter> getConverterFromSchema(
479480
break;
480481
}
481482

483+
case SnowflakeType::Type::INTERVAL_YEAR_MONTH: {
484+
converter = std::make_shared<sf::IntervalYearMonthConverter>(
485+
array, context, useNumpy);
486+
break;
487+
}
488+
489+
case SnowflakeType::Type::INTERVAL_DAY_TIME: {
490+
switch (schemaView.type) {
491+
case NANOARROW_TYPE_INT64:
492+
converter = std::make_shared<sf::IntervalDayTimeConverterInt>(
493+
array, context, useNumpy);
494+
break;
495+
case NANOARROW_TYPE_DECIMAL128:
496+
converter = std::make_shared<sf::IntervalDayTimeConverterDecimal>(
497+
array, context, useNumpy);
498+
break;
499+
default: {
500+
std::string errorInfo = Logger::formatString(
501+
"[Snowflake Exception] unknown arrow internal data type(%d) "
502+
"for OBJECT data in %s",
503+
NANOARROW_TYPE_ENUM_STRING[schemaView.type],
504+
schemaView.schema->name);
505+
logger->error(__FILE__, __func__, __LINE__, errorInfo.c_str());
506+
PyErr_SetString(PyExc_Exception, errorInfo.c_str());
507+
break;
508+
}
509+
}
510+
break;
511+
}
512+
482513
default: {
483514
std::string errorInfo = Logger::formatString(
484515
"[Snowflake Exception] unknown snowflake data type : %d", st);

0 commit comments

Comments
 (0)