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.
Files changed (246) hide show
  1. snowflake/connector/__init__.py +96 -0
  2. snowflake/connector/_query_context_cache.py +276 -0
  3. snowflake/connector/_sql_util.py +48 -0
  4. snowflake/connector/_utils.py +70 -0
  5. snowflake/connector/arrow_context.py +203 -0
  6. snowflake/connector/auth/__init__.py +53 -0
  7. snowflake/connector/auth/_auth.py +586 -0
  8. snowflake/connector/auth/_http_server.py +220 -0
  9. snowflake/connector/auth/_oauth_base.py +400 -0
  10. snowflake/connector/auth/by_plugin.py +219 -0
  11. snowflake/connector/auth/default.py +36 -0
  12. snowflake/connector/auth/idtoken.py +72 -0
  13. snowflake/connector/auth/keypair.py +222 -0
  14. snowflake/connector/auth/no_auth.py +39 -0
  15. snowflake/connector/auth/oauth.py +49 -0
  16. snowflake/connector/auth/oauth_code.py +479 -0
  17. snowflake/connector/auth/oauth_credentials.py +66 -0
  18. snowflake/connector/auth/okta.py +335 -0
  19. snowflake/connector/auth/pat.py +39 -0
  20. snowflake/connector/auth/usrpwdmfa.py +69 -0
  21. snowflake/connector/auth/webbrowser.py +502 -0
  22. snowflake/connector/auth/workload_identity.py +94 -0
  23. snowflake/connector/azure_storage_client.py +276 -0
  24. snowflake/connector/backoff_policies.py +141 -0
  25. snowflake/connector/bind_upload_agent.py +89 -0
  26. snowflake/connector/cache.py +696 -0
  27. snowflake/connector/compat.py +127 -0
  28. snowflake/connector/config_manager.py +496 -0
  29. snowflake/connector/connection.py +2273 -0
  30. snowflake/connector/connection_diagnostic.py +776 -0
  31. snowflake/connector/constants.py +442 -0
  32. snowflake/connector/converter.py +785 -0
  33. snowflake/connector/converter_issue23517.py +87 -0
  34. snowflake/connector/converter_null.py +14 -0
  35. snowflake/connector/converter_snowsql.py +205 -0
  36. snowflake/connector/cursor.py +1951 -0
  37. snowflake/connector/dbapi.py +53 -0
  38. snowflake/connector/description.py +19 -0
  39. snowflake/connector/direct_file_operation_utils.py +88 -0
  40. snowflake/connector/encryption_util.py +220 -0
  41. snowflake/connector/errorcode.py +91 -0
  42. snowflake/connector/errors.py +615 -0
  43. snowflake/connector/externals_utils/__init__.py +0 -0
  44. snowflake/connector/externals_utils/externals_setup.py +27 -0
  45. snowflake/connector/feature.py +4 -0
  46. snowflake/connector/file_compression_type.py +118 -0
  47. snowflake/connector/file_lock.py +72 -0
  48. snowflake/connector/file_transfer_agent.py +1215 -0
  49. snowflake/connector/file_util.py +153 -0
  50. snowflake/connector/gcs_storage_client.py +474 -0
  51. snowflake/connector/gzip_decoder.py +85 -0
  52. snowflake/connector/local_storage_client.py +90 -0
  53. snowflake/connector/log_configuration.py +60 -0
  54. snowflake/connector/logging_utils/__init__.py +0 -0
  55. snowflake/connector/logging_utils/filters.py +72 -0
  56. snowflake/connector/nanoarrow_arrow_iterator.cp313-win_amd64.pyd +0 -0
  57. snowflake/connector/nanoarrow_cpp/ArrowIterator/ArrayConverter.cpp +60 -0
  58. snowflake/connector/nanoarrow_cpp/ArrowIterator/ArrayConverter.hpp +29 -0
  59. snowflake/connector/nanoarrow_cpp/ArrowIterator/BinaryConverter.cpp +19 -0
  60. snowflake/connector/nanoarrow_cpp/ArrowIterator/BinaryConverter.hpp +26 -0
  61. snowflake/connector/nanoarrow_cpp/ArrowIterator/BooleanConverter.cpp +21 -0
  62. snowflake/connector/nanoarrow_cpp/ArrowIterator/BooleanConverter.hpp +23 -0
  63. snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowChunkIterator.cpp +557 -0
  64. snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowChunkIterator.hpp +98 -0
  65. snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowIterator.cpp +125 -0
  66. snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowIterator.hpp +115 -0
  67. snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowTableIterator.cpp +1005 -0
  68. snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowTableIterator.hpp +127 -0
  69. snowflake/connector/nanoarrow_cpp/ArrowIterator/DateConverter.cpp +47 -0
  70. snowflake/connector/nanoarrow_cpp/ArrowIterator/DateConverter.hpp +46 -0
  71. snowflake/connector/nanoarrow_cpp/ArrowIterator/DecFloatConverter.cpp +83 -0
  72. snowflake/connector/nanoarrow_cpp/ArrowIterator/DecFloatConverter.hpp +35 -0
  73. snowflake/connector/nanoarrow_cpp/ArrowIterator/DecimalConverter.cpp +97 -0
  74. snowflake/connector/nanoarrow_cpp/ArrowIterator/DecimalConverter.hpp +72 -0
  75. snowflake/connector/nanoarrow_cpp/ArrowIterator/FixedSizeListConverter.cpp +73 -0
  76. snowflake/connector/nanoarrow_cpp/ArrowIterator/FixedSizeListConverter.hpp +28 -0
  77. snowflake/connector/nanoarrow_cpp/ArrowIterator/FloatConverter.cpp +30 -0
  78. snowflake/connector/nanoarrow_cpp/ArrowIterator/FloatConverter.hpp +35 -0
  79. snowflake/connector/nanoarrow_cpp/ArrowIterator/IColumnConverter.hpp +17 -0
  80. snowflake/connector/nanoarrow_cpp/ArrowIterator/IntConverter.cpp +23 -0
  81. snowflake/connector/nanoarrow_cpp/ArrowIterator/IntConverter.hpp +45 -0
  82. snowflake/connector/nanoarrow_cpp/ArrowIterator/IntervalConverter.cpp +71 -0
  83. snowflake/connector/nanoarrow_cpp/ArrowIterator/IntervalConverter.hpp +56 -0
  84. snowflake/connector/nanoarrow_cpp/ArrowIterator/LICENSE.txt +209 -0
  85. snowflake/connector/nanoarrow_cpp/ArrowIterator/MapConverter.cpp +75 -0
  86. snowflake/connector/nanoarrow_cpp/ArrowIterator/MapConverter.hpp +30 -0
  87. snowflake/connector/nanoarrow_cpp/ArrowIterator/ObjectConverter.cpp +46 -0
  88. snowflake/connector/nanoarrow_cpp/ArrowIterator/ObjectConverter.hpp +29 -0
  89. snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Common.cpp +8 -0
  90. snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Common.hpp +95 -0
  91. snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Helpers.cpp +57 -0
  92. snowflake/connector/nanoarrow_cpp/ArrowIterator/Python/Helpers.hpp +36 -0
  93. snowflake/connector/nanoarrow_cpp/ArrowIterator/SnowflakeType.cpp +34 -0
  94. snowflake/connector/nanoarrow_cpp/ArrowIterator/SnowflakeType.hpp +48 -0
  95. snowflake/connector/nanoarrow_cpp/ArrowIterator/StringConverter.cpp +19 -0
  96. snowflake/connector/nanoarrow_cpp/ArrowIterator/StringConverter.hpp +26 -0
  97. snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeConverter.cpp +36 -0
  98. snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeConverter.hpp +31 -0
  99. snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeStampConverter.cpp +346 -0
  100. snowflake/connector/nanoarrow_cpp/ArrowIterator/TimeStampConverter.hpp +145 -0
  101. snowflake/connector/nanoarrow_cpp/ArrowIterator/Util/macros.hpp +14 -0
  102. snowflake/connector/nanoarrow_cpp/ArrowIterator/Util/time.cpp +65 -0
  103. snowflake/connector/nanoarrow_cpp/ArrowIterator/Util/time.hpp +68 -0
  104. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_accessors.h +101 -0
  105. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_alloc.h +127 -0
  106. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_assert.h +45 -0
  107. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_builder.h +1908 -0
  108. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_emitter.h +215 -0
  109. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_endian.h +125 -0
  110. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_epilogue.h +7 -0
  111. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_flatbuffers.h +55 -0
  112. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_identifier.h +148 -0
  113. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_iov.h +31 -0
  114. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_prologue.h +8 -0
  115. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_refmap.h +144 -0
  116. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_rtconfig.h +162 -0
  117. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_types.h +97 -0
  118. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_verifier.h +239 -0
  119. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/flatcc_portable.h +14 -0
  120. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/paligned_alloc.h +210 -0
  121. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pattributes.h +84 -0
  122. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic.h +84 -0
  123. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic_pop.h +20 -0
  124. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic_push.h +51 -0
  125. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pendian.h +206 -0
  126. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pendian_detect.h +118 -0
  127. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pinline.h +19 -0
  128. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pinttypes.h +52 -0
  129. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/portable.h +2 -0
  130. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/portable_basic.h +25 -0
  131. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstatic_assert.h +67 -0
  132. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstdalign.h +162 -0
  133. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstdint.h +898 -0
  134. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/punaligned.h +190 -0
  135. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pversion.h +6 -0
  136. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc/portable/pwarnings.h +52 -0
  137. snowflake/connector/nanoarrow_cpp/ArrowIterator/flatcc.c +3204 -0
  138. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow.c +3217 -0
  139. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow.h +3618 -0
  140. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow.hpp +379 -0
  141. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_arrow_iterator.pyx +256 -0
  142. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_device.c +512 -0
  143. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_device.h +350 -0
  144. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_ipc.c +33273 -0
  145. snowflake/connector/nanoarrow_cpp/ArrowIterator/nanoarrow_ipc.h +397 -0
  146. snowflake/connector/nanoarrow_cpp/Logging/logging.cpp +100 -0
  147. snowflake/connector/nanoarrow_cpp/Logging/logging.hpp +49 -0
  148. snowflake/connector/network.py +1297 -0
  149. snowflake/connector/ocsp_asn1crypto.py +447 -0
  150. snowflake/connector/ocsp_snowflake.py +1883 -0
  151. snowflake/connector/options.py +131 -0
  152. snowflake/connector/pandas_tools.py +732 -0
  153. snowflake/connector/proxy.py +43 -0
  154. snowflake/connector/py.typed +0 -0
  155. snowflake/connector/result_batch.py +786 -0
  156. snowflake/connector/result_set.py +319 -0
  157. snowflake/connector/s3_storage_client.py +605 -0
  158. snowflake/connector/secret_detector.py +181 -0
  159. snowflake/connector/sf_dirs.py +64 -0
  160. snowflake/connector/sfbinaryformat.py +35 -0
  161. snowflake/connector/sfdatetime.py +345 -0
  162. snowflake/connector/snow_logging.py +144 -0
  163. snowflake/connector/sqlstate.py +9 -0
  164. snowflake/connector/ssd_internal_keys.py +30 -0
  165. snowflake/connector/ssl_wrap_socket.py +134 -0
  166. snowflake/connector/storage_client.py +478 -0
  167. snowflake/connector/telemetry.py +249 -0
  168. snowflake/connector/telemetry_oob.py +543 -0
  169. snowflake/connector/test_util.py +30 -0
  170. snowflake/connector/time_util.py +159 -0
  171. snowflake/connector/token_cache.py +402 -0
  172. snowflake/connector/tool/__init__.py +0 -0
  173. snowflake/connector/tool/dump_certs.py +57 -0
  174. snowflake/connector/tool/dump_ocsp_response.py +139 -0
  175. snowflake/connector/tool/dump_ocsp_response_cache.py +194 -0
  176. snowflake/connector/tool/probe_connection.py +69 -0
  177. snowflake/connector/url_util.py +49 -0
  178. snowflake/connector/util_text.py +301 -0
  179. snowflake/connector/vendored/__init__.py +3 -0
  180. snowflake/connector/vendored/requests/LICENSE +175 -0
  181. snowflake/connector/vendored/requests/__init__.py +169 -0
  182. snowflake/connector/vendored/requests/__version__.py +14 -0
  183. snowflake/connector/vendored/requests/_internal_utils.py +50 -0
  184. snowflake/connector/vendored/requests/adapters.py +537 -0
  185. snowflake/connector/vendored/requests/api.py +157 -0
  186. snowflake/connector/vendored/requests/auth.py +315 -0
  187. snowflake/connector/vendored/requests/certs.py +17 -0
  188. snowflake/connector/vendored/requests/compat.py +79 -0
  189. snowflake/connector/vendored/requests/cookies.py +561 -0
  190. snowflake/connector/vendored/requests/exceptions.py +140 -0
  191. snowflake/connector/vendored/requests/help.py +134 -0
  192. snowflake/connector/vendored/requests/hooks.py +33 -0
  193. snowflake/connector/vendored/requests/models.py +1033 -0
  194. snowflake/connector/vendored/requests/sessions.py +833 -0
  195. snowflake/connector/vendored/requests/status_codes.py +128 -0
  196. snowflake/connector/vendored/requests/structures.py +99 -0
  197. snowflake/connector/vendored/requests/utils.py +1093 -0
  198. snowflake/connector/vendored/urllib3/LICENSE.txt +21 -0
  199. snowflake/connector/vendored/urllib3/__init__.py +85 -0
  200. snowflake/connector/vendored/urllib3/_collections.py +355 -0
  201. snowflake/connector/vendored/urllib3/_version.py +2 -0
  202. snowflake/connector/vendored/urllib3/connection.py +572 -0
  203. snowflake/connector/vendored/urllib3/connectionpool.py +1137 -0
  204. snowflake/connector/vendored/urllib3/contrib/__init__.py +0 -0
  205. snowflake/connector/vendored/urllib3/contrib/_appengine_environ.py +36 -0
  206. snowflake/connector/vendored/urllib3/contrib/_securetransport/__init__.py +0 -0
  207. snowflake/connector/vendored/urllib3/contrib/_securetransport/bindings.py +519 -0
  208. snowflake/connector/vendored/urllib3/contrib/_securetransport/low_level.py +397 -0
  209. snowflake/connector/vendored/urllib3/contrib/appengine.py +314 -0
  210. snowflake/connector/vendored/urllib3/contrib/ntlmpool.py +130 -0
  211. snowflake/connector/vendored/urllib3/contrib/pyopenssl.py +509 -0
  212. snowflake/connector/vendored/urllib3/contrib/securetransport.py +920 -0
  213. snowflake/connector/vendored/urllib3/contrib/socks.py +216 -0
  214. snowflake/connector/vendored/urllib3/exceptions.py +323 -0
  215. snowflake/connector/vendored/urllib3/fields.py +274 -0
  216. snowflake/connector/vendored/urllib3/filepost.py +98 -0
  217. snowflake/connector/vendored/urllib3/packages/__init__.py +0 -0
  218. snowflake/connector/vendored/urllib3/packages/backports/__init__.py +0 -0
  219. snowflake/connector/vendored/urllib3/packages/backports/makefile.py +51 -0
  220. snowflake/connector/vendored/urllib3/packages/backports/weakref_finalize.py +155 -0
  221. snowflake/connector/vendored/urllib3/packages/six.py +1076 -0
  222. snowflake/connector/vendored/urllib3/poolmanager.py +540 -0
  223. snowflake/connector/vendored/urllib3/request.py +191 -0
  224. snowflake/connector/vendored/urllib3/response.py +885 -0
  225. snowflake/connector/vendored/urllib3/util/__init__.py +49 -0
  226. snowflake/connector/vendored/urllib3/util/connection.py +156 -0
  227. snowflake/connector/vendored/urllib3/util/proxy.py +57 -0
  228. snowflake/connector/vendored/urllib3/util/queue.py +22 -0
  229. snowflake/connector/vendored/urllib3/util/request.py +146 -0
  230. snowflake/connector/vendored/urllib3/util/response.py +107 -0
  231. snowflake/connector/vendored/urllib3/util/retry.py +620 -0
  232. snowflake/connector/vendored/urllib3/util/ssl_.py +495 -0
  233. snowflake/connector/vendored/urllib3/util/ssl_match_hostname.py +159 -0
  234. snowflake/connector/vendored/urllib3/util/ssltransport.py +221 -0
  235. snowflake/connector/vendored/urllib3/util/timeout.py +271 -0
  236. snowflake/connector/vendored/urllib3/util/url.py +435 -0
  237. snowflake/connector/vendored/urllib3/util/wait.py +152 -0
  238. snowflake/connector/version.py +3 -0
  239. snowflake/connector/wif_util.py +407 -0
  240. snowflake_connector_python-3.16.0.dist-info/METADATA +1475 -0
  241. snowflake_connector_python-3.16.0.dist-info/RECORD +246 -0
  242. snowflake_connector_python-3.16.0.dist-info/WHEEL +5 -0
  243. snowflake_connector_python-3.16.0.dist-info/entry_points.txt +4 -0
  244. snowflake_connector_python-3.16.0.dist-info/licenses/LICENSE.txt +202 -0
  245. snowflake_connector_python-3.16.0.dist-info/licenses/NOTICE +8 -0
  246. snowflake_connector_python-3.16.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,696 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import logging
5
+ import os
6
+ import pickle
7
+ import platform
8
+ import random
9
+ import string
10
+ import tempfile
11
+ from collections.abc import Iterator
12
+ from os import makedirs, path
13
+ from threading import Lock
14
+ from typing import Generic, NoReturn, TypeVar
15
+
16
+ from filelock import FileLock, Timeout
17
+ from typing_extensions import NamedTuple, Self
18
+
19
+ from . import constants
20
+ from .constants import ENV_VAR_TEST_MODE
21
+
22
+ now = datetime.datetime.now
23
+ getmtime = os.path.getmtime
24
+
25
+ T = TypeVar("T")
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ test_mode = os.getenv(ENV_VAR_TEST_MODE, "").lower() == "true"
30
+
31
+
32
+ class CacheEntry(NamedTuple, Generic[T]):
33
+ expiry: datetime.datetime
34
+ entry: T
35
+
36
+
37
+ K = TypeVar("K")
38
+ V = TypeVar("V")
39
+
40
+
41
+ def is_expired(d: datetime.datetime) -> bool:
42
+ return now() >= d
43
+
44
+
45
+ class SFDictCache(Generic[K, V]):
46
+ """A generic in-memory cache that acts somewhat like a dictionary.
47
+
48
+ Unlike normal dictionaries keys(), values() and items() return list materialized
49
+ at call time, unlike normal dictionaries that return a view object.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ entry_lifetime: int = constants.DAY_IN_SECONDS,
55
+ ) -> None:
56
+ """Inits a SFDictCache with lifetime."""
57
+ self._entry_lifetime = datetime.timedelta(seconds=entry_lifetime)
58
+ self._cache: dict[K, CacheEntry[V]] = {}
59
+ self._lock = Lock()
60
+ self._reset_telemetry()
61
+
62
+ def __len__(self) -> int:
63
+ with self._lock:
64
+ return len(self._cache)
65
+
66
+ @classmethod
67
+ def from_dict(
68
+ cls,
69
+ _dict: dict[K, V],
70
+ **kw,
71
+ ) -> Self:
72
+ """Create an dictionary cache from an already existing dictionary.
73
+
74
+ Note that the same references will be stored in the cache than in
75
+ the dictionary provided.
76
+ """
77
+ cache = cls(**kw)
78
+ cache.update(_dict)
79
+ return cache
80
+
81
+ def _getitem(
82
+ self,
83
+ k: K,
84
+ *,
85
+ should_record_hits: bool = True,
86
+ ) -> V:
87
+ """Non-locking version of __getitem__.
88
+
89
+ This should only be used by internal functions when already
90
+ holding self._lock.
91
+ """
92
+ if test_mode:
93
+ assert (
94
+ self._lock.locked()
95
+ ), "The mutex self._lock should be locked by this thread"
96
+ try:
97
+ t, v = self._cache[k]
98
+ except KeyError:
99
+ self._miss(k)
100
+ raise
101
+ if is_expired(t):
102
+ self._expire(k)
103
+ if should_record_hits:
104
+ self._hit(k)
105
+ return v
106
+
107
+ # aliasing _getitem to unify the api with SFDictFileCache
108
+ _getitem_non_locking = _getitem
109
+
110
+ def _setitem(self, k: K, v: V) -> None:
111
+ """Non-locking version of __setitem__.
112
+
113
+ This should only be used by internal functions when already
114
+ holding self._lock.
115
+ """
116
+ if test_mode:
117
+ assert (
118
+ self._lock.locked()
119
+ ), "The mutex self._lock should be locked by this thread"
120
+ self._cache[k] = CacheEntry(
121
+ expiry=now() + self._entry_lifetime,
122
+ entry=v,
123
+ )
124
+ self._add_or_remove()
125
+
126
+ def __getitem__(
127
+ self,
128
+ k: K,
129
+ ) -> V:
130
+ """Returns an element if it hasn't expired yet in a thread-safe way."""
131
+ with self._lock:
132
+ return self._getitem(k, should_record_hits=True)
133
+
134
+ def __setitem__(
135
+ self,
136
+ k: K,
137
+ v: V,
138
+ ) -> None:
139
+ """Inserts an element in a thread-safe way."""
140
+ with self._lock:
141
+ self._setitem(k, v)
142
+
143
+ def __iter__(self) -> Iterator[K]:
144
+ return iter(self.keys())
145
+
146
+ def keys(self) -> list[K]:
147
+ return [k for k, _ in self.items()]
148
+
149
+ def items(self) -> list[tuple[K, V]]:
150
+ with self._lock:
151
+ values: list[tuple[K, V]] = []
152
+ for k in list(self._cache.keys()):
153
+ try:
154
+ values.append((k, self._getitem(k, should_record_hits=False)))
155
+ except KeyError:
156
+ pass
157
+ return values
158
+
159
+ def values(self) -> list[V]:
160
+ return [v for _, v in self.items()]
161
+
162
+ def get(
163
+ self,
164
+ k: K,
165
+ default: V | None = None,
166
+ ) -> V | None:
167
+ try:
168
+ return self[k]
169
+ except KeyError:
170
+ return default
171
+
172
+ def clear(self) -> None:
173
+ with self._lock:
174
+ self._cache.clear()
175
+ self._reset_telemetry()
176
+
177
+ def _delitem(
178
+ self,
179
+ key: K,
180
+ ) -> None:
181
+ """Non-locking version of __delitem__.
182
+
183
+ This should only be used by internal functions when already
184
+ holding self._lock.
185
+ """
186
+ if test_mode:
187
+ assert (
188
+ self._lock.locked()
189
+ ), "The mutex self._lock should be locked by this thread"
190
+ del self._cache[key]
191
+ self._add_or_remove()
192
+
193
+ def __delitem__(
194
+ self,
195
+ key: K,
196
+ ) -> None:
197
+ with self._lock:
198
+ self._delitem(key)
199
+
200
+ def __contains__(
201
+ self,
202
+ key: K,
203
+ ) -> bool:
204
+ with self._lock:
205
+ try:
206
+ self._getitem(key, should_record_hits=True)
207
+ return True
208
+ except KeyError:
209
+ # Fall through
210
+ return False
211
+
212
+ def _update(
213
+ self,
214
+ other: dict[K, V] | list[tuple[K, V]] | SFDictCache[K, V],
215
+ update_newer_only: bool = False,
216
+ ) -> bool:
217
+ """Non-locking version of update.
218
+
219
+ This should only be used by internal functions when already
220
+ holding self._lock and other._lock.
221
+ """
222
+ if test_mode:
223
+ assert (
224
+ self._lock.locked()
225
+ ), "The mutex self._lock should be locked by this thread"
226
+ to_insert: dict[K, CacheEntry[V]]
227
+ self._clear_expired_entries()
228
+ if isinstance(other, (list, dict)):
229
+ expiry = now() + self._entry_lifetime
230
+ if isinstance(other, list):
231
+ g = iter(other)
232
+ elif isinstance(other, dict):
233
+ g = iter(other.items())
234
+ to_insert = {k: CacheEntry(expiry=expiry, entry=v) for k, v in g}
235
+ elif isinstance(other, SFDictCache):
236
+ other.clear_expired_entries()
237
+ others_items = list(other._cache.items())
238
+ # Only accept values from another cache if their key is not in self,
239
+ # or if expiry is later the self known one
240
+ to_insert = {
241
+ k: v
242
+ for k, v in others_items
243
+ if (
244
+ # self doesn't have this key
245
+ k not in self._cache
246
+ # we should update entries, regardless of whether they are newer
247
+ or (not update_newer_only)
248
+ # other has newer expiry time we want to update newer values only
249
+ or self._cache[k].expiry < v.expiry
250
+ )
251
+ }
252
+ else:
253
+ raise TypeError
254
+ self._cache.update(to_insert)
255
+ if to_insert:
256
+ self._add_or_remove()
257
+ # TODO: this should really save_if_should
258
+ return len(to_insert) > 0
259
+
260
+ def update(
261
+ self,
262
+ other: dict[K, V] | list[tuple[K, V]] | SFDictCache[K, V],
263
+ update_newer_only: bool = False,
264
+ ) -> bool:
265
+ """Insert multiple values at the same time, if self could learn from the other.
266
+
267
+ If this function is given a dictionary, or list expiration timestamps
268
+ will be all the same a self._entry_lifetime form now. If it's
269
+ given another SFDictCache then the timestamps will be taken
270
+ from the other cache.
271
+
272
+ Returns a boolean. It describes whether self learnt anything from other.
273
+
274
+ Note that clear_expired_entries will be called on both caches. To
275
+ prevent deadlocks this is done without acquiring other._lock. The
276
+ intended behavior is to use this function with an unpickled/unused cache.
277
+ If live caches are being merged then use .items() on them first and merge those
278
+ into the other caches.
279
+ """
280
+ with self._lock:
281
+ return self._update(other, update_newer_only)
282
+
283
+ def update_newer(
284
+ self,
285
+ other: dict[K, V] | list[tuple[K, V]] | SFDictCache[K, V],
286
+ ) -> bool:
287
+ """This function is like update, but it only updates newer elements."""
288
+ with self._lock:
289
+ return self._update(
290
+ other,
291
+ update_newer_only=True,
292
+ )
293
+
294
+ def _clear_expired_entries(self) -> None:
295
+ if test_mode:
296
+ assert (
297
+ self._lock.locked()
298
+ ), "The mutex self._lock should be locked by this thread"
299
+ cache_updated = False
300
+ for k in list(self._cache.keys()):
301
+ try:
302
+ self._getitem(k, should_record_hits=False)
303
+ except KeyError:
304
+ # the only case KeyError raised in this method
305
+ # is that k is expired
306
+ cache_updated = True
307
+ if cache_updated:
308
+ self._add_or_remove()
309
+
310
+ def clear_expired_entries(self) -> None:
311
+ """Remove expired entries from the cache."""
312
+ with self._lock:
313
+ self._clear_expired_entries()
314
+
315
+ # Telemetry related functions, these can be plugged by child classes
316
+ def _reset_telemetry(self) -> None:
317
+ """(Re)set telemetry fields.
318
+
319
+ This function will be called by the initalizer and other functions that should
320
+ reset telemtry entries.
321
+ """
322
+ self.telemetry = {
323
+ "hit": 0,
324
+ "miss": 0,
325
+ "expiration": 0,
326
+ "size": 0,
327
+ }
328
+
329
+ def _hit(self, k: K) -> None:
330
+ """This function gets called when a hit occurs.
331
+
332
+ Functions that hit every entry (like values) is not going to count.
333
+
334
+ Note that while this function does not interact with lock, but it's only
335
+ called from contexts where the lock is already held.
336
+ """
337
+ self.telemetry["hit"] += 1
338
+
339
+ def _miss(self, k: K) -> None:
340
+ """This function gets called when a miss occurs.
341
+
342
+ Note that while this function does not interact with lock, but it's only
343
+ called from contexts where the lock is already held.
344
+ """
345
+ self.telemetry["miss"] += 1
346
+
347
+ def _expiration(self, k: K) -> None:
348
+ """This function gets called when an expiration occurs.
349
+
350
+ Note that while this function does not interact with lock, but it's only
351
+ called from contexts where the lock is already held.
352
+ """
353
+ self.telemetry["expiration"] += 1
354
+
355
+ def _expire(self, k: K) -> NoReturn:
356
+ """Helper function to call _expiration and delete an item."""
357
+ self._expiration(k)
358
+ self._delitem(k)
359
+ raise KeyError
360
+
361
+ def _add_or_remove(self) -> None:
362
+ """This function gets called when an element is added, or removed.
363
+
364
+ Note that while this function does not interact with lock, but it's only
365
+ called from contexts where the lock is already held.
366
+ """
367
+ self.telemetry["size"] = len(self._cache)
368
+
369
+
370
+ class SFDictFileCache(SFDictCache):
371
+ # This number decides the chance of saving after writing (probability: 1/n+1)
372
+ MAX_RAND_INT = 9
373
+ _ATTRIBUTES_TO_PICKLE = (
374
+ "_entry_lifetime",
375
+ "_cache",
376
+ "telemetry",
377
+ "file_path",
378
+ "file_timeout",
379
+ "_file_lock_path",
380
+ "last_loaded",
381
+ )
382
+
383
+ def __init__(
384
+ self,
385
+ file_path: str | dict[str, str],
386
+ entry_lifetime: int = constants.DAY_IN_SECONDS,
387
+ file_timeout: int = 0,
388
+ load_if_file_exists: bool = True,
389
+ ) -> None:
390
+ """Inits an SFDictFileCache with path, lifetime.
391
+
392
+ File path can be a dictionary that contains different paths for different OSes,
393
+ possible keys are: 'darwin', 'linux' and 'windows'. If a current platform
394
+ cannot be determined, or is not in the dictionary we'll use the first value.
395
+
396
+ Once we select a location based on file path, we write and read a random
397
+ temporary file to check for read/write permissions. If this fails OSError might
398
+ be thrown.
399
+ """
400
+ super().__init__(
401
+ entry_lifetime=entry_lifetime,
402
+ )
403
+ if isinstance(file_path, str):
404
+ self.file_path = os.path.expanduser(file_path)
405
+ else:
406
+ current_platform = platform.system().lower()
407
+ if current_platform is None or current_platform not in file_path:
408
+ self.file_path = next(iter(file_path.values()))
409
+ else:
410
+ self.file_path = os.path.expanduser(file_path[current_platform])
411
+ # Once we decided on where to put the file cache make sure that this
412
+ # place is readable/writable by us
413
+ random_string = "".join(random.choice(string.ascii_letters) for _ in range(5))
414
+ cache_folder = os.path.dirname(self.file_path)
415
+ if not path.exists(cache_folder):
416
+ try:
417
+ makedirs(cache_folder, mode=0o700)
418
+ except Exception as ex:
419
+ logger.debug(
420
+ "cannot create a cache directory: [%s], err=[%s]",
421
+ cache_folder,
422
+ ex,
423
+ )
424
+
425
+ try:
426
+ tmp_file, tmp_file_path = tempfile.mkstemp(
427
+ dir=cache_folder,
428
+ )
429
+ except OSError as o_err:
430
+ raise PermissionError(
431
+ o_err.errno,
432
+ "Cache folder is not writeable",
433
+ cache_folder,
434
+ )
435
+ try:
436
+ with open(tmp_file, "w") as w_file:
437
+ # If mkstemp didn't fail this shouldn't throw an error
438
+ w_file.write(random_string)
439
+ try:
440
+ with open(tmp_file_path) as r_file:
441
+ if r_file.read() != random_string:
442
+ Exception("Temporary file just written has wrong content")
443
+ except OSError as o_err:
444
+ raise PermissionError(
445
+ o_err.errno,
446
+ "Cache file is not readable",
447
+ tmp_file_path,
448
+ )
449
+ finally:
450
+ if os.path.exists(tmp_file_path) and os.path.isfile(tmp_file_path):
451
+ os.unlink(tmp_file_path)
452
+ self.file_timeout = file_timeout
453
+ self._file_lock_path = f"{self.file_path}.lock"
454
+ self._file_lock = FileLock(self._file_lock_path, timeout=self.file_timeout)
455
+ self.last_loaded: datetime.datetime | None = None
456
+ if os.path.exists(self.file_path) and load_if_file_exists:
457
+ with self._lock:
458
+ self._load()
459
+ # indicate whether the cache is modified or not, this variable is for
460
+ # SFDictFileCache to determine whether to dump cache to file when _save is called
461
+ self._cache_modified = False
462
+
463
+ def _getitem_non_locking(
464
+ self,
465
+ k: K,
466
+ *,
467
+ should_record_hits: bool = True,
468
+ ) -> V:
469
+ """Non-locking version of __getitem__ of SFDictFileCache.
470
+
471
+ This should only be used by internal functions when already
472
+ holding self._lock.
473
+
474
+ Note that we do not overwrite _getitem because _getitem is used by
475
+ self._load to clear in-memory expired caches. Overwriting would cause
476
+ infinite recursive call.
477
+ """
478
+ if k not in self._cache:
479
+ loaded = self._load_if_should()
480
+ if (not loaded) or k not in self._cache:
481
+ self._miss(k)
482
+ raise KeyError
483
+ t, v = self._cache[k]
484
+ if is_expired(t):
485
+ loaded = self._load_if_should()
486
+ expire_item = True
487
+ if loaded:
488
+ t, v = self._cache[k]
489
+ expire_item = is_expired(t)
490
+ if expire_item:
491
+ # Raises KeyError
492
+ self._expire(k)
493
+ self._hit(k)
494
+ return v
495
+
496
+ def __getitem__(self, k: K) -> V:
497
+ """Returns an element if it hasn't expired yet in a thread-safe way."""
498
+ with self._lock:
499
+ return self._getitem_non_locking(k)
500
+
501
+ def _setitem(self, k: K, v: V) -> None:
502
+ super()._setitem(k, v)
503
+ self._save_if_should()
504
+
505
+ def _load(self) -> bool:
506
+ """Load cache from disk if possible, returns whether it was able to load."""
507
+ try:
508
+ with open(self.file_path, "rb") as r_file:
509
+ other: SFDictFileCache = self._deserialize(r_file)
510
+ # Since we want to know whether we are dirty after loading
511
+ # we have to know whether the file could learn anything from self
512
+ # so instead of calling self.update we call other.update and swap
513
+ # the 2 underlying caches after.
514
+ self._lock.release()
515
+ cache_file_learnt = other.update(
516
+ self,
517
+ update_newer_only=True,
518
+ )
519
+ self._lock.acquire()
520
+ self._cache, other._cache = other._cache, self._cache
521
+ self.telemetry["size"] = other.telemetry["size"]
522
+ self._cache_modified = cache_file_learnt
523
+ self.last_loaded = now()
524
+ return True
525
+ except (AssertionError, RuntimeError):
526
+ raise
527
+ except Exception as e:
528
+ logger.debug("Fail to read cache from disk due to error: %s", e)
529
+ return False
530
+
531
+ def load(self) -> bool:
532
+ """Load cache from disk if possible, returns whether it was able to load.
533
+
534
+ This is the public version of _load, it makes sure that all the
535
+ necessary locks are acquired.
536
+ """
537
+ with self._lock:
538
+ return self._load()
539
+
540
+ def _serialize(self):
541
+ return pickle.dumps(self)
542
+
543
+ @classmethod
544
+ def _deserialize(cls, r_file):
545
+ return pickle.load(r_file)
546
+
547
+ def _save(self, load_first: bool = True, force_flush: bool = False) -> bool:
548
+ """Save cache to disk if possible, returns whether it was able to save.
549
+
550
+ This function is non-locking when it comes to self._lock.
551
+ """
552
+ if test_mode:
553
+ assert (
554
+ self._lock.locked()
555
+ ), "The mutex self._lock should be locked by this thread"
556
+ self._clear_expired_entries()
557
+ if not self._cache_modified and not force_flush:
558
+ # cache is not updated, so there is no need to dump cache to file, we just return
559
+ return False
560
+ try:
561
+ with self._file_lock:
562
+ if load_first:
563
+ self._load_if_should()
564
+ _dir, fname = os.path.split(self.file_path)
565
+ try:
566
+ tmp_file, tmp_file_path = tempfile.mkstemp(
567
+ prefix=fname,
568
+ dir=_dir,
569
+ )
570
+ # tmp_file is an opened OS level handle, which means we need to close it manually.
571
+ # https://docs.python.org/3/library/tempfile.html#tempfile.mkstemp
572
+ # ideally we shall just use the tmp_file fd to write,
573
+ # however, using os.write(tmp_file, bytes) causes seg fault during garbage collection when exiting
574
+ # python program.
575
+ # thus we fall back to the approach using the normal open() method to open a file and write.
576
+ with open(tmp_file, "wb") as w_file:
577
+ w_file.write(self._serialize())
578
+ # We write to a tmp file and then move it to have atomic write
579
+ os.replace(tmp_file_path, self.file_path)
580
+ self.last_loaded = datetime.datetime.fromtimestamp(
581
+ getmtime(self.file_path),
582
+ )
583
+ # after update, reset self._cache_modified to indicate it's up-to-update to avoid unnecessary flush
584
+ self._cache_modified = False
585
+ return True
586
+ except NameError:
587
+ # note: when exiting python program, garbage collection will kick in
588
+ # leading to `open` being garbage collected,
589
+ # calling `open` raises NameError, we close the tmp file fd here to release the tmp file fd
590
+ try:
591
+ os.close(tmp_file)
592
+ except OSError:
593
+ pass
594
+ except OSError as o_err:
595
+ raise PermissionError(
596
+ o_err.errno,
597
+ "Cache folder is not writeable",
598
+ _dir,
599
+ )
600
+ finally:
601
+ if os.path.exists(tmp_file_path) and os.path.isfile(tmp_file_path):
602
+ os.unlink(tmp_file_path)
603
+ except Timeout:
604
+ logger.debug(
605
+ f"acquiring {self._file_lock_path} timed out, skipping saving..."
606
+ )
607
+ except (AssertionError, RuntimeError):
608
+ raise
609
+ except Exception as e:
610
+ logger.debug("Fail to write cache to disk due to error: %s", e)
611
+ return False
612
+
613
+ def save(self, load_first: bool = True) -> bool:
614
+ """Save cache to disk if possible, returns whether it was able to save.
615
+
616
+ This is the public version of _save, it makes sure that all the
617
+ necessary locks are acquired.
618
+ """
619
+ with self._lock:
620
+ return self._save(load_first)
621
+
622
+ def _save_if_should(self) -> bool:
623
+ """Saves file to disk if necessary and returns whether it saved.
624
+
625
+ Uses self._should_save to decide whether to save.
626
+ """
627
+ if self._should_save():
628
+ return self._save()
629
+ return False
630
+
631
+ def _load_if_should(self) -> bool:
632
+ """Load file to disk if necessary and returns whether it loaded.
633
+
634
+ Uses self._should_load to decide whether to load.
635
+ """
636
+ if self._should_load():
637
+ return self._load()
638
+ return False
639
+
640
+ def _should_save(self) -> bool:
641
+ """Decide whether we should save.
642
+
643
+ This is a simple random number generator to randomize writes across processes
644
+ that are possibly saving the same values in this cache.
645
+ """
646
+ return random.randint(0, self.MAX_RAND_INT) == 0
647
+
648
+ def _should_load(self) -> bool:
649
+ """Decide whether we should load.
650
+
651
+ We should load if the file on disk has changed since we have last read it.
652
+ """
653
+ if os.path.exists(self.file_path) and os.path.isfile(self.file_path):
654
+ if self.last_loaded is None:
655
+ return True
656
+ return (
657
+ datetime.datetime.fromtimestamp(
658
+ getmtime(self.file_path),
659
+ )
660
+ > self.last_loaded
661
+ )
662
+ return False
663
+
664
+ def clear(self) -> None:
665
+ super().clear()
666
+ # This unlink prevents us from loading just before saving
667
+ with self._file_lock:
668
+ if os.path.exists(self.file_path) and os.path.isfile(self.file_path):
669
+ os.unlink(self.file_path)
670
+ # TODO: is this necessary?
671
+ with self._lock:
672
+ self._save(load_first=False, force_flush=True)
673
+
674
+ # Custom pickling implementation
675
+
676
+ def __getstate__(self) -> dict:
677
+ return {
678
+ k: v
679
+ for k, v in self.__dict__.items()
680
+ if k in SFDictFileCache._ATTRIBUTES_TO_PICKLE
681
+ }
682
+
683
+ def __setstate__(self, state: dict) -> None:
684
+ self.__dict__.update(state)
685
+ self._cache_modified = False
686
+ self._lock = Lock()
687
+ self._file_lock = FileLock(self._file_lock_path, timeout=self.file_timeout)
688
+
689
+ def _add_or_remove(self) -> None:
690
+ """This function gets called when an element is added, or removed.
691
+
692
+ Note that while this function does not interact with lock, but it's only
693
+ called from contexts where the lock is already held.
694
+ """
695
+ super()._add_or_remove()
696
+ self._cache_modified = True