snowflake-connector-python 3.16.0__cp313-cp313-win_amd64.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.
- snowflake/connector/__init__.py +96 -0
- snowflake/connector/_query_context_cache.py +276 -0
- snowflake/connector/_sql_util.py +48 -0
- snowflake/connector/_utils.py +70 -0
- snowflake/connector/arrow_context.py +203 -0
- snowflake/connector/auth/__init__.py +53 -0
- snowflake/connector/auth/_auth.py +586 -0
- snowflake/connector/auth/_http_server.py +220 -0
- snowflake/connector/auth/_oauth_base.py +400 -0
- snowflake/connector/auth/by_plugin.py +219 -0
- snowflake/connector/auth/default.py +36 -0
- snowflake/connector/auth/idtoken.py +72 -0
- snowflake/connector/auth/keypair.py +222 -0
- snowflake/connector/auth/no_auth.py +39 -0
- snowflake/connector/auth/oauth.py +49 -0
- snowflake/connector/auth/oauth_code.py +479 -0
- snowflake/connector/auth/oauth_credentials.py +66 -0
- snowflake/connector/auth/okta.py +335 -0
- snowflake/connector/auth/pat.py +39 -0
- snowflake/connector/auth/usrpwdmfa.py +69 -0
- snowflake/connector/auth/webbrowser.py +502 -0
- snowflake/connector/auth/workload_identity.py +94 -0
- snowflake/connector/azure_storage_client.py +276 -0
- snowflake/connector/backoff_policies.py +141 -0
- snowflake/connector/bind_upload_agent.py +89 -0
- snowflake/connector/cache.py +696 -0
- snowflake/connector/compat.py +127 -0
- snowflake/connector/config_manager.py +496 -0
- snowflake/connector/connection.py +2273 -0
- snowflake/connector/connection_diagnostic.py +776 -0
- snowflake/connector/constants.py +442 -0
- snowflake/connector/converter.py +785 -0
- snowflake/connector/converter_issue23517.py +87 -0
- snowflake/connector/converter_null.py +14 -0
- snowflake/connector/converter_snowsql.py +205 -0
- snowflake/connector/cursor.py +1951 -0
- snowflake/connector/dbapi.py +53 -0
- snowflake/connector/description.py +19 -0
- snowflake/connector/direct_file_operation_utils.py +88 -0
- snowflake/connector/encryption_util.py +220 -0
- snowflake/connector/errorcode.py +91 -0
- snowflake/connector/errors.py +615 -0
- snowflake/connector/externals_utils/__init__.py +0 -0
- snowflake/connector/externals_utils/externals_setup.py +27 -0
- snowflake/connector/feature.py +4 -0
- snowflake/connector/file_compression_type.py +118 -0
- snowflake/connector/file_lock.py +72 -0
- snowflake/connector/file_transfer_agent.py +1215 -0
- snowflake/connector/file_util.py +153 -0
- snowflake/connector/gcs_storage_client.py +474 -0
- snowflake/connector/gzip_decoder.py +85 -0
- snowflake/connector/local_storage_client.py +90 -0
- snowflake/connector/log_configuration.py +60 -0
- snowflake/connector/logging_utils/__init__.py +0 -0
- snowflake/connector/logging_utils/filters.py +72 -0
- snowflake/connector/nanoarrow_arrow_iterator.cp313-win_amd64.pyd +0 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/ArrayConverter.cpp +60 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/ArrayConverter.hpp +29 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/BinaryConverter.cpp +19 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/BinaryConverter.hpp +26 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/BooleanConverter.cpp +21 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/BooleanConverter.hpp +23 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowChunkIterator.cpp +557 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowChunkIterator.hpp +98 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowIterator.cpp +125 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowIterator.hpp +115 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowTableIterator.cpp +1005 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowTableIterator.hpp +127 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DateConverter.cpp +47 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DateConverter.hpp +46 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DecFloatConverter.cpp +83 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DecFloatConverter.hpp +35 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DecimalConverter.cpp +97 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/DecimalConverter.hpp +72 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/FixedSizeListConverter.cpp +73 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/FixedSizeListConverter.hpp +28 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/FloatConverter.cpp +30 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/FloatConverter.hpp +35 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/IColumnConverter.hpp +17 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/IntConverter.cpp +23 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/IntConverter.hpp +45 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/IntervalConverter.cpp +71 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/IntervalConverter.hpp +56 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/LICENSE.txt +209 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/MapConverter.cpp +75 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/MapConverter.hpp +30 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/ObjectConverter.cpp +46 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/ObjectConverter.hpp +29 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Common.cpp +8 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Common.hpp +95 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Helpers.cpp +57 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Helpers.hpp +36 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/SnowflakeType.cpp +34 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/SnowflakeType.hpp +48 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/StringConverter.cpp +19 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/StringConverter.hpp +26 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeConverter.cpp +36 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeConverter.hpp +31 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeStampConverter.cpp +346 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeStampConverter.hpp +145 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Util/macros.hpp +14 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Util/time.cpp +65 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/Util/time.hpp +68 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_accessors.h +101 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_alloc.h +127 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_assert.h +45 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_builder.h +1908 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_emitter.h +215 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_endian.h +125 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_epilogue.h +7 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_flatbuffers.h +55 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_identifier.h +148 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_iov.h +31 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_prologue.h +8 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_refmap.h +144 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_rtconfig.h +162 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_types.h +97 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_verifier.h +239 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/flatcc_portable.h +14 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/paligned_alloc.h +210 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pattributes.h +84 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic.h +84 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic_pop.h +20 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic_push.h +51 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pendian.h +206 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pendian_detect.h +118 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pinline.h +19 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pinttypes.h +52 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/portable.h +2 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/portable_basic.h +25 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstatic_assert.h +67 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstdalign.h +162 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstdint.h +898 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/punaligned.h +190 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pversion.h +6 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pwarnings.h +52 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc.c +3204 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow.c +3217 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow.h +3618 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow.hpp +379 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_arrow_iterator.pyx +256 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_device.c +512 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_device.h +350 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_ipc.c +33273 -0
- snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_ipc.h +397 -0
- snowflake/connector/nanoarrow_cpp/Logging/logging.cpp +100 -0
- snowflake/connector/nanoarrow_cpp/Logging/logging.hpp +49 -0
- snowflake/connector/network.py +1297 -0
- snowflake/connector/ocsp_asn1crypto.py +447 -0
- snowflake/connector/ocsp_snowflake.py +1883 -0
- snowflake/connector/options.py +131 -0
- snowflake/connector/pandas_tools.py +732 -0
- snowflake/connector/proxy.py +43 -0
- snowflake/connector/py.typed +0 -0
- snowflake/connector/result_batch.py +786 -0
- snowflake/connector/result_set.py +319 -0
- snowflake/connector/s3_storage_client.py +605 -0
- snowflake/connector/secret_detector.py +181 -0
- snowflake/connector/sf_dirs.py +64 -0
- snowflake/connector/sfbinaryformat.py +35 -0
- snowflake/connector/sfdatetime.py +345 -0
- snowflake/connector/snow_logging.py +144 -0
- snowflake/connector/sqlstate.py +9 -0
- snowflake/connector/ssd_internal_keys.py +30 -0
- snowflake/connector/ssl_wrap_socket.py +134 -0
- snowflake/connector/storage_client.py +478 -0
- snowflake/connector/telemetry.py +249 -0
- snowflake/connector/telemetry_oob.py +543 -0
- snowflake/connector/test_util.py +30 -0
- snowflake/connector/time_util.py +159 -0
- snowflake/connector/token_cache.py +402 -0
- snowflake/connector/tool/__init__.py +0 -0
- snowflake/connector/tool/dump_certs.py +57 -0
- snowflake/connector/tool/dump_ocsp_response.py +139 -0
- snowflake/connector/tool/dump_ocsp_response_cache.py +194 -0
- snowflake/connector/tool/probe_connection.py +69 -0
- snowflake/connector/url_util.py +49 -0
- snowflake/connector/util_text.py +301 -0
- snowflake/connector/vendored/__init__.py +3 -0
- snowflake/connector/vendored/requests/LICENSE +175 -0
- snowflake/connector/vendored/requests/__init__.py +169 -0
- snowflake/connector/vendored/requests/__version__.py +14 -0
- snowflake/connector/vendored/requests/_internal_utils.py +50 -0
- snowflake/connector/vendored/requests/adapters.py +537 -0
- snowflake/connector/vendored/requests/api.py +157 -0
- snowflake/connector/vendored/requests/auth.py +315 -0
- snowflake/connector/vendored/requests/certs.py +17 -0
- snowflake/connector/vendored/requests/compat.py +79 -0
- snowflake/connector/vendored/requests/cookies.py +561 -0
- snowflake/connector/vendored/requests/exceptions.py +140 -0
- snowflake/connector/vendored/requests/help.py +134 -0
- snowflake/connector/vendored/requests/hooks.py +33 -0
- snowflake/connector/vendored/requests/models.py +1033 -0
- snowflake/connector/vendored/requests/sessions.py +833 -0
- snowflake/connector/vendored/requests/status_codes.py +128 -0
- snowflake/connector/vendored/requests/structures.py +99 -0
- snowflake/connector/vendored/requests/utils.py +1093 -0
- snowflake/connector/vendored/urllib3/LICENSE.txt +21 -0
- snowflake/connector/vendored/urllib3/__init__.py +85 -0
- snowflake/connector/vendored/urllib3/_collections.py +355 -0
- snowflake/connector/vendored/urllib3/_version.py +2 -0
- snowflake/connector/vendored/urllib3/connection.py +572 -0
- snowflake/connector/vendored/urllib3/connectionpool.py +1137 -0
- snowflake/connector/vendored/urllib3/contrib/__init__.py +0 -0
- snowflake/connector/vendored/urllib3/contrib/_appengine_environ.py +36 -0
- snowflake/connector/vendored/urllib3/contrib/_securetransport/__init__.py +0 -0
- snowflake/connector/vendored/urllib3/contrib/_securetransport/bindings.py +519 -0
- snowflake/connector/vendored/urllib3/contrib/_securetransport/low_level.py +397 -0
- snowflake/connector/vendored/urllib3/contrib/appengine.py +314 -0
- snowflake/connector/vendored/urllib3/contrib/ntlmpool.py +130 -0
- snowflake/connector/vendored/urllib3/contrib/pyopenssl.py +509 -0
- snowflake/connector/vendored/urllib3/contrib/securetransport.py +920 -0
- snowflake/connector/vendored/urllib3/contrib/socks.py +216 -0
- snowflake/connector/vendored/urllib3/exceptions.py +323 -0
- snowflake/connector/vendored/urllib3/fields.py +274 -0
- snowflake/connector/vendored/urllib3/filepost.py +98 -0
- snowflake/connector/vendored/urllib3/packages/__init__.py +0 -0
- snowflake/connector/vendored/urllib3/packages/backports/__init__.py +0 -0
- snowflake/connector/vendored/urllib3/packages/backports/makefile.py +51 -0
- snowflake/connector/vendored/urllib3/packages/backports/weakref_finalize.py +155 -0
- snowflake/connector/vendored/urllib3/packages/six.py +1076 -0
- snowflake/connector/vendored/urllib3/poolmanager.py +540 -0
- snowflake/connector/vendored/urllib3/request.py +191 -0
- snowflake/connector/vendored/urllib3/response.py +885 -0
- snowflake/connector/vendored/urllib3/util/__init__.py +49 -0
- snowflake/connector/vendored/urllib3/util/connection.py +156 -0
- snowflake/connector/vendored/urllib3/util/proxy.py +57 -0
- snowflake/connector/vendored/urllib3/util/queue.py +22 -0
- snowflake/connector/vendored/urllib3/util/request.py +146 -0
- snowflake/connector/vendored/urllib3/util/response.py +107 -0
- snowflake/connector/vendored/urllib3/util/retry.py +620 -0
- snowflake/connector/vendored/urllib3/util/ssl_.py +495 -0
- snowflake/connector/vendored/urllib3/util/ssl_match_hostname.py +159 -0
- snowflake/connector/vendored/urllib3/util/ssltransport.py +221 -0
- snowflake/connector/vendored/urllib3/util/timeout.py +271 -0
- snowflake/connector/vendored/urllib3/util/url.py +435 -0
- snowflake/connector/vendored/urllib3/util/wait.py +152 -0
- snowflake/connector/version.py +3 -0
- snowflake/connector/wif_util.py +407 -0
- snowflake_connector_python-3.16.0.dist-info/METADATA +1475 -0
- snowflake_connector_python-3.16.0.dist-info/RECORD +246 -0
- snowflake_connector_python-3.16.0.dist-info/WHEEL +5 -0
- snowflake_connector_python-3.16.0.dist-info/entry_points.txt +4 -0
- snowflake_connector_python-3.16.0.dist-info/licenses/LICENSE.txt +202 -0
- snowflake_connector_python-3.16.0.dist-info/licenses/NOTICE +8 -0
- snowflake_connector_python-3.16.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2273 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import atexit
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import pathlib
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
import traceback
|
|
11
|
+
import uuid
|
|
12
|
+
import warnings
|
|
13
|
+
import weakref
|
|
14
|
+
from concurrent.futures import as_completed
|
|
15
|
+
from concurrent.futures.thread import ThreadPoolExecutor
|
|
16
|
+
from contextlib import suppress
|
|
17
|
+
from difflib import get_close_matches
|
|
18
|
+
from functools import partial
|
|
19
|
+
from io import StringIO
|
|
20
|
+
from logging import getLogger
|
|
21
|
+
from threading import Lock
|
|
22
|
+
from types import TracebackType
|
|
23
|
+
from typing import Any, Callable, Generator, Iterable, Iterator, NamedTuple, Sequence
|
|
24
|
+
from uuid import UUID
|
|
25
|
+
|
|
26
|
+
from cryptography.hazmat.backends import default_backend
|
|
27
|
+
from cryptography.hazmat.primitives import serialization
|
|
28
|
+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
|
29
|
+
|
|
30
|
+
from . import errors, proxy
|
|
31
|
+
from ._query_context_cache import QueryContextCache
|
|
32
|
+
from .auth import (
|
|
33
|
+
FIRST_PARTY_AUTHENTICATORS,
|
|
34
|
+
Auth,
|
|
35
|
+
AuthByDefault,
|
|
36
|
+
AuthByKeyPair,
|
|
37
|
+
AuthByOAuth,
|
|
38
|
+
AuthByOauthCode,
|
|
39
|
+
AuthByOauthCredentials,
|
|
40
|
+
AuthByOkta,
|
|
41
|
+
AuthByPAT,
|
|
42
|
+
AuthByPlugin,
|
|
43
|
+
AuthByUsrPwdMfa,
|
|
44
|
+
AuthByWebBrowser,
|
|
45
|
+
AuthByWorkloadIdentity,
|
|
46
|
+
AuthNoAuth,
|
|
47
|
+
)
|
|
48
|
+
from .auth.idtoken import AuthByIdToken
|
|
49
|
+
from .backoff_policies import exponential_backoff
|
|
50
|
+
from .bind_upload_agent import BindUploadError
|
|
51
|
+
from .compat import IS_LINUX, IS_WINDOWS, quote, urlencode
|
|
52
|
+
from .config_manager import CONFIG_MANAGER, _get_default_connection_params
|
|
53
|
+
from .connection_diagnostic import ConnectionDiagnostic
|
|
54
|
+
from .constants import (
|
|
55
|
+
_CONNECTIVITY_ERR_MSG,
|
|
56
|
+
_DOMAIN_NAME_MAP,
|
|
57
|
+
_OAUTH_DEFAULT_SCOPE,
|
|
58
|
+
ENV_VAR_EXPERIMENTAL_AUTHENTICATION,
|
|
59
|
+
ENV_VAR_PARTNER,
|
|
60
|
+
PARAMETER_AUTOCOMMIT,
|
|
61
|
+
PARAMETER_CLIENT_PREFETCH_THREADS,
|
|
62
|
+
PARAMETER_CLIENT_REQUEST_MFA_TOKEN,
|
|
63
|
+
PARAMETER_CLIENT_SESSION_KEEP_ALIVE,
|
|
64
|
+
PARAMETER_CLIENT_SESSION_KEEP_ALIVE_HEARTBEAT_FREQUENCY,
|
|
65
|
+
PARAMETER_CLIENT_STORE_TEMPORARY_CREDENTIAL,
|
|
66
|
+
PARAMETER_CLIENT_TELEMETRY_ENABLED,
|
|
67
|
+
PARAMETER_CLIENT_VALIDATE_DEFAULT_PARAMETERS,
|
|
68
|
+
PARAMETER_ENABLE_STAGE_S3_PRIVATELINK_FOR_US_EAST_1,
|
|
69
|
+
PARAMETER_QUERY_CONTEXT_CACHE_SIZE,
|
|
70
|
+
PARAMETER_SERVICE_NAME,
|
|
71
|
+
PARAMETER_TIMEZONE,
|
|
72
|
+
OCSPMode,
|
|
73
|
+
QueryStatus,
|
|
74
|
+
)
|
|
75
|
+
from .converter import SnowflakeConverter
|
|
76
|
+
from .cursor import LOG_MAX_QUERY_LENGTH, SnowflakeCursor
|
|
77
|
+
from .description import (
|
|
78
|
+
CLIENT_NAME,
|
|
79
|
+
CLIENT_VERSION,
|
|
80
|
+
PLATFORM,
|
|
81
|
+
PYTHON_VERSION,
|
|
82
|
+
SNOWFLAKE_CONNECTOR_VERSION,
|
|
83
|
+
)
|
|
84
|
+
from .direct_file_operation_utils import FileOperationParser, StreamDownloader
|
|
85
|
+
from .errorcode import (
|
|
86
|
+
ER_CONNECTION_IS_CLOSED,
|
|
87
|
+
ER_EXPERIMENTAL_AUTHENTICATION_NOT_SUPPORTED,
|
|
88
|
+
ER_FAILED_PROCESSING_PYFORMAT,
|
|
89
|
+
ER_FAILED_PROCESSING_QMARK,
|
|
90
|
+
ER_FAILED_TO_CONNECT_TO_DB,
|
|
91
|
+
ER_INVALID_BACKOFF_POLICY,
|
|
92
|
+
ER_INVALID_VALUE,
|
|
93
|
+
ER_INVALID_WIF_SETTINGS,
|
|
94
|
+
ER_NO_ACCOUNT_NAME,
|
|
95
|
+
ER_NO_NUMPY,
|
|
96
|
+
ER_NO_PASSWORD,
|
|
97
|
+
ER_NO_USER,
|
|
98
|
+
ER_NOT_IMPLICITY_SNOWFLAKE_DATATYPE,
|
|
99
|
+
)
|
|
100
|
+
from .errors import DatabaseError, Error, OperationalError, ProgrammingError
|
|
101
|
+
from .log_configuration import EasyLoggingConfigPython
|
|
102
|
+
from .network import (
|
|
103
|
+
DEFAULT_AUTHENTICATOR,
|
|
104
|
+
EXTERNAL_BROWSER_AUTHENTICATOR,
|
|
105
|
+
KEY_PAIR_AUTHENTICATOR,
|
|
106
|
+
NO_AUTH_AUTHENTICATOR,
|
|
107
|
+
OAUTH_AUTHENTICATOR,
|
|
108
|
+
OAUTH_AUTHORIZATION_CODE,
|
|
109
|
+
OAUTH_CLIENT_CREDENTIALS,
|
|
110
|
+
PAT_WITH_EXTERNAL_SESSION,
|
|
111
|
+
PROGRAMMATIC_ACCESS_TOKEN,
|
|
112
|
+
REQUEST_ID,
|
|
113
|
+
USR_PWD_MFA_AUTHENTICATOR,
|
|
114
|
+
WORKLOAD_IDENTITY_AUTHENTICATOR,
|
|
115
|
+
ReauthenticationRequest,
|
|
116
|
+
SnowflakeRestful,
|
|
117
|
+
)
|
|
118
|
+
from .sqlstate import SQLSTATE_CONNECTION_NOT_EXISTS, SQLSTATE_FEATURE_NOT_SUPPORTED
|
|
119
|
+
from .telemetry import TelemetryClient, TelemetryData, TelemetryField
|
|
120
|
+
from .time_util import HeartBeatTimer, get_time_millis
|
|
121
|
+
from .url_util import extract_top_level_domain_from_hostname
|
|
122
|
+
from .util_text import construct_hostname, parse_account, split_statements
|
|
123
|
+
from .wif_util import AttestationProvider
|
|
124
|
+
|
|
125
|
+
DEFAULT_CLIENT_PREFETCH_THREADS = 4
|
|
126
|
+
MAX_CLIENT_PREFETCH_THREADS = 10
|
|
127
|
+
MAX_CLIENT_FETCH_THREADS = 1024
|
|
128
|
+
DEFAULT_BACKOFF_POLICY = exponential_backoff()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def DefaultConverterClass() -> type:
|
|
132
|
+
if IS_WINDOWS:
|
|
133
|
+
from .converter_issue23517 import SnowflakeConverterIssue23517
|
|
134
|
+
|
|
135
|
+
return SnowflakeConverterIssue23517
|
|
136
|
+
else:
|
|
137
|
+
from .converter import SnowflakeConverter
|
|
138
|
+
|
|
139
|
+
return SnowflakeConverter
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _get_private_bytes_from_file(
|
|
143
|
+
private_key_file: str | bytes | os.PathLike[str] | os.PathLike[bytes],
|
|
144
|
+
private_key_file_pwd: bytes | str | None = None,
|
|
145
|
+
) -> bytes:
|
|
146
|
+
if private_key_file_pwd is not None and isinstance(private_key_file_pwd, str):
|
|
147
|
+
private_key_file_pwd = private_key_file_pwd.encode("utf-8")
|
|
148
|
+
with open(private_key_file, "rb") as key:
|
|
149
|
+
private_key = serialization.load_pem_private_key(
|
|
150
|
+
key.read(),
|
|
151
|
+
password=private_key_file_pwd,
|
|
152
|
+
backend=default_backend(),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
pkb = private_key.private_bytes(
|
|
156
|
+
encoding=serialization.Encoding.DER,
|
|
157
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
158
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return pkb
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
SUPPORTED_PARAMSTYLES = {
|
|
165
|
+
"qmark",
|
|
166
|
+
"numeric",
|
|
167
|
+
"format",
|
|
168
|
+
"pyformat",
|
|
169
|
+
}
|
|
170
|
+
# Default configs, tuple of default variable and accepted types
|
|
171
|
+
DEFAULT_CONFIGURATION: dict[str, tuple[Any, type | tuple[type, ...]]] = {
|
|
172
|
+
"dsn": (None, (type(None), str)), # standard
|
|
173
|
+
"user": ("", str), # standard
|
|
174
|
+
"password": ("", str), # standard
|
|
175
|
+
"host": ("127.0.0.1", str), # standard
|
|
176
|
+
"port": (443, (int, str)), # standard
|
|
177
|
+
"database": (None, (type(None), str)), # standard
|
|
178
|
+
"proxy_host": (None, (type(None), str)), # snowflake
|
|
179
|
+
"proxy_port": (None, (type(None), str)), # snowflake
|
|
180
|
+
"proxy_user": (None, (type(None), str)), # snowflake
|
|
181
|
+
"proxy_password": (None, (type(None), str)), # snowflake
|
|
182
|
+
"protocol": ("https", str), # snowflake
|
|
183
|
+
"warehouse": (None, (type(None), str)), # snowflake
|
|
184
|
+
"region": (None, (type(None), str)), # snowflake
|
|
185
|
+
"account": (None, (type(None), str)), # snowflake
|
|
186
|
+
"schema": (None, (type(None), str)), # snowflake
|
|
187
|
+
"role": (None, (type(None), str)), # snowflake
|
|
188
|
+
"session_id": (None, (type(None), str)), # snowflake
|
|
189
|
+
"login_timeout": (None, (type(None), int)), # login timeout
|
|
190
|
+
"network_timeout": (
|
|
191
|
+
None,
|
|
192
|
+
(type(None), int),
|
|
193
|
+
), # network timeout (infinite by default)
|
|
194
|
+
"socket_timeout": (None, (type(None), int)),
|
|
195
|
+
"external_browser_timeout": (120, int),
|
|
196
|
+
"backoff_policy": (DEFAULT_BACKOFF_POLICY, Callable),
|
|
197
|
+
"passcode_in_password": (False, bool), # Snowflake MFA
|
|
198
|
+
"passcode": (None, (type(None), str)), # Snowflake MFA
|
|
199
|
+
"private_key": (None, (type(None), bytes, str, RSAPrivateKey)),
|
|
200
|
+
"private_key_file": (None, (type(None), str)),
|
|
201
|
+
"private_key_file_pwd": (None, (type(None), str, bytes)),
|
|
202
|
+
"token": (None, (type(None), str)), # OAuth/JWT/PAT/OIDC Token
|
|
203
|
+
"token_file_path": (
|
|
204
|
+
None,
|
|
205
|
+
(type(None), str, bytes),
|
|
206
|
+
), # OAuth/JWT/PAT/OIDC Token file path
|
|
207
|
+
"authenticator": (DEFAULT_AUTHENTICATOR, (type(None), str)),
|
|
208
|
+
"workload_identity_provider": (None, (type(None), AttestationProvider)),
|
|
209
|
+
"workload_identity_entra_resource": (None, (type(None), str)),
|
|
210
|
+
"mfa_callback": (None, (type(None), Callable)),
|
|
211
|
+
"password_callback": (None, (type(None), Callable)),
|
|
212
|
+
"auth_class": (None, (type(None), AuthByPlugin)),
|
|
213
|
+
"application": (CLIENT_NAME, (type(None), str)),
|
|
214
|
+
# internal_application_name/version is used to tell the server the type of client connecting to
|
|
215
|
+
# Snowflake. There are some functionalities (e.g., MFA, Arrow result format) that
|
|
216
|
+
# Snowflake server doesn't support for new client types, which requires developers to
|
|
217
|
+
# add the new client type to the server to support these features.
|
|
218
|
+
"internal_application_name": (CLIENT_NAME, (type(None), str)),
|
|
219
|
+
"internal_application_version": (CLIENT_VERSION, (type(None), str)),
|
|
220
|
+
"disable_ocsp_checks": (False, bool),
|
|
221
|
+
"ocsp_fail_open": (True, bool), # fail open on ocsp issues, default true
|
|
222
|
+
"inject_client_pause": (0, int), # snowflake internal
|
|
223
|
+
"session_parameters": (None, (type(None), dict)), # snowflake session parameters
|
|
224
|
+
"autocommit": (None, (type(None), bool)), # snowflake
|
|
225
|
+
"client_session_keep_alive": (None, (type(None), bool)), # snowflake
|
|
226
|
+
"client_session_keep_alive_heartbeat_frequency": (
|
|
227
|
+
None,
|
|
228
|
+
(type(None), int),
|
|
229
|
+
), # snowflake
|
|
230
|
+
"client_prefetch_threads": (4, int), # snowflake
|
|
231
|
+
"client_fetch_threads": (None, (type(None), int)),
|
|
232
|
+
"client_fetch_use_mp": (False, bool),
|
|
233
|
+
"numpy": (False, bool), # snowflake
|
|
234
|
+
"ocsp_response_cache_filename": (None, (type(None), str)), # snowflake internal
|
|
235
|
+
"converter_class": (DefaultConverterClass(), SnowflakeConverter),
|
|
236
|
+
"validate_default_parameters": (False, bool), # snowflake
|
|
237
|
+
"probe_connection": (False, bool), # snowflake
|
|
238
|
+
"paramstyle": (None, (type(None), str)), # standard/snowflake
|
|
239
|
+
"timezone": (None, (type(None), str)), # snowflake
|
|
240
|
+
"consent_cache_id_token": (True, bool), # snowflake
|
|
241
|
+
"service_name": (None, (type(None), str)), # snowflake
|
|
242
|
+
"support_negative_year": (True, bool), # snowflake
|
|
243
|
+
"log_max_query_length": (LOG_MAX_QUERY_LENGTH, int), # snowflake
|
|
244
|
+
"disable_request_pooling": (False, bool), # snowflake
|
|
245
|
+
# enable temporary credential file for Linux, default false. Mac/Win will overlook this
|
|
246
|
+
"client_store_temporary_credential": (False, bool),
|
|
247
|
+
"client_request_mfa_token": (False, bool),
|
|
248
|
+
"use_openssl_only": (
|
|
249
|
+
True,
|
|
250
|
+
bool,
|
|
251
|
+
), # ignored - python only crypto modules are no longer used
|
|
252
|
+
# whether to convert Arrow number values to decimal instead of doubles
|
|
253
|
+
"arrow_number_to_decimal": (False, bool),
|
|
254
|
+
"enable_stage_s3_privatelink_for_us_east_1": (
|
|
255
|
+
False,
|
|
256
|
+
bool,
|
|
257
|
+
), # only use regional url when the param is set
|
|
258
|
+
# Allows cursors to be re-iterable
|
|
259
|
+
"reuse_results": (False, bool),
|
|
260
|
+
# parameter protecting behavior change of SNOW-501058
|
|
261
|
+
"interpolate_empty_sequences": (False, bool),
|
|
262
|
+
"enable_connection_diag": (False, bool), # Generate SnowCD like report
|
|
263
|
+
"connection_diag_log_path": (
|
|
264
|
+
None,
|
|
265
|
+
(type(None), str),
|
|
266
|
+
), # Path to connection diag report
|
|
267
|
+
"connection_diag_whitelist_path": (
|
|
268
|
+
None,
|
|
269
|
+
(type(None), str),
|
|
270
|
+
), # Path to connection diag whitelist json - Deprecated remove in future
|
|
271
|
+
"connection_diag_allowlist_path": (
|
|
272
|
+
None,
|
|
273
|
+
(type(None), str),
|
|
274
|
+
), # Path to connection diag allowlist json
|
|
275
|
+
"log_imported_packages_in_telemetry": (
|
|
276
|
+
True,
|
|
277
|
+
bool,
|
|
278
|
+
), # Whether to log imported packages in telemetry
|
|
279
|
+
"disable_query_context_cache": (
|
|
280
|
+
False,
|
|
281
|
+
bool,
|
|
282
|
+
), # Disable query context cache
|
|
283
|
+
"json_result_force_utf8_decoding": (
|
|
284
|
+
False,
|
|
285
|
+
bool,
|
|
286
|
+
), # Whether to force the JSON content to be decoded in utf-8, it is only effective when result format is JSON
|
|
287
|
+
"server_session_keep_alive": (
|
|
288
|
+
False,
|
|
289
|
+
bool,
|
|
290
|
+
), # Whether to keep session alive after connector shuts down
|
|
291
|
+
"enable_retry_reason_in_query_response": (
|
|
292
|
+
True,
|
|
293
|
+
bool,
|
|
294
|
+
), # Enable sending retryReason in response header for query-requests
|
|
295
|
+
"session_token": (
|
|
296
|
+
None,
|
|
297
|
+
(type(None), str),
|
|
298
|
+
), # session token from another connection, to be provided together with master token
|
|
299
|
+
"master_token": (
|
|
300
|
+
None,
|
|
301
|
+
(type(None), str),
|
|
302
|
+
), # master token from another connection, to be provided together with session token
|
|
303
|
+
"master_validity_in_seconds": (
|
|
304
|
+
None,
|
|
305
|
+
(type(None), int),
|
|
306
|
+
), # master token validity in seconds
|
|
307
|
+
"disable_console_login": (
|
|
308
|
+
True,
|
|
309
|
+
bool,
|
|
310
|
+
), # Disable console login and fall back to getting SSO URL from GS
|
|
311
|
+
"debug_arrow_chunk": (
|
|
312
|
+
False,
|
|
313
|
+
bool,
|
|
314
|
+
), # log raw arrow chunk for debugging purpuse in case there is malformed arrow data
|
|
315
|
+
"disable_saml_url_check": (
|
|
316
|
+
False,
|
|
317
|
+
bool,
|
|
318
|
+
), # disable saml url check in okta authentication
|
|
319
|
+
"iobound_tpe_limit": (
|
|
320
|
+
None,
|
|
321
|
+
(type(None), int),
|
|
322
|
+
), # SNOW-1817982: limit iobound TPE sizes when executing PUT/GET
|
|
323
|
+
"oauth_client_id": (
|
|
324
|
+
None,
|
|
325
|
+
(type(None), str),
|
|
326
|
+
# SNOW-1825621: OAUTH implementation
|
|
327
|
+
),
|
|
328
|
+
"oauth_client_secret": (
|
|
329
|
+
None,
|
|
330
|
+
(type(None), str),
|
|
331
|
+
# SNOW-1825621: OAUTH implementation
|
|
332
|
+
),
|
|
333
|
+
"oauth_authorization_url": (
|
|
334
|
+
"https://{host}:{port}/oauth/authorize",
|
|
335
|
+
str,
|
|
336
|
+
# SNOW-1825621: OAUTH implementation
|
|
337
|
+
),
|
|
338
|
+
"oauth_token_request_url": (
|
|
339
|
+
"https://{host}:{port}/oauth/token-request",
|
|
340
|
+
str,
|
|
341
|
+
# SNOW-1825621: OAUTH implementation
|
|
342
|
+
),
|
|
343
|
+
"oauth_redirect_uri": ("http://127.0.0.1", str),
|
|
344
|
+
"oauth_scope": (
|
|
345
|
+
"",
|
|
346
|
+
str,
|
|
347
|
+
# SNOW-1825621: OAUTH implementation
|
|
348
|
+
),
|
|
349
|
+
"oauth_disable_pkce": (
|
|
350
|
+
False,
|
|
351
|
+
bool,
|
|
352
|
+
# SNOW-1825621: OAUTH PKCE
|
|
353
|
+
),
|
|
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
|
+
),
|
|
363
|
+
"check_arrow_conversion_error_on_every_column": (
|
|
364
|
+
True,
|
|
365
|
+
bool,
|
|
366
|
+
), # 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
|
+
),
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
APPLICATION_RE = re.compile(r"[\w\d_]+")
|
|
375
|
+
|
|
376
|
+
# adding the exception class to Connection class
|
|
377
|
+
for m in [method for method in dir(errors) if callable(getattr(errors, method))]:
|
|
378
|
+
setattr(sys.modules[__name__], m, getattr(errors, m))
|
|
379
|
+
|
|
380
|
+
logger = getLogger(__name__)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class TypeAndBinding(NamedTuple):
|
|
384
|
+
"""Stores the type name and the Snowflake binding."""
|
|
385
|
+
|
|
386
|
+
type: str
|
|
387
|
+
binding: str | None
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class SnowflakeConnection:
|
|
391
|
+
"""Implementation of the connection object for the Snowflake Database.
|
|
392
|
+
|
|
393
|
+
Use connect(..) to get the object.
|
|
394
|
+
|
|
395
|
+
Attributes:
|
|
396
|
+
insecure_mode (deprecated): Whether or not the connection is in OCSP disabled mode. It means that the connection
|
|
397
|
+
validates the TLS certificate but doesn't check revocation status with OCSP provider.
|
|
398
|
+
disable_ocsp_checks: Whether or not the connection is in OCSP disabled mode. It means that the connection
|
|
399
|
+
validates the TLS certificate but doesn't check revocation status with OCSP provider.
|
|
400
|
+
ocsp_fail_open: Whether or not the connection is in fail open mode. Fail open mode decides if TLS certificates
|
|
401
|
+
continue to be validated. Revoked certificates are blocked. Any other exceptions are disregarded.
|
|
402
|
+
session_id: The session ID of the connection.
|
|
403
|
+
user: The user name used in the connection.
|
|
404
|
+
host: The host name the connection attempts to connect to.
|
|
405
|
+
port: The port to communicate with on the host.
|
|
406
|
+
region: Region name if not the default Snowflake Database deployment.
|
|
407
|
+
proxy_host: The hostname used proxy server.
|
|
408
|
+
proxy_port: Port on proxy server to communicate with.
|
|
409
|
+
proxy_user: User name to login with on the proxy sever.
|
|
410
|
+
proxy_password: Password to be used to authenticate with proxy server.
|
|
411
|
+
account: Account name to be used to authenticate with Snowflake.
|
|
412
|
+
database: Database to use on Snowflake.
|
|
413
|
+
schema: Schema in use on Snowflake.
|
|
414
|
+
warehouse: Warehouse to be used on Snowflake.
|
|
415
|
+
role: Role in use on Snowflake.
|
|
416
|
+
login_timeout: Login timeout in seconds. Login requests will not be retried after this timeout expires.
|
|
417
|
+
Note that the login attempt may still take more than login_timeout seconds as an ongoing login request
|
|
418
|
+
cannot be canceled even upon login timeout expiry. The login timeout only prevents further retries.
|
|
419
|
+
If not specified, login_timeout is set to `snowflake.connector.auth.by_plugin.DEFAULT_AUTH_CLASS_TIMEOUT`.
|
|
420
|
+
Note that the number of retries on login requests is still limited by
|
|
421
|
+
`snowflake.connector.auth.by_plugin.DEFAULT_MAX_CON_RETRY_ATTEMPTS`.
|
|
422
|
+
network_timeout: Network timeout in seconds. Network requests besides login requests will not be retried
|
|
423
|
+
after this timeout expires. Overriden in cursor query execution if timeout is passed to cursor.execute.
|
|
424
|
+
Note that an operation may still take more than network_timeout seconds for the same reason as above.
|
|
425
|
+
If not specified, network_timeout is infinite.
|
|
426
|
+
socket_timeout: Socket timeout in seconds. Sets both socket connect and read timeout.
|
|
427
|
+
backoff_policy: Backoff policy to use for login and network requests. Must be a callable generator function.
|
|
428
|
+
Standard linear and exponential backoff implementations are included in `snowflake.connector.backoff_policies`
|
|
429
|
+
See the backoff_policies module for details and implementation examples.
|
|
430
|
+
client_session_keep_alive_heartbeat_frequency: Heartbeat frequency to keep connection alive in seconds.
|
|
431
|
+
client_prefetch_threads: Number of threads to download the result set.
|
|
432
|
+
client_fetch_threads: Number of threads (or processes) to fetch staged query results.
|
|
433
|
+
If not specified, reuses client_prefetch_threads value.
|
|
434
|
+
client_fetch_use_mp: Enables multiprocessing for fetching query results in parallel.
|
|
435
|
+
rest: Snowflake REST API object. Internal use only. Maybe removed in a later release.
|
|
436
|
+
application: Application name to communicate with Snowflake as. By default, this is "PythonConnector".
|
|
437
|
+
errorhandler: Handler used with errors. By default, an exception will be raised on error.
|
|
438
|
+
converter_class: Handler used to convert data to Python native objects.
|
|
439
|
+
validate_default_parameters: Validate database, schema, role and warehouse used on Snowflake.
|
|
440
|
+
is_pyformat: Whether the current argument binding is pyformat or format.
|
|
441
|
+
consent_cache_id_token: Consented cache ID token.
|
|
442
|
+
enable_stage_s3_privatelink_for_us_east_1: when true, clients use regional s3 url to upload files.
|
|
443
|
+
enable_connection_diag: when true, clients will generate a connectivity diagnostic report.
|
|
444
|
+
connection_diag_log_path: path to location to create diag report with enable_connection_diag.
|
|
445
|
+
connection_diag_whitelist_path: path to a whitelist.json file to test with enable_connection_diag - deprecated remove in future
|
|
446
|
+
connection_diag_allowlist_path: path to a allowlist.json file to test with enable_connection_diag.
|
|
447
|
+
json_result_force_utf8_decoding: When true, json result will be decoded in utf-8,
|
|
448
|
+
when false, the encoding of the content is auto-detected. Default value is false.
|
|
449
|
+
This parameter is only effective when the result format is JSON.
|
|
450
|
+
server_session_keep_alive: When true, the connector does not destroy the session on the Snowflake server side
|
|
451
|
+
before the connector shuts down. Default value is false.
|
|
452
|
+
token_file_path: The file path of the token file. If both token and token_file_path are provided, the token in token_file_path will be used.
|
|
453
|
+
unsafe_file_write: When true, files downloaded by GET will be saved with 644 permissions. Otherwise, files will be saved with safe - owner-only permissions: 600.
|
|
454
|
+
check_arrow_conversion_error_on_every_column: When true, the error check after the conversion from arrow to python types will happen for every column in the row. This is a new behaviour which fixes the bug that caused the type errors to trigger silently when occurring at any place other than last column in a row. To revert the previous (faulty) behaviour, please set this flag to false.
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
OCSP_ENV_LOCK = Lock()
|
|
458
|
+
|
|
459
|
+
def __init__(
|
|
460
|
+
self,
|
|
461
|
+
connection_name: str | None = None,
|
|
462
|
+
connections_file_path: pathlib.Path | None = None,
|
|
463
|
+
**kwargs,
|
|
464
|
+
) -> None:
|
|
465
|
+
"""Create a new SnowflakeConnection.
|
|
466
|
+
|
|
467
|
+
Connections can be loaded from the TOML file located at
|
|
468
|
+
snowflake.connector.constants.CONNECTIONS_FILE.
|
|
469
|
+
|
|
470
|
+
When connection_name is supplied we will first load that connection
|
|
471
|
+
and then override any other values supplied.
|
|
472
|
+
|
|
473
|
+
When no arguments are given (other than connection_file_path) the
|
|
474
|
+
default connection will be loaded first. Note that no overwriting is
|
|
475
|
+
supported in this case.
|
|
476
|
+
|
|
477
|
+
If overwriting values from the default connection is desirable, supply
|
|
478
|
+
the name explicitly.
|
|
479
|
+
"""
|
|
480
|
+
# initiate easy logging during every connection
|
|
481
|
+
easy_logging = EasyLoggingConfigPython()
|
|
482
|
+
easy_logging.create_log()
|
|
483
|
+
self._lock_sequence_counter = Lock()
|
|
484
|
+
self.sequence_counter = 0
|
|
485
|
+
self._errorhandler = Error.default_errorhandler
|
|
486
|
+
self._lock_converter = Lock()
|
|
487
|
+
self.messages = []
|
|
488
|
+
self._async_sfqids: dict[str, None] = {}
|
|
489
|
+
self._done_async_sfqids: dict[str, None] = {}
|
|
490
|
+
self._client_param_telemetry_enabled = True
|
|
491
|
+
self._server_param_telemetry_enabled = False
|
|
492
|
+
self._session_parameters: dict[str, str | int | bool] = {}
|
|
493
|
+
logger.info(
|
|
494
|
+
"Snowflake Connector for Python Version: %s, "
|
|
495
|
+
"Python Version: %s, Platform: %s",
|
|
496
|
+
SNOWFLAKE_CONNECTOR_VERSION,
|
|
497
|
+
PYTHON_VERSION,
|
|
498
|
+
PLATFORM,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
self._rest = None
|
|
502
|
+
for name, (value, _) in DEFAULT_CONFIGURATION.items():
|
|
503
|
+
setattr(self, f"_{name}", value)
|
|
504
|
+
|
|
505
|
+
self.heartbeat_thread = None
|
|
506
|
+
is_kwargs_empty = not kwargs
|
|
507
|
+
|
|
508
|
+
if "application" not in kwargs:
|
|
509
|
+
app = self._detect_application()
|
|
510
|
+
if app:
|
|
511
|
+
kwargs["application"] = app
|
|
512
|
+
|
|
513
|
+
if "insecure_mode" in kwargs:
|
|
514
|
+
warn_message = "The 'insecure_mode' connection property is deprecated. Please use 'disable_ocsp_checks' instead"
|
|
515
|
+
warnings.warn(
|
|
516
|
+
warn_message,
|
|
517
|
+
DeprecationWarning,
|
|
518
|
+
stacklevel=2,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
if (
|
|
522
|
+
"disable_ocsp_checks" in kwargs
|
|
523
|
+
and kwargs["disable_ocsp_checks"] != kwargs["insecure_mode"]
|
|
524
|
+
):
|
|
525
|
+
logger.warning(
|
|
526
|
+
"The values for 'disable_ocsp_checks' and 'insecure_mode' differ. "
|
|
527
|
+
"Using the value of 'disable_ocsp_checks."
|
|
528
|
+
)
|
|
529
|
+
else:
|
|
530
|
+
self._disable_ocsp_checks = kwargs["insecure_mode"]
|
|
531
|
+
|
|
532
|
+
self.converter = None
|
|
533
|
+
self.query_context_cache: QueryContextCache | None = None
|
|
534
|
+
self.query_context_cache_size = 5
|
|
535
|
+
if connections_file_path is not None:
|
|
536
|
+
# Change config file path and force update cache
|
|
537
|
+
for i, s in enumerate(CONFIG_MANAGER._slices):
|
|
538
|
+
if s.section == "connections":
|
|
539
|
+
CONFIG_MANAGER._slices[i] = s._replace(path=connections_file_path)
|
|
540
|
+
CONFIG_MANAGER.read_config()
|
|
541
|
+
break
|
|
542
|
+
if connection_name is not None:
|
|
543
|
+
connections = CONFIG_MANAGER["connections"]
|
|
544
|
+
if connection_name not in connections:
|
|
545
|
+
raise Error(
|
|
546
|
+
f"Invalid connection_name '{connection_name}',"
|
|
547
|
+
f" known ones are {list(connections.keys())}"
|
|
548
|
+
)
|
|
549
|
+
kwargs = {**connections[connection_name], **kwargs}
|
|
550
|
+
elif is_kwargs_empty:
|
|
551
|
+
# connection_name is None and kwargs was empty when called
|
|
552
|
+
kwargs = _get_default_connection_params()
|
|
553
|
+
self.__set_error_attributes()
|
|
554
|
+
self.connect(**kwargs)
|
|
555
|
+
self._telemetry = TelemetryClient(self._rest)
|
|
556
|
+
self.expired = False
|
|
557
|
+
|
|
558
|
+
# get the imported modules from sys.modules
|
|
559
|
+
self._log_telemetry_imported_packages()
|
|
560
|
+
# check SNOW-1218851 for long term improvement plan to refactor ocsp code
|
|
561
|
+
atexit.register(self._close_at_exit)
|
|
562
|
+
|
|
563
|
+
# Set up the file operation parser and stream downloader.
|
|
564
|
+
self._file_operation_parser = FileOperationParser(self)
|
|
565
|
+
self._stream_downloader = StreamDownloader(self)
|
|
566
|
+
|
|
567
|
+
# Deprecated
|
|
568
|
+
@property
|
|
569
|
+
def insecure_mode(self) -> bool:
|
|
570
|
+
return self._disable_ocsp_checks
|
|
571
|
+
|
|
572
|
+
@property
|
|
573
|
+
def disable_ocsp_checks(self) -> bool:
|
|
574
|
+
return self._disable_ocsp_checks
|
|
575
|
+
|
|
576
|
+
@property
|
|
577
|
+
def ocsp_fail_open(self) -> bool:
|
|
578
|
+
return self._ocsp_fail_open
|
|
579
|
+
|
|
580
|
+
def _ocsp_mode(self) -> OCSPMode:
|
|
581
|
+
"""OCSP mode. DISABLE_OCSP_CHECKS, FAIL_OPEN or FAIL_CLOSED."""
|
|
582
|
+
if self.disable_ocsp_checks:
|
|
583
|
+
return OCSPMode.DISABLE_OCSP_CHECKS
|
|
584
|
+
elif self.ocsp_fail_open:
|
|
585
|
+
return OCSPMode.FAIL_OPEN
|
|
586
|
+
else:
|
|
587
|
+
return OCSPMode.FAIL_CLOSED
|
|
588
|
+
|
|
589
|
+
@property
|
|
590
|
+
def session_id(self) -> int:
|
|
591
|
+
return self._session_id
|
|
592
|
+
|
|
593
|
+
@property
|
|
594
|
+
def user(self) -> str:
|
|
595
|
+
return self._user
|
|
596
|
+
|
|
597
|
+
@property
|
|
598
|
+
def host(self) -> str:
|
|
599
|
+
return self._host
|
|
600
|
+
|
|
601
|
+
@property
|
|
602
|
+
def port(self) -> int:
|
|
603
|
+
return int(self._port)
|
|
604
|
+
|
|
605
|
+
@property
|
|
606
|
+
def region(self) -> str | None:
|
|
607
|
+
warnings.warn(
|
|
608
|
+
"Region has been deprecated and will be removed in the near future",
|
|
609
|
+
PendingDeprecationWarning,
|
|
610
|
+
# Raise warning from where this property was called from
|
|
611
|
+
stacklevel=2,
|
|
612
|
+
)
|
|
613
|
+
return self._region
|
|
614
|
+
|
|
615
|
+
@property
|
|
616
|
+
def proxy_host(self) -> str | None:
|
|
617
|
+
return self._proxy_host
|
|
618
|
+
|
|
619
|
+
@property
|
|
620
|
+
def proxy_port(self) -> str | None:
|
|
621
|
+
return self._proxy_port
|
|
622
|
+
|
|
623
|
+
@property
|
|
624
|
+
def proxy_user(self) -> str | None:
|
|
625
|
+
return self._proxy_user
|
|
626
|
+
|
|
627
|
+
@property
|
|
628
|
+
def proxy_password(self) -> str | None:
|
|
629
|
+
return self._proxy_password
|
|
630
|
+
|
|
631
|
+
@property
|
|
632
|
+
def account(self) -> str:
|
|
633
|
+
return self._account
|
|
634
|
+
|
|
635
|
+
@property
|
|
636
|
+
def database(self) -> str | None:
|
|
637
|
+
return self._database
|
|
638
|
+
|
|
639
|
+
@property
|
|
640
|
+
def schema(self) -> str | None:
|
|
641
|
+
return self._schema
|
|
642
|
+
|
|
643
|
+
@property
|
|
644
|
+
def warehouse(self) -> str | None:
|
|
645
|
+
return self._warehouse
|
|
646
|
+
|
|
647
|
+
@property
|
|
648
|
+
def role(self) -> str | None:
|
|
649
|
+
return self._role
|
|
650
|
+
|
|
651
|
+
@property
|
|
652
|
+
def login_timeout(self) -> int | None:
|
|
653
|
+
return int(self._login_timeout) if self._login_timeout is not None else None
|
|
654
|
+
|
|
655
|
+
@property
|
|
656
|
+
def network_timeout(self) -> int | None:
|
|
657
|
+
return int(self._network_timeout) if self._network_timeout is not None else None
|
|
658
|
+
|
|
659
|
+
@property
|
|
660
|
+
def socket_timeout(self) -> int | None:
|
|
661
|
+
return int(self._socket_timeout) if self._socket_timeout is not None else None
|
|
662
|
+
|
|
663
|
+
@property
|
|
664
|
+
def _backoff_generator(self) -> Iterator:
|
|
665
|
+
return self._backoff_policy()
|
|
666
|
+
|
|
667
|
+
@property
|
|
668
|
+
def client_session_keep_alive(self) -> bool | None:
|
|
669
|
+
return self._client_session_keep_alive
|
|
670
|
+
|
|
671
|
+
@client_session_keep_alive.setter
|
|
672
|
+
def client_session_keep_alive(self, value) -> None:
|
|
673
|
+
self._client_session_keep_alive = value
|
|
674
|
+
|
|
675
|
+
@property
|
|
676
|
+
def client_session_keep_alive_heartbeat_frequency(self) -> int | None:
|
|
677
|
+
return self._client_session_keep_alive_heartbeat_frequency
|
|
678
|
+
|
|
679
|
+
@client_session_keep_alive_heartbeat_frequency.setter
|
|
680
|
+
def client_session_keep_alive_heartbeat_frequency(self, value) -> None:
|
|
681
|
+
self._client_session_keep_alive_heartbeat_frequency = value
|
|
682
|
+
self._validate_client_session_keep_alive_heartbeat_frequency()
|
|
683
|
+
|
|
684
|
+
@property
|
|
685
|
+
def client_prefetch_threads(self) -> int:
|
|
686
|
+
return (
|
|
687
|
+
self._client_prefetch_threads
|
|
688
|
+
if self._client_prefetch_threads
|
|
689
|
+
else DEFAULT_CLIENT_PREFETCH_THREADS
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
@client_prefetch_threads.setter
|
|
693
|
+
def client_prefetch_threads(self, value) -> None:
|
|
694
|
+
self._client_prefetch_threads = value
|
|
695
|
+
self._validate_client_prefetch_threads()
|
|
696
|
+
|
|
697
|
+
@property
|
|
698
|
+
def client_fetch_threads(self) -> int | None:
|
|
699
|
+
return self._client_fetch_threads
|
|
700
|
+
|
|
701
|
+
@client_fetch_threads.setter
|
|
702
|
+
def client_fetch_threads(self, value: None | int) -> None:
|
|
703
|
+
if value is not None:
|
|
704
|
+
value = min(max(1, value), MAX_CLIENT_FETCH_THREADS)
|
|
705
|
+
self._client_fetch_threads = value
|
|
706
|
+
|
|
707
|
+
@property
|
|
708
|
+
def client_fetch_use_mp(self) -> bool:
|
|
709
|
+
return self._client_fetch_use_mp
|
|
710
|
+
|
|
711
|
+
@property
|
|
712
|
+
def rest(self) -> SnowflakeRestful | None:
|
|
713
|
+
return self._rest
|
|
714
|
+
|
|
715
|
+
@property
|
|
716
|
+
def application(self) -> str:
|
|
717
|
+
return self._application
|
|
718
|
+
|
|
719
|
+
@property
|
|
720
|
+
def errorhandler(self) -> Callable: # TODO: callable args
|
|
721
|
+
return self._errorhandler
|
|
722
|
+
|
|
723
|
+
@errorhandler.setter
|
|
724
|
+
# Note: Callable doesn't implement operator|
|
|
725
|
+
def errorhandler(self, value: Callable | None) -> None:
|
|
726
|
+
if value is None:
|
|
727
|
+
raise ProgrammingError("None errorhandler is specified")
|
|
728
|
+
self._errorhandler = value
|
|
729
|
+
|
|
730
|
+
@property
|
|
731
|
+
def converter_class(self) -> type[SnowflakeConverter]:
|
|
732
|
+
return self._converter_class
|
|
733
|
+
|
|
734
|
+
@property
|
|
735
|
+
def validate_default_parameters(self) -> bool:
|
|
736
|
+
return self._validate_default_parameters
|
|
737
|
+
|
|
738
|
+
@property
|
|
739
|
+
def is_pyformat(self) -> bool:
|
|
740
|
+
return self._paramstyle in ("pyformat", "format")
|
|
741
|
+
|
|
742
|
+
@property
|
|
743
|
+
def consent_cache_id_token(self):
|
|
744
|
+
return self._consent_cache_id_token
|
|
745
|
+
|
|
746
|
+
@property
|
|
747
|
+
def telemetry_enabled(self) -> bool:
|
|
748
|
+
return bool(
|
|
749
|
+
self._client_param_telemetry_enabled
|
|
750
|
+
and self._server_param_telemetry_enabled
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
@telemetry_enabled.setter
|
|
754
|
+
def telemetry_enabled(self, value) -> None:
|
|
755
|
+
self._client_param_telemetry_enabled = True if value else False
|
|
756
|
+
if (
|
|
757
|
+
self._client_param_telemetry_enabled
|
|
758
|
+
and not self._server_param_telemetry_enabled
|
|
759
|
+
):
|
|
760
|
+
logger.info(
|
|
761
|
+
"Telemetry has been disabled by the session parameter CLIENT_TELEMETRY_ENABLED."
|
|
762
|
+
" Set session parameter CLIENT_TELEMETRY_ENABLED to true to enable telemetry."
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
@property
|
|
766
|
+
def service_name(self) -> str | None:
|
|
767
|
+
return self._service_name
|
|
768
|
+
|
|
769
|
+
@service_name.setter
|
|
770
|
+
def service_name(self, value) -> None:
|
|
771
|
+
self._service_name = value
|
|
772
|
+
|
|
773
|
+
@property
|
|
774
|
+
def log_max_query_length(self) -> int:
|
|
775
|
+
return self._log_max_query_length
|
|
776
|
+
|
|
777
|
+
@property
|
|
778
|
+
def disable_request_pooling(self) -> bool:
|
|
779
|
+
return self._disable_request_pooling
|
|
780
|
+
|
|
781
|
+
@disable_request_pooling.setter
|
|
782
|
+
def disable_request_pooling(self, value) -> None:
|
|
783
|
+
self._disable_request_pooling = True if value else False
|
|
784
|
+
|
|
785
|
+
@property
|
|
786
|
+
def use_openssl_only(self) -> bool:
|
|
787
|
+
# Deprecated, kept for backwards compatibility
|
|
788
|
+
return True
|
|
789
|
+
|
|
790
|
+
@property
|
|
791
|
+
def arrow_number_to_decimal(self):
|
|
792
|
+
return self._arrow_number_to_decimal
|
|
793
|
+
|
|
794
|
+
@property
|
|
795
|
+
def enable_stage_s3_privatelink_for_us_east_1(self) -> bool:
|
|
796
|
+
return self._enable_stage_s3_privatelink_for_us_east_1
|
|
797
|
+
|
|
798
|
+
@enable_stage_s3_privatelink_for_us_east_1.setter
|
|
799
|
+
def enable_stage_s3_privatelink_for_us_east_1(self, value) -> None:
|
|
800
|
+
self._enable_stage_s3_privatelink_for_us_east_1 = True if value else False
|
|
801
|
+
|
|
802
|
+
@property
|
|
803
|
+
def enable_connection_diag(self) -> bool:
|
|
804
|
+
return self._enable_connection_diag
|
|
805
|
+
|
|
806
|
+
@property
|
|
807
|
+
def connection_diag_log_path(self):
|
|
808
|
+
return self._connection_diag_log_path
|
|
809
|
+
|
|
810
|
+
@property
|
|
811
|
+
def connection_diag_whitelist_path(self):
|
|
812
|
+
"""
|
|
813
|
+
Old version of ``connection_diag_allowlist_path``.
|
|
814
|
+
This used to be the original name, but snowflake backend
|
|
815
|
+
deprecated whitelist for allowlist. This name will be
|
|
816
|
+
deprecated in the future.
|
|
817
|
+
"""
|
|
818
|
+
warnings.warn(
|
|
819
|
+
"connection_diag_whitelist_path has been deprecated, use connection_diag_allowlist_path instead",
|
|
820
|
+
DeprecationWarning,
|
|
821
|
+
stacklevel=2,
|
|
822
|
+
)
|
|
823
|
+
return self._connection_diag_whitelist_path
|
|
824
|
+
|
|
825
|
+
@property
|
|
826
|
+
def connection_diag_allowlist_path(self):
|
|
827
|
+
return self._connection_diag_allowlist_path
|
|
828
|
+
|
|
829
|
+
@arrow_number_to_decimal.setter
|
|
830
|
+
def arrow_number_to_decimal_setter(self, value: bool) -> None:
|
|
831
|
+
self._arrow_number_to_decimal = value
|
|
832
|
+
|
|
833
|
+
@property
|
|
834
|
+
def auth_class(self) -> AuthByPlugin | None:
|
|
835
|
+
return self._auth_class
|
|
836
|
+
|
|
837
|
+
@auth_class.setter
|
|
838
|
+
def auth_class(self, value: AuthByPlugin) -> None:
|
|
839
|
+
if isinstance(value, AuthByPlugin):
|
|
840
|
+
self._auth_class = value
|
|
841
|
+
else:
|
|
842
|
+
raise TypeError("auth_class must subclass AuthByPlugin")
|
|
843
|
+
|
|
844
|
+
@property
|
|
845
|
+
def is_query_context_cache_disabled(self) -> bool:
|
|
846
|
+
return self._disable_query_context_cache
|
|
847
|
+
|
|
848
|
+
@property
|
|
849
|
+
def iobound_tpe_limit(self) -> int | None:
|
|
850
|
+
return self._iobound_tpe_limit
|
|
851
|
+
|
|
852
|
+
@property
|
|
853
|
+
def unsafe_file_write(self) -> bool:
|
|
854
|
+
return self._unsafe_file_write
|
|
855
|
+
|
|
856
|
+
@unsafe_file_write.setter
|
|
857
|
+
def unsafe_file_write(self, value: bool) -> None:
|
|
858
|
+
self._unsafe_file_write = value
|
|
859
|
+
|
|
860
|
+
@property
|
|
861
|
+
def check_arrow_conversion_error_on_every_column(self) -> bool:
|
|
862
|
+
return self._check_arrow_conversion_error_on_every_column
|
|
863
|
+
|
|
864
|
+
@check_arrow_conversion_error_on_every_column.setter
|
|
865
|
+
def check_arrow_conversion_error_on_every_column(self, value: bool) -> bool:
|
|
866
|
+
self._check_arrow_conversion_error_on_every_column = value
|
|
867
|
+
|
|
868
|
+
def connect(self, **kwargs) -> None:
|
|
869
|
+
"""Establishes connection to Snowflake."""
|
|
870
|
+
logger.debug("connect")
|
|
871
|
+
if len(kwargs) > 0:
|
|
872
|
+
self.__config(**kwargs)
|
|
873
|
+
|
|
874
|
+
if self.enable_connection_diag:
|
|
875
|
+
exceptions_dict = {}
|
|
876
|
+
connection_diag = ConnectionDiagnostic(
|
|
877
|
+
account=self.account,
|
|
878
|
+
host=self.host,
|
|
879
|
+
connection_diag_log_path=self.connection_diag_log_path,
|
|
880
|
+
connection_diag_allowlist_path=(
|
|
881
|
+
self.connection_diag_allowlist_path
|
|
882
|
+
if self.connection_diag_allowlist_path is not None
|
|
883
|
+
else self.connection_diag_whitelist_path
|
|
884
|
+
),
|
|
885
|
+
proxy_host=self.proxy_host,
|
|
886
|
+
proxy_port=self.proxy_port,
|
|
887
|
+
proxy_user=self.proxy_user,
|
|
888
|
+
proxy_password=self.proxy_password,
|
|
889
|
+
)
|
|
890
|
+
try:
|
|
891
|
+
connection_diag.run_test()
|
|
892
|
+
self.__open_connection()
|
|
893
|
+
connection_diag.cursor = self.cursor()
|
|
894
|
+
except Exception:
|
|
895
|
+
exceptions_dict["connection_test"] = traceback.format_exc()
|
|
896
|
+
logger.warning(
|
|
897
|
+
f"""Exception during connection test:\n{exceptions_dict["connection_test"]} """
|
|
898
|
+
)
|
|
899
|
+
try:
|
|
900
|
+
connection_diag.run_post_test()
|
|
901
|
+
except Exception:
|
|
902
|
+
exceptions_dict["post_test"] = traceback.format_exc()
|
|
903
|
+
logger.warning(
|
|
904
|
+
f"""Exception during post connection test:\n{exceptions_dict["post_test"]} """
|
|
905
|
+
)
|
|
906
|
+
finally:
|
|
907
|
+
connection_diag.generate_report()
|
|
908
|
+
if exceptions_dict:
|
|
909
|
+
raise Exception(str(exceptions_dict))
|
|
910
|
+
else:
|
|
911
|
+
self.__open_connection()
|
|
912
|
+
|
|
913
|
+
def close(self, retry: bool = True) -> None:
|
|
914
|
+
"""Closes the connection."""
|
|
915
|
+
# unregister to dereference connection object as it's already closed after the execution
|
|
916
|
+
atexit.unregister(self._close_at_exit)
|
|
917
|
+
try:
|
|
918
|
+
if not self.rest:
|
|
919
|
+
logger.debug("Rest object has been destroyed, cannot close session")
|
|
920
|
+
return
|
|
921
|
+
|
|
922
|
+
# will hang if the application doesn't close the connection and
|
|
923
|
+
# CLIENT_SESSION_KEEP_ALIVE is set, because the heartbeat runs on
|
|
924
|
+
# a separate thread.
|
|
925
|
+
self._cancel_heartbeat()
|
|
926
|
+
|
|
927
|
+
# close telemetry first, since it needs rest to send remaining data
|
|
928
|
+
logger.debug("closed")
|
|
929
|
+
self._telemetry.close(send_on_close=bool(retry and self.telemetry_enabled))
|
|
930
|
+
if (
|
|
931
|
+
self._all_async_queries_finished()
|
|
932
|
+
and not self._server_session_keep_alive
|
|
933
|
+
):
|
|
934
|
+
logger.debug("No async queries seem to be running, deleting session")
|
|
935
|
+
self.rest.delete_session(retry=retry)
|
|
936
|
+
else:
|
|
937
|
+
logger.debug(
|
|
938
|
+
"There are {} async queries still running, not deleting session".format(
|
|
939
|
+
len(self._async_sfqids)
|
|
940
|
+
)
|
|
941
|
+
)
|
|
942
|
+
self.rest.close()
|
|
943
|
+
self._rest = None
|
|
944
|
+
if self.query_context_cache:
|
|
945
|
+
self.query_context_cache.clear_cache()
|
|
946
|
+
del self.messages[:]
|
|
947
|
+
logger.debug("Session is closed")
|
|
948
|
+
except Exception as e:
|
|
949
|
+
logger.debug(
|
|
950
|
+
"Exception encountered in closing connection. ignoring...: %s", e
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
def is_closed(self) -> bool:
|
|
954
|
+
"""Checks whether the connection has been closed."""
|
|
955
|
+
return self.rest is None
|
|
956
|
+
|
|
957
|
+
def autocommit(self, mode) -> None:
|
|
958
|
+
"""Sets autocommit mode to True, or False. Defaults to True."""
|
|
959
|
+
if not self.rest:
|
|
960
|
+
Error.errorhandler_wrapper(
|
|
961
|
+
self,
|
|
962
|
+
None,
|
|
963
|
+
DatabaseError,
|
|
964
|
+
{
|
|
965
|
+
"msg": "Connection is closed",
|
|
966
|
+
"errno": ER_CONNECTION_IS_CLOSED,
|
|
967
|
+
"sqlstate": SQLSTATE_CONNECTION_NOT_EXISTS,
|
|
968
|
+
},
|
|
969
|
+
)
|
|
970
|
+
if not isinstance(mode, bool):
|
|
971
|
+
Error.errorhandler_wrapper(
|
|
972
|
+
self,
|
|
973
|
+
None,
|
|
974
|
+
ProgrammingError,
|
|
975
|
+
{
|
|
976
|
+
"msg": f"Invalid parameter: {mode}",
|
|
977
|
+
"errno": ER_INVALID_VALUE,
|
|
978
|
+
},
|
|
979
|
+
)
|
|
980
|
+
try:
|
|
981
|
+
self.cursor().execute(f"ALTER SESSION SET autocommit={mode}")
|
|
982
|
+
except Error as e:
|
|
983
|
+
if e.sqlstate == SQLSTATE_FEATURE_NOT_SUPPORTED:
|
|
984
|
+
logger.debug(
|
|
985
|
+
"Autocommit feature is not enabled for this " "connection. Ignored"
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
def commit(self) -> None:
|
|
989
|
+
"""Commits the current transaction."""
|
|
990
|
+
self.cursor().execute("COMMIT")
|
|
991
|
+
|
|
992
|
+
def rollback(self) -> None:
|
|
993
|
+
"""Rolls back the current transaction."""
|
|
994
|
+
self.cursor().execute("ROLLBACK")
|
|
995
|
+
|
|
996
|
+
def cursor(
|
|
997
|
+
self, cursor_class: type[SnowflakeCursor] = SnowflakeCursor
|
|
998
|
+
) -> SnowflakeCursor:
|
|
999
|
+
"""Creates a cursor object. Each statement will be executed in a new cursor object."""
|
|
1000
|
+
logger.debug("cursor")
|
|
1001
|
+
if not self.rest:
|
|
1002
|
+
Error.errorhandler_wrapper(
|
|
1003
|
+
self,
|
|
1004
|
+
None,
|
|
1005
|
+
DatabaseError,
|
|
1006
|
+
{
|
|
1007
|
+
"msg": "Connection is closed",
|
|
1008
|
+
"errno": ER_CONNECTION_IS_CLOSED,
|
|
1009
|
+
"sqlstate": SQLSTATE_CONNECTION_NOT_EXISTS,
|
|
1010
|
+
},
|
|
1011
|
+
)
|
|
1012
|
+
return cursor_class(self)
|
|
1013
|
+
|
|
1014
|
+
def execute_string(
|
|
1015
|
+
self,
|
|
1016
|
+
sql_text: str,
|
|
1017
|
+
remove_comments: bool = False,
|
|
1018
|
+
return_cursors: bool = True,
|
|
1019
|
+
cursor_class: SnowflakeCursor = SnowflakeCursor,
|
|
1020
|
+
**kwargs,
|
|
1021
|
+
) -> Iterable[SnowflakeCursor]:
|
|
1022
|
+
"""Executes a SQL text including multiple statements. This is a non-standard convenience method."""
|
|
1023
|
+
stream = StringIO(sql_text)
|
|
1024
|
+
stream_generator = self.execute_stream(
|
|
1025
|
+
stream, remove_comments=remove_comments, cursor_class=cursor_class, **kwargs
|
|
1026
|
+
)
|
|
1027
|
+
ret = list(stream_generator)
|
|
1028
|
+
return ret if return_cursors else list()
|
|
1029
|
+
|
|
1030
|
+
def execute_stream(
|
|
1031
|
+
self,
|
|
1032
|
+
stream: StringIO,
|
|
1033
|
+
remove_comments: bool = False,
|
|
1034
|
+
cursor_class: SnowflakeCursor = SnowflakeCursor,
|
|
1035
|
+
**kwargs,
|
|
1036
|
+
) -> Generator[SnowflakeCursor]:
|
|
1037
|
+
"""Executes a stream of SQL statements. This is a non-standard convenient method."""
|
|
1038
|
+
split_statements_list = split_statements(
|
|
1039
|
+
stream, remove_comments=remove_comments
|
|
1040
|
+
)
|
|
1041
|
+
# Note: split_statements_list is a list of tuples of sql statements and whether they are put/get
|
|
1042
|
+
non_empty_statements = [e for e in split_statements_list if e[0]]
|
|
1043
|
+
for sql, is_put_or_get in non_empty_statements:
|
|
1044
|
+
cur = self.cursor(cursor_class=cursor_class)
|
|
1045
|
+
cur.execute(sql, _is_put_get=is_put_or_get, **kwargs)
|
|
1046
|
+
yield cur
|
|
1047
|
+
|
|
1048
|
+
def __set_error_attributes(self) -> None:
|
|
1049
|
+
for m in [
|
|
1050
|
+
method for method in dir(errors) if callable(getattr(errors, method))
|
|
1051
|
+
]:
|
|
1052
|
+
# If name starts with _ then ignore that
|
|
1053
|
+
name = m if not m.startswith("_") else m[1:]
|
|
1054
|
+
setattr(self, name, getattr(errors, m))
|
|
1055
|
+
|
|
1056
|
+
@staticmethod
|
|
1057
|
+
def setup_ocsp_privatelink(app, hostname) -> None:
|
|
1058
|
+
hostname = hostname.lower()
|
|
1059
|
+
SnowflakeConnection.OCSP_ENV_LOCK.acquire()
|
|
1060
|
+
ocsp_cache_server = f"http://ocsp.{hostname}/ocsp_response_cache.json"
|
|
1061
|
+
os.environ["SF_OCSP_RESPONSE_CACHE_SERVER_URL"] = ocsp_cache_server
|
|
1062
|
+
logger.debug("OCSP Cache Server is updated: %s", ocsp_cache_server)
|
|
1063
|
+
SnowflakeConnection.OCSP_ENV_LOCK.release()
|
|
1064
|
+
|
|
1065
|
+
def __open_connection(self):
|
|
1066
|
+
"""Opens a new network connection."""
|
|
1067
|
+
self.converter = self._converter_class(
|
|
1068
|
+
use_numpy=self._numpy, support_negative_year=self._support_negative_year
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
proxy.set_proxies(
|
|
1072
|
+
self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
self._rest = SnowflakeRestful(
|
|
1076
|
+
host=self.host,
|
|
1077
|
+
port=self.port,
|
|
1078
|
+
protocol=self._protocol,
|
|
1079
|
+
inject_client_pause=self._inject_client_pause,
|
|
1080
|
+
connection=self,
|
|
1081
|
+
)
|
|
1082
|
+
logger.debug("REST API object was created: %s:%s", self.host, self.port)
|
|
1083
|
+
|
|
1084
|
+
if "SF_OCSP_RESPONSE_CACHE_SERVER_URL" in os.environ:
|
|
1085
|
+
logger.debug(
|
|
1086
|
+
"Custom OCSP Cache Server URL found in environment - %s",
|
|
1087
|
+
os.environ["SF_OCSP_RESPONSE_CACHE_SERVER_URL"],
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
if ".privatelink.snowflakecomputing." in self.host.lower():
|
|
1091
|
+
SnowflakeConnection.setup_ocsp_privatelink(self.application, self.host)
|
|
1092
|
+
else:
|
|
1093
|
+
if "SF_OCSP_RESPONSE_CACHE_SERVER_URL" in os.environ:
|
|
1094
|
+
del os.environ["SF_OCSP_RESPONSE_CACHE_SERVER_URL"]
|
|
1095
|
+
|
|
1096
|
+
if self._session_parameters is None:
|
|
1097
|
+
self._session_parameters = {}
|
|
1098
|
+
if self._autocommit is not None:
|
|
1099
|
+
self._session_parameters[PARAMETER_AUTOCOMMIT] = self._autocommit
|
|
1100
|
+
|
|
1101
|
+
if self._timezone is not None:
|
|
1102
|
+
self._session_parameters[PARAMETER_TIMEZONE] = self._timezone
|
|
1103
|
+
|
|
1104
|
+
if self._validate_default_parameters:
|
|
1105
|
+
# Snowflake will validate the requested database, schema, and warehouse
|
|
1106
|
+
self._session_parameters[PARAMETER_CLIENT_VALIDATE_DEFAULT_PARAMETERS] = (
|
|
1107
|
+
True
|
|
1108
|
+
)
|
|
1109
|
+
|
|
1110
|
+
if self.client_session_keep_alive is not None:
|
|
1111
|
+
self._session_parameters[PARAMETER_CLIENT_SESSION_KEEP_ALIVE] = (
|
|
1112
|
+
self._client_session_keep_alive
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
if self.client_session_keep_alive_heartbeat_frequency is not None:
|
|
1116
|
+
self._session_parameters[
|
|
1117
|
+
PARAMETER_CLIENT_SESSION_KEEP_ALIVE_HEARTBEAT_FREQUENCY
|
|
1118
|
+
] = self._validate_client_session_keep_alive_heartbeat_frequency()
|
|
1119
|
+
|
|
1120
|
+
if self.client_prefetch_threads:
|
|
1121
|
+
self._session_parameters[PARAMETER_CLIENT_PREFETCH_THREADS] = (
|
|
1122
|
+
self._validate_client_prefetch_threads()
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
# Setup authenticator
|
|
1126
|
+
auth = Auth(self.rest)
|
|
1127
|
+
|
|
1128
|
+
if self._session_token and self._master_token:
|
|
1129
|
+
auth._rest.update_tokens(
|
|
1130
|
+
self._session_token,
|
|
1131
|
+
self._master_token,
|
|
1132
|
+
self._master_validity_in_seconds,
|
|
1133
|
+
)
|
|
1134
|
+
heartbeat_ret = auth._rest._heartbeat()
|
|
1135
|
+
logger.debug(heartbeat_ret)
|
|
1136
|
+
if not heartbeat_ret or not heartbeat_ret.get("success"):
|
|
1137
|
+
Error.errorhandler_wrapper(
|
|
1138
|
+
self,
|
|
1139
|
+
None,
|
|
1140
|
+
ProgrammingError,
|
|
1141
|
+
{
|
|
1142
|
+
"msg": "Session and master tokens invalid",
|
|
1143
|
+
"errno": ER_INVALID_VALUE,
|
|
1144
|
+
},
|
|
1145
|
+
)
|
|
1146
|
+
else:
|
|
1147
|
+
logger.debug("Session and master token validation successful.")
|
|
1148
|
+
|
|
1149
|
+
else:
|
|
1150
|
+
if self.auth_class is not None:
|
|
1151
|
+
if type(
|
|
1152
|
+
self.auth_class
|
|
1153
|
+
) not in FIRST_PARTY_AUTHENTICATORS and not issubclass(
|
|
1154
|
+
type(self.auth_class), AuthByKeyPair
|
|
1155
|
+
):
|
|
1156
|
+
raise TypeError("auth_class must be a child class of AuthByKeyPair")
|
|
1157
|
+
# TODO: add telemetry for custom auth
|
|
1158
|
+
self.auth_class = self.auth_class
|
|
1159
|
+
elif self._authenticator == DEFAULT_AUTHENTICATOR:
|
|
1160
|
+
self.auth_class = AuthByDefault(
|
|
1161
|
+
password=self._password,
|
|
1162
|
+
timeout=self.login_timeout,
|
|
1163
|
+
backoff_generator=self._backoff_generator,
|
|
1164
|
+
)
|
|
1165
|
+
elif self._authenticator == EXTERNAL_BROWSER_AUTHENTICATOR:
|
|
1166
|
+
self._session_parameters[
|
|
1167
|
+
PARAMETER_CLIENT_STORE_TEMPORARY_CREDENTIAL
|
|
1168
|
+
] = (self._client_store_temporary_credential if IS_LINUX else True)
|
|
1169
|
+
auth.read_temporary_credentials(
|
|
1170
|
+
self.host,
|
|
1171
|
+
self.user,
|
|
1172
|
+
self._session_parameters,
|
|
1173
|
+
)
|
|
1174
|
+
# Depending on whether self._rest.id_token is available we do different
|
|
1175
|
+
# auth_instance
|
|
1176
|
+
if self._rest.id_token is None:
|
|
1177
|
+
self.auth_class = AuthByWebBrowser(
|
|
1178
|
+
application=self.application,
|
|
1179
|
+
protocol=self._protocol,
|
|
1180
|
+
host=self.host, # TODO: delete this?
|
|
1181
|
+
port=self.port,
|
|
1182
|
+
timeout=self.login_timeout,
|
|
1183
|
+
backoff_generator=self._backoff_generator,
|
|
1184
|
+
)
|
|
1185
|
+
else:
|
|
1186
|
+
self.auth_class = AuthByIdToken(
|
|
1187
|
+
id_token=self._rest.id_token,
|
|
1188
|
+
application=self.application,
|
|
1189
|
+
protocol=self._protocol,
|
|
1190
|
+
host=self.host,
|
|
1191
|
+
port=self.port,
|
|
1192
|
+
timeout=self.login_timeout,
|
|
1193
|
+
backoff_generator=self._backoff_generator,
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
elif self._authenticator == KEY_PAIR_AUTHENTICATOR:
|
|
1197
|
+
private_key = self._private_key
|
|
1198
|
+
|
|
1199
|
+
if self._private_key_file:
|
|
1200
|
+
private_key = _get_private_bytes_from_file(
|
|
1201
|
+
self._private_key_file,
|
|
1202
|
+
self._private_key_file_pwd,
|
|
1203
|
+
)
|
|
1204
|
+
|
|
1205
|
+
self.auth_class = AuthByKeyPair(
|
|
1206
|
+
private_key=private_key,
|
|
1207
|
+
timeout=self.login_timeout,
|
|
1208
|
+
backoff_generator=self._backoff_generator,
|
|
1209
|
+
)
|
|
1210
|
+
elif self._authenticator == OAUTH_AUTHENTICATOR:
|
|
1211
|
+
self.auth_class = AuthByOAuth(
|
|
1212
|
+
oauth_token=self._token,
|
|
1213
|
+
timeout=self.login_timeout,
|
|
1214
|
+
backoff_generator=self._backoff_generator,
|
|
1215
|
+
)
|
|
1216
|
+
elif self._authenticator == OAUTH_AUTHORIZATION_CODE:
|
|
1217
|
+
if self._role and (self._oauth_scope == ""):
|
|
1218
|
+
# if role is known then let's inject it into scope
|
|
1219
|
+
self._oauth_scope = _OAUTH_DEFAULT_SCOPE.format(role=self._role)
|
|
1220
|
+
self.auth_class = AuthByOauthCode(
|
|
1221
|
+
application=self.application,
|
|
1222
|
+
client_id=self._oauth_client_id,
|
|
1223
|
+
client_secret=self._oauth_client_secret,
|
|
1224
|
+
host=self.host,
|
|
1225
|
+
authentication_url=self._oauth_authorization_url.format(
|
|
1226
|
+
host=self.host, port=self.port
|
|
1227
|
+
),
|
|
1228
|
+
token_request_url=self._oauth_token_request_url.format(
|
|
1229
|
+
host=self.host, port=self.port
|
|
1230
|
+
),
|
|
1231
|
+
redirect_uri=self._oauth_redirect_uri,
|
|
1232
|
+
scope=self._oauth_scope,
|
|
1233
|
+
pkce_enabled=not self._oauth_disable_pkce,
|
|
1234
|
+
token_cache=(
|
|
1235
|
+
auth.get_token_cache()
|
|
1236
|
+
if self._client_store_temporary_credential
|
|
1237
|
+
else None
|
|
1238
|
+
),
|
|
1239
|
+
refresh_token_enabled=self._oauth_enable_refresh_tokens,
|
|
1240
|
+
external_browser_timeout=self._external_browser_timeout,
|
|
1241
|
+
enable_single_use_refresh_tokens=self._oauth_enable_single_use_refresh_tokens,
|
|
1242
|
+
)
|
|
1243
|
+
elif self._authenticator == OAUTH_CLIENT_CREDENTIALS:
|
|
1244
|
+
if self._role and (self._oauth_scope == ""):
|
|
1245
|
+
# if role is known then let's inject it into scope
|
|
1246
|
+
self._oauth_scope = _OAUTH_DEFAULT_SCOPE.format(role=self._role)
|
|
1247
|
+
self.auth_class = AuthByOauthCredentials(
|
|
1248
|
+
application=self.application,
|
|
1249
|
+
client_id=self._oauth_client_id,
|
|
1250
|
+
client_secret=self._oauth_client_secret,
|
|
1251
|
+
token_request_url=self._oauth_token_request_url.format(
|
|
1252
|
+
host=self.host, port=self.port
|
|
1253
|
+
),
|
|
1254
|
+
scope=self._oauth_scope,
|
|
1255
|
+
token_cache=(
|
|
1256
|
+
auth.get_token_cache()
|
|
1257
|
+
if self._client_store_temporary_credential
|
|
1258
|
+
else None
|
|
1259
|
+
),
|
|
1260
|
+
refresh_token_enabled=self._oauth_enable_refresh_tokens,
|
|
1261
|
+
connection=self,
|
|
1262
|
+
)
|
|
1263
|
+
elif self._authenticator == USR_PWD_MFA_AUTHENTICATOR:
|
|
1264
|
+
self._session_parameters[PARAMETER_CLIENT_REQUEST_MFA_TOKEN] = (
|
|
1265
|
+
self._client_request_mfa_token if IS_LINUX else True
|
|
1266
|
+
)
|
|
1267
|
+
if self._session_parameters[PARAMETER_CLIENT_REQUEST_MFA_TOKEN]:
|
|
1268
|
+
auth.read_temporary_credentials(
|
|
1269
|
+
self.host,
|
|
1270
|
+
self.user,
|
|
1271
|
+
self._session_parameters,
|
|
1272
|
+
)
|
|
1273
|
+
self.auth_class = AuthByUsrPwdMfa(
|
|
1274
|
+
password=self._password,
|
|
1275
|
+
mfa_token=self.rest.mfa_token,
|
|
1276
|
+
timeout=self.login_timeout,
|
|
1277
|
+
backoff_generator=self._backoff_generator,
|
|
1278
|
+
)
|
|
1279
|
+
elif self._authenticator == PROGRAMMATIC_ACCESS_TOKEN:
|
|
1280
|
+
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
|
+
)
|
|
1290
|
+
elif self._authenticator == WORKLOAD_IDENTITY_AUTHENTICATOR:
|
|
1291
|
+
self._check_experimental_authentication_flag()
|
|
1292
|
+
# Standardize the provider enum.
|
|
1293
|
+
if self._workload_identity_provider and isinstance(
|
|
1294
|
+
self._workload_identity_provider, str
|
|
1295
|
+
):
|
|
1296
|
+
self._workload_identity_provider = AttestationProvider.from_string(
|
|
1297
|
+
self._workload_identity_provider
|
|
1298
|
+
)
|
|
1299
|
+
self.auth_class = AuthByWorkloadIdentity(
|
|
1300
|
+
provider=self._workload_identity_provider,
|
|
1301
|
+
token=self._token,
|
|
1302
|
+
entra_resource=self._workload_identity_entra_resource,
|
|
1303
|
+
)
|
|
1304
|
+
else:
|
|
1305
|
+
# okta URL, e.g., https://<account>.okta.com/
|
|
1306
|
+
self.auth_class = AuthByOkta(
|
|
1307
|
+
application=self.application,
|
|
1308
|
+
timeout=self.login_timeout,
|
|
1309
|
+
backoff_generator=self._backoff_generator,
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
self.authenticate_with_retry(self.auth_class)
|
|
1313
|
+
|
|
1314
|
+
self._password = None # ensure password won't persist
|
|
1315
|
+
self.auth_class.reset_secrets()
|
|
1316
|
+
|
|
1317
|
+
self.initialize_query_context_cache()
|
|
1318
|
+
|
|
1319
|
+
if self.client_session_keep_alive:
|
|
1320
|
+
# This will be called after the heartbeat frequency has actually been set.
|
|
1321
|
+
# By this point it should have been decided if the heartbeat has to be enabled
|
|
1322
|
+
# and what would the heartbeat frequency be
|
|
1323
|
+
self._add_heartbeat()
|
|
1324
|
+
|
|
1325
|
+
def __config(self, **kwargs):
|
|
1326
|
+
"""Sets up parameters in the connection object."""
|
|
1327
|
+
logger.debug("__config")
|
|
1328
|
+
# Handle special cases first
|
|
1329
|
+
if "sequence_counter" in kwargs:
|
|
1330
|
+
self.sequence_counter = kwargs["sequence_counter"]
|
|
1331
|
+
if "application" in kwargs:
|
|
1332
|
+
value = kwargs["application"]
|
|
1333
|
+
if not APPLICATION_RE.match(value):
|
|
1334
|
+
msg = f"Invalid application name: {value}"
|
|
1335
|
+
raise ProgrammingError(msg=msg, errno=0)
|
|
1336
|
+
else:
|
|
1337
|
+
self._application = value
|
|
1338
|
+
if "validate_default_parameters" in kwargs:
|
|
1339
|
+
self._validate_default_parameters = kwargs["validate_default_parameters"]
|
|
1340
|
+
# Handle rest of arguments
|
|
1341
|
+
skip_list = ["validate_default_parameters", "sequence_counter", "application"]
|
|
1342
|
+
for name, value in filter(lambda e: e[0] not in skip_list, kwargs.items()):
|
|
1343
|
+
if self.validate_default_parameters:
|
|
1344
|
+
if name not in DEFAULT_CONFIGURATION.keys():
|
|
1345
|
+
close_matches = get_close_matches(
|
|
1346
|
+
name, DEFAULT_CONFIGURATION.keys(), n=1, cutoff=0.8
|
|
1347
|
+
)
|
|
1348
|
+
guess = close_matches[0] if len(close_matches) > 0 else None
|
|
1349
|
+
warnings.warn(
|
|
1350
|
+
"'{}' is an unknown connection parameter{}".format(
|
|
1351
|
+
name, f", did you mean '{guess}'?" if guess else ""
|
|
1352
|
+
),
|
|
1353
|
+
# Raise warning from where class was initiated
|
|
1354
|
+
stacklevel=4,
|
|
1355
|
+
)
|
|
1356
|
+
elif not isinstance(value, DEFAULT_CONFIGURATION[name][1]):
|
|
1357
|
+
accepted_types = DEFAULT_CONFIGURATION[name][1]
|
|
1358
|
+
warnings.warn(
|
|
1359
|
+
"'{}' connection parameter should be of type '{}', but is a '{}'".format(
|
|
1360
|
+
name,
|
|
1361
|
+
(
|
|
1362
|
+
str(tuple(e.__name__ for e in accepted_types)).replace(
|
|
1363
|
+
"'", ""
|
|
1364
|
+
)
|
|
1365
|
+
if isinstance(accepted_types, tuple)
|
|
1366
|
+
else accepted_types.__name__
|
|
1367
|
+
),
|
|
1368
|
+
type(value).__name__,
|
|
1369
|
+
),
|
|
1370
|
+
# Raise warning from where class was initiated
|
|
1371
|
+
stacklevel=4,
|
|
1372
|
+
)
|
|
1373
|
+
setattr(self, "_" + name, value)
|
|
1374
|
+
|
|
1375
|
+
if self._numpy:
|
|
1376
|
+
try:
|
|
1377
|
+
import numpy # noqa: F401
|
|
1378
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
1379
|
+
Error.errorhandler_wrapper(
|
|
1380
|
+
self,
|
|
1381
|
+
None,
|
|
1382
|
+
ProgrammingError,
|
|
1383
|
+
{
|
|
1384
|
+
"msg": "Numpy module is not installed. Cannot fetch data as numpy",
|
|
1385
|
+
"errno": ER_NO_NUMPY,
|
|
1386
|
+
},
|
|
1387
|
+
)
|
|
1388
|
+
|
|
1389
|
+
if self._paramstyle is None:
|
|
1390
|
+
import snowflake.connector
|
|
1391
|
+
|
|
1392
|
+
self._paramstyle = snowflake.connector.paramstyle
|
|
1393
|
+
elif self._paramstyle not in SUPPORTED_PARAMSTYLES:
|
|
1394
|
+
raise ProgrammingError(
|
|
1395
|
+
msg="Invalid paramstyle is specified", errno=ER_INVALID_VALUE
|
|
1396
|
+
)
|
|
1397
|
+
|
|
1398
|
+
if self._auth_class and not isinstance(self._auth_class, AuthByPlugin):
|
|
1399
|
+
raise TypeError("auth_class must subclass AuthByPlugin")
|
|
1400
|
+
|
|
1401
|
+
if "account" in kwargs:
|
|
1402
|
+
if "host" not in kwargs:
|
|
1403
|
+
self._host = construct_hostname(kwargs.get("region"), self._account)
|
|
1404
|
+
|
|
1405
|
+
if "unsafe_file_write" in kwargs:
|
|
1406
|
+
self._unsafe_file_write = kwargs["unsafe_file_write"]
|
|
1407
|
+
else:
|
|
1408
|
+
self._unsafe_file_write = False
|
|
1409
|
+
|
|
1410
|
+
logger.info(
|
|
1411
|
+
f"Connecting to {_DOMAIN_NAME_MAP.get(extract_top_level_domain_from_hostname(self._host), 'GLOBAL')} Snowflake domain"
|
|
1412
|
+
)
|
|
1413
|
+
|
|
1414
|
+
# If using a custom auth class, we should set the authenticator
|
|
1415
|
+
# type to be the same as the custom auth class
|
|
1416
|
+
if self._auth_class:
|
|
1417
|
+
self._authenticator = self._auth_class.type_.value
|
|
1418
|
+
|
|
1419
|
+
if self._authenticator:
|
|
1420
|
+
# Only upper self._authenticator if it is a non-okta link
|
|
1421
|
+
auth_tmp = self._authenticator.upper()
|
|
1422
|
+
if auth_tmp in [ # Non-okta authenticators
|
|
1423
|
+
DEFAULT_AUTHENTICATOR,
|
|
1424
|
+
EXTERNAL_BROWSER_AUTHENTICATOR,
|
|
1425
|
+
KEY_PAIR_AUTHENTICATOR,
|
|
1426
|
+
OAUTH_AUTHENTICATOR,
|
|
1427
|
+
USR_PWD_MFA_AUTHENTICATOR,
|
|
1428
|
+
WORKLOAD_IDENTITY_AUTHENTICATOR,
|
|
1429
|
+
PAT_WITH_EXTERNAL_SESSION,
|
|
1430
|
+
]:
|
|
1431
|
+
self._authenticator = auth_tmp
|
|
1432
|
+
|
|
1433
|
+
# read OAuth token from
|
|
1434
|
+
token_file_path = kwargs.get("token_file_path")
|
|
1435
|
+
if token_file_path:
|
|
1436
|
+
with open(token_file_path) as f:
|
|
1437
|
+
self._token = f.read()
|
|
1438
|
+
|
|
1439
|
+
# Set of authenticators allowing empty user.
|
|
1440
|
+
empty_user_allowed_authenticators = {
|
|
1441
|
+
OAUTH_AUTHENTICATOR,
|
|
1442
|
+
NO_AUTH_AUTHENTICATOR,
|
|
1443
|
+
WORKLOAD_IDENTITY_AUTHENTICATOR,
|
|
1444
|
+
PROGRAMMATIC_ACCESS_TOKEN,
|
|
1445
|
+
PAT_WITH_EXTERNAL_SESSION,
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
if not (self._master_token and self._session_token):
|
|
1449
|
+
if (
|
|
1450
|
+
not self.user
|
|
1451
|
+
and self._authenticator not in empty_user_allowed_authenticators
|
|
1452
|
+
):
|
|
1453
|
+
# Some authenticators do not require a username
|
|
1454
|
+
Error.errorhandler_wrapper(
|
|
1455
|
+
self,
|
|
1456
|
+
None,
|
|
1457
|
+
ProgrammingError,
|
|
1458
|
+
{"msg": "User is empty", "errno": ER_NO_USER},
|
|
1459
|
+
)
|
|
1460
|
+
|
|
1461
|
+
if self._private_key or self._private_key_file:
|
|
1462
|
+
self._authenticator = KEY_PAIR_AUTHENTICATOR
|
|
1463
|
+
|
|
1464
|
+
workload_identity_dependent_options = [
|
|
1465
|
+
"workload_identity_provider",
|
|
1466
|
+
"workload_identity_entra_resource",
|
|
1467
|
+
]
|
|
1468
|
+
for dependent_option in workload_identity_dependent_options:
|
|
1469
|
+
if (
|
|
1470
|
+
self.__getattribute__(f"_{dependent_option}") is not None
|
|
1471
|
+
and self._authenticator != WORKLOAD_IDENTITY_AUTHENTICATOR
|
|
1472
|
+
):
|
|
1473
|
+
Error.errorhandler_wrapper(
|
|
1474
|
+
self,
|
|
1475
|
+
None,
|
|
1476
|
+
ProgrammingError,
|
|
1477
|
+
{
|
|
1478
|
+
"msg": f"{dependent_option} was set but authenticator was not set to {WORKLOAD_IDENTITY_AUTHENTICATOR}",
|
|
1479
|
+
"errno": ER_INVALID_WIF_SETTINGS,
|
|
1480
|
+
},
|
|
1481
|
+
)
|
|
1482
|
+
|
|
1483
|
+
if (
|
|
1484
|
+
self.auth_class is None
|
|
1485
|
+
and self._authenticator
|
|
1486
|
+
not in (
|
|
1487
|
+
EXTERNAL_BROWSER_AUTHENTICATOR,
|
|
1488
|
+
OAUTH_AUTHENTICATOR,
|
|
1489
|
+
OAUTH_AUTHORIZATION_CODE,
|
|
1490
|
+
OAUTH_CLIENT_CREDENTIALS,
|
|
1491
|
+
KEY_PAIR_AUTHENTICATOR,
|
|
1492
|
+
PROGRAMMATIC_ACCESS_TOKEN,
|
|
1493
|
+
WORKLOAD_IDENTITY_AUTHENTICATOR,
|
|
1494
|
+
PAT_WITH_EXTERNAL_SESSION,
|
|
1495
|
+
)
|
|
1496
|
+
and not self._password
|
|
1497
|
+
):
|
|
1498
|
+
Error.errorhandler_wrapper(
|
|
1499
|
+
self,
|
|
1500
|
+
None,
|
|
1501
|
+
ProgrammingError,
|
|
1502
|
+
{"msg": "Password is empty", "errno": ER_NO_PASSWORD},
|
|
1503
|
+
)
|
|
1504
|
+
|
|
1505
|
+
# Only AuthNoAuth allows account to be omitted.
|
|
1506
|
+
if not self._account and not isinstance(self.auth_class, AuthNoAuth):
|
|
1507
|
+
Error.errorhandler_wrapper(
|
|
1508
|
+
self,
|
|
1509
|
+
None,
|
|
1510
|
+
ProgrammingError,
|
|
1511
|
+
{"msg": "Account must be specified", "errno": ER_NO_ACCOUNT_NAME},
|
|
1512
|
+
)
|
|
1513
|
+
if self._account and "." in self._account:
|
|
1514
|
+
self._account = parse_account(self._account)
|
|
1515
|
+
|
|
1516
|
+
if not isinstance(self._backoff_policy, Callable) or not isinstance(
|
|
1517
|
+
self._backoff_policy(), Iterator
|
|
1518
|
+
):
|
|
1519
|
+
Error.errorhandler_wrapper(
|
|
1520
|
+
self,
|
|
1521
|
+
None,
|
|
1522
|
+
ProgrammingError,
|
|
1523
|
+
{
|
|
1524
|
+
"msg": "Backoff policy must be a generator function",
|
|
1525
|
+
"errno": ER_INVALID_BACKOFF_POLICY,
|
|
1526
|
+
},
|
|
1527
|
+
)
|
|
1528
|
+
|
|
1529
|
+
if self.ocsp_fail_open:
|
|
1530
|
+
logger.debug(
|
|
1531
|
+
"This connection is in OCSP Fail Open Mode. "
|
|
1532
|
+
"TLS Certificates would be checked for validity "
|
|
1533
|
+
"and revocation status. Any other Certificate "
|
|
1534
|
+
"Revocation related exceptions or OCSP Responder "
|
|
1535
|
+
"failures would be disregarded in favor of "
|
|
1536
|
+
"connectivity."
|
|
1537
|
+
)
|
|
1538
|
+
|
|
1539
|
+
if self.disable_ocsp_checks:
|
|
1540
|
+
logger.debug(
|
|
1541
|
+
"This connection runs with disabled OCSP checks. "
|
|
1542
|
+
"Revocation status of the certificate will not be checked against OCSP Responder."
|
|
1543
|
+
)
|
|
1544
|
+
|
|
1545
|
+
def cmd_query(
|
|
1546
|
+
self,
|
|
1547
|
+
sql: str,
|
|
1548
|
+
sequence_counter: int,
|
|
1549
|
+
request_id: uuid.UUID,
|
|
1550
|
+
binding_params: None | tuple | dict[str, dict[str, str]] = None,
|
|
1551
|
+
binding_stage: str | None = None,
|
|
1552
|
+
is_file_transfer: bool = False,
|
|
1553
|
+
statement_params: dict[str, str] | None = None,
|
|
1554
|
+
is_internal: bool = False,
|
|
1555
|
+
describe_only: bool = False,
|
|
1556
|
+
_no_results: bool = False,
|
|
1557
|
+
_update_current_object: bool = True,
|
|
1558
|
+
_no_retry: bool = False,
|
|
1559
|
+
timeout: int | None = None,
|
|
1560
|
+
dataframe_ast: str | None = None,
|
|
1561
|
+
) -> dict[str, Any]:
|
|
1562
|
+
"""Executes a query with a sequence counter."""
|
|
1563
|
+
logger.debug("_cmd_query")
|
|
1564
|
+
data = {
|
|
1565
|
+
"sqlText": sql,
|
|
1566
|
+
"asyncExec": _no_results,
|
|
1567
|
+
"sequenceId": sequence_counter,
|
|
1568
|
+
"querySubmissionTime": get_time_millis(),
|
|
1569
|
+
}
|
|
1570
|
+
if dataframe_ast is not None:
|
|
1571
|
+
data["dataframeAst"] = dataframe_ast
|
|
1572
|
+
if statement_params is not None:
|
|
1573
|
+
data["parameters"] = statement_params
|
|
1574
|
+
if is_internal:
|
|
1575
|
+
data["isInternal"] = is_internal
|
|
1576
|
+
if describe_only:
|
|
1577
|
+
data["describeOnly"] = describe_only
|
|
1578
|
+
if binding_stage is not None:
|
|
1579
|
+
# binding stage for bulk array binding
|
|
1580
|
+
data["bindStage"] = binding_stage
|
|
1581
|
+
if binding_params is not None:
|
|
1582
|
+
# binding parameters. This is for qmarks paramstyle.
|
|
1583
|
+
data["bindings"] = binding_params
|
|
1584
|
+
if not _no_results:
|
|
1585
|
+
# not an async query.
|
|
1586
|
+
queryContext = self.get_query_context()
|
|
1587
|
+
# Here queryContextDTO should be a dict object field, same with `parameters` field
|
|
1588
|
+
data["queryContextDTO"] = queryContext
|
|
1589
|
+
client = "sfsql_file_transfer" if is_file_transfer else "sfsql"
|
|
1590
|
+
|
|
1591
|
+
if logger.getEffectiveLevel() <= logging.DEBUG:
|
|
1592
|
+
logger.debug(
|
|
1593
|
+
"sql=[%s], sequence_id=[%s], is_file_transfer=[%s]",
|
|
1594
|
+
self._format_query_for_log(data["sqlText"]),
|
|
1595
|
+
data["sequenceId"],
|
|
1596
|
+
is_file_transfer,
|
|
1597
|
+
)
|
|
1598
|
+
|
|
1599
|
+
url_parameters = {REQUEST_ID: request_id}
|
|
1600
|
+
|
|
1601
|
+
ret = self.rest.request(
|
|
1602
|
+
"/queries/v1/query-request?" + urlencode(url_parameters),
|
|
1603
|
+
data,
|
|
1604
|
+
client=client,
|
|
1605
|
+
_no_results=_no_results,
|
|
1606
|
+
_include_retry_params=True,
|
|
1607
|
+
_no_retry=_no_retry,
|
|
1608
|
+
timeout=timeout,
|
|
1609
|
+
)
|
|
1610
|
+
|
|
1611
|
+
if ret is None:
|
|
1612
|
+
ret = {"data": {}}
|
|
1613
|
+
if ret.get("data") is None:
|
|
1614
|
+
ret["data"] = {}
|
|
1615
|
+
if _update_current_object:
|
|
1616
|
+
data = ret["data"]
|
|
1617
|
+
if "finalDatabaseName" in data and data["finalDatabaseName"] is not None:
|
|
1618
|
+
self._database = data["finalDatabaseName"]
|
|
1619
|
+
if "finalSchemaName" in data and data["finalSchemaName"] is not None:
|
|
1620
|
+
self._schema = data["finalSchemaName"]
|
|
1621
|
+
if "finalWarehouseName" in data and data["finalWarehouseName"] is not None:
|
|
1622
|
+
self._warehouse = data["finalWarehouseName"]
|
|
1623
|
+
if "finalRoleName" in data:
|
|
1624
|
+
self._role = data["finalRoleName"]
|
|
1625
|
+
if "queryContext" in data and not _no_results:
|
|
1626
|
+
# here the data["queryContext"] field has been automatically converted from JSON into a dict type
|
|
1627
|
+
self.set_query_context(data["queryContext"])
|
|
1628
|
+
|
|
1629
|
+
return ret
|
|
1630
|
+
|
|
1631
|
+
def _reauthenticate(self):
|
|
1632
|
+
return self._auth_class.reauthenticate(conn=self)
|
|
1633
|
+
|
|
1634
|
+
def authenticate_with_retry(self, auth_instance) -> None:
|
|
1635
|
+
# make some changes if needed before real __authenticate
|
|
1636
|
+
try:
|
|
1637
|
+
self._authenticate(auth_instance)
|
|
1638
|
+
except ReauthenticationRequest as ex:
|
|
1639
|
+
# cached id_token expiration error, we have cleaned id_token and try to authenticate again
|
|
1640
|
+
logger.debug("ID token expired. Reauthenticating...: %s", ex)
|
|
1641
|
+
if type(auth_instance) in (
|
|
1642
|
+
AuthByIdToken,
|
|
1643
|
+
AuthByOauthCode,
|
|
1644
|
+
AuthByOauthCredentials,
|
|
1645
|
+
):
|
|
1646
|
+
# IDToken and OAuth auth need to authenticate through
|
|
1647
|
+
# SSO if its credential has expired
|
|
1648
|
+
self._reauthenticate()
|
|
1649
|
+
else:
|
|
1650
|
+
self._authenticate(auth_instance)
|
|
1651
|
+
|
|
1652
|
+
def _authenticate(self, auth_instance: AuthByPlugin):
|
|
1653
|
+
auth_instance.prepare(
|
|
1654
|
+
conn=self,
|
|
1655
|
+
authenticator=self._authenticator,
|
|
1656
|
+
service_name=self.service_name,
|
|
1657
|
+
account=self.account,
|
|
1658
|
+
user=self.user,
|
|
1659
|
+
password=self._password,
|
|
1660
|
+
)
|
|
1661
|
+
self._consent_cache_id_token = getattr(
|
|
1662
|
+
auth_instance, "consent_cache_id_token", True
|
|
1663
|
+
)
|
|
1664
|
+
|
|
1665
|
+
auth = Auth(self.rest)
|
|
1666
|
+
# record start time for computing timeout
|
|
1667
|
+
auth_instance._retry_ctx.set_start_time()
|
|
1668
|
+
try:
|
|
1669
|
+
auth.authenticate(
|
|
1670
|
+
auth_instance=auth_instance,
|
|
1671
|
+
account=self.account,
|
|
1672
|
+
user=self.user,
|
|
1673
|
+
database=self.database,
|
|
1674
|
+
schema=self.schema,
|
|
1675
|
+
warehouse=self.warehouse,
|
|
1676
|
+
role=self.role,
|
|
1677
|
+
passcode=self._passcode,
|
|
1678
|
+
passcode_in_password=self._passcode_in_password,
|
|
1679
|
+
mfa_callback=self._mfa_callback,
|
|
1680
|
+
password_callback=self._password_callback,
|
|
1681
|
+
session_parameters=self._session_parameters,
|
|
1682
|
+
)
|
|
1683
|
+
except OperationalError as e:
|
|
1684
|
+
logger.debug(
|
|
1685
|
+
"Operational Error raised at authentication"
|
|
1686
|
+
f"for authenticator: {type(auth_instance).__name__}"
|
|
1687
|
+
)
|
|
1688
|
+
while True:
|
|
1689
|
+
try:
|
|
1690
|
+
auth_instance.handle_timeout(
|
|
1691
|
+
authenticator=self._authenticator,
|
|
1692
|
+
service_name=self.service_name,
|
|
1693
|
+
account=self.account,
|
|
1694
|
+
user=self.user,
|
|
1695
|
+
password=self._password,
|
|
1696
|
+
)
|
|
1697
|
+
auth.authenticate(
|
|
1698
|
+
auth_instance=auth_instance,
|
|
1699
|
+
account=self.account,
|
|
1700
|
+
user=self.user,
|
|
1701
|
+
database=self.database,
|
|
1702
|
+
schema=self.schema,
|
|
1703
|
+
warehouse=self.warehouse,
|
|
1704
|
+
role=self.role,
|
|
1705
|
+
passcode=self._passcode,
|
|
1706
|
+
passcode_in_password=self._passcode_in_password,
|
|
1707
|
+
mfa_callback=self._mfa_callback,
|
|
1708
|
+
password_callback=self._password_callback,
|
|
1709
|
+
session_parameters=self._session_parameters,
|
|
1710
|
+
)
|
|
1711
|
+
except OperationalError as auth_op:
|
|
1712
|
+
if auth_op.errno == ER_FAILED_TO_CONNECT_TO_DB:
|
|
1713
|
+
if _CONNECTIVITY_ERR_MSG in e.msg:
|
|
1714
|
+
auth_op.msg += f"\n{_CONNECTIVITY_ERR_MSG}"
|
|
1715
|
+
raise auth_op from e
|
|
1716
|
+
logger.debug("Continuing authenticator specific timeout handling")
|
|
1717
|
+
continue
|
|
1718
|
+
break
|
|
1719
|
+
|
|
1720
|
+
def _write_params_to_byte_rows(
|
|
1721
|
+
self, params: list[tuple[Any | tuple]]
|
|
1722
|
+
) -> list[bytes]:
|
|
1723
|
+
"""Write csv-format rows of binding values as list of bytes string.
|
|
1724
|
+
|
|
1725
|
+
Args:
|
|
1726
|
+
params: Binding parameters to bulk array insertion query with qmark/numeric format.
|
|
1727
|
+
cursor: SnowflakeCursor.
|
|
1728
|
+
|
|
1729
|
+
Returns:
|
|
1730
|
+
List of bytes string corresponding to rows
|
|
1731
|
+
|
|
1732
|
+
"""
|
|
1733
|
+
res = []
|
|
1734
|
+
try:
|
|
1735
|
+
for row in params:
|
|
1736
|
+
temp = map(self.converter.to_csv_bindings, row)
|
|
1737
|
+
res.append((",".join(temp) + "\n").encode("utf-8"))
|
|
1738
|
+
except (ProgrammingError, AttributeError) as exc:
|
|
1739
|
+
raise BindUploadError from exc
|
|
1740
|
+
return res
|
|
1741
|
+
|
|
1742
|
+
def _get_snowflake_type_and_binding(
|
|
1743
|
+
self,
|
|
1744
|
+
cursor: SnowflakeCursor | None,
|
|
1745
|
+
v: tuple[str, Any] | Any,
|
|
1746
|
+
) -> TypeAndBinding:
|
|
1747
|
+
if isinstance(v, tuple):
|
|
1748
|
+
if len(v) != 2:
|
|
1749
|
+
Error.errorhandler_wrapper(
|
|
1750
|
+
self,
|
|
1751
|
+
cursor,
|
|
1752
|
+
ProgrammingError,
|
|
1753
|
+
{
|
|
1754
|
+
"msg": "Binding parameters must be a list "
|
|
1755
|
+
"where one element is a single value or "
|
|
1756
|
+
"a pair of Snowflake datatype and a value",
|
|
1757
|
+
"errno": ER_FAILED_PROCESSING_QMARK,
|
|
1758
|
+
},
|
|
1759
|
+
)
|
|
1760
|
+
snowflake_type, v = v
|
|
1761
|
+
else:
|
|
1762
|
+
snowflake_type = self.converter.snowflake_type(v)
|
|
1763
|
+
if snowflake_type is None:
|
|
1764
|
+
Error.errorhandler_wrapper(
|
|
1765
|
+
self,
|
|
1766
|
+
cursor,
|
|
1767
|
+
ProgrammingError,
|
|
1768
|
+
{
|
|
1769
|
+
"msg": "Python data type [{}] cannot be "
|
|
1770
|
+
"automatically mapped to Snowflake data "
|
|
1771
|
+
"type. Specify the snowflake data type "
|
|
1772
|
+
"explicitly.".format(v.__class__.__name__.lower()),
|
|
1773
|
+
"errno": ER_NOT_IMPLICITY_SNOWFLAKE_DATATYPE,
|
|
1774
|
+
},
|
|
1775
|
+
)
|
|
1776
|
+
return TypeAndBinding(
|
|
1777
|
+
snowflake_type,
|
|
1778
|
+
self.converter.to_snowflake_bindings(snowflake_type, v),
|
|
1779
|
+
)
|
|
1780
|
+
|
|
1781
|
+
# TODO we could probably rework this to not make dicts like this: {'1': 'value', '2': '13'}
|
|
1782
|
+
def _process_params_qmarks(
|
|
1783
|
+
self,
|
|
1784
|
+
params: Sequence | None,
|
|
1785
|
+
cursor: SnowflakeCursor | None = None,
|
|
1786
|
+
) -> dict[str, dict[str, str]] | None:
|
|
1787
|
+
if not params:
|
|
1788
|
+
return None
|
|
1789
|
+
processed_params = {}
|
|
1790
|
+
|
|
1791
|
+
get_type_and_binding = partial(self._get_snowflake_type_and_binding, cursor)
|
|
1792
|
+
|
|
1793
|
+
for idx, v in enumerate(params):
|
|
1794
|
+
if isinstance(v, list):
|
|
1795
|
+
snowflake_type = self.converter.snowflake_type(v)
|
|
1796
|
+
all_param_data = list(map(get_type_and_binding, v))
|
|
1797
|
+
first_type = all_param_data[0].type
|
|
1798
|
+
# if all elements have the same snowflake type, update snowflake_type
|
|
1799
|
+
if all(param_data.type == first_type for param_data in all_param_data):
|
|
1800
|
+
snowflake_type = first_type
|
|
1801
|
+
processed_params[str(idx + 1)] = {
|
|
1802
|
+
"type": snowflake_type,
|
|
1803
|
+
"value": [param_data.binding for param_data in all_param_data],
|
|
1804
|
+
}
|
|
1805
|
+
else:
|
|
1806
|
+
snowflake_type, snowflake_binding = get_type_and_binding(v)
|
|
1807
|
+
processed_params[str(idx + 1)] = {
|
|
1808
|
+
"type": snowflake_type,
|
|
1809
|
+
"value": snowflake_binding,
|
|
1810
|
+
}
|
|
1811
|
+
if logger.getEffectiveLevel() <= logging.DEBUG:
|
|
1812
|
+
for k, v in processed_params.items():
|
|
1813
|
+
logger.debug("idx: %s, type: %s", k, v.get("type"))
|
|
1814
|
+
return processed_params
|
|
1815
|
+
|
|
1816
|
+
def _process_params_pyformat(
|
|
1817
|
+
self,
|
|
1818
|
+
params: Any | Sequence[Any] | dict[Any, Any] | None,
|
|
1819
|
+
cursor: SnowflakeCursor | None = None,
|
|
1820
|
+
) -> tuple[Any] | dict[str, Any] | None:
|
|
1821
|
+
"""Process parameters for client-side parameter binding.
|
|
1822
|
+
|
|
1823
|
+
Args:
|
|
1824
|
+
params: Either a sequence, or a dictionary of parameters, if anything else
|
|
1825
|
+
is given then it will be put into a list and processed that way.
|
|
1826
|
+
cursor: The SnowflakeCursor used to report errors if necessary.
|
|
1827
|
+
"""
|
|
1828
|
+
if params is None:
|
|
1829
|
+
if self._interpolate_empty_sequences:
|
|
1830
|
+
return None
|
|
1831
|
+
return {}
|
|
1832
|
+
if isinstance(params, dict):
|
|
1833
|
+
return self._process_params_dict(params)
|
|
1834
|
+
|
|
1835
|
+
# TODO: remove this, callers should send in what's in the signature
|
|
1836
|
+
if not isinstance(params, (tuple, list)):
|
|
1837
|
+
params = [
|
|
1838
|
+
params,
|
|
1839
|
+
]
|
|
1840
|
+
|
|
1841
|
+
try:
|
|
1842
|
+
res = map(self._process_single_param, params)
|
|
1843
|
+
ret = tuple(res)
|
|
1844
|
+
logger.debug(f"parameters: {ret}")
|
|
1845
|
+
return ret
|
|
1846
|
+
except Exception as e:
|
|
1847
|
+
Error.errorhandler_wrapper(
|
|
1848
|
+
self,
|
|
1849
|
+
cursor,
|
|
1850
|
+
ProgrammingError,
|
|
1851
|
+
{
|
|
1852
|
+
"msg": f"Failed processing pyformat-parameters; {e}",
|
|
1853
|
+
"errno": ER_FAILED_PROCESSING_PYFORMAT,
|
|
1854
|
+
},
|
|
1855
|
+
)
|
|
1856
|
+
|
|
1857
|
+
def _process_params_dict(
|
|
1858
|
+
self, params: dict[Any, Any], cursor: SnowflakeCursor | None = None
|
|
1859
|
+
) -> dict:
|
|
1860
|
+
try:
|
|
1861
|
+
res = {k: self._process_single_param(v) for k, v in params.items()}
|
|
1862
|
+
logger.debug(f"parameters: {res}")
|
|
1863
|
+
return res
|
|
1864
|
+
except Exception as e:
|
|
1865
|
+
Error.errorhandler_wrapper(
|
|
1866
|
+
self,
|
|
1867
|
+
cursor,
|
|
1868
|
+
ProgrammingError,
|
|
1869
|
+
{
|
|
1870
|
+
"msg": f"Failed processing pyformat-parameters: {e}",
|
|
1871
|
+
"errno": ER_FAILED_PROCESSING_PYFORMAT,
|
|
1872
|
+
},
|
|
1873
|
+
)
|
|
1874
|
+
|
|
1875
|
+
def _process_single_param(self, param: Any) -> Any:
|
|
1876
|
+
"""Process a single parameter to Snowflake understandable form.
|
|
1877
|
+
|
|
1878
|
+
This is a convenience function to replace repeated multiple calls with a single
|
|
1879
|
+
function call.
|
|
1880
|
+
|
|
1881
|
+
It calls the following underlying functions in this order:
|
|
1882
|
+
1. self.converter.to_snowflake
|
|
1883
|
+
2. self.converter.escape
|
|
1884
|
+
3. self.converter.quote
|
|
1885
|
+
"""
|
|
1886
|
+
to_snowflake = self.converter.to_snowflake
|
|
1887
|
+
escape = self.converter.escape
|
|
1888
|
+
_quote = self.converter.quote
|
|
1889
|
+
return _quote(escape(to_snowflake(param)))
|
|
1890
|
+
|
|
1891
|
+
def _cancel_query(self, sql: str, request_id: UUID) -> dict[str, bool | None]:
|
|
1892
|
+
"""Cancels the query with the exact SQL query and requestId."""
|
|
1893
|
+
logger.debug("_cancel_query sql=[%s], request_id=[%s]", sql, request_id)
|
|
1894
|
+
url_parameters = {REQUEST_ID: str(uuid.uuid4())}
|
|
1895
|
+
|
|
1896
|
+
return self.rest.request(
|
|
1897
|
+
"/queries/v1/abort-request?" + urlencode(url_parameters),
|
|
1898
|
+
{
|
|
1899
|
+
"sqlText": sql,
|
|
1900
|
+
REQUEST_ID: str(request_id),
|
|
1901
|
+
},
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
def _next_sequence_counter(self) -> int:
|
|
1905
|
+
"""Gets next sequence counter. Used internally."""
|
|
1906
|
+
with self._lock_sequence_counter:
|
|
1907
|
+
self.sequence_counter += 1
|
|
1908
|
+
logger.debug("sequence counter: %s", self.sequence_counter)
|
|
1909
|
+
return self.sequence_counter
|
|
1910
|
+
|
|
1911
|
+
def _log_telemetry(self, telemetry_data) -> None:
|
|
1912
|
+
"""Logs data to telemetry."""
|
|
1913
|
+
if self.telemetry_enabled:
|
|
1914
|
+
self._telemetry.try_add_log_to_batch(telemetry_data)
|
|
1915
|
+
|
|
1916
|
+
def _add_heartbeat(self) -> None:
|
|
1917
|
+
"""Add a periodic heartbeat query in order to keep connection alive."""
|
|
1918
|
+
if not self.heartbeat_thread:
|
|
1919
|
+
self._validate_client_session_keep_alive_heartbeat_frequency()
|
|
1920
|
+
heartbeat_wref = weakref.WeakMethod(self._heartbeat_tick)
|
|
1921
|
+
|
|
1922
|
+
def beat_if_possible() -> None:
|
|
1923
|
+
heartbeat_fn = heartbeat_wref()
|
|
1924
|
+
if heartbeat_fn:
|
|
1925
|
+
heartbeat_fn()
|
|
1926
|
+
|
|
1927
|
+
self.heartbeat_thread = HeartBeatTimer(
|
|
1928
|
+
self.client_session_keep_alive_heartbeat_frequency,
|
|
1929
|
+
beat_if_possible,
|
|
1930
|
+
)
|
|
1931
|
+
self.heartbeat_thread.start()
|
|
1932
|
+
logger.debug("started heartbeat")
|
|
1933
|
+
|
|
1934
|
+
def _cancel_heartbeat(self) -> None:
|
|
1935
|
+
"""Cancel a heartbeat thread."""
|
|
1936
|
+
if self.heartbeat_thread:
|
|
1937
|
+
self.heartbeat_thread.cancel()
|
|
1938
|
+
self.heartbeat_thread.join()
|
|
1939
|
+
self.heartbeat_thread = None
|
|
1940
|
+
logger.debug("stopped heartbeat")
|
|
1941
|
+
|
|
1942
|
+
def _heartbeat_tick(self) -> None:
|
|
1943
|
+
"""Execute a heartbeat if connection isn't closed yet."""
|
|
1944
|
+
if not self.is_closed():
|
|
1945
|
+
logger.debug("heartbeating!")
|
|
1946
|
+
self.rest._heartbeat()
|
|
1947
|
+
|
|
1948
|
+
def _validate_client_session_keep_alive_heartbeat_frequency(self) -> int:
|
|
1949
|
+
"""Validate and return heartbeat frequency in seconds."""
|
|
1950
|
+
real_max = int(self.rest.master_validity_in_seconds / 4)
|
|
1951
|
+
real_min = int(real_max / 4)
|
|
1952
|
+
|
|
1953
|
+
# ensure the type is integer
|
|
1954
|
+
self._client_session_keep_alive_heartbeat_frequency = int(
|
|
1955
|
+
self.client_session_keep_alive_heartbeat_frequency
|
|
1956
|
+
)
|
|
1957
|
+
|
|
1958
|
+
if self.client_session_keep_alive_heartbeat_frequency is None:
|
|
1959
|
+
# This is an unlikely scenario but covering it just in case.
|
|
1960
|
+
self._client_session_keep_alive_heartbeat_frequency = real_min
|
|
1961
|
+
elif self.client_session_keep_alive_heartbeat_frequency > real_max:
|
|
1962
|
+
self._client_session_keep_alive_heartbeat_frequency = real_max
|
|
1963
|
+
elif self.client_session_keep_alive_heartbeat_frequency < real_min:
|
|
1964
|
+
self._client_session_keep_alive_heartbeat_frequency = real_min
|
|
1965
|
+
|
|
1966
|
+
return self.client_session_keep_alive_heartbeat_frequency
|
|
1967
|
+
|
|
1968
|
+
def _validate_client_prefetch_threads(self) -> int:
|
|
1969
|
+
if self.client_prefetch_threads <= 0:
|
|
1970
|
+
self._client_prefetch_threads = 1
|
|
1971
|
+
elif self.client_prefetch_threads > MAX_CLIENT_PREFETCH_THREADS:
|
|
1972
|
+
self._client_prefetch_threads = MAX_CLIENT_PREFETCH_THREADS
|
|
1973
|
+
self._client_prefetch_threads = int(self.client_prefetch_threads)
|
|
1974
|
+
return self.client_prefetch_threads
|
|
1975
|
+
|
|
1976
|
+
def _update_parameters(
|
|
1977
|
+
self,
|
|
1978
|
+
parameters: dict[str, str | int | bool],
|
|
1979
|
+
) -> None:
|
|
1980
|
+
"""Update session parameters."""
|
|
1981
|
+
with self._lock_converter:
|
|
1982
|
+
self.converter.set_parameters(parameters)
|
|
1983
|
+
for name, value in parameters.items():
|
|
1984
|
+
self._session_parameters[name] = value
|
|
1985
|
+
if PARAMETER_CLIENT_TELEMETRY_ENABLED == name:
|
|
1986
|
+
self._server_param_telemetry_enabled = value
|
|
1987
|
+
elif PARAMETER_CLIENT_SESSION_KEEP_ALIVE == name:
|
|
1988
|
+
# Only set if the local config is None.
|
|
1989
|
+
# Always give preference to user config.
|
|
1990
|
+
if self.client_session_keep_alive is None:
|
|
1991
|
+
self.client_session_keep_alive = value
|
|
1992
|
+
elif (
|
|
1993
|
+
PARAMETER_CLIENT_SESSION_KEEP_ALIVE_HEARTBEAT_FREQUENCY == name
|
|
1994
|
+
and self.client_session_keep_alive_heartbeat_frequency is None
|
|
1995
|
+
):
|
|
1996
|
+
# Only set if local value hasn't been set already.
|
|
1997
|
+
self.client_session_keep_alive_heartbeat_frequency = value
|
|
1998
|
+
elif PARAMETER_SERVICE_NAME == name:
|
|
1999
|
+
self.service_name = value
|
|
2000
|
+
elif PARAMETER_CLIENT_PREFETCH_THREADS == name:
|
|
2001
|
+
self.client_prefetch_threads = value
|
|
2002
|
+
elif PARAMETER_ENABLE_STAGE_S3_PRIVATELINK_FOR_US_EAST_1 == name:
|
|
2003
|
+
self.enable_stage_s3_privatelink_for_us_east_1 = value
|
|
2004
|
+
elif PARAMETER_QUERY_CONTEXT_CACHE_SIZE == name:
|
|
2005
|
+
self.query_context_cache_size = value
|
|
2006
|
+
|
|
2007
|
+
def _format_query_for_log(self, query: str) -> str:
|
|
2008
|
+
ret = " ".join(line.strip() for line in query.split("\n"))
|
|
2009
|
+
return (
|
|
2010
|
+
ret
|
|
2011
|
+
if len(ret) < self.log_max_query_length
|
|
2012
|
+
else ret[0 : self.log_max_query_length] + "..."
|
|
2013
|
+
)
|
|
2014
|
+
|
|
2015
|
+
def __enter__(self) -> SnowflakeConnection:
|
|
2016
|
+
"""Context manager."""
|
|
2017
|
+
return self
|
|
2018
|
+
|
|
2019
|
+
def __exit__(
|
|
2020
|
+
self,
|
|
2021
|
+
exc_type: type[BaseException] | None,
|
|
2022
|
+
exc_val: BaseException | None,
|
|
2023
|
+
exc_tb: TracebackType | None,
|
|
2024
|
+
) -> None:
|
|
2025
|
+
"""Context manager with commit or rollback teardown."""
|
|
2026
|
+
if not self._session_parameters.get("AUTOCOMMIT", False):
|
|
2027
|
+
# Either AUTOCOMMIT is turned off, or is not set so we default to old behavior
|
|
2028
|
+
if exc_tb is None:
|
|
2029
|
+
self.commit()
|
|
2030
|
+
else:
|
|
2031
|
+
self.rollback()
|
|
2032
|
+
self.close()
|
|
2033
|
+
|
|
2034
|
+
def _get_query_status(self, sf_qid: str) -> tuple[QueryStatus, dict[str, Any]]:
|
|
2035
|
+
"""Retrieves the status of query with sf_qid and returns it with the raw response.
|
|
2036
|
+
|
|
2037
|
+
This is the underlying function used by the public get_status functions.
|
|
2038
|
+
|
|
2039
|
+
Args:
|
|
2040
|
+
sf_qid: Snowflake query id of interest.
|
|
2041
|
+
|
|
2042
|
+
Raises:
|
|
2043
|
+
ValueError: if sf_qid is not a valid UUID string.
|
|
2044
|
+
"""
|
|
2045
|
+
try:
|
|
2046
|
+
uuid.UUID(sf_qid)
|
|
2047
|
+
except ValueError:
|
|
2048
|
+
raise ValueError(f"Invalid UUID: '{sf_qid}'")
|
|
2049
|
+
logger.debug(f"get_query_status sf_qid='{sf_qid}'")
|
|
2050
|
+
|
|
2051
|
+
status = "NO_DATA"
|
|
2052
|
+
if self.is_closed():
|
|
2053
|
+
return QueryStatus.DISCONNECTED, {"data": {"queries": []}}
|
|
2054
|
+
status_resp = self.rest.request(
|
|
2055
|
+
"/monitoring/queries/" + quote(sf_qid), method="get", client="rest"
|
|
2056
|
+
)
|
|
2057
|
+
if "queries" not in status_resp["data"]:
|
|
2058
|
+
return QueryStatus.FAILED_WITH_ERROR, status_resp
|
|
2059
|
+
queries = status_resp["data"]["queries"]
|
|
2060
|
+
if len(queries) > 0:
|
|
2061
|
+
status = queries[0]["status"]
|
|
2062
|
+
status_ret = QueryStatus[status]
|
|
2063
|
+
return status_ret, status_resp
|
|
2064
|
+
|
|
2065
|
+
def _cache_query_status(self, sf_qid: str, status_ret: QueryStatus) -> None:
|
|
2066
|
+
# If query was started by us and it has finished let's cache this info
|
|
2067
|
+
if sf_qid in self._async_sfqids and not self.is_still_running(status_ret):
|
|
2068
|
+
self._async_sfqids.pop(
|
|
2069
|
+
sf_qid, None
|
|
2070
|
+
) # Prevent KeyError when multiple threads try to remove the same query id
|
|
2071
|
+
self._done_async_sfqids[sf_qid] = None
|
|
2072
|
+
|
|
2073
|
+
def _close_at_exit(self):
|
|
2074
|
+
with suppress(Exception):
|
|
2075
|
+
self.close(retry=False)
|
|
2076
|
+
|
|
2077
|
+
def _process_error_query_status(
|
|
2078
|
+
self,
|
|
2079
|
+
sf_qid: str,
|
|
2080
|
+
status_resp: dict,
|
|
2081
|
+
error_message: str = "",
|
|
2082
|
+
error_cls: type[Exception] = ProgrammingError,
|
|
2083
|
+
) -> None:
|
|
2084
|
+
status_resp = status_resp or {}
|
|
2085
|
+
data = status_resp.get("data", {})
|
|
2086
|
+
queries = data.get("queries")
|
|
2087
|
+
|
|
2088
|
+
if sf_qid in self._async_sfqids:
|
|
2089
|
+
self._async_sfqids.pop(sf_qid, None)
|
|
2090
|
+
message = status_resp.get("message")
|
|
2091
|
+
if message is None:
|
|
2092
|
+
message = ""
|
|
2093
|
+
code = queries[0].get("errorCode", -1) if queries else -1
|
|
2094
|
+
sql_state = None
|
|
2095
|
+
if "data" in status_resp:
|
|
2096
|
+
message += queries[0].get("errorMessage", "") if queries else ""
|
|
2097
|
+
sql_state = data.get("sqlState")
|
|
2098
|
+
Error.errorhandler_wrapper(
|
|
2099
|
+
self,
|
|
2100
|
+
None,
|
|
2101
|
+
error_cls,
|
|
2102
|
+
{
|
|
2103
|
+
"msg": message or error_message,
|
|
2104
|
+
"errno": int(code),
|
|
2105
|
+
"sqlstate": sql_state,
|
|
2106
|
+
"sfqid": sf_qid,
|
|
2107
|
+
},
|
|
2108
|
+
)
|
|
2109
|
+
|
|
2110
|
+
def get_query_status(self, sf_qid: str) -> QueryStatus:
|
|
2111
|
+
"""Retrieves the status of query with sf_qid.
|
|
2112
|
+
|
|
2113
|
+
Query status is returned as a QueryStatus.
|
|
2114
|
+
|
|
2115
|
+
Args:
|
|
2116
|
+
sf_qid: Snowflake query id of interest.
|
|
2117
|
+
|
|
2118
|
+
Raises:
|
|
2119
|
+
ValueError: if sf_qid is not a valid UUID string.
|
|
2120
|
+
"""
|
|
2121
|
+
status, _ = self._get_query_status(sf_qid)
|
|
2122
|
+
self._cache_query_status(sf_qid, status)
|
|
2123
|
+
return status
|
|
2124
|
+
|
|
2125
|
+
def get_query_status_throw_if_error(self, sf_qid: str) -> QueryStatus:
|
|
2126
|
+
"""Retrieves the status of query with sf_qid as a QueryStatus and raises an exception if the query terminated with an error.
|
|
2127
|
+
|
|
2128
|
+
Query status is returned as a QueryStatus.
|
|
2129
|
+
|
|
2130
|
+
Args:
|
|
2131
|
+
sf_qid: Snowflake query id of interest.
|
|
2132
|
+
|
|
2133
|
+
Raises:
|
|
2134
|
+
ValueError: if sf_qid is not a valid UUID string.
|
|
2135
|
+
"""
|
|
2136
|
+
status, status_resp = self._get_query_status(sf_qid)
|
|
2137
|
+
self._cache_query_status(sf_qid, status)
|
|
2138
|
+
if self.is_an_error(status):
|
|
2139
|
+
self._process_error_query_status(sf_qid, status_resp)
|
|
2140
|
+
return status
|
|
2141
|
+
|
|
2142
|
+
def initialize_query_context_cache(self) -> None:
|
|
2143
|
+
if not self.is_query_context_cache_disabled:
|
|
2144
|
+
self.query_context_cache = QueryContextCache(self.query_context_cache_size)
|
|
2145
|
+
|
|
2146
|
+
def get_query_context(self) -> dict | None:
|
|
2147
|
+
if self.is_query_context_cache_disabled:
|
|
2148
|
+
return None
|
|
2149
|
+
return self.query_context_cache.serialize_to_dict()
|
|
2150
|
+
|
|
2151
|
+
def set_query_context(self, data: dict) -> None:
|
|
2152
|
+
if not self.is_query_context_cache_disabled:
|
|
2153
|
+
self.query_context_cache.deserialize_json_dict(data)
|
|
2154
|
+
|
|
2155
|
+
@staticmethod
|
|
2156
|
+
def is_still_running(status: QueryStatus) -> bool:
|
|
2157
|
+
"""Checks whether given status is currently running."""
|
|
2158
|
+
return status in (
|
|
2159
|
+
QueryStatus.RUNNING,
|
|
2160
|
+
QueryStatus.QUEUED,
|
|
2161
|
+
QueryStatus.RESUMING_WAREHOUSE,
|
|
2162
|
+
QueryStatus.QUEUED_REPARING_WAREHOUSE,
|
|
2163
|
+
QueryStatus.BLOCKED,
|
|
2164
|
+
QueryStatus.NO_DATA,
|
|
2165
|
+
)
|
|
2166
|
+
|
|
2167
|
+
@staticmethod
|
|
2168
|
+
def is_an_error(status: QueryStatus) -> bool:
|
|
2169
|
+
"""Checks whether given status means that there has been an error."""
|
|
2170
|
+
return status in (
|
|
2171
|
+
QueryStatus.ABORTING,
|
|
2172
|
+
QueryStatus.FAILED_WITH_ERROR,
|
|
2173
|
+
QueryStatus.ABORTED,
|
|
2174
|
+
QueryStatus.FAILED_WITH_INCIDENT,
|
|
2175
|
+
QueryStatus.DISCONNECTED,
|
|
2176
|
+
)
|
|
2177
|
+
|
|
2178
|
+
def _all_async_queries_finished(self) -> bool:
|
|
2179
|
+
"""Checks whether all async queries started by this Connection have finished executing."""
|
|
2180
|
+
|
|
2181
|
+
if not self._async_sfqids:
|
|
2182
|
+
return True
|
|
2183
|
+
|
|
2184
|
+
queries = list(reversed(self._async_sfqids.keys()))
|
|
2185
|
+
|
|
2186
|
+
num_workers = min(self.client_prefetch_threads, len(queries))
|
|
2187
|
+
found_unfinished_query = False
|
|
2188
|
+
|
|
2189
|
+
def async_query_check_helper(
|
|
2190
|
+
sfq_id: str,
|
|
2191
|
+
) -> bool:
|
|
2192
|
+
nonlocal found_unfinished_query
|
|
2193
|
+
return found_unfinished_query or self.is_still_running(
|
|
2194
|
+
self.get_query_status(sfq_id)
|
|
2195
|
+
)
|
|
2196
|
+
|
|
2197
|
+
with ThreadPoolExecutor(
|
|
2198
|
+
max_workers=num_workers, thread_name_prefix="async_query_check_"
|
|
2199
|
+
) as tpe: # We should upgrade to using cancel_futures=True once supporting 3.9+
|
|
2200
|
+
futures = (tpe.submit(async_query_check_helper, sfqid) for sfqid in queries)
|
|
2201
|
+
for f in as_completed(futures):
|
|
2202
|
+
if f.result():
|
|
2203
|
+
found_unfinished_query = True
|
|
2204
|
+
break
|
|
2205
|
+
for f in futures:
|
|
2206
|
+
f.cancel()
|
|
2207
|
+
|
|
2208
|
+
return not found_unfinished_query
|
|
2209
|
+
|
|
2210
|
+
def _log_telemetry_imported_packages(self) -> None:
|
|
2211
|
+
if self._log_imported_packages_in_telemetry:
|
|
2212
|
+
# filter out duplicates caused by submodules
|
|
2213
|
+
# and internal modules with names starting with an underscore
|
|
2214
|
+
imported_modules = {
|
|
2215
|
+
k.split(".", maxsplit=1)[0]
|
|
2216
|
+
for k in list(sys.modules)
|
|
2217
|
+
if not k.startswith("_")
|
|
2218
|
+
}
|
|
2219
|
+
ts = get_time_millis()
|
|
2220
|
+
self._log_telemetry(
|
|
2221
|
+
TelemetryData.from_telemetry_data_dict(
|
|
2222
|
+
from_dict={
|
|
2223
|
+
TelemetryField.KEY_TYPE.value: TelemetryField.IMPORTED_PACKAGES.value,
|
|
2224
|
+
TelemetryField.KEY_VALUE.value: str(imported_modules),
|
|
2225
|
+
},
|
|
2226
|
+
timestamp=ts,
|
|
2227
|
+
connection=self,
|
|
2228
|
+
)
|
|
2229
|
+
)
|
|
2230
|
+
|
|
2231
|
+
def is_valid(self) -> bool:
|
|
2232
|
+
"""This function tries to answer the question: Is this connection still good for sending queries?
|
|
2233
|
+
Attempts to validate the connections both on the TCP/IP and Session levels."""
|
|
2234
|
+
logger.debug("validating connection and session")
|
|
2235
|
+
if self.is_closed():
|
|
2236
|
+
logger.debug("connection is already closed and not valid")
|
|
2237
|
+
return False
|
|
2238
|
+
|
|
2239
|
+
try:
|
|
2240
|
+
logger.debug("trying to heartbeat into the session to validate")
|
|
2241
|
+
hb_result = self.rest._heartbeat()
|
|
2242
|
+
session_valid = hb_result.get("success")
|
|
2243
|
+
logger.debug("session still valid? %s", session_valid)
|
|
2244
|
+
return bool(session_valid)
|
|
2245
|
+
except Exception as e:
|
|
2246
|
+
logger.debug("session could not be validated due to exception: %s", e)
|
|
2247
|
+
return False
|
|
2248
|
+
|
|
2249
|
+
def _check_experimental_authentication_flag(self) -> None:
|
|
2250
|
+
if os.getenv(ENV_VAR_EXPERIMENTAL_AUTHENTICATION, "false").lower() != "true":
|
|
2251
|
+
Error.errorhandler_wrapper(
|
|
2252
|
+
self,
|
|
2253
|
+
None,
|
|
2254
|
+
ProgrammingError,
|
|
2255
|
+
{
|
|
2256
|
+
"msg": f"Please set the '{ENV_VAR_EXPERIMENTAL_AUTHENTICATION}' environment variable true to use the '{self._authenticator}' authenticator.",
|
|
2257
|
+
"errno": ER_EXPERIMENTAL_AUTHENTICATION_NOT_SUPPORTED,
|
|
2258
|
+
},
|
|
2259
|
+
)
|
|
2260
|
+
|
|
2261
|
+
@staticmethod
|
|
2262
|
+
def _detect_application() -> None | str:
|
|
2263
|
+
if ENV_VAR_PARTNER in os.environ.keys():
|
|
2264
|
+
return os.environ[ENV_VAR_PARTNER]
|
|
2265
|
+
if "streamlit" in sys.modules:
|
|
2266
|
+
return "streamlit"
|
|
2267
|
+
if all(
|
|
2268
|
+
(jpmod in sys.modules)
|
|
2269
|
+
for jpmod in ("ipykernel", "jupyter_core", "jupyter_client")
|
|
2270
|
+
):
|
|
2271
|
+
return "jupyter_notebook"
|
|
2272
|
+
if "snowbooks" in sys.modules:
|
|
2273
|
+
return "snowflake_notebook"
|