Skip to content

Commit

Permalink
unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-gbloom committed Dec 10, 2024
1 parent afacaea commit 5014ad1
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 78 deletions.
109 changes: 56 additions & 53 deletions src/snowflake/cli/_plugins/nativeapp/entities/application_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import (
InsufficientPrivilegesError,
)
from snowflake.cli._plugins.nativeapp.utils import needs_confirmation
from snowflake.cli._plugins.nativeapp.utils import needs_confirmation, sanitize_dir_name
from snowflake.cli._plugins.snowpark.snowpark_entity_model import (
FunctionEntityModel,
ProcedureEntityModel,
Expand Down Expand Up @@ -112,7 +112,7 @@ class ApplicationPackageChildIdentifier(UpdatableModel):
)


class GrantUsageField(UpdatableModel):
class EnsureUsableByField(UpdatableModel):
application_roles: Optional[Union[str, Set[str]]] = Field(
title="One or more application roles",
default=None,
Expand All @@ -130,7 +130,7 @@ def ensure_app_roles_is_a_set(

class ApplicationPackageChildField(UpdatableModel):
target: str = Field(title="The key of the entity to include in this package")
grant_usage: Optional[GrantUsageField] = Field(
ensure_usable_by: Optional[EnsureUsableByField] = Field(
title="Use to automatically grant USAGE privilege on the child object",
default=None,
)
Expand Down Expand Up @@ -625,55 +625,6 @@ def action_version_drop(
def _bundle(self, action_ctx: ActionContext = None):
model = self._entity_model
bundle_map = build_bundle(self.project_root, self.deploy_root, model.artifacts)
if self._entity_model.children:
# Create _children directory
children_artifacts_dir = self.children_artifacts_deploy_root
os.makedirs(children_artifacts_dir)
children_sql = []
for child in self._entity_model.children:
# Create child sub directory
children_artifacts_dir = (
children_artifacts_dir / child.target
) # TODO Sanitize dir name
os.makedirs(children_artifacts_dir)
child_entity: ApplicationPackageChildInterface = action_ctx.get_entity(
child.target
)
child_entity.bundle(children_artifacts_dir)
app_role = (
to_identifier(
child.grant_usage.application_roles.pop() # TODO Support more than one application role
)
if child.grant_usage and child.grant_usage.application_roles
else None
)
child_schema = (
to_identifier(child.identifier.schema_)
if child.identifier and child.identifier.schema_
else None
)
children_sql.append(
child_entity.get_deploy_sql(
artifacts_dir=Path(
self._entity_model.children_artifacts_dir, child.target
),
schema=child_schema,
)
)
if app_role:
children_sql.append(
f"CREATE APPLICATION ROLE IF NOT EXISTS {app_role};"
)
if child_schema:
children_sql.append(
f"GRANT USAGE ON SCHEMA {child_schema} TO APPLICATION ROLE {app_role};"
)
children_sql.append(
child_entity.get_usage_grant_sql(
app_role=app_role, schema=child_schema
)
)

bundle_context = BundleContext(
package_name=self.name,
artifacts=model.artifacts,
Expand All @@ -686,7 +637,8 @@ def _bundle(self, action_ctx: ActionContext = None):
compiler.compile_artifacts()

if self._entity_model.children:
# Append children SQL to setup script
# Bundle children and append their SQL to setup script
children_sql = self._bundle_children(action_ctx=action_ctx)
setup_file_path = find_setup_script_file(deploy_root=self.deploy_root)
with open(setup_file_path, "r", encoding="utf-8") as file:
existing_setup_script = file.read()
Expand All @@ -696,9 +648,60 @@ def _bundle(self, action_ctx: ActionContext = None):
file.write(existing_setup_script)
file.write("\n-- AUTO GENERATED CHILDREN SECTION\n")
file.write("\n".join(children_sql))
file.write("\n")

return bundle_map

def _bundle_children(self, action_ctx: ActionContext) -> List[str]:
# Create _children directory
children_artifacts_dir = self.children_artifacts_deploy_root
os.makedirs(children_artifacts_dir)
children_sql = []
for child in self._entity_model.children:
# Create child sub directory
children_artifacts_dir = children_artifacts_dir / sanitize_dir_name(
child.target
)
os.makedirs(children_artifacts_dir)
child_entity: ApplicationPackageChildInterface = action_ctx.get_entity(
child.target
)
child_entity.bundle(children_artifacts_dir)
app_role = (
to_identifier(
child.ensure_usable_by.application_roles.pop() # TODO Support more than one application role
)
if child.ensure_usable_by and child.ensure_usable_by.application_roles
else None
)
child_schema = (
to_identifier(child.identifier.schema_)
if child.identifier and child.identifier.schema_
else None
)
children_sql.append(
child_entity.get_deploy_sql(
artifacts_dir=Path(
self._entity_model.children_artifacts_dir, child.target
),
schema=child_schema,
)
)
if app_role:
children_sql.append(
f"CREATE APPLICATION ROLE IF NOT EXISTS {app_role};"
)
if child_schema:
children_sql.append(
f"GRANT USAGE ON SCHEMA {child_schema} TO APPLICATION ROLE {app_role};"
)
children_sql.append(
child_entity.get_usage_grant_sql(
app_role=app_role, schema=child_schema
)
)
return children_sql

def _deploy(
self,
action_ctx: ActionContext,
Expand Down
8 changes: 8 additions & 0 deletions src/snowflake/cli/_plugins/nativeapp/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,11 @@ def verify_no_directories(paths_to_sync: Iterable[Path]):
def verify_exists(path: Path):
if not path.exists():
raise ClickException(f"The following path does not exist: {path}")


def sanitize_dir_name(dir_name: str) -> str:
"""
Strips non alphanumeric characters, unless listed in the allow list
"""
allowed_chars = [" ", "_"]
return "".join(char for char in dir_name if char in allowed_chars or char.isalnum())
15 changes: 5 additions & 10 deletions src/snowflake/cli/_plugins/streamlit/streamlit_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
StreamlitEntityModel,
)
from snowflake.cli._plugins.workspace.context import ActionContext
from snowflake.cli.api.entities.common import EntityBase
from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import PathMapping

Expand All @@ -23,6 +22,11 @@ class StreamlitEntity(
A Streamlit app.
"""

def __init__(self, *args, **kwargs):
if not FeatureFlag.ENABLE_NATIVE_APP_CHILDREN.is_enabled():
raise NotImplementedError("Streamlit entity is not implemented yet")
super().__init__(*args, **kwargs)

@property
def project_root(self) -> Path:
return self._workspace_ctx.project_root
Expand All @@ -31,21 +35,14 @@ def project_root(self) -> Path:
def deploy_root(self) -> Path:
return self.project_root / "output" / "deploy"

def _verify_feature_flag_enabled(self):
if not FeatureFlag.ENABLE_NATIVE_APP_CHILDREN.is_enabled():
raise NotImplementedError("Streamlit entity is not implemented yet")

def action_bundle(
self,
action_ctx: ActionContext,
*args,
**kwargs,
):
self._verify_feature_flag_enabled()
return self.bundle()

def bundle(self, bundle_root=None):
self._verify_feature_flag_enabled()
return build_bundle(
self.project_root,
bundle_root or self.deploy_root,
Expand All @@ -60,7 +57,6 @@ def get_deploy_sql(
artifacts_dir: Optional[Path] = None,
schema: Optional[str] = None,
):
self._verify_feature_flag_enabled()
entity_id = self.entity_id
if artifacts_dir:
streamlit_name = f"{schema}.{entity_id}" if schema else entity_id
Expand All @@ -69,7 +65,6 @@ def get_deploy_sql(
return f"CREATE OR REPLACE STREAMLIT {entity_id} MAIN_FILE='{self._entity_model.main_file}';"

def get_usage_grant_sql(self, app_role: str, schema: Optional[str] = None):
self._verify_feature_flag_enabled()
entity_id = self.entity_id
streamlit_name = f"{schema}.{entity_id}" if schema else entity_id
return (
Expand Down
4 changes: 2 additions & 2 deletions tests/nativeapp/test_application_package_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
)


def _get_app_pkg_entity(project_directory):
with project_directory("workspaces_simple") as project_root:
def _get_app_pkg_entity(project_directory, test_dir="workspaces_simple"):
with project_directory(test_dir) as project_root:
with Path(project_root / "snowflake.yml").open() as definition_file_path:
project_definition = yaml.safe_load(definition_file_path)
model = ApplicationPackageEntityModel(
Expand Down
69 changes: 56 additions & 13 deletions tests/nativeapp/test_children.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
# Copyright (c) 2024 Snowflake Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations

from pathlib import Path
from textwrap import dedent

import pytest
import yaml
from snowflake.cli._plugins.nativeapp.entities.application_package import (
ApplicationPackageEntityModel,
)
from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag
from snowflake.cli._plugins.streamlit.streamlit_entity import StreamlitEntity
from snowflake.cli._plugins.workspace.context import ActionContext
from snowflake.cli._plugins.workspace.manager import WorkspaceManager
from snowflake.cli.api.project.errors import SchemaValidationError
from snowflake.cli.api.project.schemas.project_definition import (
Expand All @@ -28,6 +20,25 @@
from tests.testing_utils.mock_config import mock_config_key


def _get_app_pkg_entity(project_directory):
with project_directory("napp_children") as project_root:
with Path(project_root / "snowflake.yml").open() as definition_file_path:
project_definition = DefinitionV20(**yaml.safe_load(definition_file_path))
wm = WorkspaceManager(
project_definition=project_definition,
project_root=project_root,
)
pkg_entity = wm.get_entity("pkg")
streamlit_entity = wm.get_entity("my_streamlit")
action_ctx = ActionContext(
get_entity=lambda entity_id: streamlit_entity,
)
return (
pkg_entity,
action_ctx,
)


def test_children_feature_flag_is_disabled():
assert FeatureFlag.ENABLE_NATIVE_APP_CHILDREN.is_enabled() == False
with pytest.raises(AttributeError) as err:
Expand Down Expand Up @@ -106,3 +117,35 @@ def test_valid_children():
child_entity_id = project_definition.entities["pkg"].children[0]
child_entity = wm.get_entity(child_entity_id.target)
assert child_entity.__class__ == StreamlitEntity


def test_children_bundle_with_custom_dir(project_directory):
with mock_config_key("enable_native_app_children", True):
app_pkg, action_ctx = _get_app_pkg_entity(project_directory)
bundle_result = app_pkg.action_bundle(action_ctx)
deploy_root = bundle_result.deploy_root()

# Application package artifacts
assert (deploy_root / "README.md").exists()
assert (deploy_root / "manifest.yml").exists()
assert (deploy_root / "setup_script.sql").exists()

# Child artifacts
assert (
deploy_root / "_entities" / "my_streamlit" / "streamlit_app.py"
).exists()

# Generated setup script section
with open(deploy_root / "setup_script.sql", "r") as f:
setup_script_content = f.read()
assert setup_script_content.endswith(
dedent(
"""
-- AUTO GENERATED CHILDREN SECTION
CREATE OR REPLACE STREAMLIT v_schema.my_streamlit FROM '_entities/my_streamlit' MAIN_FILE='streamlit_app.py';
CREATE APPLICATION ROLE IF NOT EXISTS my_app_role;
GRANT USAGE ON SCHEMA v_schema TO APPLICATION ROLE my_app_role;
GRANT USAGE ON STREAMLIT v_schema.my_streamlit TO APPLICATION ROLE my_app_role;
"""
)
)
53 changes: 53 additions & 0 deletions tests/streamlit/test_streamlit_entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from __future__ import annotations

from pathlib import Path

import pytest
from snowflake.cli._plugins.streamlit.streamlit_entity import (
StreamlitEntity,
)
from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
StreamlitEntityModel,
)
from snowflake.cli._plugins.workspace.context import WorkspaceContext
from snowflake.cli.api.console import cli_console as cc
from snowflake.cli.api.project.definition_manager import DefinitionManager

from tests.testing_utils.mock_config import mock_config_key


def test_cannot_instantiate_without_feature_flag():
with pytest.raises(NotImplementedError) as err:
StreamlitEntity()
assert str(err.value) == "Streamlit entity is not implemented yet"


def test_nativeapp_children_interface(temp_dir):
with mock_config_key("enable_native_app_children", True):
dm = DefinitionManager()
ctx = WorkspaceContext(
console=cc,
project_root=dm.project_root,
get_default_role=lambda: "mock_role",
get_default_warehouse=lambda: "mock_warehouse",
)
main_file = "main.py"
(Path(temp_dir) / main_file).touch()
model = StreamlitEntityModel(
type="streamlit",
main_file=main_file,
artifacts=[main_file],
)
sl = StreamlitEntity(model, ctx)

sl.bundle()
bundle_artifact = Path(temp_dir) / "output" / "deploy" / main_file
deploy_sql_str = sl.get_deploy_sql()
grant_sql_str = sl.get_usage_grant_sql(app_role="app_role")

assert bundle_artifact.exists()
assert deploy_sql_str == "CREATE OR REPLACE STREAMLIT None MAIN_FILE='main.py';"
assert (
grant_sql_str
== "GRANT USAGE ON STREAMLIT None TO APPLICATION ROLE app_role;"
)
1 change: 1 addition & 0 deletions tests/test_data/projects/napp_children/app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# README
7 changes: 7 additions & 0 deletions tests/test_data/projects/napp_children/app/manifest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# This is the v2 version of the napp_init_v1 project

manifest_version: 1

artifacts:
setup_script: setup_script.sql
readme: README.md
3 changes: 3 additions & 0 deletions tests/test_data/projects/napp_children/app/setup_script.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CREATE OR ALTER VERSIONED SCHEMA v_schema;
CREATE APPLICATION ROLE IF NOT EXISTS my_app_role;
GRANT USAGE ON SCHEMA v_schema TO APPLICATION ROLE my_app_role;
Loading

0 comments on commit 5014ad1

Please sign in to comment.