Skip to content

Commit 2805ad5

Browse files
Add SiS on SPCS vNext support in Snow Cli (#2531)
1 parent c69d6d0 commit 2805ad5

File tree

10 files changed

+438
-6
lines changed

10 files changed

+438
-6
lines changed

src/snowflake/cli/_plugins/streamlit/streamlit_entity.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from snowflake.cli._plugins.stage.manager import StageManager
99
from snowflake.cli._plugins.streamlit.manager import StreamlitManager
1010
from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
11+
SPCS_RUNTIME_V2_NAME,
1112
StreamlitEntityModel,
1213
)
1314
from snowflake.cli._plugins.workspace.context import ActionContext
@@ -65,6 +66,14 @@ def action_get_url(
6566
self._conn, f"/#/streamlit-apps/{name.url_identifier}"
6667
)
6768

69+
def _is_spcs_runtime_v2_mode(self, experimental: bool = False) -> bool:
70+
"""Check if SPCS runtime v2 mode is enabled."""
71+
return (
72+
experimental
73+
and self.model.runtime_name == SPCS_RUNTIME_V2_NAME
74+
and self.model.compute_pool
75+
)
76+
6877
def bundle(self, output_dir: Optional[Path] = None) -> BundleMap:
6978
return build_bundle(
7079
self.root,
@@ -84,7 +93,7 @@ def deploy(
8493
replace: bool,
8594
prune: bool = False,
8695
bundle_map: Optional[BundleMap] = None,
87-
experimental: Optional[bool] = False,
96+
experimental: bool = False,
8897
*args,
8998
**kwargs,
9099
):
@@ -130,7 +139,11 @@ def deploy(
130139
console.step(f"Creating Streamlit object {self.model.fqn.sql_identifier}")
131140

132141
self._execute_query(
133-
self.get_deploy_sql(replace=replace, from_stage_name=stage_root)
142+
self.get_deploy_sql(
143+
replace=replace,
144+
from_stage_name=stage_root,
145+
experimental=False,
146+
)
134147
)
135148

136149
StreamlitManager(connection=self._conn).grant_privileges(self.model)
@@ -159,6 +172,7 @@ def get_deploy_sql(
159172
artifacts_dir: Optional[Path] = None,
160173
schema: Optional[str] = None,
161174
database: Optional[str] = None,
175+
experimental: bool = False,
162176
*args,
163177
**kwargs,
164178
) -> str:
@@ -202,6 +216,12 @@ def get_deploy_sql(
202216
if self.model.secrets:
203217
query += "\n" + self.model.get_secrets_sql()
204218

219+
# SPCS runtime fields are only supported for FBE/versioned streamlits (FROM syntax)
220+
# Never add these fields for stage-based deployments (ROOT_LOCATION syntax)
221+
if not from_stage_name and self._is_spcs_runtime_v2_mode(experimental):
222+
query += f"\nRUNTIME_NAME = '{self.model.runtime_name}'"
223+
query += f"\nCOMPUTE_POOL = '{self.model.compute_pool}'"
224+
205225
return query + ";"
206226

207227
def get_describe_sql(self) -> str:
@@ -236,6 +256,7 @@ def _deploy_experimental(
236256
self.get_deploy_sql(
237257
if_not_exists=True,
238258
replace=replace,
259+
experimental=True,
239260
)
240261
)
241262
try:

src/snowflake/cli/_plugins/streamlit/streamlit_entity_model.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,18 @@
1515

1616
from typing import Literal, Optional
1717

18-
from pydantic import Field
18+
from pydantic import Field, model_validator
1919
from snowflake.cli.api.project.schemas.entities.common import (
2020
Artifacts,
2121
EntityModelBaseWithArtifacts,
2222
ExternalAccessBaseModel,
2323
GrantBaseModel,
2424
ImportsBaseModel,
2525
)
26-
from snowflake.cli.api.project.schemas.updatable_model import (
27-
DiscriminatorField,
28-
)
26+
from snowflake.cli.api.project.schemas.updatable_model import DiscriminatorField
27+
28+
# SPCS Runtime v2 constants
29+
SPCS_RUNTIME_V2_NAME = "SYSTEM$ST_CONTAINER_RUNTIME_PY3_11"
2930

3031

3132
class StreamlitEntityModel(
@@ -54,3 +55,22 @@ class StreamlitEntityModel(
5455
title="List of paths or file source/destination pairs to add to the deploy root",
5556
default=None,
5657
)
58+
runtime_name: Optional[str] = Field(
59+
title="The runtime name to run the streamlit app on", default=None
60+
)
61+
compute_pool: Optional[str] = Field(
62+
title="The compute pool name of the snowservices running the streamlit app",
63+
default=None,
64+
)
65+
66+
@model_validator(mode="after")
67+
def validate_spcs_runtime_fields(self):
68+
"""Validate that runtime_name and compute_pool are provided together for SPCS container runtime."""
69+
# Only validate for SPCS container runtime, not warehouse runtime
70+
if self.compute_pool and not self.runtime_name:
71+
raise ValueError("compute_pool is specified without runtime_name")
72+
if self.runtime_name == SPCS_RUNTIME_V2_NAME and not self.compute_pool:
73+
raise ValueError(
74+
f"compute_pool is required when using {SPCS_RUNTIME_V2_NAME}"
75+
)
76+
return self

src/snowflake/cli/api/feature_flags.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,6 @@ class FeatureFlag(FeatureFlagMixin):
7373
# TODO 4.0: remove ENABLE_RELEASE_CHANNELS
7474
ENABLE_RELEASE_CHANNELS = BooleanFlag("ENABLE_RELEASE_CHANNELS", None)
7575
ENABLE_SNOWFLAKE_PROJECTS = BooleanFlag("ENABLE_SNOWFLAKE_PROJECTS", False)
76+
ENABLE_STREAMLIT_SPCS_RUNTIME_V2 = BooleanFlag(
77+
"ENABLE_STREAMLIT_SPCS_RUNTIME_V2", False
78+
)

tests/streamlit/test_streamlit_entity.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
from pathlib import Path
12
from unittest import mock
23

34
import pytest
5+
from snowflake.cli._plugins.streamlit.streamlit_entity import StreamlitEntity
6+
from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
7+
SPCS_RUNTIME_V2_NAME,
8+
StreamlitEntityModel,
9+
)
410

511
from tests.streamlit.streamlit_test_class import STREAMLIT_NAME, StreamlitTestClass
612

@@ -112,3 +118,212 @@ def test_if_attribute_is_not_set_correct_error_is_raised(
112118
with pytest.raises(ValueError) as e:
113119
result = getattr(example_entity, attribute)
114120
assert str(e.value) == f"Could not determine {attribute} for {STREAMLIT_NAME}"
121+
122+
def test_spcs_runtime_v2_model_fields(self, workspace_context):
123+
"""Test that StreamlitEntityModel accepts runtime_name and compute_pool fields"""
124+
model = StreamlitEntityModel(
125+
type="streamlit",
126+
identifier="test_streamlit",
127+
runtime_name=SPCS_RUNTIME_V2_NAME,
128+
compute_pool="MYPOOL",
129+
main_file="streamlit_app.py",
130+
artifacts=["streamlit_app.py"],
131+
)
132+
model.set_entity_id("test_streamlit")
133+
134+
assert model.runtime_name == SPCS_RUNTIME_V2_NAME
135+
assert model.compute_pool == "MYPOOL"
136+
137+
def test_get_deploy_sql_with_spcs_runtime_v2(self, workspace_context):
138+
"""Test that get_deploy_sql includes RUNTIME_NAME and COMPUTE_POOL when experimental is True"""
139+
140+
model = StreamlitEntityModel(
141+
type="streamlit",
142+
identifier="test_streamlit",
143+
runtime_name=SPCS_RUNTIME_V2_NAME,
144+
compute_pool="MYPOOL",
145+
main_file="streamlit_app.py",
146+
artifacts=["streamlit_app.py"],
147+
)
148+
model.set_entity_id("test_streamlit")
149+
150+
entity = StreamlitEntity(workspace_ctx=workspace_context, entity_model=model)
151+
152+
# Test with FROM syntax (artifacts_dir provided)
153+
sql = entity.get_deploy_sql(
154+
artifacts_dir=Path("/tmp/artifacts"), experimental=True
155+
)
156+
157+
assert f"RUNTIME_NAME = '{SPCS_RUNTIME_V2_NAME}'" in sql
158+
assert "COMPUTE_POOL = 'MYPOOL'" in sql
159+
160+
def test_get_deploy_sql_spcs_runtime_v2_with_stage(self, workspace_context):
161+
"""Test that SPCS runtime v2 clauses are NOT added with stage-based deployment (old-style streamlits)"""
162+
163+
model = StreamlitEntityModel(
164+
type="streamlit",
165+
identifier="test_streamlit",
166+
runtime_name=SPCS_RUNTIME_V2_NAME,
167+
compute_pool="MYPOOL",
168+
main_file="streamlit_app.py",
169+
artifacts=["streamlit_app.py"],
170+
)
171+
model.set_entity_id("test_streamlit")
172+
173+
entity = StreamlitEntity(workspace_ctx=workspace_context, entity_model=model)
174+
175+
# Test with stage-based deployment - should NOT include SPCS runtime fields
176+
# even when experimental=True, as stage-based deployments are old-style
177+
sql = entity.get_deploy_sql(from_stage_name="@stage/path", experimental=True)
178+
179+
assert "ROOT_LOCATION = '@stage/path'" in sql
180+
assert "RUNTIME_NAME" not in sql
181+
assert "COMPUTE_POOL" not in sql
182+
183+
def test_get_deploy_sql_without_spcs_runtime_v2(self, workspace_context):
184+
"""Test that get_deploy_sql works normally when experimental is False"""
185+
model = StreamlitEntityModel(
186+
type="streamlit",
187+
identifier="test_streamlit",
188+
runtime_name=SPCS_RUNTIME_V2_NAME,
189+
compute_pool="MYPOOL",
190+
main_file="streamlit_app.py",
191+
artifacts=["streamlit_app.py"],
192+
)
193+
model.set_entity_id("test_streamlit")
194+
195+
entity = StreamlitEntity(workspace_ctx=workspace_context, entity_model=model)
196+
197+
# Test without experimental flag enabled
198+
sql = entity.get_deploy_sql(
199+
artifacts_dir=Path("/tmp/artifacts"), experimental=False
200+
)
201+
202+
assert "RUNTIME_NAME" not in sql
203+
assert "COMPUTE_POOL" not in sql
204+
205+
def test_spcs_runtime_v2_requires_correct_runtime_name(self, workspace_context):
206+
"""Test that SPCS runtime v2 requires correct runtime name to be enabled"""
207+
model = StreamlitEntityModel(
208+
type="streamlit",
209+
identifier="test_streamlit",
210+
runtime_name=SPCS_RUNTIME_V2_NAME,
211+
compute_pool="MYPOOL",
212+
main_file="streamlit_app.py",
213+
artifacts=["streamlit_app.py"],
214+
)
215+
model.set_entity_id("test_streamlit")
216+
217+
entity = StreamlitEntity(workspace_ctx=workspace_context, entity_model=model)
218+
219+
# Test with experimental=True and correct runtime_name
220+
sql = entity.get_deploy_sql(
221+
artifacts_dir=Path("/tmp/artifacts"), experimental=True
222+
)
223+
assert f"RUNTIME_NAME = '{SPCS_RUNTIME_V2_NAME}'" in sql
224+
assert "COMPUTE_POOL = 'MYPOOL'" in sql
225+
226+
# Test with experimental=False, should not add SPCS fields
227+
sql = entity.get_deploy_sql(
228+
artifacts_dir=Path("/tmp/artifacts"), experimental=False
229+
)
230+
assert "RUNTIME_NAME" not in sql
231+
assert "COMPUTE_POOL" not in sql
232+
233+
# Test with wrong runtime_name
234+
model.runtime_name = "SOME_OTHER_RUNTIME"
235+
entity = StreamlitEntity(workspace_ctx=workspace_context, entity_model=model)
236+
sql = entity.get_deploy_sql(
237+
artifacts_dir=Path("/tmp/artifacts"), experimental=True
238+
)
239+
assert "RUNTIME_NAME" not in sql
240+
assert "COMPUTE_POOL" not in sql
241+
242+
def test_spcs_runtime_v2_requires_runtime_and_pool(self, workspace_context):
243+
"""Test that SPCS runtime v2 SQL generation works with valid models"""
244+
245+
# Test with valid container runtime and compute_pool
246+
model = StreamlitEntityModel(
247+
type="streamlit",
248+
identifier="test_streamlit",
249+
runtime_name=SPCS_RUNTIME_V2_NAME,
250+
compute_pool="MYPOOL",
251+
main_file="streamlit_app.py",
252+
artifacts=["streamlit_app.py"],
253+
)
254+
model.set_entity_id("test_streamlit")
255+
entity = StreamlitEntity(workspace_ctx=workspace_context, entity_model=model)
256+
sql = entity.get_deploy_sql(
257+
artifacts_dir=Path("/tmp/artifacts"), experimental=True
258+
)
259+
assert f"RUNTIME_NAME = '{SPCS_RUNTIME_V2_NAME}'" in sql
260+
assert "COMPUTE_POOL = 'MYPOOL'" in sql
261+
262+
# Test with warehouse runtime (no compute_pool needed)
263+
model = StreamlitEntityModel(
264+
type="streamlit",
265+
identifier="test_streamlit",
266+
runtime_name="SYSTEM$WAREHOUSE_RUNTIME",
267+
main_file="streamlit_app.py",
268+
artifacts=["streamlit_app.py"],
269+
)
270+
model.set_entity_id("test_streamlit")
271+
entity = StreamlitEntity(workspace_ctx=workspace_context, entity_model=model)
272+
sql = entity.get_deploy_sql(
273+
artifacts_dir=Path("/tmp/artifacts"), experimental=True
274+
)
275+
# Warehouse runtime should not trigger SPCS runtime v2 mode
276+
assert "RUNTIME_NAME" not in sql
277+
assert "COMPUTE_POOL" not in sql
278+
279+
def test_spcs_runtime_validation(self, workspace_context):
280+
"""Test validation for SPCS runtime configuration"""
281+
282+
# Test: SYSTEM$ST_CONTAINER_RUNTIME_PY3_11 requires compute_pool
283+
escaped_runtime_name = SPCS_RUNTIME_V2_NAME.replace("$", r"\$")
284+
with pytest.raises(
285+
ValueError,
286+
match=rf"compute_pool is required when using {escaped_runtime_name}",
287+
):
288+
StreamlitEntityModel(
289+
type="streamlit",
290+
identifier="test_streamlit",
291+
runtime_name=SPCS_RUNTIME_V2_NAME,
292+
main_file="streamlit_app.py",
293+
artifacts=["streamlit_app.py"],
294+
)
295+
296+
# Test: compute_pool without runtime_name is invalid
297+
with pytest.raises(
298+
ValueError, match="compute_pool is specified without runtime_name"
299+
):
300+
StreamlitEntityModel(
301+
type="streamlit",
302+
identifier="test_streamlit",
303+
compute_pool="MYPOOL",
304+
main_file="streamlit_app.py",
305+
artifacts=["streamlit_app.py"],
306+
)
307+
308+
# Test: warehouse runtime without compute_pool is valid
309+
model = StreamlitEntityModel(
310+
type="streamlit",
311+
identifier="test_streamlit",
312+
runtime_name="SYSTEM$WAREHOUSE_RUNTIME",
313+
main_file="streamlit_app.py",
314+
artifacts=["streamlit_app.py"],
315+
)
316+
assert model.runtime_name == "SYSTEM$WAREHOUSE_RUNTIME"
317+
assert model.compute_pool is None
318+
319+
# Test: container runtime with compute_pool is valid
320+
model = StreamlitEntityModel(
321+
type="streamlit",
322+
identifier="test_streamlit",
323+
runtime_name=SPCS_RUNTIME_V2_NAME,
324+
compute_pool="MYPOOL",
325+
main_file="streamlit_app.py",
326+
artifacts=["streamlit_app.py"],
327+
)
328+
assert model.runtime_name == SPCS_RUNTIME_V2_NAME
329+
assert model.compute_pool == "MYPOOL"
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import streamlit as st
2+
from utils.utils import hello_world
3+
4+
st.title("Simple Page")
5+
st.write("This page demonstrates basic Streamlit functionality in SPCS runtime v2.")
6+
7+
# Simple content
8+
st.header("Page Content")
9+
st.write(hello_world())
10+
11+
# Simple interactive elements
12+
st.header("Interactive Elements")
13+
name = st.text_input("Enter your name:", "World")
14+
st.write(f"Hello, {name}!")
15+
16+
# Simple columns layout
17+
col1, col2 = st.columns(2)
18+
19+
with col1:
20+
st.subheader("Column 1")
21+
st.write("This is the first column")
22+
23+
with col2:
24+
st.subheader("Column 2")
25+
st.write("This is the second column")
26+
27+
# Simple metrics
28+
st.header("Simple Metrics")
29+
st.metric("Test Metric", 42)
30+
st.metric("Another Metric", 100, delta=10)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# No additional requirements needed for basic Streamlit testing

0 commit comments

Comments
 (0)