Skip to content

Commit

Permalink
Merge branch 'main' into jsikorski/1163968-Pydantic-PoC-for-Streamlit
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-jsikorski committed Mar 8, 2024
2 parents 75d7467 + ed437e4 commit 876b4fc
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 10 deletions.
4 changes: 2 additions & 2 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
* @sfc-gh-turbaszek @sfc-gh-pjob @sfc-gh-jsikorski @sfc-gh-astus @sfc-gh-mraba @sfc-gh-pczajka
* @snowflakedb/snowcli

# Native Apps Owners
src/snowflake/cli/plugins/nativeapp/ @snowflakedb/nade
Expand All @@ -7,4 +7,4 @@ tests_integration/test_nativeapp.py @snowflakedb/nade

# Project Definition Owners
src/snowflake/cli/api/project/schemas/native_app.py @snowflakedb/nade
tests/project/ @sfc-gh-turbaszek @sfc-gh-pjob @sfc-gh-jsikorski @sfc-gh-astus @sfc-gh-mraba @sfc-gh-pczajka @snowflakedb/nade
tests/project/ @snowflakedb/snowcli @snowflakedb/nade
5 changes: 4 additions & 1 deletion src/snowflake/cli/api/commands/snow_typer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import logging
from functools import wraps
from typing import Optional
from typing import Callable, Optional

import typer
from snowflake.cli.api.commands.decorators import (
Expand Down Expand Up @@ -30,13 +30,16 @@ def command(
name: Optional[str] = None,
requires_global_options: bool = True,
requires_connection: bool = False,
is_enabled: Callable[[], bool] | None = None,
**kwargs,
):
"""
Custom implementation of Typer.command that adds ability to execute additional
logic before and after execution as well as process the result and act on possible
errors.
"""
if is_enabled is not None and not is_enabled():
return lambda func: func

def custom_command(command_callable):
"""Custom command wrapper similar to Typer.command."""
Expand Down
34 changes: 28 additions & 6 deletions src/snowflake/cli/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import Any, Dict, Optional, Union

import tomlkit
from click import ClickException
from snowflake.cli.api.exceptions import (
ConfigFileTooWidePermissionsError,
MissingConfiguration,
Expand Down Expand Up @@ -37,6 +38,7 @@ class Empty:

LOGS_SECTION_PATH = [CLI_SECTION, LOGS_SECTION]
PLUGINS_SECTION_PATH = [CLI_SECTION, PLUGINS_SECTION]
FEATURE_FLAGS_SECTION_PATH = [CLI_SECTION, "features"]

CONFIG_MANAGER.add_option(
name=CLI_SECTION,
Expand Down Expand Up @@ -147,7 +149,7 @@ def get_config_section(*path) -> dict:

def get_config_value(*path, key: str, default: Optional[Any] = Empty) -> Any:
"""Looks for given key under nested path in toml file."""
env_variable = _get_env_value(*path, key=key)
env_variable = get_env_value(*path, key=key)
if env_variable:
return env_variable
try:
Expand All @@ -158,6 +160,25 @@ def get_config_value(*path, key: str, default: Optional[Any] = Empty) -> Any:
raise


def get_config_bool_value(*path, key: str, default: Optional[Any] = Empty) -> bool:
value = get_config_value(*path, key=key, default=default)
# If we get bool then we can return
if isinstance(value, bool):
return value

# Now if value is not string then cast it to str. Simplifies logic for 1 and 0
if not isinstance(value, str):
value = str(value)

know_booleans_mapping = {"true": True, "false": False, "1": True, "0": False}

if value.lower() not in know_booleans_mapping:
raise ClickException(
f"Expected boolean value for {'.'.join((*path, key))} option."
)
return know_booleans_mapping[value.lower()]


def _initialise_config(config_file: Path) -> None:
config_file = SecurePath(config_file)
config_file.parent.mkdir(parents=True, exist_ok=True)
Expand All @@ -166,11 +187,12 @@ def _initialise_config(config_file: Path) -> None:
log.info("Created Snowflake configuration file at %s", CONFIG_MANAGER.file_path)


def _get_env_value(*path, key: str) -> str | None:
env_variable_name = (
"SNOWFLAKE_" + "_".join(p.upper() for p in path) + f"_{key.upper()}"
)
return os.environ.get(env_variable_name)
def get_env_variable_name(*path, key: str) -> str:
return "SNOWFLAKE_" + "_".join(p.upper() for p in path) + f"_{key.upper()}"


def get_env_value(*path, key: str) -> str | None:
return os.environ.get(get_env_variable_name(*path, key=key))


def _find_section(*path) -> TOMLDocument:
Expand Down
36 changes: 36 additions & 0 deletions src/snowflake/cli/api/feature_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from enum import Enum, unique
from typing import NamedTuple

from snowflake.cli.api.config import (
FEATURE_FLAGS_SECTION_PATH,
get_config_bool_value,
get_env_variable_name,
)


class BooleanFlag(NamedTuple):
name: str
default: bool = False


@unique
class FeatureFlagMixin(Enum):
def is_enabled(self) -> bool:
return get_config_bool_value(
*FEATURE_FLAGS_SECTION_PATH,
key=self.value.name.lower(),
default=self.value.default,
)

def is_disabled(self):
return not self.is_enabled()

def env_variable(self):
return get_env_variable_name(*FEATURE_FLAGS_SECTION_PATH, key=self.value.name)


@unique
class FeatureFlag(FeatureFlagMixin):
ENABLE_STREAMLIT_EMBEDDED_STAGE = BooleanFlag(
"ENABLE_STREAMLIT_EMBEDDED_STAGE", False
)
6 changes: 5 additions & 1 deletion src/snowflake/cli/plugins/streamlit/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from snowflake.cli.api.commands.experimental_behaviour import (
experimental_behaviour_enabled,
)
from snowflake.cli.api.feature_flags import FeatureFlag
from snowflake.cli.api.project.util import unquote_identifier
from snowflake.cli.api.sql_execution import SqlExecutionMixin
from snowflake.cli.plugins.connection.util import (
Expand Down Expand Up @@ -106,7 +107,10 @@ def deploy(
)
fully_qualified_name = stage_manager.to_fully_qualified_name(streamlit_name)
streamlit_name = self.get_name_from_fully_qualified_name(fully_qualified_name)
if experimental_behaviour_enabled():
if (
experimental_behaviour_enabled()
or FeatureFlag.ENABLE_STREAMLIT_EMBEDDED_STAGE.is_enabled()
):
"""
1. Create streamlit object
2. Upload files to embedded stage
Expand Down
32 changes: 32 additions & 0 deletions tests/api/commands/__snapshots__/test_snow_typer.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,37 @@
╰──────────────────────────────────────────────────────────────────────────────╯


'''
# ---
# name: test_enabled_command_is_not_visible
'''
Usage: snow [OPTIONS] COMMAND [ARGS]...
Try 'snow --help' for help.
╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ No such command 'switchable_cmd'. │
╰──────────────────────────────────────────────────────────────────────────────╯

'''
# ---
# name: test_enabled_command_is_visible
'''

Usage: snow switchable_cmd [OPTIONS]

╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --help -h Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ 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. │
╰──────────────────────────────────────────────────────────────────────────────╯


'''
# ---
23 changes: 23 additions & 0 deletions tests/api/commands/test_snow_typer.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ def exception_handler(err):
return _CustomTyper


_ENABLED_FLAG = False


def app_factory(typer_cls):
app = typer_cls(name="snow")

Expand All @@ -62,6 +65,10 @@ def cmd_with_global_options(name: str = typer.Argument()):
def cmd_with_connection_options(name: str = typer.Argument()):
return MessageResult(f"hello {name}")

@app.command("switchable_cmd", is_enabled=lambda: _ENABLED_FLAG)
def cmd_witch_enabled_switch():
return MessageResult("Enabled")

return app


Expand Down Expand Up @@ -147,6 +154,22 @@ def test_command_with_connection_options(cli, snapshot):
assert result.output == snapshot


def test_enabled_command_is_visible(cli, snapshot):
global _ENABLED_FLAG
_ENABLED_FLAG = True
result = cli(app_factory(SnowTyper))(["switchable_cmd", "--help"])
assert result.exit_code == 0
assert result.output == snapshot


def test_enabled_command_is_not_visible(cli, snapshot):
global _ENABLED_FLAG
_ENABLED_FLAG = False
result = cli(app_factory(SnowTyper))(["switchable_cmd", "--help"])
assert result.exit_code == 2
assert result.output == snapshot


@mock.patch("snowflake.cli.app.telemetry.log_command_usage")
def test_snow_typer_pre_execute_sends_telemetry(mock_log_command_usage, cli):
result = cli(app_factory(SnowTyper))(["simple_cmd", "Norma"])
Expand Down
65 changes: 65 additions & 0 deletions tests/api/test_feature_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from unittest import mock

import pytest
from click import ClickException
from snowflake.cli.api.feature_flags import BooleanFlag, FeatureFlagMixin


class _TestFlags(FeatureFlagMixin):
# Intentional inconsistency between constant and the enum name to make sure there's no strict relation
ENABLED_BY_DEFAULT = BooleanFlag("ENABLED_DEFAULT", True)
DISABLED_BY_DEFAULT = BooleanFlag("DISABLED_DEFAULT", False)
NON_BOOLEAN = BooleanFlag("NON_BOOLEAN", "xys") # type: ignore


def test_flag_value_has_to_be_boolean():
with pytest.raises(ClickException):
_TestFlags.NON_BOOLEAN.is_enabled()


def test_flag_is_enabled():
assert _TestFlags.ENABLED_BY_DEFAULT.is_enabled() is True
assert _TestFlags.ENABLED_BY_DEFAULT.is_disabled() is False


def test_flag_is_disabled():
assert _TestFlags.DISABLED_BY_DEFAULT.is_enabled() is False
assert _TestFlags.DISABLED_BY_DEFAULT.is_disabled() is True


def test_flag_env_variable_value():
assert (
_TestFlags.ENABLED_BY_DEFAULT.env_variable()
== "SNOWFLAKE_CLI_FEATURES_ENABLED_DEFAULT"
)
assert (
_TestFlags.DISABLED_BY_DEFAULT.env_variable()
== "SNOWFLAKE_CLI_FEATURES_DISABLED_DEFAULT"
)


@mock.patch("snowflake.cli.api.config.get_config_value")
@pytest.mark.parametrize("value_from_config", [True, False])
def test_flag_from_config_file(mock_get_config_value, value_from_config):
mock_get_config_value.return_value = value_from_config

assert _TestFlags.DISABLED_BY_DEFAULT.is_enabled() is value_from_config
mock_get_config_value.assert_called_once_with(
"cli", "features", key="disabled_default", default=False
)


@pytest.mark.parametrize("value_from_env", ["1", "true", "True", "TRUE", "TruE"])
def test_flag_is_enabled_from_env_var(value_from_env):
with mock.patch.dict(
"os.environ", {"SNOWFLAKE_CLI_FEATURES_DISABLED_DEFAULT": value_from_env}
):
assert _TestFlags.DISABLED_BY_DEFAULT.is_enabled() is True


@pytest.mark.parametrize("value_from_env", ["0", "false", "False", "FALSE", "FaLse"])
def test_flag_is_disabled_from_env_var(value_from_env):
with mock.patch.dict(
"os.environ", {"SNOWFLAKE_CLI_FEATURES_ENABLED_DEFAULT": value_from_env}
):
assert _TestFlags.ENABLED_BY_DEFAULT.is_enabled() is False
19 changes: 19 additions & 0 deletions tests/plugin/__snapshots__/test_override_by_external_plugins.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# serializer version: 1
# name: test_corrupted_config_raises_human_friendly_error[[corrupted0]
'''
╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ Configuration file seems to be corrupted. Unexpected end of file at line 1 │
│ col 10 │
╰──────────────────────────────────────────────────────────────────────────────╯

'''
# ---
# name: test_corrupted_config_raises_human_friendly_error[[corrupted1]
'''
╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ Configuration file seems to be corrupted. Unexpected end of file at line 1 │
│ col 10 │
╰──────────────────────────────────────────────────────────────────────────────╯

'''
# ---

0 comments on commit 876b4fc

Please sign in to comment.