Skip to content

Commit

Permalink
Merge branch 'main' into jsikorski/stop_workflows_after_new_commit
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-jsikorski committed Mar 6, 2024
2 parents 364f2e5 + 22949d8 commit 6884780
Show file tree
Hide file tree
Showing 44 changed files with 996 additions and 321 deletions.
10 changes: 5 additions & 5 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
* @sfc-gh-turbaszek @sfc-gh-pjob @sfc-gh-jsikorski @sfc-gh-astus @sfc-gh-mraba @sfc-gh-pczajka

# Native Apps Owners
src/snowflake/cli/plugins/nativeapp/ @sfc-gh-bgoel @sfc-gh-cgorrie @sfc-gh-bdufour @sfc-gh-melnacouzi
tests/nativeapp/ @sfc-gh-bgoel @sfc-gh-cgorrie @sfc-gh-bdufour @sfc-gh-melnacouzi
tests_integration/test_nativeapp.py @sfc-gh-bgoel @sfc-gh-cgorrie @sfc-gh-bdufour @sfc-gh-melnacouzi
src/snowflake/cli/plugins/nativeapp/ @snowflakedb/nade
tests/nativeapp/ @sfc-gh-bgoel @snowflakedb/nade
tests_integration/test_nativeapp.py @snowflakedb/nade

# Project Definition Owners
src/snowflake/cli/api/project/schemas/native_app.py @sfc-gh-bgoel @sfc-gh-cgorrie @sfc-gh-bdufour @sfc-gh-melnacouzi
tests/project/ @sfc-gh-turbaszek @sfc-gh-pjob @sfc-gh-jsikorski @sfc-gh-astus @sfc-gh-mraba @sfc-gh-pczajka @sfc-gh-bgoel @sfc-gh-cgorrie @sfc-gh-bdufour @sfc-gh-melnacouzi
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
29 changes: 29 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,32 @@ repos:
hooks:
- id: mypy
additional_dependencies: [types-all]
- repo: local
hooks:
- id: check-print-in-code
language: pygrep
name: "Check for print statements"
entry: "print\\(|echo\\("
pass_filenames: true
files: ^src/snowflake/.*\.py$
exclude: >
(?x)
^src/snowflake/cli/api/console/.*$|
^src/snowflake/cli/app/printing.py$|
^src/snowflake/cli/app/dev/.*$|
^src/snowflake/cli/templates/.*$|
^src/snowflake/cli/api/utils/rendering.py$|
^src/snowflake/cli/plugins/spcs/common.py$
- id: check-app-imports-in-api
language: pygrep
name: "No top level cli.app imports in cli.api"
entry: "^from snowflake\\.cli\\.app"
pass_filenames: true
files: ^src/snowflake/cli/api/.*\.py$
- id: avoid-snowcli
language: pygrep
name: "Prefer snowflake CLI over snowcli"
entry: "snowcli"
pass_filenames: true
files: ^src/.*\.py$
exclude: ^src/snowflake/cli/app/telemetry.py$
2 changes: 2 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

## New additions
* Added support for fully qualified name (`database.schema.name`) in `name` parameter in streamlit project definition
* Added support for fully qualified image repository names in `spcs image-repository` commands.
* Added `--if-not-exists` option to `create` commands for `service`, and `compute-pool`. Added `--replace` and `--if-not-exists` options for `image-repository create`.

## Fixes and improvements
* Adding `--image-name` option for image name argument in `spcs image-repository list-tags` for consistency with other commands.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ dependencies = [
"setuptools==69.1.1",
"snowflake-connector-python[secure-local-storage]==3.7.1",
"strictyaml==1.7.3",
"tomlkit==0.12.4",
"tomlkit==0.12.3",
"typer==0.9.0",
"urllib3>=1.21.1,<2.3",
"GitPython==3.1.42",
Expand Down
159 changes: 129 additions & 30 deletions src/snowflake/cli/api/commands/flags.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from __future__ import annotations

from typing import Any, Callable, Optional
from inspect import signature
from typing import Any, Callable, List, Optional, Tuple

import click
import typer
from click import ClickException
from snowflake.cli.api.cli_global_context import cli_context_manager
from snowflake.cli.api.console import cli_console
from snowflake.cli.api.output.formats import OutputFormat

DEFAULT_CONTEXT_SETTINGS = {"help_option_names": ["--help", "-h"]}
Expand All @@ -13,6 +16,103 @@
_CLI_BEHAVIOUR = "Global configuration"


class OverrideableOption:
"""
Class that allows you to generate instances of typer.models.OptionInfo with some default properties while allowing
specific values to be overriden.
Custom parameters:
- mutually_exclusive (Tuple[str]|List[str]): A list of parameter names that this Option is not compatible with. If this Option has
a truthy value and any of the other parameters in the mutually_exclusive list has a truthy value, a
ClickException will be thrown. Note that mutually_exclusive can contain an option's own name but does not require
it.
"""

def __init__(
self,
default: Any,
*param_decls: str,
mutually_exclusive: Optional[List[str] | Tuple[str]] = None,
**kwargs,
):
self.default = default
self.param_decls = param_decls
self.mutually_exclusive = mutually_exclusive
self.kwargs = kwargs

def __call__(self, **kwargs) -> typer.models.OptionInfo:
"""
Returns a typer.models.OptionInfo instance initialized with the specified default values along with any overrides
from kwargs. Note that if you are overriding param_decls, you must pass an iterable of strings, you cannot use
positional arguments like you can with typer.Option. Does not modify the original instance.
"""
default = kwargs.get("default", self.default)
param_decls = kwargs.get("param_decls", self.param_decls)
mutually_exclusive = kwargs.get("mutually_exclusive", self.mutually_exclusive)
if not isinstance(param_decls, list) and not isinstance(param_decls, tuple):
raise TypeError("param_decls must be a list or tuple")
passed_kwargs = self.kwargs.copy()
passed_kwargs.update(kwargs)
if passed_kwargs.get("callback", None) or mutually_exclusive:
passed_kwargs["callback"] = self._callback_factory(
passed_kwargs.get("callback", None), mutually_exclusive
)
for non_kwarg in ["default", "param_decls", "mutually_exclusive"]:
passed_kwargs.pop(non_kwarg, None)
return typer.Option(default, *param_decls, **passed_kwargs)

class InvalidCallbackSignature(ClickException):
def __init__(self, callback):
super().__init__(
f"Signature {signature(callback)} is not valid for an OverrideableOption callback function. Must have at most one parameter with each of the following types: (typer.Context, typer.CallbackParam, Any Other Type)"
)

def _callback_factory(
self, callback, mutually_exclusive: Optional[List[str] | Tuple[str]]
):
callback = callback if callback else lambda x: x

# inspect existing_callback to make sure signature is valid
existing_params = signature(callback).parameters
# at most one parameter with each type in [typer.Context, typer.CallbackParam, any other type]
limits = [
lambda x: x == typer.Context,
lambda x: x == typer.CallbackParam,
lambda x: x != typer.Context and x != typer.CallbackParam,
]
for limit in limits:
if len([v for v in existing_params.values() if limit(v.annotation)]) > 1:
raise self.InvalidCallbackSignature(callback)

def generated_callback(ctx: typer.Context, param: typer.CallbackParam, value):
if mutually_exclusive:
for name in mutually_exclusive:
if value and ctx.params.get(
name, False
): # if the current parameter is set to True and a previous parameter is also Truthy
curr_opt = param.opts[0]
other_opt = [x for x in ctx.command.params if x.name == name][
0
].opts[0]
raise click.ClickException(
f"Options '{curr_opt}' and '{other_opt}' are incompatible."
)

# pass args to existing callback based on its signature (this is how Typer infers callback args)
passed_params = {}
for existing_param in existing_params:
annotation = existing_params[existing_param].annotation
if annotation == typer.Context:
passed_params[existing_param] = ctx
elif annotation == typer.CallbackParam:
passed_params[existing_param] = param
else:
passed_params[existing_param] = value
return callback(**passed_params)

return generated_callback


def _callback(provide_setter: Callable[[], Callable[[Any], Any]]):
def callback(value):
set_value = provide_setter()
Expand Down Expand Up @@ -73,7 +173,7 @@ def callback(value):

def _password_callback(value: str):
if value:
click.echo(PLAIN_PASSWORD_MSG)
cli_console.message(PLAIN_PASSWORD_MSG)

return _callback(lambda: cli_context_manager.connection_context.set_password)(value)

Expand Down Expand Up @@ -181,6 +281,7 @@ def _password_callback(value: str):
callback=_callback(lambda: cli_context_manager.set_silent),
is_flag=True,
rich_help_panel=_CLI_BEHAVIOUR,
is_eager=True,
)

VerboseOption = typer.Option(
Expand Down Expand Up @@ -209,6 +310,32 @@ def _password_callback(value: str):
help='Regular expression for filtering objects by name. For example, `list --like "my%"` lists all objects that begin with “my”.',
)

# If IfExistsOption, IfNotExistsOption, or ReplaceOption are used with names other than those in CREATE_MODE_OPTION_NAMES,
# you must also override mutually_exclusive if you want to retain the validation that at most one of these flags is
# passed.
CREATE_MODE_OPTION_NAMES = ["if_exists", "if_not_exists", "replace"]

IfExistsOption = OverrideableOption(
False,
"--if-exists",
help="Only apply this operation if the specified object exists.",
mutually_exclusive=CREATE_MODE_OPTION_NAMES,
)

IfNotExistsOption = OverrideableOption(
False,
"--if-not-exists",
help="Only apply this operation if the specified object does not already exist.",
mutually_exclusive=CREATE_MODE_OPTION_NAMES,
)

ReplaceOption = OverrideableOption(
False,
"--replace",
help="Replace this object if it already exists.",
mutually_exclusive=CREATE_MODE_OPTION_NAMES,
)


def experimental_option(
experimental_behaviour_description: Optional[str] = None,
Expand Down Expand Up @@ -268,31 +395,3 @@ def _callback(project_path: Optional[str]):
callback=_callback,
show_default=False,
)


class OverrideableOption:
"""
Class that allows you to generate instances of typer.models.OptionInfo with some default properties while allowing specific values to be overriden.
"""

def __init__(self, default: Any, *param_decls: str, **kwargs):
self.default = default
self.param_decls = param_decls
self.kwargs = kwargs

def __call__(self, **kwargs) -> typer.models.OptionInfo:
"""
Returns a typer.models.OptionInfo instance initialized with the specified default values along with any overrides
from kwargs.Note that if you are overriding param_decls,
you must pass an iterable of strings, you cannot use positional arguments like you can with typer.Option.
Does not modify the original instance.
"""
default = kwargs.get("default", self.default)
param_decls = kwargs.get("param_decls", self.param_decls)
if not isinstance(param_decls, list) and not isinstance(param_decls, tuple):
raise TypeError("param_decls must be a list or tuple")
passed_kwargs = self.kwargs.copy()
passed_kwargs.update(kwargs)
passed_kwargs.pop("default", None)
passed_kwargs.pop("param_decls", None)
return typer.Option(default, *param_decls, **passed_kwargs)
8 changes: 6 additions & 2 deletions src/snowflake/cli/api/commands/snow_typer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
from snowflake.cli.api.commands.flags import DEFAULT_CONTEXT_SETTINGS
from snowflake.cli.api.exceptions import CommandReturnTypeError
from snowflake.cli.api.output.types import CommandResult
from snowflake.cli.app.printing import print_result
from snowflake.cli.app.telemetry import flush_telemetry, log_command_usage

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -73,12 +71,16 @@ def pre_execute():
Pay attention to make this method safe to use if performed operations are not necessary
for executing the command in proper way.
"""
from snowflake.cli.app.telemetry import log_command_usage

log.debug("Executing command pre execution callback")
log_command_usage()

@staticmethod
def process_result(result):
"""Command result processor"""
from snowflake.cli.app.printing import print_result

# Because we still have commands like "logs" that do not return anything.
# We should improve it in future.
if not result:
Expand All @@ -100,5 +102,7 @@ def post_execute():
Callback executed after running any command callable. Pay attention to make this method safe to
use if performed operations are not necessary for executing the command in proper way.
"""
from snowflake.cli.app.telemetry import flush_telemetry

log.debug("Executing command post execution callback")
flush_telemetry()
2 changes: 1 addition & 1 deletion src/snowflake/cli/api/secure_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ def temporary_directory(cls):
Works similarly to tempfile.TemporaryDirectory
"""
with tempfile.TemporaryDirectory(prefix="snowcli") as tmpdir:
with tempfile.TemporaryDirectory(prefix="snowflake-cli") as tmpdir:
log.info("Created temporary directory %s", tmpdir)
yield SecurePath(tmpdir)
log.info("Removing temporary directory %s", tmpdir)
Expand Down
Loading

0 comments on commit 6884780

Please sign in to comment.