streamlit-nightly 1.52.3.dev20260107__py3-none-any.whl → 1.52.3.dev20260108__py3-none-any.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.
- streamlit/connections/__init__.py +5 -1
- streamlit/connections/snowflake_connection.py +203 -57
- streamlit/runtime/connection_factory.py +25 -0
- streamlit/static/index.html +1 -1
- streamlit/static/manifest.json +300 -300
- streamlit/static/static/js/{ErrorOutline.esm.D2r71uaM.js → ErrorOutline.esm.j3b3OjAK.js} +1 -1
- streamlit/static/static/js/{FileDownload.esm.BWEHU45R.js → FileDownload.esm.DCizXv6Q.js} +1 -1
- streamlit/static/static/js/{FileHelper.bM5nEcHb.js → FileHelper.EpMV5UVe.js} +1 -1
- streamlit/static/static/js/{FormClearHelper.DrhgNNta.js → FormClearHelper.lF7Ran5M.js} +1 -1
- streamlit/static/static/js/{InputInstructions.DPiGX0Th.js → InputInstructions.CMvqhPhy.js} +1 -1
- streamlit/static/static/js/{Particles.NjZlN0Ji.js → Particles.DsGe8psi.js} +1 -1
- streamlit/static/static/js/{ProgressBar.BC4DXqMl.js → ProgressBar.DzoKn4D-.js} +1 -1
- streamlit/static/static/js/{StreamlitSyntaxHighlighter.CbRgSaN7.js → StreamlitSyntaxHighlighter.CFab0b_b.js} +1 -1
- streamlit/static/static/js/{TableChart.esm.gttTfh7r.js → TableChart.esm.nZsTq1Sb.js} +1 -1
- streamlit/static/static/js/{Toolbar.DO2JfHk1.js → Toolbar.CFMvwQYl.js} +1 -1
- streamlit/static/static/js/{WidgetLabelHelpIconInline.NVlzvmTZ.js → WidgetLabelHelpIconInline.D2EEUEQX.js} +1 -1
- streamlit/static/static/js/{base-input.CmG3KTuv.js → base-input.DKTA2QNz.js} +1 -1
- streamlit/static/static/js/{checkbox.cfHeGdGI.js → checkbox.D9H9J-_W.js} +1 -1
- streamlit/static/static/js/{createDownloadLinkElement.B59a5_bn.js → createDownloadLinkElement.DCk6EhPM.js} +1 -1
- streamlit/static/static/js/{data-grid-overlay-editor.DpTe47v0.js → data-grid-overlay-editor.DExrGdqs.js} +1 -1
- streamlit/static/static/js/{downloader.CYtKIJ9J.js → downloader.CLJ7BreF.js} +1 -1
- streamlit/static/static/js/{embed.Dfyy6eKR.js → embed.CxOHZWx2.js} +1 -1
- streamlit/static/static/js/{es6.x2A9auGg.js → es6.C99ebre4.js} +2 -2
- streamlit/static/static/js/{formatNumber.D8YUVHBi.js → formatNumber.D_w4fBsk.js} +1 -1
- streamlit/static/static/js/{iconPosition.C6Qxuo2L.js → iconPosition.Cfhw1RkE.js} +1 -1
- streamlit/static/static/js/{iframeResizer.contentWindow.Q0Cy4mzM.js → iframeResizer.contentWindow.BcWUIYOe.js} +1 -1
- streamlit/static/static/js/{index.WMC0NWRg.js → index.1AemKTSK.js} +1 -1
- streamlit/static/static/js/{index.DfXZgqLi.js → index.7Q3Iaebc.js} +1 -1
- streamlit/static/static/js/{index.DMtSM2QJ.js → index.ATP5607r.js} +1 -1
- streamlit/static/static/js/{index.Bs9bS4ZQ.js → index.BHx4Qw7z.js} +1 -1
- streamlit/static/static/js/{index.DlXiDt-m.js → index.BOGNGR9a.js} +1 -1
- streamlit/static/static/js/{index.BICn75_M.js → index.BWK_h3IL.js} +1 -1
- streamlit/static/static/js/{index.DzZhu8Ry.js → index.BXQNt1hj.js} +1 -1
- streamlit/static/static/js/{index.BtasgGzT.js → index.BXnQdCa5.js} +1 -1
- streamlit/static/static/js/{index.D2pbLWvp.js → index.BcbR2mbc.js} +1 -1
- streamlit/static/static/js/{index.mRYkGVRy.js → index.Bp_LrAiI.js} +1 -1
- streamlit/static/static/js/{index.Zffuf6-0.js → index.Bu3Lto_G.js} +1 -1
- streamlit/static/static/js/index.BzQChe4y.js +1 -0
- streamlit/static/static/js/{index.HTZptmDX.js → index.C-lnh8pI.js} +1 -1
- streamlit/static/static/js/{index.Bmo0a9c3.js → index.C0VFHmJN.js} +1 -1
- streamlit/static/static/js/{index.72U0nUaY.js → index.C7_5JMRC.js} +1 -1
- streamlit/static/static/js/index.C98anBCM.js +1 -0
- streamlit/static/static/js/{index.DFrG8naT.js → index.CBZQ_6AF.js} +1 -1
- streamlit/static/static/js/{index.CCkh_YSD.js → index.CFE-yHdT.js} +1 -1
- streamlit/static/static/js/{index.mUwcAQrC.js → index.CFtGP8pH.js} +1 -1
- streamlit/static/static/js/{index.DDUzbOzY.js → index.CGX2fllG.js} +1 -1
- streamlit/static/static/js/{index.DemMZEQt.js → index.COZICZL6.js} +1 -1
- streamlit/static/static/js/{index.WW20lmQl.js → index.COjurlZk.js} +1 -1
- streamlit/static/static/js/{index.BSULROGA.js → index.CPo5dtx7.js} +1 -1
- streamlit/static/static/js/{index.7BBVjqsx.js → index.Cg59Loqx.js} +1 -1
- streamlit/static/static/js/{index.CgFM5y1b.js → index.CiU2Tdcl.js} +1 -1
- streamlit/static/static/js/{index.CEB-gMZu.js → index.CrJ9KZpt.js} +1 -1
- streamlit/static/static/js/{index.BHzkSOU6.js → index.CrzXL2V8.js} +1 -1
- streamlit/static/static/js/{index.9i44yl-A.js → index.CyVBY8PG.js} +1 -1
- streamlit/static/static/js/{index.Dmaco99Q.js → index.D18KqoUa.js} +1 -1
- streamlit/static/static/js/{index.CzMaYu3-.js → index.DDx6TP95.js} +1 -1
- streamlit/static/static/js/{index.DMgiqc8r.js → index.DIIdzDwK.js} +1 -1
- streamlit/static/static/js/{index.9lrRft8u.js → index.DJ4GBc1k.js} +1 -1
- streamlit/static/static/js/{index.BA25ZBoD.js → index.DSSQzzPk.js} +1 -1
- streamlit/static/static/js/{index.4DpGzJxM.js → index.DbmtfcDm.js} +1 -1
- streamlit/static/static/js/{index.CWEfMTRP.js → index.DcudoGfL.js} +1 -1
- streamlit/static/static/js/{index.AF9wK9Wi.js → index.DiZfOR0A.js} +1 -1
- streamlit/static/static/js/{index.BuCGo7-5.js → index.DkSjHoXw.js} +5 -5
- streamlit/static/static/js/{index.CcpuNeOs.js → index.DoLorXMA.js} +1 -1
- streamlit/static/static/js/{index.up8HPR4v.js → index.DpnqUQVD.js} +1 -1
- streamlit/static/static/js/{index.DrHoCPek.js → index.Dqphk1ee.js} +2 -2
- streamlit/static/static/js/{index.Cto6hKVz.js → index.Ds-w0zIo.js} +1 -1
- streamlit/static/static/js/{index.5zBrLB5N.js → index.DuFqxjbN.js} +1 -1
- streamlit/static/static/js/{index.BTcImDDt.js → index.Dx8TcTHV.js} +6 -6
- streamlit/static/static/js/{index.DltYrNjp.js → index.V4C1Oi-F.js} +1 -1
- streamlit/static/static/js/{index.DyL5do6X.js → index.aJ3XRx8R.js} +1 -1
- streamlit/static/static/js/{index.i5tC1QD-.js → index.f_s01aPm.js} +1 -1
- streamlit/static/static/js/{index.4evoB7lO.js → index.gnFSTAhI.js} +1 -1
- streamlit/static/static/js/{index.Dm8qt8Me.js → index.mZ1qbnKs.js} +1 -1
- streamlit/static/static/js/{input.C76A8APa.js → input.CZPD7mCu.js} +1 -1
- streamlit/static/static/js/{main.CFbvktEv.js → main.CX1jAiMw.js} +1 -1
- streamlit/static/static/js/{memory.BkhqbxJl.js → memory.m0jC5ULx.js} +1 -1
- streamlit/static/static/js/{number-overlay-editor.Dpeh6_Ag.js → number-overlay-editor.Dy0iTeCo.js} +1 -1
- streamlit/static/static/js/{pandasStylerUtils.D8MDzfFI.js → pandasStylerUtils.D9jj-wHU.js} +1 -1
- streamlit/static/static/js/{sandbox.m1TYtBuF.js → sandbox.CbvG1iAz.js} +1 -1
- streamlit/static/static/js/{styled-components.yin3sIMA.js → styled-components.Cw3ioniY.js} +1 -1
- streamlit/static/static/js/{throttle.Df6MiNTy.js → throttle.CAwhGpn0.js} +1 -1
- streamlit/static/static/js/{timepicker.gcx4yeRx.js → timepicker.Bh3m6Pjp.js} +1 -1
- streamlit/static/static/js/{toConsumableArray.Ca7RO9Jm.js → toConsumableArray.DM0o32JS.js} +1 -1
- streamlit/static/static/js/uniqueId.DtV_RZzG.js +1 -0
- streamlit/static/static/js/{useBasicWidgetState.CRkPCtR5.js → useBasicWidgetState.C9zOVP8a.js} +1 -1
- streamlit/static/static/js/{useIntlLocale.CFOMULNz.js → useIntlLocale.Bo42aN1U.js} +1 -1
- streamlit/static/static/js/{useTextInputAutoExpand.9c205rhQ.js → useTextInputAutoExpand.DmyHLDxl.js} +1 -1
- streamlit/static/static/js/{useUpdateUiValue.SYJJuEJM.js → useUpdateUiValue.aWXWpqmw.js} +1 -1
- streamlit/static/static/js/{useWaveformController.CCs9fdSv.js → useWaveformController.DlE14M1X.js} +1 -1
- streamlit/static/static/js/{withCalculatedWidth.MG9RY_Y-.js → withCalculatedWidth.B5JFJSmG.js} +1 -1
- streamlit/static/static/js/{withFullScreenWrapper.BgGLtZrC.js → withFullScreenWrapper.dgVioHk1.js} +1 -1
- {streamlit_nightly-1.52.3.dev20260107.dist-info → streamlit_nightly-1.52.3.dev20260108.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.52.3.dev20260107.dist-info → streamlit_nightly-1.52.3.dev20260108.dist-info}/RECORD +98 -98
- streamlit/static/static/js/index.Cszkm2ii.js +0 -1
- streamlit/static/static/js/index.xQYe72hk.js +0 -1
- streamlit/static/static/js/uniqueId.Bf2Hsb60.js +0 -1
- {streamlit_nightly-1.52.3.dev20260107.data → streamlit_nightly-1.52.3.dev20260108.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.52.3.dev20260107.dist-info → streamlit_nightly-1.52.3.dev20260108.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.52.3.dev20260107.dist-info → streamlit_nightly-1.52.3.dev20260108.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.52.3.dev20260107.dist-info → streamlit_nightly-1.52.3.dev20260108.dist-info}/top_level.txt +0 -0
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
from streamlit.connections.base_connection import BaseConnection
|
|
16
|
-
from streamlit.connections.snowflake_connection import
|
|
16
|
+
from streamlit.connections.snowflake_connection import (
|
|
17
|
+
SnowflakeCallersRightsConnection,
|
|
18
|
+
SnowflakeConnection,
|
|
19
|
+
)
|
|
17
20
|
from streamlit.connections.snowpark_connection import SnowparkConnection
|
|
18
21
|
from streamlit.connections.sql_connection import SQLConnection
|
|
19
22
|
|
|
@@ -23,6 +26,7 @@ __all__ = [
|
|
|
23
26
|
"BaseConnection",
|
|
24
27
|
"ExperimentalBaseConnection",
|
|
25
28
|
"SQLConnection",
|
|
29
|
+
"SnowflakeCallersRightsConnection",
|
|
26
30
|
"SnowflakeConnection",
|
|
27
31
|
"SnowparkConnection",
|
|
28
32
|
]
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
import os
|
|
24
|
+
from typing import TYPE_CHECKING, Any, Final, Literal, cast
|
|
24
25
|
|
|
25
26
|
from streamlit import logger
|
|
26
27
|
from streamlit.connections import BaseConnection
|
|
@@ -45,11 +46,18 @@ if TYPE_CHECKING:
|
|
|
45
46
|
# (see docs: https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-api#id6)
|
|
46
47
|
SQLSTATE_CONNECTION_WAS_NOT_ESTABLISHED: Final = "08001"
|
|
47
48
|
|
|
49
|
+
# The location on disk where Snowpark Container Services will mount service connection tokens.
|
|
50
|
+
SNOWPARK_CONNECTION_TOKEN_FILE = "/snowflake/session/token" # noqa: S105 (not a password)
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
# The header where Snowpark Container Services will put per-user connection tokens.
|
|
53
|
+
SNOWPARK_USER_TOKEN_HEADER_NAME = "Sf-Context-Current-User-Token" # noqa: S105 (not a password)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BaseSnowflakeConnection(BaseConnection["InternalSnowflakeConnection"]):
|
|
50
57
|
"""A connection to Snowflake using the Snowflake Connector for Python.
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
For standard connections, create an instance of this using
|
|
60
|
+
``st.connection("snowflake")`` or
|
|
53
61
|
``st.connection("<name>", type="snowflake")``. Connection parameters for a
|
|
54
62
|
SnowflakeConnection can be specified using ``secrets.toml`` and/or
|
|
55
63
|
``**kwargs``. Connection parameters are passed to
|
|
@@ -61,7 +69,14 @@ class SnowflakeConnection(BaseConnection["InternalSnowflakeConnection"]):
|
|
|
61
69
|
case. Use ``secrets.toml`` and ``**kwargs`` to configure your connection
|
|
62
70
|
for local development.
|
|
63
71
|
|
|
64
|
-
|
|
72
|
+
When an app is running in Snowpark Container Services and has caller's rights
|
|
73
|
+
enabled, ``st.connection("snowflake-callers-rights")`` connects automatically
|
|
74
|
+
using the current user's identity tokens. This will be a session-scoped connection
|
|
75
|
+
to ensure that the identity stays tied to the active user. Unlike with
|
|
76
|
+
``"snowflake"`` connections, it will use the Snowpark Container Services connection
|
|
77
|
+
settings even when other ``**kwargs`` are provided.
|
|
78
|
+
|
|
79
|
+
BaseSnowflakeConnection includes several convenience methods. For example, you
|
|
65
80
|
can directly execute a SQL query with ``.query()`` or access the underlying
|
|
66
81
|
Snowflake Connector object with ``.raw_connection``.
|
|
67
82
|
|
|
@@ -85,6 +100,12 @@ class SnowflakeConnection(BaseConnection["InternalSnowflakeConnection"]):
|
|
|
85
100
|
in SQL queries. For more information, see `Account identifiers
|
|
86
101
|
<https://docs.snowflake.com/en/user-guide/admin-account-identifier>`_.
|
|
87
102
|
|
|
103
|
+
.. Important::
|
|
104
|
+
Caller's rights connections rely on credentials provided when a user first
|
|
105
|
+
connects to a Streamlit app. These credentials are only valid for a short period
|
|
106
|
+
of time - so caller's rights connections must be created at the top of an app
|
|
107
|
+
or else the connection may fail.
|
|
108
|
+
|
|
88
109
|
Examples
|
|
89
110
|
--------
|
|
90
111
|
**Example 1: Configuration with Streamlit secrets**
|
|
@@ -222,64 +243,23 @@ class SnowflakeConnection(BaseConnection["InternalSnowflakeConnection"]):
|
|
|
222
243
|
>>> conn = st.connection("snowflake")
|
|
223
244
|
>>> df = conn.query("SELECT * FROM my_table")
|
|
224
245
|
|
|
225
|
-
|
|
246
|
+
**Example 7: Caller's rights connection when running in Snowpark Container Services**
|
|
226
247
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
248
|
+
This connection will work for any Streamlit-in-Snowflake app using a container
|
|
249
|
+
runtime, as well as any self-managed caller's rights Service in Snowpark Container
|
|
250
|
+
Services that is hosting Streamlit.
|
|
230
251
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
from snowflake.snowpark.context import ( # type:ignore[import] # isort: skip
|
|
235
|
-
get_active_session,
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
session = get_active_session()
|
|
252
|
+
This will use the Snowpark-provided account, host, database, and schema to connect.
|
|
253
|
+
Additionally, it will set ``client_session_keep_alive`` to ``True``. These values
|
|
254
|
+
may be overridden with ``**kwargs``.
|
|
239
255
|
|
|
240
|
-
|
|
241
|
-
return session.connection
|
|
242
|
-
# session.connection is only a valid attr in more recent versions of
|
|
243
|
-
# snowflake-connector-python, so we fall back to grabbing
|
|
244
|
-
# session._conn._conn if `.connection` is unavailable.
|
|
245
|
-
return session._conn._conn
|
|
246
|
-
|
|
247
|
-
# We require qmark-style parameters everywhere for consistency across different
|
|
248
|
-
# environments where SnowflakeConnections may be used.
|
|
249
|
-
snowflake.connector.paramstyle = "qmark"
|
|
250
|
-
|
|
251
|
-
# Otherwise, attempt to create a new connection from whatever credentials we
|
|
252
|
-
# have available.
|
|
253
|
-
st_secrets = self._secrets.to_dict()
|
|
254
|
-
try:
|
|
255
|
-
if len(st_secrets):
|
|
256
|
-
_LOGGER.info(
|
|
257
|
-
"Connect to Snowflake using the Streamlit secret defined under "
|
|
258
|
-
"[connections.snowflake]."
|
|
259
|
-
)
|
|
260
|
-
conn_kwargs = {**st_secrets, **kwargs}
|
|
261
|
-
return snowflake.connector.connect(**conn_kwargs)
|
|
256
|
+
Your app code:
|
|
262
257
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
"Connect to Snowflake using the default configuration as defined "
|
|
267
|
-
"in https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-connect#setting-a-default-connection"
|
|
268
|
-
)
|
|
269
|
-
return snowflake.connector.connect()
|
|
258
|
+
>>> import streamlit as st
|
|
259
|
+
>>> conn = st.connection("snowflake-callers-rights")
|
|
260
|
+
>>> df = conn.query("SELECT * FROM my_table")
|
|
270
261
|
|
|
271
|
-
|
|
272
|
-
except SnowflakeError:
|
|
273
|
-
if not len(st_secrets) and not kwargs:
|
|
274
|
-
raise StreamlitAPIException(
|
|
275
|
-
"Missing Snowflake connection configuration. "
|
|
276
|
-
"Did you forget to set this in `secrets.toml`, a Snowflake configuration file, "
|
|
277
|
-
"or as kwargs to `st.connection`? "
|
|
278
|
-
"See the [SnowflakeConnection configuration documentation]"
|
|
279
|
-
"(https://docs.streamlit.io/st.connections.snowflakeconnection-configuration) "
|
|
280
|
-
"for more details and examples."
|
|
281
|
-
)
|
|
282
|
-
raise
|
|
262
|
+
"""
|
|
283
263
|
|
|
284
264
|
def query(
|
|
285
265
|
self,
|
|
@@ -563,3 +543,169 @@ class SnowflakeConnection(BaseConnection["InternalSnowflakeConnection"]):
|
|
|
563
543
|
return cast(
|
|
564
544
|
"Session", Session.builder.configs({"connection": self._instance}).create()
|
|
565
545
|
)
|
|
546
|
+
|
|
547
|
+
def close(self) -> None:
|
|
548
|
+
"""Closes the underlying Snowflake connection."""
|
|
549
|
+
if self._raw_instance is not None:
|
|
550
|
+
self._raw_instance.close()
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
class SnowflakeConnection(BaseSnowflakeConnection):
|
|
554
|
+
"""A connection to Snowflake using the Snowflake Connector for Python.
|
|
555
|
+
|
|
556
|
+
See ``BaseSnowflakeConnection`` for complete docs.
|
|
557
|
+
"""
|
|
558
|
+
|
|
559
|
+
def _connect(self, **kwargs: Any) -> InternalSnowflakeConnection:
|
|
560
|
+
import snowflake.connector # type:ignore[import]
|
|
561
|
+
from snowflake.connector import Error as SnowflakeError # type:ignore[import]
|
|
562
|
+
|
|
563
|
+
# If we're running in SiS-on-warehouses, just call get_active_session() and
|
|
564
|
+
# retrieve the lower-level connection from it.
|
|
565
|
+
if running_in_sis():
|
|
566
|
+
from snowflake.snowpark.context import ( # type:ignore[import] # isort: skip
|
|
567
|
+
get_active_session,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
session = get_active_session()
|
|
571
|
+
|
|
572
|
+
if hasattr(session, "connection"):
|
|
573
|
+
return session.connection
|
|
574
|
+
# session.connection is only a valid attr in more recent versions of
|
|
575
|
+
# snowflake-connector-python, so we fall back to grabbing
|
|
576
|
+
# session._conn._conn if `.connection` is unavailable.
|
|
577
|
+
return session._conn._conn
|
|
578
|
+
|
|
579
|
+
# We require qmark-style parameters everywhere for consistency across different
|
|
580
|
+
# environments where SnowflakeConnections may be used.
|
|
581
|
+
snowflake.connector.paramstyle = "qmark"
|
|
582
|
+
|
|
583
|
+
# Otherwise, attempt to create a new connection from whatever credentials we
|
|
584
|
+
# have available.
|
|
585
|
+
st_secrets = self._secrets.to_dict()
|
|
586
|
+
try:
|
|
587
|
+
if len(st_secrets):
|
|
588
|
+
_LOGGER.info(
|
|
589
|
+
"Connect to Snowflake using the Streamlit secret defined under "
|
|
590
|
+
"[connections.snowflake]."
|
|
591
|
+
)
|
|
592
|
+
conn_kwargs = {**st_secrets, **kwargs}
|
|
593
|
+
return snowflake.connector.connect(**conn_kwargs)
|
|
594
|
+
|
|
595
|
+
# Use the default configuration as defined in https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-connect#setting-a-default-connection
|
|
596
|
+
if self._connection_name == "snowflake":
|
|
597
|
+
_LOGGER.info(
|
|
598
|
+
"Connect to Snowflake using the default configuration as defined "
|
|
599
|
+
"in https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-connect#setting-a-default-connection"
|
|
600
|
+
)
|
|
601
|
+
return snowflake.connector.connect()
|
|
602
|
+
|
|
603
|
+
return snowflake.connector.connect(**kwargs)
|
|
604
|
+
except SnowflakeError:
|
|
605
|
+
if not len(st_secrets) and not kwargs:
|
|
606
|
+
raise StreamlitAPIException(
|
|
607
|
+
"Missing Snowflake connection configuration. "
|
|
608
|
+
"Did you forget to set this in `secrets.toml`, a Snowflake configuration file, "
|
|
609
|
+
"or as kwargs to `st.connection`? "
|
|
610
|
+
"See the [SnowflakeConnection configuration documentation]"
|
|
611
|
+
"(https://docs.streamlit.io/st.connections.snowflakeconnection-configuration) "
|
|
612
|
+
"for more details and examples."
|
|
613
|
+
)
|
|
614
|
+
raise
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
class SnowflakeCallersRightsConnection(SnowflakeConnection):
|
|
618
|
+
"""A caller's rights connection to Snowflake using the Snowflake Connector for Python.
|
|
619
|
+
|
|
620
|
+
This will only work when running on Snowpark Container Services or another
|
|
621
|
+
compatible platform.
|
|
622
|
+
|
|
623
|
+
See ``BaseSnowflakeConnection`` for complete docs.
|
|
624
|
+
"""
|
|
625
|
+
|
|
626
|
+
@classmethod
|
|
627
|
+
def scope(cls) -> Literal["session"]:
|
|
628
|
+
"""Returns ``"session"``.
|
|
629
|
+
|
|
630
|
+
Caller's rights Snowflake connections rely on per-session connection tokens and
|
|
631
|
+
therefore must be session-scoped.
|
|
632
|
+
"""
|
|
633
|
+
return "session"
|
|
634
|
+
|
|
635
|
+
@classmethod
|
|
636
|
+
def _read_token_file(cls) -> str:
|
|
637
|
+
"""Returns the contents of the Snowpark token file on disk."""
|
|
638
|
+
with open(SNOWPARK_CONNECTION_TOKEN_FILE) as token_file:
|
|
639
|
+
return token_file.read()
|
|
640
|
+
|
|
641
|
+
@classmethod
|
|
642
|
+
def _get_connection_params(cls) -> dict[str, str | bool]:
|
|
643
|
+
"""Returns caller's rights connection parameters for the current session.
|
|
644
|
+
|
|
645
|
+
Raises
|
|
646
|
+
------
|
|
647
|
+
StreamlitAPIException: if any Snowpark environment variables or connection
|
|
648
|
+
tokens are missing; or if this is called outside of a session context.
|
|
649
|
+
"""
|
|
650
|
+
# Local import needed to avoid cycles.
|
|
651
|
+
from streamlit import context as st_context
|
|
652
|
+
|
|
653
|
+
# See Snowflake docs:
|
|
654
|
+
# https://docs.snowflake.com/en/developer-guide/snowpark-container-services/additional-considerations-services-jobs#configuring-caller-s-rights-for-your-service
|
|
655
|
+
|
|
656
|
+
# Base parameters, common to all connections.
|
|
657
|
+
params: dict[str, str | bool] = {
|
|
658
|
+
"authenticator": "oauth",
|
|
659
|
+
# OCSP checks do not work in Snowflake containers.
|
|
660
|
+
"ocsp_fail_open": True,
|
|
661
|
+
# We want to keep this alive, as the user token will expire fairly quickly
|
|
662
|
+
# - so we can't create a new session on expiration.
|
|
663
|
+
"client_session_keep_alive": True,
|
|
664
|
+
}
|
|
665
|
+
for param_name, env_var_name in (
|
|
666
|
+
("account", "SNOWFLAKE_ACCOUNT"),
|
|
667
|
+
("host", "SNOWFLAKE_HOST"),
|
|
668
|
+
("database", "SNOWFLAKE_DATABASE"),
|
|
669
|
+
("schema", "SNOWFLAKE_SCHEMA"),
|
|
670
|
+
):
|
|
671
|
+
value = os.getenv(env_var_name)
|
|
672
|
+
if value is None:
|
|
673
|
+
raise StreamlitAPIException(
|
|
674
|
+
f"Environment variable `{env_var_name}` not found. Is this app "
|
|
675
|
+
"running in a Snowflake container environment?"
|
|
676
|
+
)
|
|
677
|
+
params[param_name] = value
|
|
678
|
+
|
|
679
|
+
# Validate the token file exists, and read it.
|
|
680
|
+
if not os.path.exists(SNOWPARK_CONNECTION_TOKEN_FILE):
|
|
681
|
+
raise StreamlitAPIException(
|
|
682
|
+
f"Token file `{SNOWPARK_CONNECTION_TOKEN_FILE}` not found. Is this app "
|
|
683
|
+
"running in a Snowflake container environment?"
|
|
684
|
+
)
|
|
685
|
+
login_token = cls._read_token_file()
|
|
686
|
+
|
|
687
|
+
# Validate the token header exists, and read it.
|
|
688
|
+
if SNOWPARK_USER_TOKEN_HEADER_NAME not in st_context.headers:
|
|
689
|
+
raise StreamlitAPIException(
|
|
690
|
+
"Token header not found. Is this app running with caller's "
|
|
691
|
+
"rights enabled, and is this connection being created in an app "
|
|
692
|
+
"execution thread?"
|
|
693
|
+
)
|
|
694
|
+
user_token = st_context.headers[SNOWPARK_USER_TOKEN_HEADER_NAME]
|
|
695
|
+
|
|
696
|
+
# Build the actual caller's rights token.
|
|
697
|
+
params["token"] = f"{login_token}.{user_token}"
|
|
698
|
+
|
|
699
|
+
return params
|
|
700
|
+
|
|
701
|
+
def _connect(self, **kwargs: Any) -> InternalSnowflakeConnection:
|
|
702
|
+
import snowflake.connector # type:ignore[import]
|
|
703
|
+
|
|
704
|
+
# We require qmark-style parameters everywhere for consistency across different
|
|
705
|
+
# environments where SnowflakeConnections may be used.
|
|
706
|
+
snowflake.connector.paramstyle = "qmark"
|
|
707
|
+
|
|
708
|
+
params = self._get_connection_params()
|
|
709
|
+
|
|
710
|
+
# Connect with the params we generated, overriding with user-specified params.
|
|
711
|
+
return snowflake.connector.connect(**{**params, **kwargs})
|
|
@@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar, overload
|
|
|
20
20
|
|
|
21
21
|
from streamlit.connections import (
|
|
22
22
|
BaseConnection,
|
|
23
|
+
SnowflakeCallersRightsConnection,
|
|
23
24
|
SnowflakeConnection,
|
|
24
25
|
SnowparkConnection,
|
|
25
26
|
SQLConnection,
|
|
@@ -40,6 +41,7 @@ if TYPE_CHECKING:
|
|
|
40
41
|
# 3. Updating test_get_first_party_connection_helper in connection_factory_test.py.
|
|
41
42
|
_FIRST_PARTY_CONNECTIONS: Final[dict[str, type[BaseConnection[Any]]]] = {
|
|
42
43
|
"snowflake": SnowflakeConnection,
|
|
44
|
+
"snowflake-callers-rights": SnowflakeCallersRightsConnection,
|
|
43
45
|
"snowpark": SnowparkConnection,
|
|
44
46
|
"sql": SQLConnection,
|
|
45
47
|
}
|
|
@@ -173,6 +175,29 @@ def connection_factory(
|
|
|
173
175
|
pass
|
|
174
176
|
|
|
175
177
|
|
|
178
|
+
@overload
|
|
179
|
+
def connection_factory(
|
|
180
|
+
name: Literal["snowflake-callers-rights"],
|
|
181
|
+
max_entries: int | None = None,
|
|
182
|
+
ttl: float | timedelta | None = None,
|
|
183
|
+
autocommit: bool = False,
|
|
184
|
+
**kwargs: Any,
|
|
185
|
+
) -> SnowflakeCallersRightsConnection:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@overload
|
|
190
|
+
def connection_factory(
|
|
191
|
+
name: str,
|
|
192
|
+
type: Literal["snowflake-callers-rights"],
|
|
193
|
+
max_entries: int | None = None,
|
|
194
|
+
ttl: float | timedelta | None = None,
|
|
195
|
+
autocommit: bool = False,
|
|
196
|
+
**kwargs: Any,
|
|
197
|
+
) -> SnowflakeCallersRightsConnection:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
|
|
176
201
|
@overload
|
|
177
202
|
def connection_factory(
|
|
178
203
|
name: Literal["snowpark"],
|
streamlit/static/index.html
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<script>
|
|
38
38
|
window.prerenderReady = false
|
|
39
39
|
</script>
|
|
40
|
-
<script type="module" crossorigin src="./static/js/index.
|
|
40
|
+
<script type="module" crossorigin src="./static/js/index.DkSjHoXw.js"></script>
|
|
41
41
|
<link rel="stylesheet" crossorigin href="./static/css/index.BUP6fTcR.css">
|
|
42
42
|
</head>
|
|
43
43
|
<body>
|