Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into jsikorski/streamlit_e…
Browse files Browse the repository at this point in the history
…ntities

# Conflicts:
#	tests/streamlit/__snapshots__/test_commands.ambr
#	tests/test_data/projects/example_streamlit_v2/snowflake.yml
  • Loading branch information
sfc-gh-jsikorski committed Dec 11, 2024
2 parents 6bdb1af + ca0879b commit 3d1a0d5
Show file tree
Hide file tree
Showing 91 changed files with 4,917 additions and 1,975 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ Snowflake CLI is an open-source command-line tool explicitly designed for develo

With Snowflake CLI, developers can create, manage, update, and view apps running on Snowflake across workloads such as Streamlit in Snowflake, the Snowflake Native App Framework, Snowpark Container Services, and Snowpark. It supports a range of Snowflake features, including user-defined functions, stored procedures, Streamlit in Snowflake, and SQL execution.

**Note**: Snowflake CLI is in Public Preview (PuPr).

Docs: <https://docs.snowflake.com/en/developer-guide/snowflake-cli-v2/index>.

Quick start: <https://quickstarts.snowflake.com/guide/getting-started-with-snowflake-cli>
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ dependencies = [
"requests==2.32.3",
"requirements-parser==0.11.0",
"setuptools==75.6.0",
'snowflake.core==0.12.1; python_version < "3.12"',
'snowflake.core==1.0.2; python_version < "3.12"',
"snowflake-connector-python[secure-local-storage]==3.12.3",
'snowflake-snowpark-python>=1.15.0;python_version < "3.12"',
'snowflake-snowpark-python>=1.15.0,<1.26.0;python_version < "3.12"',
"tomlkit==0.13.2",
"typer==0.12.5",
"urllib3>=1.24.3,<2.3",
Expand All @@ -59,11 +59,11 @@ classifiers = [
development = [
"coverage==7.6.8",
"pre-commit>=3.5.0",
"pytest==8.3.3",
"pytest==8.3.4",
"pytest-randomly==3.16.0",
"syrupy==4.8.0",
"factory-boy==3.3.1",
"Faker==33.0.0",
"Faker==33.1.0",
]
packaging = ["pyinstaller~=6.10"]

Expand Down
8 changes: 4 additions & 4 deletions snyk/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ rich==13.9.4
requests==2.32.3
requirements-parser==0.11.0
setuptools==75.6.0
snowflake.core==0.12.1; python_version < "3.12"
snowflake.core==1.0.2; python_version < "3.12"
snowflake-connector-python[secure-local-storage]==3.12.3
snowflake-snowpark-python>=1.15.0;python_version < "3.12"
snowflake-snowpark-python>=1.15.0,<1.26.0;python_version < "3.12"
tomlkit==0.13.2
typer==0.12.5
urllib3>=1.24.3,<2.3
Expand All @@ -17,8 +17,8 @@ pip
pydantic==2.9.2
coverage==7.6.8
pre-commit>=3.5.0
pytest==8.3.3
pytest==8.3.4
pytest-randomly==3.16.0
syrupy==4.8.0
factory-boy==3.3.1
Faker==33.0.0
Faker==33.1.0
31 changes: 12 additions & 19 deletions src/snowflake/cli/_plugins/connection/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import os
from enum import Enum
from functools import lru_cache
from textwrap import dedent
from typing import Any, Dict, Optional

from click.exceptions import ClickException
Expand Down Expand Up @@ -57,11 +56,12 @@ class UIParameter(Enum):
NA_ENFORCE_MANDATORY_FILTERS = (
"ENFORCE_MANDATORY_FILTERS_FOR_SAME_ACCOUNT_INSTALLATION"
)
NA_FEATURE_RELEASE_CHANNELS = "FEATURE_RELEASE_CHANNELS"


def get_ui_parameter(
conn: SnowflakeConnection, parameter: UIParameter, default: Any
) -> str:
) -> Any:
"""
Returns the value of a single UI parameter.
If the parameter is not found, the default value is returned.
Expand All @@ -77,21 +77,19 @@ def get_ui_parameters(conn: SnowflakeConnection) -> Dict[UIParameter, Any]:
Returns the UI parameters from the SYSTEM$BOOTSTRAP_DATA_REQUEST function
"""

parameters_to_fetch = sorted([param.value for param in UIParameter])
parameters_to_fetch = [param.value for param in UIParameter]

query = dedent(
f"""
select value['value']::string as PARAM_VALUE, value['name']::string as PARAM_NAME from table(flatten(
input => parse_json(SYSTEM$BOOTSTRAP_DATA_REQUEST()),
path => 'clientParamsInfo'
)) where value['name'] in ('{"', '".join(parameters_to_fetch)}');
"""
)
# Parsing of the Json and the filtering is happening here in Snowflake CLI
# in order to avoid requiring a warehouse in Snowflake
query = "call system$bootstrap_data_request('CLIENT_PARAMS_INFO')"
*_, cursor = conn.execute_string(query)

*_, cursor = conn.execute_string(query, cursor_class=DictCursor)
json_map = json.loads(cursor.fetchone()[0])

return {
UIParameter(row["PARAM_NAME"]): row["PARAM_VALUE"] for row in cursor.fetchall()
UIParameter(row["name"]): row["value"]
for row in json_map["clientParamsInfo"]
if row["name"] in parameters_to_fetch
}


Expand All @@ -103,12 +101,7 @@ def is_regionless_redirect(conn: SnowflakeConnection) -> bool:
assume it's regionless, as this is true for most production deployments.
"""
try:
return (
get_ui_parameter(
conn, UIParameter.NA_ENABLE_REGIONLESS_REDIRECT, "true"
).lower()
== "true"
)
return get_ui_parameter(conn, UIParameter.NA_ENABLE_REGIONLESS_REDIRECT, True)
except:
log.warning(
"Cannot determine regionless redirect; assuming True.", exc_info=True
Expand Down
53 changes: 37 additions & 16 deletions src/snowflake/cli/_plugins/nativeapp/codegen/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,14 @@
from snowflake.cli._plugins.nativeapp.codegen.templates.templates_processor import (
TemplatesProcessor,
)
from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag
from snowflake.cli.api.cli_global_context import get_cli_context
from snowflake.cli.api.console import cli_console as cc
from snowflake.cli.api.metrics import CLICounterField
from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import (
ProcessorMapping,
)

SNOWPARK_PROCESSOR = "snowpark"
NA_SETUP_PROCESSOR = "native app setup"
TEMPLATES_PROCESSOR = "templates"

_REGISTERED_PROCESSORS_BY_NAME = {
SNOWPARK_PROCESSOR: SnowparkAnnotationProcessor,
NA_SETUP_PROCESSOR: NativeAppSetupProcessor,
TEMPLATES_PROCESSOR: TemplatesProcessor,
}
ProcessorClassType = type[ArtifactProcessor]


class NativeAppCompiler:
Expand All @@ -66,10 +57,28 @@ def __init__(
bundle_ctx: BundleContext,
):
self._assert_absolute_paths(bundle_ctx)
self._processor_classes_by_name: Dict[str, ProcessorClassType] = {}
self._bundle_ctx = bundle_ctx
# dictionary of all processors created and shared between different artifact objects.
self.cached_processors: Dict[str, ArtifactProcessor] = {}

self.register(SnowparkAnnotationProcessor)
self.register(NativeAppSetupProcessor)
self.register(TemplatesProcessor)

def register(self, processor_cls: ProcessorClassType):
"""
Registers a processor class to enable.
"""

name = getattr(processor_cls, "NAME", None)
assert name is not None

if name in self._processor_classes_by_name:
raise ValueError(f"Processor {name} is already registered")

self._processor_classes_by_name[str(name)] = processor_cls

@staticmethod
def _assert_absolute_paths(bundle_ctx: BundleContext):
for name in ["Project", "Deploy", "Bundle", "Generated"]:
Expand Down Expand Up @@ -128,8 +137,8 @@ def _try_create_processor(
if current_processor is not None:
return current_processor

processor_factory = _REGISTERED_PROCESSORS_BY_NAME.get(processor_name)
if processor_factory is None:
processor_cls = self._processor_classes_by_name.get(processor_name)
if processor_cls is None:
# No registered processor with the specified name
return None

Expand All @@ -141,7 +150,7 @@ def _try_create_processor(
processor_ctx.generated_root = (
self._bundle_ctx.generated_root / processor_subdirectory
)
current_processor = processor_factory(processor_ctx)
current_processor = processor_cls(processor_ctx)
self.cached_processors[processor_name] = current_processor

return current_processor
Expand All @@ -154,6 +163,18 @@ def _should_invoke_processors(self):
return False

def _is_enabled(self, processor: ProcessorMapping) -> bool:
if processor.name.lower() == NA_SETUP_PROCESSOR:
return FeatureFlag.ENABLE_NATIVE_APP_PYTHON_SETUP.is_enabled()
return True
"""
Determines is a process is enabled. All processors are considered enabled
unless they are explicitly disabled, typically via a feature flag.
"""
processor_name = processor.name.lower()
processor_cls = self._processor_classes_by_name.get(processor_name)
if processor_cls is None:
# Unknown processor, consider it enabled, even though trying to
# invoke it later will raise an exception
return True

# if the processor class defines a static method named "is_enabled", then
# call it. Otherwise, it's considered enabled by default.
is_enabled_fn = getattr(processor_cls, "is_enabled", lambda: True)
return is_enabled_fn()
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
SandboxEnvBuilder,
execute_script_in_sandbox,
)
from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag
from snowflake.cli._plugins.stage.diff import to_stage_path
from snowflake.cli.api.console import cli_console as cc
from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import (
Expand Down Expand Up @@ -74,9 +75,15 @@ def safe_set(d: dict, *keys: str, **kwargs) -> None:


class NativeAppSetupProcessor(ArtifactProcessor):
NAME = "native app setup"

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

@staticmethod
def is_enabled() -> bool:
return FeatureFlag.ENABLE_NATIVE_APP_PYTHON_SETUP.is_enabled()

def process(
self,
artifact_to_process: PathMapping,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
and generate SQL code for creation of extension functions based on those discovered objects.
"""

NAME = "snowpark"

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class TemplatesProcessor(ArtifactProcessor):
Processor class to perform template expansion on all relevant artifacts (specified in the project definition file).
"""

NAME = "templates"

def expand_templates_in_file(
self, src: Path, dest: Path, template_context: dict[str, Any] | None = None
) -> None:
Expand All @@ -58,38 +60,45 @@ def expand_templates_in_file(
if src.is_dir():
return

with self.edit_file(dest) as file:
if not has_client_side_templates(file.contents) and not (
_is_sql_file(dest) and has_sql_templates(file.contents)
):
return

src_file_name = src.relative_to(self._bundle_ctx.project_root)
cc.step(f"Expanding templates in {src_file_name}")
with cc.indented():
try:
jinja_env = (
choose_sql_jinja_env_based_on_template_syntax(
file.contents, reference_name=src_file_name
src_file_name = src.relative_to(self._bundle_ctx.project_root)

try:
with self.edit_file(dest) as file:
if not has_client_side_templates(file.contents) and not (
_is_sql_file(dest) and has_sql_templates(file.contents)
):
return
cc.step(f"Expanding templates in {src_file_name}")
with cc.indented():
try:
jinja_env = (
choose_sql_jinja_env_based_on_template_syntax(
file.contents, reference_name=src_file_name
)
if _is_sql_file(dest)
else get_client_side_jinja_env()
)
expanded_template = jinja_env.from_string(file.contents).render(
template_context or get_cli_context().template_context
)
if _is_sql_file(dest)
else get_client_side_jinja_env()
)
expanded_template = jinja_env.from_string(file.contents).render(
template_context or get_cli_context().template_context
)

# For now, we are printing the source file path in the error message
# instead of the destination file path to make it easier for the user
# to identify the file that has the error, and edit the correct file.
except jinja2.TemplateSyntaxError as e:
raise InvalidTemplateInFileError(src_file_name, e, e.lineno) from e

except jinja2.UndefinedError as e:
raise InvalidTemplateInFileError(src_file_name, e) from e

if expanded_template != file.contents:
file.edited_contents = expanded_template

# For now, we are printing the source file path in the error message
# instead of the destination file path to make it easier for the user
# to identify the file that has the error, and edit the correct file.
except jinja2.TemplateSyntaxError as e:
raise InvalidTemplateInFileError(
src_file_name, e, e.lineno
) from e

except jinja2.UndefinedError as e:
raise InvalidTemplateInFileError(src_file_name, e) from e

if expanded_template != file.contents:
file.edited_contents = expanded_template
except UnicodeDecodeError as err:
cc.warning(
f"Could not read file {src_file_name}, error: {err.reason}. Skipping this file."
)

@span("templates_processor")
def process(
Expand Down
3 changes: 2 additions & 1 deletion src/snowflake/cli/_plugins/nativeapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from snowflake.cli._plugins.nativeapp.entities.application_package import (
ApplicationPackageEntityModel,
)
from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade
from snowflake.cli._plugins.nativeapp.v2_conversions.compat import (
find_entity,
force_project_definition_v2,
Expand Down Expand Up @@ -198,7 +199,7 @@ def app_open(
)
app_id = options["app_entity_id"]
app = ws.get_entity(app_id)
if app.get_existing_app_info():
if get_snowflake_facade().get_existing_app_info(app.name, app.role):
typer.launch(app.get_snowsight_url())
return MessageResult(f"Snowflake Native App opened in browser.")
else:
Expand Down
Loading

0 comments on commit 3d1a0d5

Please sign in to comment.