Skip to content

Commit

Permalink
[NADE] Edits to snow app init command for Native Apps (snowflakedb#414)
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-bgoel authored Oct 6, 2023
1 parent ec7a9c2 commit 3dd5af6
Show file tree
Hide file tree
Showing 10 changed files with 36 additions and 209 deletions.
1 change: 1 addition & 0 deletions src/snowcli/cli/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ def generic_render_template(
env = jinja2.Environment(
loader=jinja2.loaders.FileSystemLoader(template_path.parent),
keep_trailing_newline=True,
undefined=jinja2.StrictUndefined,
)
filters = [render_metadata, read_file_content, procedure_from_js_file]
for custom_filter in filters:
Expand Down
14 changes: 9 additions & 5 deletions src/snowcli/cli/nativeapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,24 @@ def app_init(
name: str = typer.Argument(
..., help="Name of the Native Apps project to be initiated."
),
git_url: str = typer.Option(
template_repo: str = typer.Option(
None,
help="A git URL to use as template for the Native Apps project. Example: https://github.com/Snowflake-Labs/native-apps-templates.git for all official Snowflake templates.",
help=f"""A git URL to a template repository, which can be a template itself or contain many templates inside it.
Example: https://github.com/Snowflake-Labs/native-apps-templates.git for all official Snowflake templates.
If using a private Github repo, you may be prompted to enter your Github username and password.
Please use your personal access token in the password prompt, and refer to
https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.""",
),
template: str = typer.Option(
None,
help="A specific directory within the git URL to use as template for the Native Apps project. Example: Default is native-app-basic if --git-url is https://github.com/Snowflake-Labs/native-apps-templates.git, and None if any other --git-url.",
help="A specific template name within the template repo to use as template for the Native Apps project. Example: Default is basic if --template-repo is https://github.com/Snowflake-Labs/native-apps-templates.git, and None if any other --template-repo is specified.",
),
**options,
) -> CommandResult:
"""
Initializes a Native Apps project, optionally with a --git-url and a --template.
Initialize a Native Apps project, optionally with a --template-repo and a --template.
"""
nativeapp_init(name, git_url, template)
nativeapp_init(name, template_repo, template)
return MessageResult(
f"Native Apps project {name} has been created in your local directory."
)
Expand Down
69 changes: 17 additions & 52 deletions src/snowcli/cli/nativeapp/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@


from typing import Optional
from snowcli.cli.project.definition import DEFAULT_USERNAME
from snowcli.cli.project.util import clean_identifier, get_env_username
from snowcli.cli.common.utils import generic_render_template
from snowcli.cli.project.definition_manager import DefinitionManager


log = logging.getLogger(__name__)

SNOWFLAKELABS_GITHUB_URL = "https://github.com/Snowflake-Labs/native-apps-templates"
BASIC_TEMPLATE = "native-apps-basic"
BASIC_TEMPLATE = "basic"

# Based on first two rules for unquoted object identifier: https://docs.snowflake.com/en/sql-reference/identifiers-syntax
PROJECT_NAME_REGEX = r"(^[a-zA-Z_])([a-zA-Z0-9_$]{0,254})"
Expand Down Expand Up @@ -108,38 +106,6 @@ def render_snowflake_yml(parent_to_snowflake_yml: Path):
raise RenderingFromJinjaError(snowflake_yml_jinja)


def render_nativeapp_readme(parent_to_readme: Path, project_name: str):
"""
Create a README.yml file from a jinja template at a given path.
Args:
parent_to_readme (Path): The parent directory of README.md.jinja, and later README.md
Returns:
None
"""

readme_jinja = "README.md.jinja"

default_application_name_prefix = clean_identifier(project_name)
default_application_name_suffix = clean_identifier(
get_env_username() or DEFAULT_USERNAME
)

try:
generic_render_template(
template_path=parent_to_readme / readme_jinja,
data={
"application_name": f"{default_application_name_prefix}_{default_application_name_suffix}"
},
output_file_path=parent_to_readme / "README.md",
)
os.remove(parent_to_readme / readme_jinja)
except Exception as err:
log.error(err)
raise RenderingFromJinjaError(readme_jinja)


def replace_snowflake_yml_name_with_project(target_directory: Path):
"""
Replace the native_app schema's "name" field in a snowflake.yml file with its parent directory name, i.e. the native app project, as the default start.
Expand All @@ -158,16 +124,21 @@ def replace_snowflake_yml_name_with_project(target_directory: Path):
with open(path_to_snowflake_yml) as f:
contents = load(f.read()).data

if "native_app" in contents and "name" in contents["native_app"]:
contents["native_app"]["name"] = target_directory.name
project_name = target_directory.name
if (
("native_app" in contents)
and ("name" in contents["native_app"])
and (contents["native_app"]["name"] != project_name)
):
contents["native_app"]["name"] = project_name
with open(path_to_snowflake_yml, "w") as f:
f.write(as_document(contents).as_yaml())
# If there are no such keys, the Definition Manager will catch that during validation


def validate_and_update_snowflake_yml(target_directory: Path):
"""
Update the native_app name key in the snowflake.yml file and perform validation on the entire file.
This step is useful when cloning from a non-Snowflake template repo which may directly have a snowflake.yml file.
Args:
target_directory (str): The directory containing snowflake.yml at its root.
Expand All @@ -178,11 +149,10 @@ def validate_and_update_snowflake_yml(target_directory: Path):
# 1. Determine if a snowflake.yml file exists, at the very least
definition_manager = DefinitionManager(target_directory)

# 2. Change the project name in snowflake.yml to project_name
# 2. Change the project name in snowflake.yml to project_name if not already assigned to project_name
replace_snowflake_yml_name_with_project(target_directory=target_directory)

# 3. Validate the Project Definition File(s)
# We do not need to use the result of the call below, we just want to validate the file
definition_manager.project_definition


Expand Down Expand Up @@ -220,9 +190,11 @@ def _init_with_url_and_no_template(
# Remove all git history
rmtree(target_directory.joinpath(".git").resolve())

# Non-Snowflake git URLs should not have jinja files in their directory structure.
# If they do, snowCLI is not responsible for rendering them during init.
# If the SNOWFLAKELABS_GITHUB_URL is provided here, rendering is additionally skipped as there is no jinja file at the root.
# Non-Snowflake git URLs may have jinja files in their directory structure, but only with one variable: {{project_name}}.
# snowCLI will throw an error during rendering if it has other variables because we do not expose a way to provide the
# values to those variables through command line, and hence will not be able to fully render the file.
if Path.exists(target_directory / "snowflake.yml.jinja"):
render_snowflake_yml(parent_to_snowflake_yml=target_directory)

# If not an official Snowflake Native App template
if git_url != SNOWFLAKELABS_GITHUB_URL:
Expand Down Expand Up @@ -272,18 +244,11 @@ def _init_with_url_and_template(
)

path_to_project = current_working_directory / project_name
# Rendering should be conditinal on the below combination,
# as right now, we only allow rendering of a jinja file within this combination
if (git_url == SNOWFLAKELABS_GITHUB_URL) and (template == BASIC_TEMPLATE):

if Path.exists(path_to_project / "snowflake.yml.jinja"):
# Render snowflake.yml file from its jinja template
render_snowflake_yml(parent_to_snowflake_yml=path_to_project)

# Render README.md file from its jinja template
render_nativeapp_readme(
parent_to_readme=path_to_project / "app",
project_name=project_name,
)

# If not an official Snowflake Native App template
if git_url != SNOWFLAKELABS_GITHUB_URL:
validate_and_update_snowflake_yml(target_directory=path_to_project)
Expand Down
1 change: 0 additions & 1 deletion src/templates/default_nativeapp/.gitignore

This file was deleted.

30 changes: 0 additions & 30 deletions src/templates/default_nativeapp/README.md

This file was deleted.

22 changes: 0 additions & 22 deletions src/templates/default_nativeapp/app/README.md.jinja

This file was deleted.

9 changes: 0 additions & 9 deletions src/templates/default_nativeapp/app/manifest.yml

This file was deleted.

48 changes: 0 additions & 48 deletions src/templates/default_nativeapp/app/setup_script.sql

This file was deleted.

9 changes: 0 additions & 9 deletions src/templates/default_nativeapp/snowflake.yml.jinja

This file was deleted.

42 changes: 9 additions & 33 deletions tests/nativeapp/test_init.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from snowcli.cli.nativeapp.init import (
is_valid_project_name,
render_snowflake_yml,
render_nativeapp_readme,
nativeapp_init,
replace_snowflake_yml_name_with_project,
validate_and_update_snowflake_yml,
Expand All @@ -11,6 +10,7 @@
DirectoryAlreadyExistsError,
InitError,
ProjectNameInvalidError,
RenderingFromJinjaError,
)
from snowcli.exception import MissingConfiguration
from tests.testing_utils.fixtures import *
Expand Down Expand Up @@ -81,40 +81,24 @@ def test_render_snowflake_yml(other_directory):
assert temp_dir.joinpath("snowflake.yml").read_text() == expected


@mock.patch("os.getenv", return_value="pytest_user")
def test_render_nativeapp_readme(mock_get_env_username, other_directory):
def test_render_snowflake_yml_raises_exception(other_directory):
temp_dir = Path(other_directory)
create_named_file(
file_name="README.md.jinja",
file_name="snowflake.yml.jinja",
dir=temp_dir,
contents=[
dedent(
"""\
### Calling a function
SELECT {{application_name}}.versioned_schema.hello_world();
which should output 'hello world!'
```
SELECT {{application_name}}.versioned_schema.hello_world();
```
native_app:
name: {{project_name}}
artifacts:
- {{one_more_variable}}
"""
)
],
)
expected = dedent(
"""\
### Calling a function
SELECT random_project_pytest_user.versioned_schema.hello_world();
which should output 'hello world!'
```
SELECT random_project_pytest_user.versioned_schema.hello_world();
```
"""
)
render_nativeapp_readme(temp_dir, "random_project")
assert Path.exists(temp_dir / "README.md")
assert not Path.exists(temp_dir / "README.md.jinja")
assert temp_dir.joinpath("README.md").read_text() == expected
with pytest.raises(RenderingFromJinjaError):
render_snowflake_yml(temp_dir)


def test_replace_snowflake_yml_name_with_project_populated_file(other_directory):
Expand Down Expand Up @@ -335,13 +319,6 @@ def test_init_with_url_and_template_w_native_app_url_and_template(
"""
)

create_named_file(
file_name="README.md.jinja",
dir=current_working_directory / fake_repo / "app",
contents=[dedent("{{application_name}}")],
)
expected_readme = dedent("fake_repo_pytest_user\n")

_init_with_url_and_template(
current_working_directory=Path.cwd(),
project_name=fake_repo,
Expand All @@ -355,7 +332,6 @@ def test_init_with_url_and_template_w_native_app_url_and_template(
assert (
fake_repo_path.joinpath("snowflake.yml").read_text() == expected_snowflake_yml
)
assert fake_repo_path.joinpath("app", "README.md").read_text() == expected_readme


@mock.patch("snowcli.cli.nativeapp.init.Repo.clone_from", side_effect=None)
Expand Down

0 comments on commit 3dd5af6

Please sign in to comment.