Skip to content

Commit 4600710

Browse files
Merge branch 'main' into SNOW-2062305-process-pool-batch-fetcher
2 parents 1118fc8 + 08dbaa8 commit 4600710

File tree

11 files changed

+359
-13
lines changed

11 files changed

+359
-13
lines changed

DESCRIPTION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
1111
- Bumped numpy dependency from <2.1.0 to <=2.2.4
1212
- Added Windows support for Python 3.13.
1313
- Add `bulk_upload_chunks` parameter to `write_pandas` function. Setting this parameter to True changes the behaviour of write_pandas function to first write all the data chunks to the local disk and then perform the wildcard upload of the chunks folder to the stage. In default behaviour the chunks are being saved, uploaded and deleted one by one.
14+
- Added support for new authentication mechanism PAT with external session ID.
1415
- Added `client_fetch_use_mp` parameter that enables multiprocessed fetching of result batches.
1516

16-
1717
- v3.15.1(May 20, 2025)
1818
- Added basic arrow support for Interval types.
1919
- Fix `write_pandas` special characters usage in the location name.

prober/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ ENV PATH="${PYENV_ROOT}/shims:${PYENV_ROOT}/bin:${PATH}"
4343
RUN git clone --depth=1 https://github.com/pyenv/pyenv.git ${PYENV_ROOT}
4444

4545
# Build arguments for Python versions and Snowflake connector versions
46-
ARG MATRIX_VERSION='{"3.13.4": ["3.15.0", "3.13.2", "3.14.0", "3.12.3", "3.0.4", "3.12.1", "3.12.4", "3.11.0", "3.12.2", "3.6.0", "3.7.0"], "3.9.22": ["3.15.0", "3.13.2", "3.14.0", "3.12.3", "3.0.4", "3.12.1", "3.12.4", "3.11.0", "3.12.2", "3.6.0", "3.7.0"]}'
46+
ARG MATRIX_VERSION='{"3.13.4": ["3.15.0", "3.13.2", "3.14.0", "3.12.3", "3.12.1", "3.12.4", "3.11.0", "3.12.2", "3.6.0", "3.7.0"], "3.9.22": ["3.15.0", "3.13.2", "3.14.0", "3.12.3", "3.12.1", "3.12.4", "3.11.0", "3.12.2", "3.6.0", "3.7.0"]}'
4747

4848

4949
# Install Python versions from ARG MATRIX_VERSION

prober/testing_matrix.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
"python-version": [
33
{
44
"version": "3.13.4",
5-
"snowflake-connector-python": ["3.13.2", "3.14.0", "3.12.3", "3.0.4", "3.12.1", "3.12.4", "3.11.0", "3.12.2", "3.6.0", "3.7.0"]
5+
"snowflake-connector-python": ["3.15.0" ,"3.13.2", "3.14.0", "3.12.3", "3.12.1", "3.12.4", "3.11.0", "3.12.2", "3.6.0", "3.7.0"]
66
},
77
{
88
"version": "3.9.22",
9-
"snowflake-connector-python": ["3.13.2", "3.14.0", "3.12.3", "3.0.4", "3.12.1", "3.12.4", "3.11.0", "3.12.2", "3.6.0", "3.7.0"]
9+
"snowflake-connector-python": ["3.15.0", "3.13.2", "3.14.0", "3.12.3", "3.12.1", "3.12.4", "3.11.0", "3.12.2", "3.6.0", "3.7.0"]
1010
}
1111
]
1212
}

src/snowflake/connector/auth/by_plugin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class AuthType(Enum):
5353
PAT = "PROGRAMMATIC_ACCESS_TOKEN'"
5454
NO_AUTH = "NO_AUTH"
5555
WORKLOAD_IDENTITY = "WORKLOAD_IDENTITY"
56+
PAT_WITH_EXTERNAL_SESSION = "PAT_WITH_EXTERNAL_SESSION"
5657

5758

5859
class AuthByPlugin(ABC):

src/snowflake/connector/connection.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
OAUTH_AUTHENTICATOR,
108108
OAUTH_AUTHORIZATION_CODE,
109109
OAUTH_CLIENT_CREDENTIALS,
110+
PAT_WITH_EXTERNAL_SESSION,
110111
PROGRAMMATIC_ACCESS_TOKEN,
111112
REQUEST_ID,
112113
USR_PWD_MFA_AUTHENTICATOR,
@@ -363,6 +364,11 @@ def _get_private_bytes_from_file(
363364
True,
364365
bool,
365366
), # SNOW-XXXXX: remove the check_arrow_conversion_error_on_every_column flag
367+
"external_session_id": (
368+
None,
369+
str,
370+
# SNOW-2096721: External (Spark) session ID
371+
),
366372
}
367373

368374
APPLICATION_RE = re.compile(r"[\w\d_]+")
@@ -1272,6 +1278,15 @@ def __open_connection(self):
12721278
)
12731279
elif self._authenticator == PROGRAMMATIC_ACCESS_TOKEN:
12741280
self.auth_class = AuthByPAT(self._token)
1281+
elif self._authenticator == PAT_WITH_EXTERNAL_SESSION:
1282+
# We don't need to do a POST to /v1/login-request to get session and master tokens at the startup
1283+
# time. PAT with external (Spark) session ID creates a new session when it encounters the unique
1284+
# (PAT, external session ID) combination for the first time and then onwards use the (PAT, external
1285+
# session id) as a key to identify and authenticate the session. So we bypass actual AuthN here.
1286+
self.auth_class = AuthNoAuth()
1287+
self._rest.set_pat_and_external_session(
1288+
self._token, self._external_session_id
1289+
)
12751290
elif self._authenticator == WORKLOAD_IDENTITY_AUTHENTICATOR:
12761291
self._check_experimental_authentication_flag()
12771292
# Standardize the provider enum.
@@ -1411,6 +1426,7 @@ def __config(self, **kwargs):
14111426
OAUTH_AUTHENTICATOR,
14121427
USR_PWD_MFA_AUTHENTICATOR,
14131428
WORKLOAD_IDENTITY_AUTHENTICATOR,
1429+
PAT_WITH_EXTERNAL_SESSION,
14141430
]:
14151431
self._authenticator = auth_tmp
14161432

@@ -1426,6 +1442,7 @@ def __config(self, **kwargs):
14261442
NO_AUTH_AUTHENTICATOR,
14271443
WORKLOAD_IDENTITY_AUTHENTICATOR,
14281444
PROGRAMMATIC_ACCESS_TOKEN,
1445+
PAT_WITH_EXTERNAL_SESSION,
14291446
}
14301447

14311448
if not (self._master_token and self._session_token):
@@ -1474,6 +1491,7 @@ def __config(self, **kwargs):
14741491
KEY_PAIR_AUTHENTICATOR,
14751492
PROGRAMMATIC_ACCESS_TOKEN,
14761493
WORKLOAD_IDENTITY_AUTHENTICATOR,
1494+
PAT_WITH_EXTERNAL_SESSION,
14771495
)
14781496
and not self._password
14791497
):

src/snowflake/connector/network.py

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,12 @@
148148

149149
HEADER_AUTHORIZATION_KEY = "Authorization"
150150
HEADER_SNOWFLAKE_TOKEN = 'Snowflake Token="{token}"'
151+
HEADER_EXTERNAL_SESSION_KEY = "X-Snowflake-External-Session-ID"
151152

152153
REQUEST_ID = "requestId"
153154
REQUEST_GUID = "request_guid"
154155
SNOWFLAKE_HOST_SUFFIX = ".snowflakecomputing.com"
155156

156-
157157
SNOWFLAKE_CONNECTOR_VERSION = SNOWFLAKE_CONNECTOR_VERSION
158158
PYTHON_VERSION = PYTHON_VERSION
159159
OPERATING_SYSTEM = OPERATING_SYSTEM
@@ -166,6 +166,7 @@
166166
PYTHON_CONNECTOR_USER_AGENT = f"{CLIENT_NAME}/{SNOWFLAKE_CONNECTOR_VERSION} ({PLATFORM}) {IMPLEMENTATION}/{PYTHON_VERSION}"
167167

168168
NO_TOKEN = "no-token"
169+
NO_EXTERNAL_SESSION_ID = "no-external-session-id"
169170

170171
STATUS_TO_EXCEPTION: dict[int, type[Error]] = {
171172
INTERNAL_SERVER_ERROR: InternalServerError,
@@ -189,6 +190,7 @@
189190
PROGRAMMATIC_ACCESS_TOKEN = "PROGRAMMATIC_ACCESS_TOKEN"
190191
NO_AUTH_AUTHENTICATOR = "NO_AUTH"
191192
WORKLOAD_IDENTITY_AUTHENTICATOR = "WORKLOAD_IDENTITY"
193+
PAT_WITH_EXTERNAL_SESSION = "PAT_WITH_EXTERNAL_SESSION"
192194

193195

194196
def is_retryable_http_code(code: int) -> bool:
@@ -313,6 +315,25 @@ def __call__(self, r: PreparedRequest) -> PreparedRequest:
313315
return r
314316

315317

318+
class PATWithExternalSessionAuth(AuthBase):
319+
"""Attaches HTTP Authorization headers for PAT with External Session."""
320+
321+
def __init__(self, token, external_session_id) -> None:
322+
# setup any auth-related data here
323+
self.token = token
324+
self.external_session_id = external_session_id
325+
326+
def __call__(self, r: PreparedRequest) -> PreparedRequest:
327+
"""Modifies and returns the request."""
328+
if HEADER_AUTHORIZATION_KEY in r.headers:
329+
del r.headers[HEADER_AUTHORIZATION_KEY]
330+
if self.token != NO_TOKEN:
331+
r.headers[HEADER_AUTHORIZATION_KEY] = "Bearer " + self.token
332+
if self.external_session_id != NO_EXTERNAL_SESSION_ID:
333+
r.headers[HEADER_EXTERNAL_SESSION_KEY] = self.external_session_id
334+
return r
335+
336+
316337
class SessionPool:
317338
def __init__(self, rest: SnowflakeRestful) -> None:
318339
# A stack of the idle sessions
@@ -404,6 +425,12 @@ def __init__(
404425
def token(self) -> str | None:
405426
return self._token if hasattr(self, "_token") else None
406427

428+
@property
429+
def external_session_id(self) -> str | None:
430+
return (
431+
self._external_session_id if hasattr(self, "_external_session_id") else None
432+
)
433+
407434
@property
408435
def master_token(self) -> str | None:
409436
return self._master_token if hasattr(self, "_master_token") else None
@@ -513,6 +540,7 @@ def request(
513540
headers,
514541
json.dumps(body, cls=SnowflakeRestfulJsonEncoder),
515542
token=self.token,
543+
external_session_id=self.external_session_id,
516544
_no_results=_no_results,
517545
timeout=timeout,
518546
_include_retry_params=_include_retry_params,
@@ -523,6 +551,7 @@ def request(
523551
url,
524552
headers,
525553
token=self.token,
554+
external_session_id=self.external_session_id,
526555
timeout=timeout,
527556
)
528557

@@ -542,6 +571,17 @@ def update_tokens(
542571
self._mfa_token = mfa_token
543572
self._master_validity_in_seconds = master_validity_in_seconds
544573

574+
def set_pat_and_external_session(
575+
self,
576+
personal_access_token,
577+
external_session_id,
578+
) -> None:
579+
"""Updates session and master tokens and optionally temporary credential."""
580+
with self._lock_token:
581+
self._personal_access_token = personal_access_token
582+
self._token = personal_access_token
583+
self._external_session_id = external_session_id
584+
545585
def _renew_session(self):
546586
"""Renew a session and master token."""
547587
return self._token_request(REQUEST_TYPE_RENEW)
@@ -698,6 +738,7 @@ def _get_request(
698738
url: str,
699739
headers: dict[str, str],
700740
token: str = None,
741+
external_session_id: str = None,
701742
timeout: int | None = None,
702743
is_fetch_query_status: bool = False,
703744
) -> dict[str, Any]:
@@ -713,9 +754,13 @@ def _get_request(
713754
headers,
714755
timeout=timeout,
715756
token=token,
757+
external_session_id=external_session_id,
716758
is_fetch_query_status=is_fetch_query_status,
717759
)
718-
if ret.get("code") == SESSION_EXPIRED_GS_CODE:
760+
if (
761+
ret.get("code") == SESSION_EXPIRED_GS_CODE
762+
and self._connection._authenticator != PAT_WITH_EXTERNAL_SESSION
763+
):
719764
try:
720765
ret = self._renew_session()
721766
except ReauthenticationRequest as ex:
@@ -743,6 +788,7 @@ def _post_request(
743788
headers,
744789
body,
745790
token=None,
791+
external_session_id: str | None = None,
746792
timeout: int | None = None,
747793
socket_timeout: int | None = None,
748794
_no_results: bool = False,
@@ -763,6 +809,7 @@ def _post_request(
763809
data=body,
764810
timeout=timeout,
765811
token=token,
812+
external_session_id=external_session_id,
766813
no_retry=no_retry,
767814
_include_retry_params=_include_retry_params,
768815
socket_timeout=socket_timeout,
@@ -775,7 +822,10 @@ def _post_request(
775822

776823
if ret.get("code") == MASTER_TOKEN_EXPIRED_GS_CODE:
777824
self._connection.expired = True
778-
elif ret.get("code") == SESSION_EXPIRED_GS_CODE:
825+
elif (
826+
ret.get("code") == SESSION_EXPIRED_GS_CODE
827+
and self._connection._authenticator != PAT_WITH_EXTERNAL_SESSION
828+
):
779829
try:
780830
ret = self._renew_session()
781831
except ReauthenticationRequest as ex:
@@ -900,6 +950,7 @@ def _request_exec_wrapper(
900950
retry_ctx,
901951
no_retry: bool = False,
902952
token=NO_TOKEN,
953+
external_session_id=NO_EXTERNAL_SESSION_ID,
903954
**kwargs,
904955
):
905956
conn = self._connection
@@ -928,6 +979,7 @@ def _request_exec_wrapper(
928979
headers=headers,
929980
data=data,
930981
token=token,
982+
external_session_id=external_session_id,
931983
raise_raw_http_failure=raise_raw_http_failure,
932984
**kwargs,
933985
)
@@ -1061,6 +1113,7 @@ def _request_exec(
10611113
headers,
10621114
data,
10631115
token,
1116+
external_session_id=None,
10641117
catch_okta_unauthorized_error: bool = False,
10651118
is_raw_text: bool = False,
10661119
is_raw_binary: bool = False,
@@ -1088,6 +1141,11 @@ def _request_exec(
10881141
# socket timeout is constant. You should be able to receive
10891142
# the response within the time. If not, ConnectReadTimeout or
10901143
# ReadTimeout is raised.
1144+
auth = (
1145+
PATWithExternalSessionAuth(token, external_session_id)
1146+
if (external_session_id is not None and token is not None)
1147+
else SnowflakeAuth(token)
1148+
)
10911149
raw_ret = session.request(
10921150
method=method,
10931151
url=full_url,
@@ -1096,7 +1154,7 @@ def _request_exec(
10961154
timeout=socket_timeout,
10971155
verify=True,
10981156
stream=is_raw_binary,
1099-
auth=SnowflakeAuth(token),
1157+
auth=auth,
11001158
)
11011159
download_end_time = get_time_millis()
11021160

0 commit comments

Comments
 (0)