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,1215 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import binascii
|
|
5
|
+
import glob
|
|
6
|
+
import math
|
|
7
|
+
import mimetypes
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
from concurrent.futures.thread import ThreadPoolExecutor
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from functools import partial
|
|
14
|
+
from logging import getLogger
|
|
15
|
+
from time import time
|
|
16
|
+
from typing import IO, TYPE_CHECKING, Any, Callable, TypeVar
|
|
17
|
+
|
|
18
|
+
from .azure_storage_client import SnowflakeAzureRestClient
|
|
19
|
+
from .compat import IS_WINDOWS
|
|
20
|
+
from .constants import (
|
|
21
|
+
AZURE_CHUNK_SIZE,
|
|
22
|
+
AZURE_FS,
|
|
23
|
+
CMD_TYPE_DOWNLOAD,
|
|
24
|
+
CMD_TYPE_UPLOAD,
|
|
25
|
+
GCS_FS,
|
|
26
|
+
LOCAL_FS,
|
|
27
|
+
S3_DEFAULT_CHUNK_SIZE,
|
|
28
|
+
S3_FS,
|
|
29
|
+
S3_MAX_OBJECT_SIZE,
|
|
30
|
+
S3_MAX_PARTS,
|
|
31
|
+
S3_MIN_PART_SIZE,
|
|
32
|
+
ResultStatus,
|
|
33
|
+
megabyte,
|
|
34
|
+
)
|
|
35
|
+
from .converter_snowsql import SnowflakeConverterSnowSQL
|
|
36
|
+
from .errorcode import (
|
|
37
|
+
ER_COMPRESSION_NOT_SUPPORTED,
|
|
38
|
+
ER_FAILED_TO_DOWNLOAD_FROM_STAGE,
|
|
39
|
+
ER_FAILED_TO_UPLOAD_TO_STAGE,
|
|
40
|
+
ER_FILE_NOT_EXISTS,
|
|
41
|
+
ER_INTERNAL_NOT_MATCH_ENCRYPT_MATERIAL,
|
|
42
|
+
ER_INVALID_STAGE_FS,
|
|
43
|
+
ER_INVALID_STAGE_LOCATION,
|
|
44
|
+
ER_LOCAL_PATH_NOT_DIRECTORY,
|
|
45
|
+
)
|
|
46
|
+
from .errors import (
|
|
47
|
+
DatabaseError,
|
|
48
|
+
Error,
|
|
49
|
+
InternalError,
|
|
50
|
+
OperationalError,
|
|
51
|
+
ProgrammingError,
|
|
52
|
+
)
|
|
53
|
+
from .file_compression_type import CompressionTypes, lookup_by_mime_sub_type
|
|
54
|
+
from .gcs_storage_client import SnowflakeGCSRestClient
|
|
55
|
+
from .local_storage_client import SnowflakeLocalStorageClient
|
|
56
|
+
from .s3_storage_client import SnowflakeS3RestClient
|
|
57
|
+
from .storage_client import SnowflakeFileEncryptionMaterial, SnowflakeStorageClient
|
|
58
|
+
|
|
59
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
60
|
+
from .connection import SnowflakeConnection
|
|
61
|
+
from .cursor import SnowflakeCursor
|
|
62
|
+
from .file_compression_type import CompressionType
|
|
63
|
+
|
|
64
|
+
VALID_STORAGE = [LOCAL_FS, S3_FS, AZURE_FS, GCS_FS]
|
|
65
|
+
|
|
66
|
+
INJECT_WAIT_IN_PUT = 0
|
|
67
|
+
|
|
68
|
+
logger = getLogger(__name__)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def result_text_column_desc(name: str) -> dict[str, Any]:
|
|
72
|
+
return {
|
|
73
|
+
"name": name,
|
|
74
|
+
"type": "text",
|
|
75
|
+
"length": 16777216,
|
|
76
|
+
"precision": None,
|
|
77
|
+
"scale": None,
|
|
78
|
+
"nullable": False,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def result_fixed_column_desc(name: str) -> dict[str, Any]:
|
|
83
|
+
return {
|
|
84
|
+
"name": name,
|
|
85
|
+
"type": "fixed",
|
|
86
|
+
"length": 5,
|
|
87
|
+
"precision": 0,
|
|
88
|
+
"scale": 0,
|
|
89
|
+
"nullable": False,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# TODO: rewrite, we use this class to store information about file transfers
|
|
94
|
+
# It'd make more sense to define a new object, like FileTransferMeta that then has
|
|
95
|
+
# more FileMetas inside of it. This would help in some cases, where for example
|
|
96
|
+
# consider the case where we run into an unrecoverable error for the whole transfer
|
|
97
|
+
# job and we need to convey an error to the main thread and that error needs to be
|
|
98
|
+
# raised by the main thread. Where should this go? Currently the answer could be
|
|
99
|
+
# all of the current FileMetas. Hmmm...
|
|
100
|
+
@dataclass
|
|
101
|
+
class SnowflakeFileMeta:
|
|
102
|
+
"""Class to keep track of information necessary for file operations."""
|
|
103
|
+
|
|
104
|
+
name: str
|
|
105
|
+
src_file_name: str
|
|
106
|
+
stage_location_type: str
|
|
107
|
+
result_status: ResultStatus | None = None
|
|
108
|
+
|
|
109
|
+
sfagent: SnowflakeFileTransferAgent | None = None
|
|
110
|
+
put_callback: type[SnowflakeProgressPercentage] | None = None
|
|
111
|
+
put_azure_callback: type[SnowflakeProgressPercentage] | None = None
|
|
112
|
+
put_callback_output_stream: IO[str] | None = None
|
|
113
|
+
get_callback: type[SnowflakeProgressPercentage] | None = None
|
|
114
|
+
get_azure_callback: type[SnowflakeProgressPercentage] | None = None
|
|
115
|
+
get_callback_output_stream: IO[str] | None = None
|
|
116
|
+
show_progress_bar: bool = False
|
|
117
|
+
multipart_threshold: int = 67108864 # Historical value
|
|
118
|
+
presigned_url: str | None = None
|
|
119
|
+
overwrite: bool = False
|
|
120
|
+
sha256_digest: str | None = None
|
|
121
|
+
upload_size: int | None = None
|
|
122
|
+
real_src_file_name: str | None = None
|
|
123
|
+
error_details: Exception | None = None
|
|
124
|
+
last_error: Exception | None = None
|
|
125
|
+
no_sleeping_time: bool = False
|
|
126
|
+
gcs_file_header_digest: str | None = None
|
|
127
|
+
gcs_file_header_content_length: int | None = None
|
|
128
|
+
gcs_file_header_encryption_metadata: dict[str, Any] | None = None
|
|
129
|
+
|
|
130
|
+
encryption_material: SnowflakeFileEncryptionMaterial | None = None
|
|
131
|
+
# Specific to Uploads only
|
|
132
|
+
src_file_size: int = 0
|
|
133
|
+
src_compression_type: CompressionType | None = None
|
|
134
|
+
dst_compression_type: CompressionType = None
|
|
135
|
+
require_compress: bool = False
|
|
136
|
+
dst_file_name: str | None = None
|
|
137
|
+
dst_file_size: int = -1
|
|
138
|
+
intermediate_stream: IO[bytes] | None = None
|
|
139
|
+
src_stream: IO[bytes] | None = None
|
|
140
|
+
skip_upload_on_content_match: bool = False
|
|
141
|
+
# Specific to Downloads only
|
|
142
|
+
local_location: str | None = None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _update_progress(
|
|
146
|
+
file_name: str,
|
|
147
|
+
start_time: float,
|
|
148
|
+
total_size: float,
|
|
149
|
+
progress: float | int,
|
|
150
|
+
output_stream: IO | None = sys.stdout,
|
|
151
|
+
show_progress_bar: bool | None = True,
|
|
152
|
+
) -> bool:
|
|
153
|
+
bar_length = 10 # Modify this to change the length of the progress bar
|
|
154
|
+
total_size /= megabyte
|
|
155
|
+
status = ""
|
|
156
|
+
elapsed_time = time() - start_time
|
|
157
|
+
throughput = (total_size / elapsed_time) if elapsed_time != 0.0 else 0.0
|
|
158
|
+
if isinstance(progress, int):
|
|
159
|
+
progress = float(progress)
|
|
160
|
+
if not isinstance(progress, float):
|
|
161
|
+
progress = 0
|
|
162
|
+
status = "error: progress var must be float\r\n"
|
|
163
|
+
if progress < 0:
|
|
164
|
+
progress = 0
|
|
165
|
+
status = "Halt...\r\n"
|
|
166
|
+
if progress >= 1:
|
|
167
|
+
progress = 1
|
|
168
|
+
status = f"Done ({elapsed_time:.3f}s, {throughput:.2f}MB/s).\r\n"
|
|
169
|
+
if not status and show_progress_bar:
|
|
170
|
+
status = f"({elapsed_time:.3f}s, {throughput:.2f}MB/s)"
|
|
171
|
+
if status:
|
|
172
|
+
block = int(round(bar_length * progress))
|
|
173
|
+
|
|
174
|
+
text = (
|
|
175
|
+
f"\r{file_name}({total_size:.2f}MB): "
|
|
176
|
+
f"[{'#' * block + '-' * (bar_length - block)}] "
|
|
177
|
+
f"{progress * 100.0:.2f}% {status}"
|
|
178
|
+
)
|
|
179
|
+
output_stream.write(text)
|
|
180
|
+
output_stream.flush()
|
|
181
|
+
logger.debug(
|
|
182
|
+
f"filename: {file_name}, start_time: {start_time}, total_size: {total_size}, "
|
|
183
|
+
f"progress: {progress}, show_progress_bar: {show_progress_bar}"
|
|
184
|
+
)
|
|
185
|
+
return progress == 1.0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _chunk_size_calculator(file_size: int) -> int:
|
|
189
|
+
# S3 has limitation on the num of parts to be uploaded, this helper method recalculate the num of parts
|
|
190
|
+
if file_size > S3_MAX_OBJECT_SIZE:
|
|
191
|
+
# check if we don't exceed the allowed S3 max file size 5 TiB
|
|
192
|
+
raise ValueError(
|
|
193
|
+
f"File size {file_size} exceeds the maximum file size {S3_MAX_OBJECT_SIZE} allowed in S3."
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# num_parts = math.ceil(file_size / default_chunk_size)
|
|
197
|
+
# if num_parts is greater than the allowed S3_MAX_PARTS, we update our chunk_size, otherwise we use the default one
|
|
198
|
+
calculated_chunk_size = (
|
|
199
|
+
max(math.ceil(file_size / S3_MAX_PARTS), S3_MIN_PART_SIZE)
|
|
200
|
+
if math.ceil(file_size / S3_DEFAULT_CHUNK_SIZE) > S3_MAX_PARTS
|
|
201
|
+
else S3_DEFAULT_CHUNK_SIZE
|
|
202
|
+
)
|
|
203
|
+
if calculated_chunk_size != S3_DEFAULT_CHUNK_SIZE:
|
|
204
|
+
logger.debug(
|
|
205
|
+
f"Setting chunksize to {calculated_chunk_size} instead of the default {S3_DEFAULT_CHUNK_SIZE}."
|
|
206
|
+
)
|
|
207
|
+
return calculated_chunk_size
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def percent(seen_so_far: int, size: float) -> float:
|
|
211
|
+
return 1.0 if seen_so_far >= size or size <= 0 else float(seen_so_far / size)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class SnowflakeProgressPercentage:
|
|
215
|
+
"""Built-in Progress bar for PUT commands."""
|
|
216
|
+
|
|
217
|
+
def __init__(
|
|
218
|
+
self,
|
|
219
|
+
filename: str,
|
|
220
|
+
filesize: int | float,
|
|
221
|
+
output_stream: IO | None = sys.stdout,
|
|
222
|
+
show_progress_bar: bool | None = True,
|
|
223
|
+
) -> None:
|
|
224
|
+
last_pound_char = filename.rfind("#")
|
|
225
|
+
if last_pound_char < 0:
|
|
226
|
+
last_pound_char = len(filename)
|
|
227
|
+
self._filename = os.path.basename(filename[0:last_pound_char])
|
|
228
|
+
self._output_stream = output_stream
|
|
229
|
+
self._show_progress_bar = show_progress_bar
|
|
230
|
+
self._size = float(filesize)
|
|
231
|
+
self._seen_so_far = 0
|
|
232
|
+
self._done = False
|
|
233
|
+
self._start_time = time()
|
|
234
|
+
self._lock = threading.Lock()
|
|
235
|
+
|
|
236
|
+
def __call__(self, bytes_amount: int):
|
|
237
|
+
raise NotImplementedError
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class SnowflakeS3ProgressPercentage(SnowflakeProgressPercentage):
|
|
241
|
+
def __init__(
|
|
242
|
+
self,
|
|
243
|
+
filename: str,
|
|
244
|
+
filesize: int | float,
|
|
245
|
+
output_stream: IO | None = sys.stdout,
|
|
246
|
+
show_progress_bar: bool | None = True,
|
|
247
|
+
) -> None:
|
|
248
|
+
super().__init__(
|
|
249
|
+
filename,
|
|
250
|
+
filesize,
|
|
251
|
+
output_stream=output_stream,
|
|
252
|
+
show_progress_bar=show_progress_bar,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def __call__(self, bytes_amount: int) -> None:
|
|
256
|
+
with self._lock:
|
|
257
|
+
if self._output_stream:
|
|
258
|
+
self._seen_so_far += bytes_amount
|
|
259
|
+
percentage = percent(self._seen_so_far, self._size)
|
|
260
|
+
if not self._done:
|
|
261
|
+
self._done = _update_progress(
|
|
262
|
+
self._filename,
|
|
263
|
+
self._start_time,
|
|
264
|
+
self._size,
|
|
265
|
+
percentage,
|
|
266
|
+
output_stream=self._output_stream,
|
|
267
|
+
show_progress_bar=self._show_progress_bar,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class SnowflakeAzureProgressPercentage(SnowflakeProgressPercentage):
|
|
272
|
+
def __init__(
|
|
273
|
+
self,
|
|
274
|
+
filename: str,
|
|
275
|
+
filesize: int | float,
|
|
276
|
+
output_stream: IO | None = sys.stdout,
|
|
277
|
+
show_progress_bar: bool | None = True,
|
|
278
|
+
) -> None:
|
|
279
|
+
super().__init__(
|
|
280
|
+
filename,
|
|
281
|
+
filesize,
|
|
282
|
+
output_stream=output_stream,
|
|
283
|
+
show_progress_bar=show_progress_bar,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def __call__(self, current: int) -> None:
|
|
287
|
+
with self._lock:
|
|
288
|
+
if self._output_stream:
|
|
289
|
+
self._seen_so_far = current
|
|
290
|
+
percentage = percent(self._seen_so_far, self._size)
|
|
291
|
+
if not self._done:
|
|
292
|
+
self._done = _update_progress(
|
|
293
|
+
self._filename,
|
|
294
|
+
self._start_time,
|
|
295
|
+
self._size,
|
|
296
|
+
percentage,
|
|
297
|
+
output_stream=self._output_stream,
|
|
298
|
+
show_progress_bar=self._show_progress_bar,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class StorageCredential:
|
|
303
|
+
def __init__(
|
|
304
|
+
self,
|
|
305
|
+
credentials: dict[str, Any],
|
|
306
|
+
connection: SnowflakeConnection,
|
|
307
|
+
command: str,
|
|
308
|
+
) -> None:
|
|
309
|
+
self.creds = credentials
|
|
310
|
+
self.timestamp = time()
|
|
311
|
+
self.lock = threading.Lock()
|
|
312
|
+
self.connection = connection
|
|
313
|
+
self._command = command
|
|
314
|
+
|
|
315
|
+
def update(self, cur_timestamp) -> None:
|
|
316
|
+
with self.lock:
|
|
317
|
+
if cur_timestamp < self.timestamp:
|
|
318
|
+
logger.debug(
|
|
319
|
+
"Omitting renewal of storage token, as it already happened."
|
|
320
|
+
)
|
|
321
|
+
return
|
|
322
|
+
logger.debug("Renewing expired storage token.")
|
|
323
|
+
ret = self.connection.cursor()._execute_helper(self._command)
|
|
324
|
+
self.creds = ret["data"]["stageInfo"]["creds"]
|
|
325
|
+
self.timestamp = time()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@dataclass
|
|
329
|
+
class TransferMetadata:
|
|
330
|
+
num_files_started: int = 0
|
|
331
|
+
num_files_completed: int = 0
|
|
332
|
+
chunks_in_queue: int = 0
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class SnowflakeFileTransferAgent:
|
|
336
|
+
"""Snowflake File Transfer Agent provides cloud provider independent implementation for putting/getting files."""
|
|
337
|
+
|
|
338
|
+
def __init__(
|
|
339
|
+
self,
|
|
340
|
+
cursor: SnowflakeCursor,
|
|
341
|
+
command: str,
|
|
342
|
+
ret: dict[str, Any],
|
|
343
|
+
put_callback: type[SnowflakeProgressPercentage] | None = None,
|
|
344
|
+
put_azure_callback: type[SnowflakeProgressPercentage] | None = None,
|
|
345
|
+
put_callback_output_stream: IO[str] = sys.stdout,
|
|
346
|
+
get_callback: type[SnowflakeProgressPercentage] | None = None,
|
|
347
|
+
get_azure_callback: type[SnowflakeProgressPercentage] | None = None,
|
|
348
|
+
get_callback_output_stream: IO[str] = sys.stdout,
|
|
349
|
+
show_progress_bar: bool = True,
|
|
350
|
+
raise_put_get_error: bool = True,
|
|
351
|
+
force_put_overwrite: bool = True,
|
|
352
|
+
skip_upload_on_content_match: bool = False,
|
|
353
|
+
multipart_threshold: int | None = None,
|
|
354
|
+
source_from_stream: IO[bytes] | None = None,
|
|
355
|
+
use_s3_regional_url: bool = False,
|
|
356
|
+
iobound_tpe_limit: int | None = None,
|
|
357
|
+
unsafe_file_write: bool = False,
|
|
358
|
+
) -> None:
|
|
359
|
+
self._cursor = cursor
|
|
360
|
+
self._command = command
|
|
361
|
+
self._ret = ret
|
|
362
|
+
self._put_callback = put_callback
|
|
363
|
+
self._put_azure_callback = (
|
|
364
|
+
put_azure_callback if put_azure_callback else put_callback
|
|
365
|
+
)
|
|
366
|
+
self._put_callback_output_stream = put_callback_output_stream
|
|
367
|
+
self._get_callback = get_callback
|
|
368
|
+
self._get_azure_callback = (
|
|
369
|
+
get_azure_callback if get_azure_callback else get_callback
|
|
370
|
+
)
|
|
371
|
+
self._get_callback_output_stream = get_callback_output_stream
|
|
372
|
+
# when we have not checked whether we should use accelerate, this boolean is None
|
|
373
|
+
# _use_accelerate_endpoint in SnowflakeFileTransferAgent could be passed to each SnowflakeS3RestClient
|
|
374
|
+
# so we could avoid check accelerate configuration for each S3 client created for each file meta.
|
|
375
|
+
self._use_accelerate_endpoint: bool | None = None
|
|
376
|
+
self._raise_put_get_error = raise_put_get_error
|
|
377
|
+
self._show_progress_bar = show_progress_bar
|
|
378
|
+
self._force_put_overwrite = force_put_overwrite
|
|
379
|
+
self._skip_upload_on_content_match = skip_upload_on_content_match
|
|
380
|
+
self._source_from_stream = source_from_stream
|
|
381
|
+
# The list of self-sufficient file metas that are sent to
|
|
382
|
+
# remote storage clients to get operated on.
|
|
383
|
+
self._file_metadata: list[SnowflakeFileMeta] = []
|
|
384
|
+
self._results: list[SnowflakeFileMeta] = []
|
|
385
|
+
self._multipart_threshold = multipart_threshold or 67108864 # Historical value
|
|
386
|
+
self._use_s3_regional_url = use_s3_regional_url
|
|
387
|
+
self._credentials: StorageCredential | None = None
|
|
388
|
+
self._iobound_tpe_limit = iobound_tpe_limit
|
|
389
|
+
self._unsafe_file_write = unsafe_file_write
|
|
390
|
+
|
|
391
|
+
def execute(self) -> None:
|
|
392
|
+
self._parse_command()
|
|
393
|
+
self._init_file_metadata()
|
|
394
|
+
|
|
395
|
+
if self._command_type == CMD_TYPE_UPLOAD:
|
|
396
|
+
self._process_file_compression_type()
|
|
397
|
+
|
|
398
|
+
for m in self._file_metadata:
|
|
399
|
+
m.sfagent = self
|
|
400
|
+
|
|
401
|
+
self._transfer_accelerate_config()
|
|
402
|
+
|
|
403
|
+
if self._command_type == CMD_TYPE_DOWNLOAD:
|
|
404
|
+
if not os.path.isdir(self._local_location):
|
|
405
|
+
os.makedirs(self._local_location)
|
|
406
|
+
|
|
407
|
+
if self._stage_location_type == LOCAL_FS:
|
|
408
|
+
if not os.path.isdir(self._stage_info["location"]):
|
|
409
|
+
os.makedirs(self._stage_info["location"])
|
|
410
|
+
|
|
411
|
+
for m in self._file_metadata:
|
|
412
|
+
m.overwrite = self._overwrite
|
|
413
|
+
m.skip_upload_on_content_match = self._skip_upload_on_content_match
|
|
414
|
+
m.sfagent = self
|
|
415
|
+
if self._stage_location_type != LOCAL_FS:
|
|
416
|
+
m.put_callback = self._put_callback
|
|
417
|
+
m.put_azure_callback = self._put_azure_callback
|
|
418
|
+
m.put_callback_output_stream = self._put_callback_output_stream
|
|
419
|
+
m.get_callback = self._get_callback
|
|
420
|
+
m.get_azure_callback = self._get_azure_callback
|
|
421
|
+
m.get_callback_output_stream = self._get_callback_output_stream
|
|
422
|
+
m.show_progress_bar = self._show_progress_bar
|
|
423
|
+
|
|
424
|
+
# multichunk threshold
|
|
425
|
+
m.multipart_threshold = self._multipart_threshold
|
|
426
|
+
|
|
427
|
+
logger.debug(f"parallel=[{self._parallel}]")
|
|
428
|
+
if self._raise_put_get_error and not self._file_metadata:
|
|
429
|
+
Error.errorhandler_wrapper(
|
|
430
|
+
self._cursor.connection,
|
|
431
|
+
self._cursor,
|
|
432
|
+
OperationalError,
|
|
433
|
+
{
|
|
434
|
+
"msg": "While getting file(s) there was an error: "
|
|
435
|
+
"the file does not exist.",
|
|
436
|
+
"errno": ER_FILE_NOT_EXISTS,
|
|
437
|
+
},
|
|
438
|
+
)
|
|
439
|
+
self.transfer(self._file_metadata)
|
|
440
|
+
|
|
441
|
+
# turn enum to string, in order to have backward compatible interface
|
|
442
|
+
for result in self._results:
|
|
443
|
+
result.result_status = result.result_status.value
|
|
444
|
+
|
|
445
|
+
def transfer(self, metas: list[SnowflakeFileMeta]) -> None:
|
|
446
|
+
iobound_tpe_limit = min(len(metas), os.cpu_count())
|
|
447
|
+
logger.debug("Decided IO-bound TPE size: %d", iobound_tpe_limit)
|
|
448
|
+
if self._iobound_tpe_limit is not None:
|
|
449
|
+
logger.debug("IO-bound TPE size is limited to: %d", self._iobound_tpe_limit)
|
|
450
|
+
iobound_tpe_limit = min(iobound_tpe_limit, self._iobound_tpe_limit)
|
|
451
|
+
max_concurrency = self._parallel
|
|
452
|
+
network_tpe = ThreadPoolExecutor(max_concurrency)
|
|
453
|
+
preprocess_tpe = ThreadPoolExecutor(iobound_tpe_limit)
|
|
454
|
+
postprocess_tpe = ThreadPoolExecutor(iobound_tpe_limit)
|
|
455
|
+
logger.debug(f"Chunk ThreadPoolExecutor size: {max_concurrency}")
|
|
456
|
+
cv_main_thread = threading.Condition() # to signal the main thread
|
|
457
|
+
cv_chunk_process = (
|
|
458
|
+
threading.Condition()
|
|
459
|
+
) # to get more chunks into the chunk_tpe
|
|
460
|
+
files = [self._create_file_transfer_client(m) for m in metas]
|
|
461
|
+
num_total_files = len(metas)
|
|
462
|
+
transfer_metadata = TransferMetadata() # this is protected by cv_chunk_process
|
|
463
|
+
is_upload = self._command_type == CMD_TYPE_UPLOAD
|
|
464
|
+
exception_caught_in_callback: Exception | None = None
|
|
465
|
+
logger.debug(
|
|
466
|
+
"Going to %sload %d files", "up" if is_upload else "down", len(metas)
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
def notify_file_completed() -> None:
|
|
470
|
+
# Increment the number of completed files, then notify the main thread.
|
|
471
|
+
with cv_main_thread:
|
|
472
|
+
transfer_metadata.num_files_completed += 1
|
|
473
|
+
cv_main_thread.notify()
|
|
474
|
+
|
|
475
|
+
def preprocess_done_cb(
|
|
476
|
+
success: bool,
|
|
477
|
+
result: Any,
|
|
478
|
+
file_meta: SnowflakeFileMeta,
|
|
479
|
+
done_client: SnowflakeStorageClient,
|
|
480
|
+
) -> None:
|
|
481
|
+
if not success:
|
|
482
|
+
logger.debug(f"Failed to prepare {done_client.meta.name}.")
|
|
483
|
+
if is_upload:
|
|
484
|
+
done_client.finish_upload()
|
|
485
|
+
done_client.delete_client_data()
|
|
486
|
+
else:
|
|
487
|
+
done_client.finish_download()
|
|
488
|
+
notify_file_completed()
|
|
489
|
+
elif done_client.meta.result_status == ResultStatus.SKIPPED:
|
|
490
|
+
# this case applies to upload only
|
|
491
|
+
notify_file_completed()
|
|
492
|
+
else:
|
|
493
|
+
logger.debug(f"Finished preparing file {done_client.meta.name}")
|
|
494
|
+
with cv_chunk_process:
|
|
495
|
+
while transfer_metadata.chunks_in_queue > 2 * max_concurrency:
|
|
496
|
+
logger.debug(
|
|
497
|
+
"Chunk queue busy, waiting in file done callback..."
|
|
498
|
+
)
|
|
499
|
+
cv_chunk_process.wait()
|
|
500
|
+
for _chunk_id in range(done_client.num_of_chunks):
|
|
501
|
+
_callback = partial(
|
|
502
|
+
transfer_done_cb,
|
|
503
|
+
done_client=done_client,
|
|
504
|
+
chunk_id=_chunk_id,
|
|
505
|
+
)
|
|
506
|
+
if is_upload:
|
|
507
|
+
network_tpe.submit(
|
|
508
|
+
function_and_callback_wrapper,
|
|
509
|
+
# Work fn
|
|
510
|
+
done_client.upload_chunk,
|
|
511
|
+
# Callback fn
|
|
512
|
+
_callback,
|
|
513
|
+
file_meta,
|
|
514
|
+
# Arguments for work fn
|
|
515
|
+
_chunk_id,
|
|
516
|
+
)
|
|
517
|
+
else:
|
|
518
|
+
network_tpe.submit(
|
|
519
|
+
function_and_callback_wrapper,
|
|
520
|
+
# Work fn
|
|
521
|
+
done_client.download_chunk,
|
|
522
|
+
# Callback fn
|
|
523
|
+
_callback,
|
|
524
|
+
file_meta,
|
|
525
|
+
# Arguments for work fn
|
|
526
|
+
_chunk_id,
|
|
527
|
+
)
|
|
528
|
+
transfer_metadata.chunks_in_queue += 1
|
|
529
|
+
cv_chunk_process.notify()
|
|
530
|
+
|
|
531
|
+
def transfer_done_cb(
|
|
532
|
+
success: bool,
|
|
533
|
+
result: Any,
|
|
534
|
+
file_meta: SnowflakeFileMeta,
|
|
535
|
+
done_client: SnowflakeStorageClient,
|
|
536
|
+
chunk_id: int,
|
|
537
|
+
) -> None:
|
|
538
|
+
# Note: chunk_id is 0 based while num_of_chunks is count
|
|
539
|
+
logger.debug(
|
|
540
|
+
f"Chunk(id: {chunk_id}) {chunk_id+1}/{done_client.num_of_chunks} of file {done_client.meta.name} reached callback"
|
|
541
|
+
)
|
|
542
|
+
with cv_chunk_process:
|
|
543
|
+
transfer_metadata.chunks_in_queue -= 1
|
|
544
|
+
cv_chunk_process.notify()
|
|
545
|
+
|
|
546
|
+
with done_client.lock:
|
|
547
|
+
if not success:
|
|
548
|
+
# TODO: Cancel other chunks?
|
|
549
|
+
done_client.failed_transfers += 1
|
|
550
|
+
logger.debug(
|
|
551
|
+
f"Chunk {chunk_id} of file {done_client.meta.name} failed to transfer for unexpected exception {result}"
|
|
552
|
+
)
|
|
553
|
+
else:
|
|
554
|
+
done_client.successful_transfers += 1
|
|
555
|
+
logger.debug(
|
|
556
|
+
f"Chunk progress: {done_client.meta.name}: completed: {done_client.successful_transfers} failed: {done_client.failed_transfers} total: {done_client.num_of_chunks}"
|
|
557
|
+
)
|
|
558
|
+
if (
|
|
559
|
+
done_client.successful_transfers + done_client.failed_transfers
|
|
560
|
+
== done_client.num_of_chunks
|
|
561
|
+
):
|
|
562
|
+
if is_upload:
|
|
563
|
+
done_client.finish_upload()
|
|
564
|
+
done_client.delete_client_data()
|
|
565
|
+
notify_file_completed()
|
|
566
|
+
else:
|
|
567
|
+
postprocess_tpe.submit(
|
|
568
|
+
function_and_callback_wrapper,
|
|
569
|
+
# Work fn
|
|
570
|
+
done_client.finish_download,
|
|
571
|
+
# Callback fn
|
|
572
|
+
partial(postprocess_done_cb, done_client=done_client),
|
|
573
|
+
transfer_metadata,
|
|
574
|
+
)
|
|
575
|
+
logger.debug(
|
|
576
|
+
f"submitting {done_client.meta.name} to done_postprocess"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
def postprocess_done_cb(
|
|
580
|
+
success: bool,
|
|
581
|
+
result: Any,
|
|
582
|
+
file_meta: SnowflakeFileMeta,
|
|
583
|
+
done_client: SnowflakeStorageClient,
|
|
584
|
+
) -> None:
|
|
585
|
+
logger.debug(f"File {done_client.meta.name} reached postprocess callback")
|
|
586
|
+
|
|
587
|
+
with done_client.lock:
|
|
588
|
+
if not success:
|
|
589
|
+
done_client.failed_transfers += 1
|
|
590
|
+
logger.debug(
|
|
591
|
+
f"File {done_client.meta.name} failed to transfer for unexpected exception {result}"
|
|
592
|
+
)
|
|
593
|
+
# Whether there was an exception or not, we're done the file.
|
|
594
|
+
notify_file_completed()
|
|
595
|
+
|
|
596
|
+
_T = TypeVar("_T")
|
|
597
|
+
|
|
598
|
+
def function_and_callback_wrapper(
|
|
599
|
+
work: Callable[..., _T],
|
|
600
|
+
_callback: Callable[[bool, _T | Exception, SnowflakeFileMeta], None],
|
|
601
|
+
file_meta: SnowflakeFileMeta,
|
|
602
|
+
*args: Any,
|
|
603
|
+
**kwargs: Any,
|
|
604
|
+
) -> None:
|
|
605
|
+
"""This wrapper makes sure that callbacks are called from the TPEs.
|
|
606
|
+
|
|
607
|
+
If the main thread adds a callback to a future that has already been
|
|
608
|
+
fulfilled then the callback is executed by the main thread. This can
|
|
609
|
+
lead to unexpected slowdowns and behavior.
|
|
610
|
+
"""
|
|
611
|
+
try:
|
|
612
|
+
result: tuple[bool, _T | Exception] = (
|
|
613
|
+
True,
|
|
614
|
+
work(*args, **kwargs),
|
|
615
|
+
)
|
|
616
|
+
except Exception as e:
|
|
617
|
+
logger.error(f"An exception was raised in {repr(work)}", exc_info=True)
|
|
618
|
+
file_meta.error_details = e
|
|
619
|
+
result = (False, e)
|
|
620
|
+
try:
|
|
621
|
+
_callback(*result, file_meta)
|
|
622
|
+
except Exception as e:
|
|
623
|
+
# TODO: if an exception happens in a callback, the exception will not
|
|
624
|
+
# propagate to the main thread. We need to save these Exceptions
|
|
625
|
+
# somewhere and then re-raise by the main thread. For now let's log
|
|
626
|
+
# this exception, but for a long term solution see my
|
|
627
|
+
# TODO comment for SnowflakeFileMeta
|
|
628
|
+
with cv_main_thread:
|
|
629
|
+
nonlocal exception_caught_in_callback
|
|
630
|
+
exception_caught_in_callback = e
|
|
631
|
+
cv_main_thread.notify()
|
|
632
|
+
if not result[0]:
|
|
633
|
+
# Re-raising the exception from the work function, it would already
|
|
634
|
+
# be logged at this point
|
|
635
|
+
logger.error(
|
|
636
|
+
f"An exception was raised in {repr(callback)}", exc_info=True
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
for file_client in files:
|
|
640
|
+
callback = partial(preprocess_done_cb, done_client=file_client)
|
|
641
|
+
if is_upload:
|
|
642
|
+
preprocess_tpe.submit(
|
|
643
|
+
function_and_callback_wrapper,
|
|
644
|
+
# Work fn
|
|
645
|
+
file_client.prepare_upload,
|
|
646
|
+
# Callback fn
|
|
647
|
+
callback,
|
|
648
|
+
file_client.meta,
|
|
649
|
+
)
|
|
650
|
+
else:
|
|
651
|
+
preprocess_tpe.submit(
|
|
652
|
+
function_and_callback_wrapper,
|
|
653
|
+
# Work fn
|
|
654
|
+
file_client.prepare_download,
|
|
655
|
+
# Callback fn
|
|
656
|
+
callback,
|
|
657
|
+
file_client.meta,
|
|
658
|
+
)
|
|
659
|
+
transfer_metadata.num_files_started += 1 # TODO: do we need this?
|
|
660
|
+
|
|
661
|
+
with cv_main_thread:
|
|
662
|
+
while transfer_metadata.num_files_completed < num_total_files:
|
|
663
|
+
cv_main_thread.wait()
|
|
664
|
+
if exception_caught_in_callback is not None:
|
|
665
|
+
raise exception_caught_in_callback
|
|
666
|
+
|
|
667
|
+
self._results = metas
|
|
668
|
+
|
|
669
|
+
def _create_file_transfer_client(
|
|
670
|
+
self, meta: SnowflakeFileMeta
|
|
671
|
+
) -> SnowflakeStorageClient:
|
|
672
|
+
if self._stage_location_type == LOCAL_FS:
|
|
673
|
+
return SnowflakeLocalStorageClient(
|
|
674
|
+
meta,
|
|
675
|
+
self._stage_info,
|
|
676
|
+
4 * megabyte,
|
|
677
|
+
unsafe_file_write=self._unsafe_file_write,
|
|
678
|
+
)
|
|
679
|
+
elif self._stage_location_type == AZURE_FS:
|
|
680
|
+
return SnowflakeAzureRestClient(
|
|
681
|
+
meta,
|
|
682
|
+
self._credentials,
|
|
683
|
+
AZURE_CHUNK_SIZE,
|
|
684
|
+
self._stage_info,
|
|
685
|
+
unsafe_file_write=self._unsafe_file_write,
|
|
686
|
+
)
|
|
687
|
+
elif self._stage_location_type == S3_FS:
|
|
688
|
+
return SnowflakeS3RestClient(
|
|
689
|
+
meta,
|
|
690
|
+
self._credentials,
|
|
691
|
+
self._stage_info,
|
|
692
|
+
_chunk_size_calculator(meta.src_file_size),
|
|
693
|
+
use_accelerate_endpoint=self._use_accelerate_endpoint,
|
|
694
|
+
use_s3_regional_url=self._use_s3_regional_url,
|
|
695
|
+
unsafe_file_write=self._unsafe_file_write,
|
|
696
|
+
)
|
|
697
|
+
elif self._stage_location_type == GCS_FS:
|
|
698
|
+
return SnowflakeGCSRestClient(
|
|
699
|
+
meta,
|
|
700
|
+
self._credentials,
|
|
701
|
+
self._stage_info,
|
|
702
|
+
self._cursor._connection,
|
|
703
|
+
self._command,
|
|
704
|
+
unsafe_file_write=self._unsafe_file_write,
|
|
705
|
+
)
|
|
706
|
+
raise Exception(f"{self._stage_location_type} is an unknown stage type")
|
|
707
|
+
|
|
708
|
+
def _transfer_accelerate_config(self) -> None:
|
|
709
|
+
if self._stage_location_type == S3_FS and self._file_metadata:
|
|
710
|
+
client = self._create_file_transfer_client(self._file_metadata[0])
|
|
711
|
+
self._use_accelerate_endpoint = client.transfer_accelerate_config()
|
|
712
|
+
|
|
713
|
+
def result(self) -> dict[str, Any]:
|
|
714
|
+
converter_class = self._cursor._connection.converter_class
|
|
715
|
+
rowset = []
|
|
716
|
+
if self._command_type == CMD_TYPE_UPLOAD:
|
|
717
|
+
if hasattr(self, "_results"):
|
|
718
|
+
for meta in self._results:
|
|
719
|
+
if meta.src_compression_type is not None:
|
|
720
|
+
src_compression_type = meta.src_compression_type.name
|
|
721
|
+
else:
|
|
722
|
+
src_compression_type = "NONE"
|
|
723
|
+
|
|
724
|
+
if meta.dst_compression_type is not None:
|
|
725
|
+
dst_compression_type = meta.dst_compression_type.name
|
|
726
|
+
else:
|
|
727
|
+
dst_compression_type = "NONE"
|
|
728
|
+
|
|
729
|
+
error_details: str = (
|
|
730
|
+
repr(meta.error_details)
|
|
731
|
+
if meta.error_details is not None
|
|
732
|
+
else ""
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
src_file_size = (
|
|
736
|
+
meta.src_file_size
|
|
737
|
+
if converter_class != SnowflakeConverterSnowSQL
|
|
738
|
+
else str(meta.src_file_size)
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
dst_file_size = (
|
|
742
|
+
meta.dst_file_size
|
|
743
|
+
if converter_class != SnowflakeConverterSnowSQL
|
|
744
|
+
else str(meta.dst_file_size)
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
logger.debug(
|
|
748
|
+
f"raise_put_get_error: {self._raise_put_get_error}, "
|
|
749
|
+
f"{meta.result_status}, {type(meta.result_status)}, "
|
|
750
|
+
f"{ResultStatus.ERROR}, {type(ResultStatus.ERROR)}",
|
|
751
|
+
)
|
|
752
|
+
if self._raise_put_get_error and error_details:
|
|
753
|
+
Error.errorhandler_wrapper(
|
|
754
|
+
self._cursor.connection,
|
|
755
|
+
self._cursor,
|
|
756
|
+
OperationalError,
|
|
757
|
+
{
|
|
758
|
+
"msg": "While putting file(s) there was an error: "
|
|
759
|
+
f"'{error_details}', this might be caused by "
|
|
760
|
+
f"your access to the blob storage provider, "
|
|
761
|
+
f"or by Snowflake.",
|
|
762
|
+
"errno": ER_FAILED_TO_UPLOAD_TO_STAGE,
|
|
763
|
+
},
|
|
764
|
+
)
|
|
765
|
+
rowset.append(
|
|
766
|
+
[
|
|
767
|
+
meta.name,
|
|
768
|
+
meta.dst_file_name,
|
|
769
|
+
src_file_size,
|
|
770
|
+
dst_file_size,
|
|
771
|
+
src_compression_type,
|
|
772
|
+
dst_compression_type,
|
|
773
|
+
meta.result_status,
|
|
774
|
+
error_details,
|
|
775
|
+
]
|
|
776
|
+
)
|
|
777
|
+
return {
|
|
778
|
+
"rowtype": [
|
|
779
|
+
result_text_column_desc("source"),
|
|
780
|
+
result_text_column_desc("target"),
|
|
781
|
+
result_fixed_column_desc("source_size"),
|
|
782
|
+
result_fixed_column_desc("target_size"),
|
|
783
|
+
result_text_column_desc("source_compression"),
|
|
784
|
+
result_text_column_desc("target_compression"),
|
|
785
|
+
result_text_column_desc("status"),
|
|
786
|
+
result_text_column_desc("message"),
|
|
787
|
+
],
|
|
788
|
+
"rowset": sorted(rowset),
|
|
789
|
+
}
|
|
790
|
+
else: # DOWNLOAD
|
|
791
|
+
if hasattr(self, "_results"):
|
|
792
|
+
for meta in self._results:
|
|
793
|
+
dst_file_size = (
|
|
794
|
+
meta.dst_file_size
|
|
795
|
+
if converter_class != SnowflakeConverterSnowSQL
|
|
796
|
+
else str(meta.dst_file_size)
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
error_details: str = (
|
|
800
|
+
repr(meta.error_details)
|
|
801
|
+
if meta.error_details is not None
|
|
802
|
+
else ""
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
if self._raise_put_get_error and error_details:
|
|
806
|
+
Error.errorhandler_wrapper(
|
|
807
|
+
self._cursor.connection,
|
|
808
|
+
self._cursor,
|
|
809
|
+
OperationalError,
|
|
810
|
+
{
|
|
811
|
+
"msg": "While getting file(s) there was an error: "
|
|
812
|
+
f"'{error_details}', this might be caused by "
|
|
813
|
+
f"your access to the blob storage provider, "
|
|
814
|
+
f"or by Snowflake.",
|
|
815
|
+
"errno": ER_FAILED_TO_DOWNLOAD_FROM_STAGE,
|
|
816
|
+
},
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
rowset.append(
|
|
820
|
+
[
|
|
821
|
+
meta.dst_file_name,
|
|
822
|
+
dst_file_size,
|
|
823
|
+
meta.result_status,
|
|
824
|
+
error_details,
|
|
825
|
+
]
|
|
826
|
+
)
|
|
827
|
+
return {
|
|
828
|
+
"rowtype": [
|
|
829
|
+
result_text_column_desc("file"),
|
|
830
|
+
result_fixed_column_desc("size"),
|
|
831
|
+
result_text_column_desc("status"),
|
|
832
|
+
result_text_column_desc("message"),
|
|
833
|
+
],
|
|
834
|
+
"rowset": sorted(rowset),
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
def _expand_filenames(self, locations: list[str]) -> list[str]:
|
|
838
|
+
canonical_locations = []
|
|
839
|
+
for file_name in locations:
|
|
840
|
+
if self._command_type == CMD_TYPE_UPLOAD:
|
|
841
|
+
file_name = os.path.expanduser(file_name)
|
|
842
|
+
if (
|
|
843
|
+
IS_WINDOWS
|
|
844
|
+
and len(file_name) > 2
|
|
845
|
+
and file_name[0] == "/"
|
|
846
|
+
and file_name[2] == ":"
|
|
847
|
+
):
|
|
848
|
+
# Since python 3.13 os.path.isabs returns different values for URI or paths starting with a '/' etc. on Windows (https://github.com/python/cpython/issues/125283)
|
|
849
|
+
# Windows path: /C:/data/file1.txt is not treated as absolute - could be prefixed with another Windows driver's letter and colon.
|
|
850
|
+
file_name = file_name[1:]
|
|
851
|
+
if not os.path.isabs(file_name):
|
|
852
|
+
file_name = os.path.abspath(file_name)
|
|
853
|
+
files = glob.glob(file_name)
|
|
854
|
+
canonical_locations += files
|
|
855
|
+
else:
|
|
856
|
+
canonical_locations.append(file_name)
|
|
857
|
+
|
|
858
|
+
return canonical_locations
|
|
859
|
+
|
|
860
|
+
def _init_encryption_material(self) -> None:
|
|
861
|
+
self._encryption_material = []
|
|
862
|
+
if self._ret["data"].get("encryptionMaterial") is not None:
|
|
863
|
+
root_node = self._ret["data"]["encryptionMaterial"]
|
|
864
|
+
logger.debug(self._command_type)
|
|
865
|
+
|
|
866
|
+
if self._command_type == CMD_TYPE_UPLOAD:
|
|
867
|
+
self._encryption_material.append(
|
|
868
|
+
SnowflakeFileEncryptionMaterial(
|
|
869
|
+
query_stage_master_key=root_node["queryStageMasterKey"],
|
|
870
|
+
query_id=root_node["queryId"],
|
|
871
|
+
smk_id=root_node["smkId"],
|
|
872
|
+
)
|
|
873
|
+
)
|
|
874
|
+
else:
|
|
875
|
+
for elem in root_node:
|
|
876
|
+
if elem is not None:
|
|
877
|
+
self._encryption_material.append(
|
|
878
|
+
SnowflakeFileEncryptionMaterial(
|
|
879
|
+
query_stage_master_key=elem["queryStageMasterKey"],
|
|
880
|
+
query_id=elem["queryId"],
|
|
881
|
+
smk_id=elem["smkId"],
|
|
882
|
+
)
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
def _parse_command(self) -> None:
|
|
886
|
+
if "data" not in self._ret:
|
|
887
|
+
Error.errorhandler_wrapper(
|
|
888
|
+
self._cursor.connection,
|
|
889
|
+
self._cursor,
|
|
890
|
+
DatabaseError,
|
|
891
|
+
{
|
|
892
|
+
"msg": "Failed to parse server's response",
|
|
893
|
+
"errno": ER_INVALID_STAGE_LOCATION,
|
|
894
|
+
},
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
response = self._ret["data"]
|
|
898
|
+
|
|
899
|
+
self._command_type = response["command"]
|
|
900
|
+
|
|
901
|
+
self._init_encryption_material()
|
|
902
|
+
|
|
903
|
+
if not isinstance(response.get("src_locations"), list):
|
|
904
|
+
Error.errorhandler_wrapper(
|
|
905
|
+
self._cursor.connection,
|
|
906
|
+
self._cursor,
|
|
907
|
+
DatabaseError,
|
|
908
|
+
{
|
|
909
|
+
"msg": "Failed to parse the location",
|
|
910
|
+
"errno": ER_INVALID_STAGE_LOCATION,
|
|
911
|
+
},
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
self._src_locations = response["src_locations"]
|
|
915
|
+
|
|
916
|
+
self._parallel = response.get("parallel", 1)
|
|
917
|
+
self._overwrite = self._force_put_overwrite or response.get("overwrite", False)
|
|
918
|
+
self._stage_location_type = response["stageInfo"]["locationType"].upper()
|
|
919
|
+
|
|
920
|
+
if self._stage_location_type not in VALID_STORAGE:
|
|
921
|
+
Error.errorhandler_wrapper(
|
|
922
|
+
self._cursor.connection,
|
|
923
|
+
self._cursor,
|
|
924
|
+
OperationalError,
|
|
925
|
+
{
|
|
926
|
+
"msg": f"Destination location type is not valid: {self._stage_location_type}",
|
|
927
|
+
"errno": ER_INVALID_STAGE_FS,
|
|
928
|
+
},
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
self._stage_location = response["stageInfo"]["location"]
|
|
932
|
+
self._stage_info = response["stageInfo"]
|
|
933
|
+
self._credentials = StorageCredential(
|
|
934
|
+
self._stage_info["creds"], self._cursor.connection, self._command
|
|
935
|
+
)
|
|
936
|
+
self._presigned_urls = self._ret["data"].get("presignedUrls")
|
|
937
|
+
|
|
938
|
+
if self._command_type == CMD_TYPE_UPLOAD:
|
|
939
|
+
if self._source_from_stream:
|
|
940
|
+
self._src_files = self._src_locations
|
|
941
|
+
else:
|
|
942
|
+
self._src_files = list(self._expand_filenames(self._src_locations))
|
|
943
|
+
self._auto_compress = (
|
|
944
|
+
"autoCompress" not in response or response["autoCompress"]
|
|
945
|
+
)
|
|
946
|
+
self._source_compression = (
|
|
947
|
+
response["sourceCompression"].lower()
|
|
948
|
+
if "sourceCompression" in response
|
|
949
|
+
else ""
|
|
950
|
+
)
|
|
951
|
+
else:
|
|
952
|
+
self._src_files = list(self._src_locations)
|
|
953
|
+
self._src_file_to_encryption_material = {}
|
|
954
|
+
if len(response["src_locations"]) == len(self._encryption_material):
|
|
955
|
+
for idx, src_file in enumerate(self._src_files):
|
|
956
|
+
logger.debug(src_file)
|
|
957
|
+
self._src_file_to_encryption_material[src_file] = (
|
|
958
|
+
self._encryption_material[idx]
|
|
959
|
+
)
|
|
960
|
+
elif len(self._encryption_material) != 0:
|
|
961
|
+
# some encryption material exists. Zero means no encryption
|
|
962
|
+
Error.errorhandler_wrapper(
|
|
963
|
+
self._cursor.connection,
|
|
964
|
+
self._cursor,
|
|
965
|
+
InternalError,
|
|
966
|
+
{
|
|
967
|
+
"msg": (
|
|
968
|
+
"The number of downloading files doesn't match "
|
|
969
|
+
f"the encryption materials: files={len(response['src_locations'])}, "
|
|
970
|
+
f"encmat={len(self._encryption_material)}"
|
|
971
|
+
),
|
|
972
|
+
"errno": ER_INTERNAL_NOT_MATCH_ENCRYPT_MATERIAL,
|
|
973
|
+
},
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
self._local_location = os.path.expanduser(response["localLocation"])
|
|
977
|
+
if not os.path.isdir(self._local_location):
|
|
978
|
+
# NOTE: isdir follows the symlink
|
|
979
|
+
Error.errorhandler_wrapper(
|
|
980
|
+
self._cursor.connection,
|
|
981
|
+
self._cursor,
|
|
982
|
+
ProgrammingError,
|
|
983
|
+
{
|
|
984
|
+
"msg": f"The local path is not a directory: {self._local_location}",
|
|
985
|
+
"errno": ER_LOCAL_PATH_NOT_DIRECTORY,
|
|
986
|
+
},
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
def _init_file_metadata(self) -> None:
|
|
990
|
+
logger.debug(f"command type: {self._command_type}")
|
|
991
|
+
|
|
992
|
+
if self._command_type == CMD_TYPE_UPLOAD:
|
|
993
|
+
if not self._src_files:
|
|
994
|
+
file_name = (
|
|
995
|
+
self._ret["data"]["src_locations"]
|
|
996
|
+
if "data" in self._ret and "src_locations" in self._ret["data"]
|
|
997
|
+
else "None"
|
|
998
|
+
)
|
|
999
|
+
Error.errorhandler_wrapper(
|
|
1000
|
+
self._cursor.connection,
|
|
1001
|
+
self._cursor,
|
|
1002
|
+
ProgrammingError,
|
|
1003
|
+
{
|
|
1004
|
+
"msg": f"File doesn't exist: {file_name}",
|
|
1005
|
+
"errno": ER_FILE_NOT_EXISTS,
|
|
1006
|
+
},
|
|
1007
|
+
)
|
|
1008
|
+
if self._source_from_stream:
|
|
1009
|
+
self._file_metadata.append(
|
|
1010
|
+
SnowflakeFileMeta(
|
|
1011
|
+
name=os.path.basename(self._src_files[0]),
|
|
1012
|
+
src_file_name=self._src_files[0],
|
|
1013
|
+
intermediate_stream=self._source_from_stream,
|
|
1014
|
+
src_file_size=self._source_from_stream.seek(0, os.SEEK_END),
|
|
1015
|
+
stage_location_type=self._stage_location_type,
|
|
1016
|
+
encryption_material=(
|
|
1017
|
+
self._encryption_material[0]
|
|
1018
|
+
if len(self._encryption_material) > 0
|
|
1019
|
+
else None
|
|
1020
|
+
),
|
|
1021
|
+
)
|
|
1022
|
+
)
|
|
1023
|
+
self._source_from_stream.seek(0)
|
|
1024
|
+
else:
|
|
1025
|
+
for file_name in self._src_files:
|
|
1026
|
+
if not os.path.exists(file_name):
|
|
1027
|
+
Error.errorhandler_wrapper(
|
|
1028
|
+
self._cursor.connection,
|
|
1029
|
+
self._cursor,
|
|
1030
|
+
ProgrammingError,
|
|
1031
|
+
{
|
|
1032
|
+
"msg": f"File doesn't exist: {file_name}",
|
|
1033
|
+
"errno": ER_FILE_NOT_EXISTS,
|
|
1034
|
+
},
|
|
1035
|
+
)
|
|
1036
|
+
elif os.path.isdir(file_name):
|
|
1037
|
+
Error.errorhandler_wrapper(
|
|
1038
|
+
self._cursor.connection,
|
|
1039
|
+
self._cursor,
|
|
1040
|
+
ProgrammingError,
|
|
1041
|
+
{
|
|
1042
|
+
"msg": f"Not a file but a directory: {file_name}",
|
|
1043
|
+
"errno": ER_FILE_NOT_EXISTS,
|
|
1044
|
+
},
|
|
1045
|
+
)
|
|
1046
|
+
statinfo = os.stat(file_name)
|
|
1047
|
+
self._file_metadata.append(
|
|
1048
|
+
SnowflakeFileMeta(
|
|
1049
|
+
name=os.path.basename(file_name),
|
|
1050
|
+
src_file_name=file_name,
|
|
1051
|
+
src_file_size=statinfo.st_size,
|
|
1052
|
+
stage_location_type=self._stage_location_type,
|
|
1053
|
+
encryption_material=(
|
|
1054
|
+
self._encryption_material[0]
|
|
1055
|
+
if len(self._encryption_material) > 0
|
|
1056
|
+
else None
|
|
1057
|
+
),
|
|
1058
|
+
)
|
|
1059
|
+
)
|
|
1060
|
+
elif self._command_type == CMD_TYPE_DOWNLOAD:
|
|
1061
|
+
basename_counts = dict()
|
|
1062
|
+
for idx, file_name in enumerate(self._src_files):
|
|
1063
|
+
if not file_name:
|
|
1064
|
+
continue
|
|
1065
|
+
dst_file_name = (
|
|
1066
|
+
self._strip_stage_prefix_from_dst_file_name_for_download(file_name)
|
|
1067
|
+
)
|
|
1068
|
+
first_path_sep = dst_file_name.find("/")
|
|
1069
|
+
dst_file_name = (
|
|
1070
|
+
dst_file_name[first_path_sep + 1 :]
|
|
1071
|
+
if first_path_sep >= 0
|
|
1072
|
+
else dst_file_name
|
|
1073
|
+
)
|
|
1074
|
+
url = None
|
|
1075
|
+
if self._presigned_urls and idx < len(self._presigned_urls):
|
|
1076
|
+
url = self._presigned_urls[idx]
|
|
1077
|
+
|
|
1078
|
+
basename = os.path.basename(file_name)
|
|
1079
|
+
basename_counts[basename] = basename_counts.get(basename, 0) + 1
|
|
1080
|
+
|
|
1081
|
+
self._file_metadata.append(
|
|
1082
|
+
SnowflakeFileMeta(
|
|
1083
|
+
name=basename,
|
|
1084
|
+
src_file_name=file_name,
|
|
1085
|
+
dst_file_name=dst_file_name,
|
|
1086
|
+
stage_location_type=self._stage_location_type,
|
|
1087
|
+
local_location=self._local_location,
|
|
1088
|
+
presigned_url=url,
|
|
1089
|
+
encryption_material=(
|
|
1090
|
+
self._src_file_to_encryption_material[file_name]
|
|
1091
|
+
if file_name in self._src_file_to_encryption_material
|
|
1092
|
+
else None
|
|
1093
|
+
),
|
|
1094
|
+
)
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
# TODO: remove this warning once we support directory structure for GET
|
|
1098
|
+
duplicate_basenames = [k for k, v in basename_counts.items() if v > 1]
|
|
1099
|
+
if duplicate_basenames:
|
|
1100
|
+
logger.warning(
|
|
1101
|
+
f"Downloading multiple files with the same name could cause failures. "
|
|
1102
|
+
f"File names with multiple entries: {duplicate_basenames}"
|
|
1103
|
+
)
|
|
1104
|
+
|
|
1105
|
+
def _process_file_compression_type(self) -> None:
|
|
1106
|
+
user_specified_source_compression = None
|
|
1107
|
+
if self._source_compression == "auto_detect":
|
|
1108
|
+
auto_detect = True
|
|
1109
|
+
elif self._source_compression == "none":
|
|
1110
|
+
auto_detect = False
|
|
1111
|
+
else:
|
|
1112
|
+
user_specified_source_compression = lookup_by_mime_sub_type(
|
|
1113
|
+
self._source_compression
|
|
1114
|
+
)
|
|
1115
|
+
if (
|
|
1116
|
+
user_specified_source_compression is None
|
|
1117
|
+
or not user_specified_source_compression.is_supported
|
|
1118
|
+
):
|
|
1119
|
+
Error.errorhandler_wrapper(
|
|
1120
|
+
self._cursor.connection,
|
|
1121
|
+
self._cursor,
|
|
1122
|
+
ProgrammingError,
|
|
1123
|
+
{
|
|
1124
|
+
"msg": f"Feature is not supported: {user_specified_source_compression}",
|
|
1125
|
+
"errno": ER_COMPRESSION_NOT_SUPPORTED,
|
|
1126
|
+
},
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
auto_detect = False
|
|
1130
|
+
|
|
1131
|
+
for m in self._file_metadata:
|
|
1132
|
+
file_name = m.src_file_name
|
|
1133
|
+
|
|
1134
|
+
current_file_compression_type: CompressionType | None = None
|
|
1135
|
+
if auto_detect:
|
|
1136
|
+
mimetypes.init()
|
|
1137
|
+
_, encoding = mimetypes.guess_type(file_name)
|
|
1138
|
+
|
|
1139
|
+
if encoding is None:
|
|
1140
|
+
test = None
|
|
1141
|
+
if not self._source_from_stream:
|
|
1142
|
+
with open(file_name, "rb") as f:
|
|
1143
|
+
test = f.read(4)
|
|
1144
|
+
else:
|
|
1145
|
+
test = self._source_from_stream.read(4)
|
|
1146
|
+
self._source_from_stream.seek(0)
|
|
1147
|
+
if file_name.endswith(".br"):
|
|
1148
|
+
encoding = "br"
|
|
1149
|
+
elif test and test[:3] == b"ORC":
|
|
1150
|
+
encoding = "orc"
|
|
1151
|
+
elif test and test == b"PAR1":
|
|
1152
|
+
encoding = "parquet"
|
|
1153
|
+
elif test and (int(binascii.hexlify(test), 16) == 0x28B52FFD):
|
|
1154
|
+
encoding = "zstd"
|
|
1155
|
+
|
|
1156
|
+
if encoding is not None:
|
|
1157
|
+
logger.debug(
|
|
1158
|
+
f"detected the encoding {encoding}: file={file_name}",
|
|
1159
|
+
)
|
|
1160
|
+
current_file_compression_type = lookup_by_mime_sub_type(encoding)
|
|
1161
|
+
else:
|
|
1162
|
+
logger.debug(f"no file encoding was detected: file={file_name}")
|
|
1163
|
+
|
|
1164
|
+
if (
|
|
1165
|
+
current_file_compression_type is not None
|
|
1166
|
+
and not current_file_compression_type.is_supported
|
|
1167
|
+
):
|
|
1168
|
+
Error.errorhandler_wrapper(
|
|
1169
|
+
self._cursor.connection,
|
|
1170
|
+
self._cursor,
|
|
1171
|
+
ProgrammingError,
|
|
1172
|
+
{
|
|
1173
|
+
"msg": f"Feature is not supported: {current_file_compression_type}",
|
|
1174
|
+
"errno": ER_COMPRESSION_NOT_SUPPORTED,
|
|
1175
|
+
},
|
|
1176
|
+
)
|
|
1177
|
+
else:
|
|
1178
|
+
current_file_compression_type = user_specified_source_compression
|
|
1179
|
+
|
|
1180
|
+
if current_file_compression_type is not None:
|
|
1181
|
+
m.src_compression_type = current_file_compression_type
|
|
1182
|
+
if current_file_compression_type.is_supported:
|
|
1183
|
+
m.dst_compression_type = current_file_compression_type
|
|
1184
|
+
m.require_compress = False
|
|
1185
|
+
m.dst_file_name = m.name
|
|
1186
|
+
else:
|
|
1187
|
+
Error.errorhandler_wrapper(
|
|
1188
|
+
self._cursor.connection,
|
|
1189
|
+
self._cursor,
|
|
1190
|
+
ProgrammingError,
|
|
1191
|
+
{
|
|
1192
|
+
"msg": f"Feature is not supported: {current_file_compression_type}",
|
|
1193
|
+
"errno": ER_COMPRESSION_NOT_SUPPORTED,
|
|
1194
|
+
},
|
|
1195
|
+
)
|
|
1196
|
+
else:
|
|
1197
|
+
# src is not compressed but the destination want to be
|
|
1198
|
+
# compressed unless the users disable it
|
|
1199
|
+
m.require_compress = self._auto_compress
|
|
1200
|
+
m.src_compression_type = None
|
|
1201
|
+
if self._auto_compress:
|
|
1202
|
+
m.dst_file_name = m.name + CompressionTypes["GZIP"].file_extension
|
|
1203
|
+
m.dst_compression_type = CompressionTypes["GZIP"]
|
|
1204
|
+
else:
|
|
1205
|
+
m.dst_file_name = m.name
|
|
1206
|
+
m.dst_compression_type = None
|
|
1207
|
+
|
|
1208
|
+
def _strip_stage_prefix_from_dst_file_name_for_download(self, dst_file_name):
|
|
1209
|
+
"""Strips the stage prefix from dst_file_name for download.
|
|
1210
|
+
|
|
1211
|
+
Note that this is no-op in most cases, and therefore we return as is.
|
|
1212
|
+
But for some workloads they will monkeypatch this method to add their
|
|
1213
|
+
stripping logic.
|
|
1214
|
+
"""
|
|
1215
|
+
return dst_file_name
|