Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Execute Streamlit command for running Streamlit apps #1603

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
* Added `SNOWFLAKE_..._PRIVATE_KEY_RAW` environment variable to pass private key as a raw string.
* Added periodic check for newest version of Snowflake CLI. When new version is available, user will be notified.
* Added support for `imports` in Streamlit definition.
* Added `snow streamlit execute app-name` command to run Streamlit apps in a Snowflake environment in headless mode.

## Fixes and improvements
* Fixed problem with whitespaces in `snow connection add` command.
Expand Down
12 changes: 12 additions & 0 deletions src/snowflake/cli/_plugins/streamlit/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@
)


@app.command(requires_connection=True)
def execute(
name: FQN = StreamlitNameArgument,
**options,
):
"""
Executes a streamlit in a headless mode.
"""
_ = StreamlitManager().execute(app_name=name)
return MessageResult(f"Streamlit {name} executed.")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually we return message from Snowflake, in your case, what message is returned?

Copy link
Collaborator Author

@sfc-gh-vmaleki sfc-gh-vmaleki Sep 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After running snow execute streamlit my-app the message was Streamlit my-app executed..



@app.command("share", requires_connection=True)
def streamlit_share(
name: FQN = StreamlitNameArgument,
Expand Down
4 changes: 4 additions & 0 deletions src/snowflake/cli/_plugins/streamlit/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@


class StreamlitManager(SqlExecutionMixin):
def execute(self, app_name: FQN):
query = f"EXECUTE STREAMLIT {app_name.sql_identifier}()"
return self._execute_query(query=query)

def share(self, streamlit_name: FQN, to_role: str) -> SnowflakeCursor:
return self._execute_query(
f"grant usage on streamlit {streamlit_name.sql_identifier} to role {to_role}"
Expand Down
78 changes: 78 additions & 0 deletions tests/__snapshots__/test_help_messages.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -7815,6 +7815,82 @@
+------------------------------------------------------------------------------+


'''
# ---
# name: test_help_messages[streamlit.execute]
'''

Usage: default streamlit execute [OPTIONS] NAME

Executes a streamlit in a headless mode.

+- Arguments ------------------------------------------------------------------+
| * name TEXT Identifier of the Streamlit app. For example: |
| my_streamlit |
| [required] |
+------------------------------------------------------------------------------+
+- Options --------------------------------------------------------------------+
| --help -h Show this message and exit. |
+------------------------------------------------------------------------------+
+- Connection configuration ---------------------------------------------------+
| --connection,--environment -c TEXT Name of the connection, as |
| defined in your config.toml. |
| Default: default. |
| --account,--accountname TEXT Name assigned to your |
| Snowflake account. Overrides |
| the value specified for the |
| connection. |
| --user,--username TEXT Username to connect to |
| Snowflake. Overrides the value |
| specified for the connection. |
| --password TEXT Snowflake password. Overrides |
| the value specified for the |
| connection. |
| --authenticator TEXT Snowflake authenticator. |
| Overrides the value specified |
| for the connection. |
| --private-key-file,--private-… TEXT Snowflake private key file |
| path. Overrides the value |
| specified for the connection. |
| --token-file-path TEXT Path to file with an OAuth |
| token that should be used when |
| connecting to Snowflake |
| --database,--dbname TEXT Database to use. Overrides the |
| value specified for the |
| connection. |
| --schema,--schemaname TEXT Database schema to use. |
| Overrides the value specified |
| for the connection. |
| --role,--rolename TEXT Role to use. Overrides the |
| value specified for the |
| connection. |
| --warehouse TEXT Warehouse to use. Overrides |
| the value specified for the |
| connection. |
| --temporary-connection -x Uses connection defined with |
| command line parameters, |
| instead of one defined in |
| config |
| --mfa-passcode TEXT Token to use for multi-factor |
| authentication (MFA) |
| --enable-diag Run python connector |
| diagnostic test |
| --diag-log-path TEXT Diagnostic report path |
| --diag-allowlist-path TEXT Diagnostic report path to |
| optional allowlist |
+------------------------------------------------------------------------------+
+- Global configuration -------------------------------------------------------+
| --format [TABLE|JSON] Specifies the output format. |
| [default: TABLE] |
| --verbose -v Displays log entries for log levels info |
| and higher. |
| --debug Displays log entries for log levels debug |
| and higher; debug logs contains additional |
| information. |
| --silent Turns off intermediate output to console. |
+------------------------------------------------------------------------------+


'''
# ---
# name: test_help_messages[streamlit.get-url]
Expand Down Expand Up @@ -8073,6 +8149,7 @@
| command will raise an error. |
| describe Provides description of streamlit. |
| drop Drops streamlit with given name. |
| execute Executes a streamlit in a headless mode. |
| get-url Returns a URL to the specified Streamlit app |
| list Lists all available streamlits. |
| share Shares a Streamlit app with another role. |
Expand Down Expand Up @@ -8494,6 +8571,7 @@
| command will raise an error. |
| describe Provides description of streamlit. |
| drop Drops streamlit with given name. |
| execute Executes a streamlit in a headless mode. |
| get-url Returns a URL to the specified Streamlit app |
| list Lists all available streamlits. |
| share Shares a Streamlit app with another role. |
Expand Down
22 changes: 0 additions & 22 deletions tests/stage/__snapshots__/test_stage.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,6 @@

'''
# ---
# name: test_execute[@DB.SCHEMA.EXE/s1.sql-@db.schema.exe-expected_files19]
'''
SUCCESS - @db.schema.exe/s1.sql
+-----------------------------------------+
| File | Status | Error |
|-----------------------+---------+-------|
| @db.schema.exe/s1.sql | SUCCESS | None |
+-----------------------------------------+

'''
# ---
# name: test_execute[@DB.schema.EXE/a/S3.sql-@DB.schema.EXE-expected_files20]
'''
SUCCESS - @DB.schema.EXE/a/S3.sql
Expand All @@ -56,17 +45,6 @@

'''
# ---
# name: test_execute[@DB.schema.EXE/a/S3.sql-@db.schema.exe-expected_files20]
'''
SUCCESS - @db.schema.exe/a/S3.sql
+-------------------------------------------+
| File | Status | Error |
|-------------------------+---------+-------|
| @db.schema.exe/a/S3.sql | SUCCESS | None |
+-------------------------------------------+

'''
# ---
# name: test_execute[@db.schema.exe-@db.schema.exe-expected_files15]
'''
SUCCESS - @db.schema.exe/s1.sql
Expand Down
11 changes: 11 additions & 0 deletions tests/streamlit/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import pytest
from snowflake.cli._plugins.connection.util import REGIONLESS_QUERY
from snowflake.cli._plugins.streamlit.manager import StreamlitManager
from snowflake.cli.api.identifiers import FQN

STREAMLIT_NAME = "test_streamlit"
TEST_WAREHOUSE = "test_warehouse"
Expand Down Expand Up @@ -937,3 +939,12 @@ def test_deploy_streamlit_with_comment_v2(
REGIONLESS_QUERY,
"select current_account_name()",
]


@mock.patch.object(StreamlitManager, "execute")
def test_execute_streamlit(mock_execute, runner):
result = runner.invoke(["streamlit", "execute", STREAMLIT_NAME])

assert result.exit_code == 0, result.output
assert result.output == f"Streamlit {STREAMLIT_NAME} executed.\n"
mock_execute.assert_called_once_with(app_name=FQN.from_string(STREAMLIT_NAME))
13 changes: 13 additions & 0 deletions tests/streamlit/test_streamlit_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
StreamlitEntityModel,
)
from snowflake.cli.api.identifiers import FQN

mock_streamlit_exists = mock.patch(
"snowflake.cli._plugins.streamlit.manager.ObjectManager.object_exists",
Expand Down Expand Up @@ -131,3 +132,15 @@ def test_deploy_streamlit_with_comment(
COMMENT = 'This is a test comment'"""
)
)


@mock.patch("snowflake.cli._plugins.streamlit.manager.StreamlitManager._execute_query")
@mock_streamlit_exists
def test_execute_streamlit(mock_execute_query):
app_name = FQN(database="DB", schema="SH", name="my_streamlit_app")

StreamlitManager(MagicMock()).execute(app_name=app_name)

mock_execute_query.assert_called_once_with(
query="EXECUTE STREAMLIT IDENTIFIER('DB.SH.my_streamlit_app')()"
)
58 changes: 58 additions & 0 deletions tests_integration/test_streamlit.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
rows_from_snowflake_session,
)
from tests_integration.testing_utils import assert_that_result_is_successful
from snowflake.cli._plugins.streamlit.manager import StreamlitManager


@pytest.mark.integration
Expand Down Expand Up @@ -367,3 +368,60 @@ def _new_streamlit_role(snowflake_session, test_database):
row_from_snowflake_session(result),
{"status": f"{role_name.upper()} successfully dropped."},
)


def _execute_streamlit(runner, streamlit_name):
result = runner.invoke_with_connection_json(
["streamlit", "execute", streamlit_name, "--format", "json"]
)
assert result.exit_code == 0
assert result.json == {"message": f"Streamlit {streamlit_name} executed."}


def _execute_streamlit_failure(runner, streamlit_name):
result = runner.invoke_with_connection(["streamlit", "execute", streamlit_name])
assert result.exit_code == 1
assert f"NameError: name {streamlit_name} is not defined" in result.output


@pytest.mark.integration
def test_execute_streamlit(runner, snowflake_session, test_database):
stage_name = "streamlit_stage"
snowflake_session.execute_string(f"create stage {stage_name};")

# Create a Streamlit app and upload it to the stage
streamlit_app = Path(
"tests_integration/test_data/projects/streamlit_v2/streamlit_app.py"
)

_create_streamlit(
local_streamlit_file=streamlit_app,
runner=runner,
snowflake_session=snowflake_session,
stage_name=stage_name,
)

# Test successful and failure executions
_execute_streamlit(runner, streamlit_app.stem)
_execute_streamlit_failure(runner, "nonexistent_app")


def _create_streamlit(local_streamlit_file, runner, snowflake_session, stage_name):
streamlit_name = local_streamlit_file.stem
stage_path = f"@{stage_name}/{local_streamlit_file.name}"
snowflake_session.execute_string(
f"put file://{local_streamlit_file.absolute()} @{stage_name} AUTO_COMPRESS=FALSE;"
)
command = (
"streamlit",
"create",
streamlit_name,
"--streamlit-file",
stage_path,
"--format",
"json",
)
result = runner.invoke_with_connection_json(command)
assert result.exit_code == 0
message: str = result.json.get("message", "")
assert message.endswith(streamlit_name.upper())