Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6ab6618
Make versioned Streamlit deployment the default behavior
sfc-gh-svishnu Nov 4, 2025
1309439
Update --legacy flag help text: deployment → stages
sfc-gh-svishnu Nov 4, 2025
2b149b9
Refactor: Extract legacy deployment logic into _deploy_legacy() method
sfc-gh-svishnu Nov 4, 2025
740cffd
Use positive condition for legacy flag check
sfc-gh-svishnu Nov 4, 2025
7c8c298
Fix lint: Break long lines to comply with 88 char limit
sfc-gh-svishnu Nov 4, 2025
4f56d88
Fix black formatting
sfc-gh-svishnu Nov 4, 2025
fec02b3
Add validation to prevent SPCS runtime v2 usage with --legacy flag
sfc-gh-svishnu Nov 4, 2025
0754b83
Add documentation for deprecated --experimental flag and parameter na…
sfc-gh-svishnu Nov 4, 2025
da1fbdf
Fix test_artifacts.py tests to use --legacy flag for artifact tests
sfc-gh-svishnu Nov 4, 2025
2f75530
Fix integration tests for versioned Streamlit deployment
sfc-gh-svishnu Nov 4, 2025
540c2d0
Fix unit tests for versioned Streamlit deployment
sfc-gh-svishnu Nov 4, 2025
3ba38ed
Update help message snapshots for streamlit deploy
sfc-gh-svishnu Nov 4, 2025
4361312
Fix help message snapshots - restore oauth-token-request-url for all …
sfc-gh-svishnu Nov 4, 2025
c14eadd
Restore oauth-token-request-url to streamlit.deploy help message
sfc-gh-svishnu Nov 4, 2025
50600d8
Fix test_streamlit_deploy_prune_flag: add --overwrite flag for stream…
sfc-gh-svishnu Nov 5, 2025
adc859f
Fix test_streamlit_deploy_prune_flag: test with --legacy flag
sfc-gh-svishnu Nov 5, 2025
f6ac72f
Address PR review comments: simplify help text, use CliError, and red…
sfc-gh-svishnu Nov 5, 2025
53e9220
Fix test_streamlit_deploy_prune_flag database context mismatch
sfc-gh-svishnu Nov 6, 2025
158030d
Fix unit test: update exception type to CliError and mock bundle to a…
sfc-gh-svishnu Nov 6, 2025
6ba62e8
Update help message snapshot for --legacy flag text change
sfc-gh-svishnu Nov 6, 2025
58e55c2
Add gitattributes to ensure snapshot files use consistent encoding ac…
sfc-gh-svishnu Nov 7, 2025
7893253
Fix Unicode smart quotes in snapshot file - replace with ASCII apostr…
sfc-gh-svishnu Nov 7, 2025
731b0da
Address PR review comments: add RELEASE-NOTES entry, move test import…
sfc-gh-svishnu Nov 12, 2025
6b82bac
Fix integration test: use fully qualified stage name to match deploym…
sfc-gh-svishnu Nov 12, 2025
a31d6ce
Fix integration test: explicitly set database and schema for streamli…
sfc-gh-svishnu Nov 12, 2025
4635b8a
Fix integration test: remove explicit database/schema flags, rely on …
sfc-gh-svishnu Nov 12, 2025
78d7301
Fix integration test
sfc-gh-svishnu Nov 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Ensure snapshot files are treated as text with LF line endings and UTF-8 encoding
*.ambr text eol=lf
1,062 changes: 542 additions & 520 deletions RELEASE-NOTES.md

Large diffs are not rendered by default.

27 changes: 23 additions & 4 deletions src/snowflake/cli/_plugins/streamlit/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,17 @@ def _check_file_exists_if_not_default(ctx: click.Context, value):
return _check_file_exists_if_not_default


LegacyOption = typer.Option(
False,
"--legacy",
help="Use legacy ROOT_LOCATION SQL syntax.",
is_flag=True,
)


@app.command("deploy", requires_connection=True)
@with_project_definition()
@with_experimental_behaviour()
@with_experimental_behaviour() # Kept for backward compatibility
def streamlit_deploy(
replace: bool = ReplaceOption(
help="Replaces the Streamlit app if it already exists. It only uploads new and overwrites existing files, "
Expand All @@ -138,16 +146,27 @@ def streamlit_deploy(
prune: bool = PruneOption(),
entity_id: str = entity_argument("streamlit"),
open_: bool = OpenOption,
legacy: bool = LegacyOption,
**options,
) -> CommandResult:
"""
Deploys a Streamlit app defined in the project definition file (snowflake.yml). By default, the command uploads
environment.yml and any other pages or folders, if present. If you dont specify a stage name, the `streamlit`
environment.yml and any other pages or folders, if present. If you don't specify a stage name, the `streamlit`
stage is used. If the specified stage does not exist, the command creates it. If multiple Streamlits are defined
in snowflake.yml and no entity_id is provided then command will raise an error.
"""

cli_context = get_cli_context()
workspace_ctx = _get_current_workspace_context()

# Handle deprecated --experimental flag for backward compatibility
if options.get("experimental"):
workspace_ctx.console.warning(
"[Deprecation] The --experimental flag is deprecated. "
"Versioned deployment is now the default behavior. "
"This flag will be removed in a future version."
)

pd = cli_context.project_definition
if not pd.meets_version_requirement("2"):
if not pd.streamlit:
Expand All @@ -163,7 +182,7 @@ def streamlit_deploy(
project_definition=pd,
entity_type=ObjectType.STREAMLIT.value.cli_name,
),
workspace_ctx=_get_current_workspace_context(),
workspace_ctx=workspace_ctx,
)

url = streamlit.perform(
Expand All @@ -173,7 +192,7 @@ def streamlit_deploy(
),
_open=open_,
replace=replace,
experimental=options.get("experimental"),
legacy=legacy,
prune=prune,
)

Expand Down
126 changes: 81 additions & 45 deletions src/snowflake/cli/_plugins/streamlit/streamlit_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from snowflake.cli.api.artifacts.bundle_map import BundleMap
from snowflake.cli.api.entities.common import EntityBase
from snowflake.cli.api.entities.utils import EntityActions, sync_deploy_root_with_stage
from snowflake.cli.api.feature_flags import FeatureFlag as GlobalFeatureFlag
from snowflake.cli.api.exceptions import CliError
from snowflake.cli.api.identifiers import FQN
from snowflake.cli.api.project.project_paths import bundle_root
from snowflake.cli.api.project.schemas.entities.common import Identifier, PathMapping
Expand Down Expand Up @@ -66,12 +66,10 @@ def action_get_url(
self._conn, f"/#/streamlit-apps/{name.url_identifier}"
)

def _is_spcs_runtime_v2_mode(self, experimental: bool = False) -> bool:
def _is_spcs_runtime_v2_mode(self) -> bool:
"""Check if SPCS runtime v2 mode is enabled."""
return (
experimental
and self.model.runtime_name == SPCS_RUNTIME_V2_NAME
and self.model.compute_pool
self.model.runtime_name == SPCS_RUNTIME_V2_NAME and self.model.compute_pool
)

def bundle(self, output_dir: Optional[Path] = None) -> BundleMap:
Expand All @@ -93,7 +91,7 @@ def deploy(
replace: bool,
prune: bool = False,
bundle_map: Optional[BundleMap] = None,
experimental: bool = False,
legacy: bool = False,
*args,
**kwargs,
):
Expand All @@ -104,49 +102,40 @@ def deploy(

console = self._workspace_ctx.console
console.step(f"Checking if object exists")
if self._object_exists() and not replace:
object_exists = self._object_exists()

if object_exists and not replace:
raise ClickException(
f"Streamlit {self.model.fqn.sql_identifier} already exists. Use 'replace' option to overwrite."
)

if (
experimental
or GlobalFeatureFlag.ENABLE_STREAMLIT_VERSIONED_STAGE.is_enabled()
):
self._deploy_experimental(bundle_map=bundle_map, replace=replace)
else:
console.step(f"Uploading artifacts to stage {self.model.stage}")

# We use a static method from StageManager here, but maybe this logic could be implemented elswhere, as we implement entities?
name = (
self.model.identifier.name
if isinstance(self.model.identifier, Identifier)
else self.model.identifier or self.entity_id
)
stage_root = StageManager.get_standard_stage_prefix(
f"{FQN.from_string(self.model.stage).using_connection(self._conn)}/{name}"
if legacy and self._is_spcs_runtime_v2_mode():
raise CliError(
"runtime_name and compute_pool are not compatible with --legacy flag. "
"Please remove the --legacy flag to use versioned deployment, or remove "
"runtime_name and compute_pool from your snowflake.yml to use legacy deployment."
)
sync_deploy_root_with_stage(
console=self._workspace_ctx.console,
deploy_root=bundle_map.deploy_root(),
bundle_map=bundle_map,
prune=prune,
recursive=True,
stage_path_parts=StageManager().stage_path_parts_from_str(stage_root),
print_diff=True,
)

console.step(f"Creating Streamlit object {self.model.fqn.sql_identifier}")

self._execute_query(
self.get_deploy_sql(
replace=replace,
from_stage_name=stage_root,
experimental=False,
# Warn if replacing with a different deployment style
if object_exists and replace:
existing_is_legacy = self._is_legacy_deployment()
if existing_is_legacy and not legacy:
console.warning(
"Replacing legacy ROOT_LOCATION deployment with versioned deployment. "
"Files from the old stage location will not be automatically migrated. "
"The new deployment will use a separate versioned stage location."
)
elif not existing_is_legacy and legacy:
console.warning(
"Deployment style is changing from versioned to legacy. "
"Your existing files will remain in the versioned stage. "
"If needed, manually copy any additional files to the legacy stage after deployment."
)
)

StreamlitManager(connection=self._conn).grant_privileges(self.model)
if legacy:
self._deploy_legacy(bundle_map=bundle_map, replace=replace, prune=prune)
else:
self._deploy_versioned(bundle_map=bundle_map, replace=replace, prune=prune)

return self.perform(EntityActions.GET_URL, action_context, *args, **kwargs)

Expand All @@ -172,7 +161,7 @@ def get_deploy_sql(
artifacts_dir: Optional[Path] = None,
schema: Optional[str] = None,
database: Optional[str] = None,
experimental: bool = False,
legacy: bool = False,
*args,
**kwargs,
) -> str:
Expand Down Expand Up @@ -218,7 +207,7 @@ def get_deploy_sql(

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

Expand Down Expand Up @@ -249,14 +238,61 @@ def _object_exists(self) -> bool:
except ProgrammingError:
return False

def _deploy_experimental(
def _is_legacy_deployment(self) -> bool:
"""Check if the existing streamlit uses legacy ROOT_LOCATION deployment."""
try:
result = self.describe().fetchone()
# Versioned deployments have live_version_location_uri, legacy ones don't
return result.get("live_version_location_uri") is None
except (ProgrammingError, AttributeError, KeyError):
# If we can't determine, assume it doesn't exist or is inaccessible
return False

def _deploy_legacy(
self, bundle_map: BundleMap, replace: bool = False, prune: bool = False
):
console = self._workspace_ctx.console
console.step(f"Uploading artifacts to stage {self.model.stage}")

# We use a static method from StageManager here, but maybe this logic could be implemented elswhere, as we implement entities?
name = (
self.model.identifier.name
if isinstance(self.model.identifier, Identifier)
else self.model.identifier or self.entity_id
)
stage_root = StageManager.get_standard_stage_prefix(
f"{FQN.from_string(self.model.stage).using_connection(self._conn)}/{name}"
)
sync_deploy_root_with_stage(
console=self._workspace_ctx.console,
deploy_root=bundle_map.deploy_root(),
bundle_map=bundle_map,
prune=prune,
recursive=True,
stage_path_parts=StageManager().stage_path_parts_from_str(stage_root),
print_diff=True,
)

console.step(f"Creating Streamlit object {self.model.fqn.sql_identifier}")

self._execute_query(
self.get_deploy_sql(
replace=replace,
from_stage_name=stage_root,
legacy=True,
)
)

StreamlitManager(connection=self._conn).grant_privileges(self.model)

def _deploy_versioned(
self, bundle_map: BundleMap, replace: bool = False, prune: bool = False
):
self._execute_query(
self.get_deploy_sql(
if_not_exists=True,
replace=replace,
experimental=True,
legacy=False,
)
)
try:
Expand Down
3 changes: 0 additions & 3 deletions src/snowflake/cli/api/feature_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ def env_variable(self) -> str:

@unique
class FeatureFlag(FeatureFlagMixin):
ENABLE_STREAMLIT_VERSIONED_STAGE = BooleanFlag(
"ENABLE_STREAMLIT_VERSIONED_STAGE", False
)
ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID = BooleanFlag(
"ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID", False
)
Expand Down
7 changes: 4 additions & 3 deletions tests/__snapshots__/test_help_messages.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -20589,7 +20589,7 @@

Deploys a Streamlit app defined in the project definition file
(snowflake.yml). By default, the command uploads environment.yml and any other
pages or folders, if present. If you dont specify a stage name, the streamlit
pages or folders, if present. If you don't specify a stage name, the streamlit
stage is used. If the specified stage does not exist, the command creates it.
If multiple Streamlits are defined in snowflake.yml and no entity_id is
provided then command will raise an error.
Expand All @@ -20607,6 +20607,7 @@
| [default: no-prune] |
| --open Whether to open the Streamlit app in a |
| browser. |
| --legacy Use legacy ROOT_LOCATION SQL syntax. |
| --project -p TEXT Path where the Snowflake project is |
| stored. Defaults to the current working |
| directory. |
Expand Down Expand Up @@ -21601,7 +21602,7 @@
+- Commands -------------------------------------------------------------------+
| deploy Deploys a Streamlit app defined in the project definition file |
| (snowflake.yml). By default, the command uploads environment.yml |
| and any other pages or folders, if present. If you dont specify |
| and any other pages or folders, if present. If you don't specify |
| a stage name, the streamlit stage is used. If the specified stage |
| does not exist, the command creates it. If multiple Streamlits |
| are defined in snowflake.yml and no entity_id is provided then |
Expand Down Expand Up @@ -22378,7 +22379,7 @@
+- Commands -------------------------------------------------------------------+
| deploy Deploys a Streamlit app defined in the project definition file |
| (snowflake.yml). By default, the command uploads environment.yml |
| and any other pages or folders, if present. If you dont specify |
| and any other pages or folders, if present. If you don't specify |
| a stage name, the streamlit stage is used. If the specified stage |
| does not exist, the command creates it. If multiple Streamlits |
| are defined in snowflake.yml and no entity_id is provided then |
Expand Down
10 changes: 10 additions & 0 deletions tests/streamlit/streamlit_test_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ def setup_method(self):
lambda _, **kwargs: False,
).start()

# Mock describe() to return a versioned stage path for versioned deployments
self.mock_describe = mock.patch(
"snowflake.cli._plugins.streamlit.streamlit_entity.StreamlitEntity.describe"
).start()
mock_cursor = mock.Mock()
mock_cursor.fetchone.return_value = {
"live_version_location_uri": f"snow://streamlit/DB.PUBLIC.{STREAMLIT_NAME}/versions/live/"
}
self.mock_describe.return_value = mock_cursor

def teardown_method(self):
mock.patch.stopall()

Expand Down
5 changes: 4 additions & 1 deletion tests/streamlit/test_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ def test_deploy_with_artifacts(
"streamlit",
"deploy",
"--replace",
"--legacy",
]
)
assert result.exit_code == 0, result.output
Expand Down Expand Up @@ -261,7 +262,9 @@ def test_deploy_with_artifacts_from_other_directory(
STREAMLIT_FILES + [artifacts],
)

result = runner.invoke(["streamlit", "deploy", "-p", tmp, "--replace"])
result = runner.invoke(
["streamlit", "deploy", "-p", tmp, "--replace", "--legacy"]
)
assert result.exit_code == 0, result.output

self._assert_that_exactly_those_files_were_put_to_stage(
Expand Down
Loading