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

[SNOW-1039218] feat: add hidden app list-templates command #887

Merged
merged 17 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
73d7c32
[SNOW-1039218] feat: add hidden app list-templates command
sfc-gh-mchok Mar 8, 2024
761abaf
Merge branch 'main' into mchok-SNOW-1039218-app-list-templates
sfc-gh-mchok Mar 8, 2024
6e8aa60
[SNOW-1039218] feat: factor out shallow clone and add None fallback t…
sfc-gh-mchok Mar 8, 2024
eb1c588
[SNOW-1039218] refactor: change union typing for version support
sfc-gh-mchok Mar 9, 2024
6482d3c
[SNOW-1039218] test: adjust test parameters with dedent
sfc-gh-mchok Mar 9, 2024
37c6e78
[SNOW-1039218] fix: only import git locally
sfc-gh-mchok Mar 9, 2024
b038622
[SNOW-1039218] fix: PathLike not subscriptable
sfc-gh-mchok Mar 9, 2024
6c7e108
Merge branch 'main' into mchok-SNOW-1039218-app-list-templates
sfc-gh-mchok Mar 11, 2024
a108901
[SNOW-1039218] feat: raise Exception when file not found
sfc-gh-mchok Mar 11, 2024
4d2f2ad
Merge branch 'main' into mchok-SNOW-1039218-app-list-templates
sfc-gh-mchok Mar 11, 2024
628ecf5
Merge branch 'main' into mchok-SNOW-1039218-app-list-templates
sfc-gh-mchok Mar 12, 2024
34f4126
[SNOW-1039218] docs: clarify only official templates for command
sfc-gh-mchok Mar 12, 2024
3ef5408
[SNOW-1039218] test: update snapshot for new help message
sfc-gh-mchok Mar 12, 2024
5433c9f
Merge branch 'main' into mchok-SNOW-1039218-app-list-templates
sfc-gh-mchok Mar 12, 2024
f468f39
Merge branch 'main' into mchok-SNOW-1039218-app-list-templates
sfc-gh-mchok Mar 13, 2024
8147cd1
Merge branch 'main' into mchok-SNOW-1039218-app-list-templates
sfc-gh-mchok Mar 13, 2024
1f475c6
Merge branch 'main' into mchok-SNOW-1039218-app-list-templates
sfc-gh-mchok Mar 14, 2024
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
51 changes: 48 additions & 3 deletions src/snowflake/cli/plugins/nativeapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,17 @@
with_project_definition,
)
from snowflake.cli.api.commands.snow_typer import SnowTyper
from snowflake.cli.api.output.types import CommandResult, MessageResult
from snowflake.cli.api.output.types import (
CollectionResult,
CommandResult,
MessageResult,
)
from snowflake.cli.api.secure_path import SecurePath
from snowflake.cli.plugins.nativeapp.common_flags import ForceOption, InteractiveOption
from snowflake.cli.plugins.nativeapp.init import nativeapp_init
from snowflake.cli.plugins.nativeapp.init import (
OFFICIAL_TEMPLATES_GITHUB_URL,
nativeapp_init,
)
from snowflake.cli.plugins.nativeapp.manager import NativeAppManager
from snowflake.cli.plugins.nativeapp.policy import (
AllowAlwaysPolicy,
Expand All @@ -20,7 +28,11 @@
from snowflake.cli.plugins.nativeapp.teardown_processor import (
NativeAppTeardownProcessor,
)
from snowflake.cli.plugins.nativeapp.utils import is_tty_interactive
from snowflake.cli.plugins.nativeapp.utils import (
get_first_paragraph_from_markdown_file,
is_tty_interactive,
shallow_git_clone,
)
from snowflake.cli.plugins.nativeapp.version.commands import app as versions_app

app = SnowTyper(
Expand Down Expand Up @@ -69,6 +81,39 @@ def app_init(
)


@app.command("list-templates", hidden=True)
Copy link
Contributor

Choose a reason for hiding this comment

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

Today we have feature flags. Have you consider using them?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I believe this was clarified offline, please see the slack thread of the PR for the full discussion, but to summarize:

  • For now, we intend to make it available for our IDE plugin, but not expose directly it to users
  • We don't want users to have to enable a feature flag before the IDE plugin can work
  • It doesn't fit the use case of an experimental flag since this command doesn't change the behaviour of any existing commands nor is it interacting with Snowflake

Please let me know if this works for you :)

def app_list_templates(**options) -> CommandResult:
"""
Prints information regarding the official templates that can be used with snow app init.
"""
with SecurePath.temporary_directory() as temp_path:
repo = shallow_git_clone(OFFICIAL_TEMPLATES_GITHUB_URL, temp_path.path)

# Mark a directory as a template if a project definition jinja template is inside
template_directories = [
entry.name
for entry in repo.head.commit.tree
if (temp_path / entry.name / "snowflake.yml.jinja").exists()
]

# get the template descriptions from the README.md in its directory
template_descriptions = [
get_first_paragraph_from_markdown_file(
(temp_path / directory / "README.md").path
Copy link
Contributor

Choose a reason for hiding this comment

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

Right now, this will fail if any templates are created that don't have top-level README.md files. I suggest we provide a fallback to None both in this case and when there is no paragraph text in the readme

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good callout - added a test for it as well, thanks!

)
for directory in template_directories
]

result = (
{"template": directory, "description": description}
Copy link
Contributor

Choose a reason for hiding this comment

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

I really like the idea of a little description along with template names!

for directory, description in zip(
template_directories, template_descriptions
)
)

return CollectionResult(result)


@app.command("bundle", hidden=True)
@with_project_definition("native_app")
def app_bundle(
Expand Down
12 changes: 2 additions & 10 deletions src/snowflake/cli/plugins/nativeapp/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,18 +222,10 @@ def _init_from_template(

try:
with SecurePath.temporary_directory() as temp_path:
from git import Repo
from git import rmtree as git_rmtree
from snowflake.cli.plugins.nativeapp.utils import shallow_git_clone

# Clone the repository in the temporary directory with options.
repo = Repo.clone_from(
url=git_url,
to_path=temp_path.path,
filter=["tree:0"],
depth=1,
)
# Close repo to avoid issues with permissions on Windows
repo.close()
shallow_git_clone(git_url, temp_path.path)

if use_whole_repo_as_template:
# the template is the entire git repository
Expand Down
58 changes: 58 additions & 0 deletions src/snowflake/cli/plugins/nativeapp/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from os import PathLike
from pathlib import Path
from sys import stdin, stdout
from typing import Optional, Union


def needs_confirmation(needs_confirm: bool, auto_yes: bool) -> bool:
Expand All @@ -7,3 +10,58 @@ def needs_confirmation(needs_confirm: bool, auto_yes: bool) -> bool:

def is_tty_interactive():
return stdin.isatty() and stdout.isatty()


def get_first_paragraph_from_markdown_file(file_path: Path) -> Optional[str]:
"""
Reads a Markdown file at the given file path and finds the first paragraph

Parameters:
file_path (Path): Path to Markdown file

Returns:
Optional[str]: the first paragraph as a string, or None
if no paragraph could be found

Raises:
FileNotFoundError: if file_path to Markdown file does not exist
"""
if not file_path.exists():
raise FileNotFoundError(file_path)

with open(file_path, "r") as markdown_file:
paragraph_text = None

for line in markdown_file:
stripped_line = line.strip()
if not stripped_line.startswith("#") and stripped_line:
paragraph_text = stripped_line
break

return paragraph_text


def shallow_git_clone(url: Union[str, PathLike], to_path: Union[str, PathLike]):
"""
Performs a shallow clone of the repository at the provided url to the path specified

Parameters:
url (str | PathLike): Valid git url.
to_path (str | PathLike): Path to which the repository should be cloned to.

Returns:
Repo: the repository that was cloned
"""
from git import Repo

# Clone the repository in the directory with options.
repo = Repo.clone_from(
url=url,
to_path=to_path,
filter=["tree:0"],
depth=1,
)
# Close repo to avoid issues with permissions on Windows
repo.close()

return repo
25 changes: 25 additions & 0 deletions tests/__snapshots__/test_help_messages.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,31 @@
╰──────────────────────────────────────────────────────────────────────────────╯


'''
# ---
# name: test_help_messages[app.list-templates]
'''

Usage: default app list-templates [OPTIONS]

Prints information regarding the official templates that can be used with snow
app init.

╭─ 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. │
╰──────────────────────────────────────────────────────────────────────────────╯


'''
# ---
# name: test_help_messages[app.open]
Expand Down
25 changes: 25 additions & 0 deletions tests/nativeapp/__snapshots__/test_commands.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -358,3 +358,28 @@

'''
# ---
# name: test_list_templates_no_options_success
'''
+------------------------------------------------------------------------------+
| template | description |
|------------------+-----------------------------------------------------------|
| basic | This is the basic project template for a Snowflake Native |
| | App project. It contains minimal code meant to help you |
| | set up your first application object in your account |
| | quickly. |
| streamlit-java | This is an example template for a Snowflake Native App |
| | project which demonstrates the use of Java extension code |
| | and adding Streamlit code. This template is meant to |
| | guide developers towards a possible project structure on |
| | the basis of functionality, as well as to indicate the |
| | contents of some common and useful files. |
| streamlit-python | This is an example template for a Snowflake Native App |
| | project which demonstrates the use of Python extension |
| | code and adding Streamlit code. This template is meant to |
| | guide developers towards a possible project structure on |
| | the basis of functionality, as well as to indicate the |
| | contents of some common and useful files. |
+------------------------------------------------------------------------------+

'''
# ---
8 changes: 8 additions & 0 deletions tests/nativeapp/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,11 @@ def test_init_no_template_failure(

assert result.exit_code == 1
assert result.output == snapshot


def test_list_templates_no_options_success(runner, temp_dir, snapshot):
args = ["app", "list-templates"]
result = runner.invoke(args)

assert result.exit_code == 0
assert result.output == snapshot
77 changes: 77 additions & 0 deletions tests/nativeapp/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from textwrap import dedent

import pytest
from snowflake.cli.api.secure_path import SecurePath
from snowflake.cli.plugins.nativeapp.utils import get_first_paragraph_from_markdown_file


@pytest.mark.parametrize(
"file_content, expected_paragraph",
[
(
dedent(
"""
## Introduction

This is an example template for a Snowflake Native App project which demonstrates the use of Python extension code and adding Streamlit code. This template is meant to guide developers towards a possible project structure on the basis of functionality, as well as to indicate the contents of some common and useful files.

Since this template contains Python files only, you do not need to perform any additional steps to build the source code. You can directly go to the next section. However, if there were any source code that needed to be built, you must manually perform the build steps here before proceeding to the next section.

Similarly, you can also use your own build steps for any other languages supported by Snowflake that you wish to write your code in. For more information on supported languages, visit [docs](https://docs.snowflake.com/en/developer-guide/stored-procedures-vs-udfs#label-sp-udf-languages).
"""
),
"This is an example template for a Snowflake Native App project which demonstrates the use of Python extension code and adding Streamlit code. This template is meant to guide developers towards a possible project structure on the basis of functionality, as well as to indicate the contents of some common and useful files.",
),
(
"Similarly, you can also use your own build steps for any other languages supported by Snowflake that you wish to write your code in. For more information on supported languages, visit [docs](https://docs.snowflake.com/en/developer-guide/stored-procedures-vs-udfs#label-sp-udf-languages).",
"Similarly, you can also use your own build steps for any other languages supported by Snowflake that you wish to write your code in. For more information on supported languages, visit [docs](https://docs.snowflake.com/en/developer-guide/stored-procedures-vs-udfs#label-sp-udf-languages).",
),
],
)
def test_get_first_paragraph_from_markdown_file_with_valid_path_and_paragraph_content(
file_content, expected_paragraph
):
with SecurePath.temporary_directory() as temp_path:
temp_readme_path = (temp_path / "README.md").path

with open(temp_readme_path, "w+") as temp_readme_file:
temp_readme_file.write(file_content)

actual_paragraph = get_first_paragraph_from_markdown_file(temp_readme_path)
sfc-gh-cgorrie marked this conversation as resolved.
Show resolved Hide resolved

assert actual_paragraph == expected_paragraph


@pytest.mark.parametrize(
"file_content",
[
dedent(
"""
# Just some Headings

## And some whitespace.
"""
),
"",
],
)
def test_get_first_paragraph_from_markdown_file_with_valid_path_and_no_paragraph_content(
file_content,
):
with SecurePath.temporary_directory() as temp_path:
temp_readme_path = (temp_path / "README.md").path

with open(temp_readme_path, "w+") as temp_readme_file:
temp_readme_file.write(file_content)

result = get_first_paragraph_from_markdown_file(temp_readme_path)

assert result is None


def test_get_first_paragraph_from_markdown_file_with_invalid_path():
with SecurePath.temporary_directory() as temp_path:
temp_readme_path = (temp_path / "README.md").path

with pytest.raises(FileNotFoundError):
get_first_paragraph_from_markdown_file(temp_readme_path)
Loading