diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 3ad969422..b731d7005 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -24,6 +24,20 @@ ## Fixes and improvements +# v2.8.1 +## Backward incompatibility + +## Deprecations + +## New additions + +## Fixes and improvements +* Fixed git execute not working with upper case in directory name. +* Fixed `snow git setup` command behaviour for fully qualified repository names. +* Fixed `snow git setup` command behaviour in case API integration or secret with default name already exists. +* Fixed `snow snowpark package create` creating empty zip when package name contained capital letters. + + # v2.8.0 ## Backward incompatibility diff --git a/src/snowflake/cli/__about__.py b/src/snowflake/cli/__about__.py index 9441c99a8..1b9546082 100644 --- a/src/snowflake/cli/__about__.py +++ b/src/snowflake/cli/__about__.py @@ -14,4 +14,4 @@ from __future__ import annotations -VERSION = "2.8.0rc1" +VERSION = "2.8.1" diff --git a/src/snowflake/cli/api/commands/flags.py b/src/snowflake/cli/api/commands/flags.py index 1ba564e75..fa115cdbc 100644 --- a/src/snowflake/cli/api/commands/flags.py +++ b/src/snowflake/cli/api/commands/flags.py @@ -28,6 +28,7 @@ from snowflake.cli.api.commands.typer_pre_execute import register_pre_execute_command from snowflake.cli.api.console import cli_console from snowflake.cli.api.exceptions import MissingConfiguration +from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.output.formats import OutputFormat from snowflake.cli.api.project.definition_manager import DefinitionManager from snowflake.cli.api.rendering.jinja import CONTEXT_KEY @@ -350,13 +351,23 @@ def _password_callback(value: str): rich_help_panel=_CONNECTION_SECTION, ) +# Set default via callback to avoid including tempdir path in generated docs (snow --docs). +# Use constant instead of None, as None is removed from telemetry data. +_DIAG_LOG_DEFAULT_VALUE = "" + + +def _diag_log_path_callback(path: str): + if path == _DIAG_LOG_DEFAULT_VALUE: + path = tempfile.gettempdir() + cli_context_manager.connection_context.set_diag_log_path(Path(path)) + return path + + DiagLogPathOption: Path = typer.Option( tempfile.gettempdir(), "--diag-log-path", help="Diagnostic report path", - callback=_callback( - lambda: cli_context_manager.connection_context.set_diag_log_path - ), + callback=_diag_log_path_callback, show_default=False, rich_help_panel=_CONNECTION_SECTION, exists=True, @@ -514,11 +525,15 @@ def experimental_option( ) -def identifier_argument(sf_object: str, example: str) -> typer.Argument: +def identifier_argument( + sf_object: str, example: str, callback: Callable | None = None +) -> typer.Argument: return typer.Argument( ..., help=f"Identifier of the {sf_object}. For example: {example}", show_default=False, + click_type=IdentifierType(), + callback=callback, ) @@ -638,3 +653,10 @@ def parse_key_value_variables(variables: Optional[List[str]]) -> List[Variable]: key, value = p.split("=", 1) result.append(Variable(key.strip(), value.strip())) return result + + +class IdentifierType(click.ParamType): + name = "TEXT" + + def convert(self, value, param, ctx): + return FQN.from_string(value) diff --git a/src/snowflake/cli/api/identifiers.py b/src/snowflake/cli/api/identifiers.py index 886cbc72e..6cbeaee41 100644 --- a/src/snowflake/cli/api/identifiers.py +++ b/src/snowflake/cli/api/identifiers.py @@ -35,10 +35,17 @@ class FQN: fqn = FQN.from_string("my_name").set_database("db").set_schema("foo") """ - def __init__(self, database: str | None, schema: str | None, name: str): + def __init__( + self, + database: str | None, + schema: str | None, + name: str, + signature: str | None = None, + ): self._database = database self._schema = schema self._name = name + self.signature = signature @property def database(self) -> str | None: @@ -72,6 +79,8 @@ def url_identifier(self) -> str: @property def sql_identifier(self) -> str: + if self.signature: + return f"IDENTIFIER('{self.identifier}'){self.signature}" return f"IDENTIFIER('{self.identifier}')" def __str__(self): @@ -98,9 +107,13 @@ def from_string(cls, identifier: str) -> "FQN": else: database = None schema = result.group("first_qualifier") - if signature := result.group("signature"): - unqualified_name = unqualified_name + signature - return cls(name=unqualified_name, schema=schema, database=database) + + signature = None + if result.group("signature"): + signature = result.group("signature") + return cls( + name=unqualified_name, schema=schema, database=database, signature=signature + ) @classmethod def from_stage(cls, stage: str) -> "FQN": diff --git a/src/snowflake/cli/api/sql_execution.py b/src/snowflake/cli/api/sql_execution.py index 38a2c6d00..3416b5e0d 100644 --- a/src/snowflake/cli/api/sql_execution.py +++ b/src/snowflake/cli/api/sql_execution.py @@ -147,11 +147,11 @@ def use_warehouse(self, new_wh: str): self.use(object_type=ObjectType.WAREHOUSE, name=prev_wh) def create_password_secret( - self, name: str, username: str, password: str + self, name: FQN, username: str, password: str ) -> SnowflakeCursor: return self._execute_query( f""" - create secret {name} + create secret {name.sql_identifier} type = password username = '{username}' password = '{password}' @@ -159,11 +159,11 @@ def create_password_secret( ) def create_api_integration( - self, name: str, api_provider: str, allowed_prefix: str, secret: Optional[str] + self, name: FQN, api_provider: str, allowed_prefix: str, secret: Optional[str] ) -> SnowflakeCursor: return self._execute_query( f""" - create api integration {name} + create api integration {name.sql_identifier} api_provider = {api_provider} api_allowed_prefixes = ('{allowed_prefix}') allowed_authentication_secrets = ({secret if secret else ''}) diff --git a/src/snowflake/cli/plugins/git/commands.py b/src/snowflake/cli/plugins/git/commands.py index 478d66604..26371c6dc 100644 --- a/src/snowflake/cli/plugins/git/commands.py +++ b/src/snowflake/cli/plugins/git/commands.py @@ -18,7 +18,7 @@ import logging from os import path from pathlib import Path -from typing import List, Optional +from typing import Dict, List, Optional import typer from click import ClickException @@ -41,6 +41,7 @@ ) from snowflake.cli.plugins.object.manager import ObjectManager from snowflake.cli.plugins.stage.manager import OnErrorType +from snowflake.connector import DictCursor app = SnowTyperFactory( name="git", @@ -82,10 +83,12 @@ def _repo_path_argument_callback(path): scope_option=scope_option(help_example="`list --in database my_db`"), ) +from snowflake.cli.api.identifiers import FQN -def _assure_repository_does_not_exist(om: ObjectManager, repository_name: str) -> None: + +def _assure_repository_does_not_exist(om: ObjectManager, repository_name: FQN) -> None: if om.object_exists( - object_type=ObjectType.GIT_REPOSITORY.value.cli_name, name=repository_name + object_type=ObjectType.GIT_REPOSITORY.value.cli_name, fqn=repository_name ): raise ClickException(f"Repository '{repository_name}' already exists") @@ -95,9 +98,27 @@ def _validate_origin_url(url: str) -> None: raise ClickException("Url address should start with 'https'") +def _unique_new_object_name( + om: ObjectManager, object_type: ObjectType, proposed_fqn: FQN +) -> str: + existing_objects: List[Dict] = om.show( + object_type=object_type.value.cli_name, + like=f"{proposed_fqn.name}%", + cursor_class=DictCursor, + ).fetchall() + existing_names = set(o["name"].upper() for o in existing_objects) + + result = proposed_fqn.name + i = 1 + while result.upper() in existing_names: + result = proposed_fqn.name + str(i) + i += 1 + return result + + @app.command("setup", requires_connection=True) def setup( - repository_name: str = RepoNameArgument, + repository_name: FQN = RepoNameArgument, **options, ) -> CommandResult: """ @@ -123,12 +144,29 @@ def setup( should_create_secret = False secret_name = None if secret_needed: - secret_name = f"{repository_name}_secret" - secret_name = typer.prompt( - "Secret identifier (will be created if not exists)", default=secret_name + default_secret_name = ( + FQN.from_string(f"{repository_name.name}_secret") + .set_schema(repository_name.schema) + .set_database(repository_name.database) + ) + default_secret_name.set_name( + _unique_new_object_name( + om, object_type=ObjectType.SECRET, proposed_fqn=default_secret_name + ), + ) + secret_name = FQN.from_string( + typer.prompt( + "Secret identifier (will be created if not exists)", + default=default_secret_name.name, + ) ) + if not secret_name.database: + secret_name.set_database(repository_name.database) + if not secret_name.schema: + secret_name.set_schema(repository_name.schema) + if om.object_exists( - object_type=ObjectType.SECRET.value.cli_name, name=secret_name + object_type=ObjectType.SECRET.value.cli_name, fqn=secret_name ): cli_console.step(f"Using existing secret '{secret_name}'") else: @@ -137,10 +175,17 @@ def setup( secret_username = typer.prompt("username") secret_password = typer.prompt("password/token", hide_input=True) - api_integration = f"{repository_name}_api_integration" - api_integration = typer.prompt( - "API integration identifier (will be created if not exists)", - default=api_integration, + # API integration is an account-level object + api_integration = FQN.from_string(f"{repository_name.name}_api_integration") + api_integration.set_name( + typer.prompt( + "API integration identifier (will be created if not exists)", + default=_unique_new_object_name( + om, + object_type=ObjectType.INTEGRATION, + proposed_fqn=api_integration, + ), + ) ) if should_create_secret: @@ -150,7 +195,7 @@ def setup( cli_console.step(f"Secret '{secret_name}' successfully created.") if not om.object_exists( - object_type=ObjectType.INTEGRATION.value.cli_name, name=api_integration + object_type=ObjectType.INTEGRATION.value.cli_name, fqn=api_integration ): manager.create_api_integration( name=api_integration, @@ -177,7 +222,7 @@ def setup( requires_connection=True, ) def list_branches( - repository_name: str = RepoNameArgument, + repository_name: FQN = RepoNameArgument, like=like_option( help_example='`list-branches --like "%_test"` lists all branches that end with "_test"' ), @@ -186,7 +231,9 @@ def list_branches( """ List all branches in the repository. """ - return QueryResult(GitManager().show_branches(repo_name=repository_name, like=like)) + return QueryResult( + GitManager().show_branches(repo_name=repository_name.identifier, like=like) + ) @app.command( @@ -194,7 +241,7 @@ def list_branches( requires_connection=True, ) def list_tags( - repository_name: str = RepoNameArgument, + repository_name: FQN = RepoNameArgument, like=like_option( help_example='`list-tags --like "v2.0%"` lists all tags that start with "v2.0"' ), @@ -203,7 +250,9 @@ def list_tags( """ List all tags in the repository. """ - return QueryResult(GitManager().show_tags(repo_name=repository_name, like=like)) + return QueryResult( + GitManager().show_tags(repo_name=repository_name.identifier, like=like) + ) @app.command( @@ -228,13 +277,13 @@ def list_files( requires_connection=True, ) def fetch( - repository_name: str = RepoNameArgument, + repository_name: FQN = RepoNameArgument, **options, ) -> CommandResult: """ Fetch changes from origin to Snowflake repository. """ - return QueryResult(GitManager().fetch(repo_name=repository_name)) + return QueryResult(GitManager().fetch(fqn=repository_name)) @app.command( diff --git a/src/snowflake/cli/plugins/git/manager.py b/src/snowflake/cli/plugins/git/manager.py index 94a7ad547..03b3cc9c2 100644 --- a/src/snowflake/cli/plugins/git/manager.py +++ b/src/snowflake/cli/plugins/git/manager.py @@ -18,6 +18,7 @@ from textwrap import dedent from typing import List +from snowflake.cli.api.identifiers import FQN from snowflake.cli.plugins.stage.manager import ( USER_STAGE_PREFIX, StageManager, @@ -40,17 +41,25 @@ def __init__(self, stage_path: str): @property def path(self) -> str: - return ( - f"{self.stage_name}{self.directory}" - if self.stage_name.endswith("/") - else f"{self.stage_name}/{self.directory}" - ) + return f"{self.stage_name.rstrip('/')}/{self.directory}" - def add_stage_prefix(self, file_path: str) -> str: + @classmethod + def get_directory(cls, stage_path: str) -> str: + return "/".join(Path(stage_path).parts[3:]) + + @property + def full_path(self) -> str: + return f"{self.stage.rstrip('/')}/{self.directory}" + + def replace_stage_prefix(self, file_path: str) -> str: stage = Path(self.stage).parts[0] file_path_without_prefix = Path(file_path).parts[1:] return f"{stage}/{'/'.join(file_path_without_prefix)}" + def add_stage_prefix(self, file_path: str) -> str: + stage = self.stage.rstrip("/") + return f"{stage}/{file_path.lstrip('/')}" + def get_directory_from_file_path(self, file_path: str) -> List[str]: stage_path_length = len(Path(self.directory).parts) return list(Path(file_path).parts[3 + stage_path_length : -1]) @@ -63,15 +72,15 @@ def show_branches(self, repo_name: str, like: str) -> SnowflakeCursor: def show_tags(self, repo_name: str, like: str) -> SnowflakeCursor: return self._execute_query(f"show git tags like '{like}' in {repo_name}") - def fetch(self, repo_name: str) -> SnowflakeCursor: - return self._execute_query(f"alter git repository {repo_name} fetch") + def fetch(self, fqn: FQN) -> SnowflakeCursor: + return self._execute_query(f"alter git repository {fqn} fetch") def create( - self, repo_name: str, api_integration: str, url: str, secret: str + self, repo_name: FQN, api_integration: str, url: str, secret: str ) -> SnowflakeCursor: query = dedent( f""" - create git repository {repo_name} + create git repository {repo_name.sql_identifier} api_integration = {api_integration} origin = '{url}' """ diff --git a/src/snowflake/cli/plugins/notebook/commands.py b/src/snowflake/cli/plugins/notebook/commands.py index 2f0e94b3c..c436cb395 100644 --- a/src/snowflake/cli/plugins/notebook/commands.py +++ b/src/snowflake/cli/plugins/notebook/commands.py @@ -17,9 +17,10 @@ import typer from snowflake.cli.api.commands.flags import identifier_argument from snowflake.cli.api.commands.snow_typer import SnowTyperFactory +from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.output.types import MessageResult from snowflake.cli.plugins.notebook.manager import NotebookManager -from snowflake.cli.plugins.notebook.types import NotebookName, NotebookStagePath +from snowflake.cli.plugins.notebook.types import NotebookStagePath from typing_extensions import Annotated app = SnowTyperFactory( @@ -38,7 +39,7 @@ @app.command(requires_connection=True) def execute( - identifier: str = NOTEBOOK_IDENTIFIER, + identifier: FQN = NOTEBOOK_IDENTIFIER, **options, ): """ @@ -51,7 +52,7 @@ def execute( @app.command(requires_connection=True) def get_url( - identifier: str = NOTEBOOK_IDENTIFIER, + identifier: FQN = NOTEBOOK_IDENTIFIER, **options, ): """Return a url to a notebook.""" @@ -61,7 +62,7 @@ def get_url( @app.command(name="open", requires_connection=True) def open_cmd( - identifier: str = NOTEBOOK_IDENTIFIER, + identifier: FQN = NOTEBOOK_IDENTIFIER, **options, ): """Opens a notebook in default browser""" @@ -72,7 +73,7 @@ def open_cmd( @app.command(requires_connection=True) def create( - identifier: Annotated[NotebookName, NOTEBOOK_IDENTIFIER], + identifier: Annotated[FQN, NOTEBOOK_IDENTIFIER], notebook_file: Annotated[NotebookStagePath, NotebookFile], **options, ): diff --git a/src/snowflake/cli/plugins/notebook/manager.py b/src/snowflake/cli/plugins/notebook/manager.py index 5cdac19c6..efad57550 100644 --- a/src/snowflake/cli/plugins/notebook/manager.py +++ b/src/snowflake/cli/plugins/notebook/manager.py @@ -20,23 +20,23 @@ from snowflake.cli.api.sql_execution import SqlExecutionMixin from snowflake.cli.plugins.connection.util import make_snowsight_url from snowflake.cli.plugins.notebook.exceptions import NotebookStagePathError -from snowflake.cli.plugins.notebook.types import NotebookName, NotebookStagePath +from snowflake.cli.plugins.notebook.types import NotebookStagePath class NotebookManager(SqlExecutionMixin): - def execute(self, notebook_name: NotebookName): - query = f"EXECUTE NOTEBOOK {notebook_name}()" + def execute(self, notebook_name: FQN): + query = f"EXECUTE NOTEBOOK {notebook_name.sql_identifier}()" return self._execute_query(query=query) - def get_url(self, notebook_name: NotebookName): - fqn = FQN.from_string(notebook_name).using_connection(self._conn) + def get_url(self, notebook_name: FQN): + fqn = notebook_name.using_connection(self._conn) return make_snowsight_url( self._conn, f"/#/notebooks/{fqn.url_identifier}", ) @staticmethod - def parse_stage_as_path(notebook_file: NotebookName) -> Path: + def parse_stage_as_path(notebook_file: str) -> Path: """Parses notebook file path to pathlib.Path.""" if not notebook_file.endswith(".ipynb"): raise NotebookStagePathError(notebook_file) @@ -48,19 +48,19 @@ def parse_stage_as_path(notebook_file: NotebookName) -> Path: def create( self, - notebook_name: NotebookName, + notebook_name: FQN, notebook_file: NotebookStagePath, ) -> str: - notebook_fqn = FQN.from_string(notebook_name).using_connection(self._conn) + notebook_fqn = notebook_name.using_connection(self._conn) stage_path = self.parse_stage_as_path(notebook_file) queries = dedent( f""" - CREATE OR REPLACE NOTEBOOK {notebook_fqn.identifier} + CREATE OR REPLACE NOTEBOOK {notebook_fqn.sql_identifier} FROM '{stage_path.parent}' QUERY_WAREHOUSE = '{cli_context.connection.warehouse}' MAIN_FILE = '{stage_path.name}'; - + // Cannot use IDENTIFIER(...) ALTER NOTEBOOK {notebook_fqn.identifier} ADD LIVE VERSION FROM LAST; """ ) diff --git a/src/snowflake/cli/plugins/notebook/types.py b/src/snowflake/cli/plugins/notebook/types.py index 7b97d7995..8de7efac6 100644 --- a/src/snowflake/cli/plugins/notebook/types.py +++ b/src/snowflake/cli/plugins/notebook/types.py @@ -12,5 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -NotebookName = str NotebookStagePath = str diff --git a/src/snowflake/cli/plugins/object/command_aliases.py b/src/snowflake/cli/plugins/object/command_aliases.py index 7905edf73..f62f9953c 100644 --- a/src/snowflake/cli/plugins/object/command_aliases.py +++ b/src/snowflake/cli/plugins/object/command_aliases.py @@ -20,6 +20,7 @@ from click import ClickException from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.constants import ObjectType +from snowflake.cli.api.identifiers import FQN from snowflake.cli.plugins.object.commands import ( ScopeOption, describe, @@ -72,7 +73,7 @@ def list_cmd( if "drop" not in ommit_commands: @app.command("drop", requires_connection=True) - def drop_cmd(name: str = name_argument, **options): + def drop_cmd(name: FQN = name_argument, **options): return drop( object_type=object_type.value.cli_name, object_name=name, @@ -84,7 +85,7 @@ def drop_cmd(name: str = name_argument, **options): if "describe" not in ommit_commands: @app.command("describe", requires_connection=True) - def describe_cmd(name: str = name_argument, **options): + def describe_cmd(name: FQN = name_argument, **options): return describe( object_type=object_type.value.cli_name, object_name=name, diff --git a/src/snowflake/cli/plugins/object/commands.py b/src/snowflake/cli/plugins/object/commands.py index 5c3b976ee..b03e32f0e 100644 --- a/src/snowflake/cli/plugins/object/commands.py +++ b/src/snowflake/cli/plugins/object/commands.py @@ -18,9 +18,14 @@ import typer from click import ClickException -from snowflake.cli.api.commands.flags import like_option, parse_key_value_variables +from snowflake.cli.api.commands.flags import ( + IdentifierType, + like_option, + parse_key_value_variables, +) from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.constants import SUPPORTED_OBJECTS, VALID_SCOPES +from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.output.types import MessageResult, QueryResult from snowflake.cli.api.project.util import is_valid_identifier from snowflake.cli.plugins.object.manager import ObjectManager @@ -31,7 +36,9 @@ ) -NameArgument = typer.Argument(help="Name of the object") +NameArgument = typer.Argument( + help="Name of the object.", show_default=False, click_type=IdentifierType() +) ObjectArgument = typer.Argument( help="Type of object. For example table, database, compute-pool.", case_sensitive=False, @@ -112,8 +119,8 @@ def list_( help=f"Drops Snowflake object of given name and type. {SUPPORTED_TYPES_MSG}", requires_connection=True, ) -def drop(object_type: str = ObjectArgument, object_name: str = NameArgument, **options): - return QueryResult(ObjectManager().drop(object_type=object_type, name=object_name)) +def drop(object_type: str = ObjectArgument, object_name: FQN = NameArgument, **options): + return QueryResult(ObjectManager().drop(object_type=object_type, fqn=object_name)) # Image repository is the only supported object that does not have a DESCRIBE command. @@ -125,10 +132,10 @@ def drop(object_type: str = ObjectArgument, object_name: str = NameArgument, **o requires_connection=True, ) def describe( - object_type: str = ObjectArgument, object_name: str = NameArgument, **options + object_type: str = ObjectArgument, object_name: FQN = NameArgument, **options ): return QueryResult( - ObjectManager().describe(object_type=object_type, name=object_name) + ObjectManager().describe(object_type=object_type, fqn=object_name) ) diff --git a/src/snowflake/cli/plugins/object/manager.py b/src/snowflake/cli/plugins/object/manager.py index d1a321f6e..bbc047884 100644 --- a/src/snowflake/cli/plugins/object/manager.py +++ b/src/snowflake/cli/plugins/object/manager.py @@ -22,6 +22,7 @@ OBJECT_TO_NAMES, ObjectNames, ) +from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.rest_api import RestApi from snowflake.cli.api.sql_execution import SqlExecutionMixin from snowflake.connector import ProgrammingError @@ -53,22 +54,22 @@ def show( query += f" in {scope[0].replace('-', ' ')} {scope[1]}" return self._execute_query(query, **kwargs) - def drop(self, *, object_type: str, name: str) -> SnowflakeCursor: + def drop(self, *, object_type: str, fqn: FQN) -> SnowflakeCursor: object_name = _get_object_names(object_type).sf_name - return self._execute_query(f"drop {object_name} {name}") + return self._execute_query(f"drop {object_name} {fqn.sql_identifier}") - def describe(self, *, object_type: str, name: str): + def describe(self, *, object_type: str, fqn: FQN): # Image repository is the only supported object that does not have a DESCRIBE command. if object_type == "image-repository": raise ClickException( f"Describe is currently not supported for object of type image-repository" ) object_name = _get_object_names(object_type).sf_name - return self._execute_query(f"describe {object_name} {name}") + return self._execute_query(f"describe {object_name} {fqn.sql_identifier}") - def object_exists(self, *, object_type: str, name: str): + def object_exists(self, *, object_type: str, fqn: FQN): try: - self.describe(object_type=object_type, name=name) + self.describe(object_type=object_type, fqn=fqn) return True except ProgrammingError: return False diff --git a/src/snowflake/cli/plugins/snowpark/commands.py b/src/snowflake/cli/plugins/snowpark/commands.py index af7618f29..2020989b4 100644 --- a/src/snowflake/cli/plugins/snowpark/commands.py +++ b/src/snowflake/cli/plugins/snowpark/commands.py @@ -173,9 +173,7 @@ def deploy( stage_name = snowpark.stage_name stage_manager = StageManager() stage_name = FQN.from_string(stage_name).using_context() - stage_manager.create( - stage_name=stage_name, comment="deployments managed by Snowflake CLI" - ) + stage_manager.create(fqn=stage_name, comment="deployments managed by Snowflake CLI") snowflake_dependencies = _read_snowflake_requrements_file( paths.snowflake_requirements_file @@ -251,7 +249,7 @@ def _find_existing_objects( try: current_state = om.describe( object_type=object_type.value.sf_name, - name=identifier, + fqn=FQN.from_string(identifier), ) existing_objects[identifier] = current_state except ProgrammingError: @@ -528,7 +526,7 @@ def list_( @app.command("drop", requires_connection=True) def drop( object_type: _SnowparkObject = ObjectTypeArgument, - identifier: str = IdentifierArgument, + identifier: FQN = IdentifierArgument, **options, ): """Drop procedure or function.""" @@ -538,7 +536,7 @@ def drop( @app.command("describe", requires_connection=True) def describe( object_type: _SnowparkObject = ObjectTypeArgument, - identifier: str = IdentifierArgument, + identifier: FQN = IdentifierArgument, **options, ): """Provides description of a procedure or function.""" diff --git a/src/snowflake/cli/plugins/snowpark/models.py b/src/snowflake/cli/plugins/snowpark/models.py index 454976e34..5c30da0c4 100644 --- a/src/snowflake/cli/plugins/snowpark/models.py +++ b/src/snowflake/cli/plugins/snowpark/models.py @@ -128,13 +128,14 @@ def from_wheel(cls, wheel_path: Path): if line.startswith(dep_keyword) ] name = cls._get_name_from_wheel_filename(wheel_path.name) + return cls(name=name, wheel_path=wheel_path, dependencies=dependencies) @staticmethod def _get_name_from_wheel_filename(wheel_filename: str) -> str: # wheel filename is in format {name}-{version}[-{extra info}] # https://peps.python.org/pep-0491/#file-name-convention - return wheel_filename.split("-")[0] + return wheel_filename.split("-")[0].lower() @staticmethod def to_wheel_name_format(package_name: str) -> str: diff --git a/src/snowflake/cli/plugins/snowpark/package/manager.py b/src/snowflake/cli/plugins/snowpark/package/manager.py index ba1026698..0e1c14f3d 100644 --- a/src/snowflake/cli/plugins/snowpark/package/manager.py +++ b/src/snowflake/cli/plugins/snowpark/package/manager.py @@ -17,6 +17,7 @@ import logging from pathlib import Path +from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.secure_path import SecurePath from snowflake.cli.plugins.snowpark.package.utils import prepare_app_zip from snowflake.cli.plugins.stage.manager import StageManager @@ -30,7 +31,7 @@ def upload(file: Path, stage: str, overwrite: bool): temp_app_zip_path = prepare_app_zip(SecurePath(file), temp_dir) sm = StageManager() - sm.create(sm.get_stage_from_path(stage)) + sm.create(FQN.from_string(sm.get_stage_from_path(stage))) put_response = sm.put( temp_app_zip_path.path, stage, overwrite=overwrite ).fetchone() diff --git a/src/snowflake/cli/plugins/spcs/compute_pool/commands.py b/src/snowflake/cli/plugins/spcs/compute_pool/commands.py index 9b4b7653f..e6d1940e8 100644 --- a/src/snowflake/cli/plugins/spcs/compute_pool/commands.py +++ b/src/snowflake/cli/plugins/spcs/compute_pool/commands.py @@ -21,10 +21,12 @@ from snowflake.cli.api.commands.flags import ( IfNotExistsOption, OverrideableOption, + identifier_argument, like_option, ) from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.constants import ObjectType +from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.output.types import CommandResult, SingleQueryResult from snowflake.cli.api.project.util import is_valid_object_name from snowflake.cli.plugins.object.command_aliases import ( @@ -43,22 +45,21 @@ ) -def _compute_pool_name_callback(name: str) -> str: +def _compute_pool_name_callback(name: FQN) -> FQN: """ Verifies that compute pool name is a single valid identifier. """ - if not is_valid_object_name(name, max_depth=0, allow_quoted=False): + if not is_valid_object_name(name.identifier, max_depth=0, allow_quoted=False): raise ClickException( f"'{name}' is not a valid compute pool name. Note that compute pool names must be unquoted identifiers." ) return name -ComputePoolNameArgument = typer.Argument( - ..., - help="Name of the compute pool.", +ComputePoolNameArgument = identifier_argument( + sf_object="compute pool", + example="my_compute_pool", callback=_compute_pool_name_callback, - show_default=False, ) @@ -106,7 +107,7 @@ def _compute_pool_name_callback(name: str) -> str: @app.command(requires_connection=True) def create( - name: str = ComputePoolNameArgument, + name: FQN = ComputePoolNameArgument, instance_family: str = typer.Option( ..., "--family", @@ -131,7 +132,7 @@ def create( """ max_nodes = validate_and_set_instances(min_nodes, max_nodes, "nodes") cursor = ComputePoolManager().create( - pool_name=name, + pool_name=name.identifier, min_nodes=min_nodes, max_nodes=max_nodes, instance_family=instance_family, @@ -145,33 +146,33 @@ def create( @app.command("stop-all", requires_connection=True) -def stop_all(name: str = ComputePoolNameArgument, **options) -> CommandResult: +def stop_all(name: FQN = ComputePoolNameArgument, **options) -> CommandResult: """ Deletes all services running on the compute pool. """ - cursor = ComputePoolManager().stop(pool_name=name) + cursor = ComputePoolManager().stop(pool_name=name.identifier) return SingleQueryResult(cursor) @app.command(requires_connection=True) -def suspend(name: str = ComputePoolNameArgument, **options) -> CommandResult: +def suspend(name: FQN = ComputePoolNameArgument, **options) -> CommandResult: """ Suspends the compute pool by suspending all currently running services and then releasing compute pool nodes. """ - return SingleQueryResult(ComputePoolManager().suspend(name)) + return SingleQueryResult(ComputePoolManager().suspend(name.identifier)) @app.command(requires_connection=True) -def resume(name: str = ComputePoolNameArgument, **options) -> CommandResult: +def resume(name: FQN = ComputePoolNameArgument, **options) -> CommandResult: """ Resumes the compute pool from a SUSPENDED state. """ - return SingleQueryResult(ComputePoolManager().resume(name)) + return SingleQueryResult(ComputePoolManager().resume(name.identifier)) @app.command("set", requires_connection=True) def set_property( - name: str = ComputePoolNameArgument, + name: FQN = ComputePoolNameArgument, min_nodes: Optional[int] = MinNodesOption(default=None, show_default=False), max_nodes: Optional[int] = MaxNodesOption(show_default=False), auto_resume: Optional[bool] = AutoResumeOption(default=None, show_default=False), @@ -187,7 +188,7 @@ def set_property( Sets one or more properties for the compute pool. """ cursor = ComputePoolManager().set_property( - pool_name=name, + pool_name=name.identifier, min_nodes=min_nodes, max_nodes=max_nodes, auto_resume=auto_resume, @@ -199,7 +200,7 @@ def set_property( @app.command("unset", requires_connection=True) def unset_property( - name: str = ComputePoolNameArgument, + name: FQN = ComputePoolNameArgument, auto_resume: bool = AutoResumeOption( default=False, param_decls=["--auto-resume"], @@ -223,7 +224,7 @@ def unset_property( Resets one or more properties for the compute pool to their default value(s). """ cursor = ComputePoolManager().unset_property( - pool_name=name, + pool_name=name.identifier, auto_resume=auto_resume, auto_suspend_secs=auto_suspend_secs, comment=comment, @@ -232,9 +233,9 @@ def unset_property( @app.command(requires_connection=True) -def status(pool_name: str = ComputePoolNameArgument, **options) -> CommandResult: +def status(pool_name: FQN = ComputePoolNameArgument, **options) -> CommandResult: """ Retrieves the status of a compute pool along with a relevant message, if one exists. """ - cursor = ComputePoolManager().status(pool_name=pool_name) + cursor = ComputePoolManager().status(pool_name=pool_name.identifier) return SingleQueryResult(cursor) diff --git a/src/snowflake/cli/plugins/spcs/image_repository/commands.py b/src/snowflake/cli/plugins/spcs/image_repository/commands.py index 5c27a9061..f5d3dc8b3 100644 --- a/src/snowflake/cli/plugins/spcs/image_repository/commands.py +++ b/src/snowflake/cli/plugins/spcs/image_repository/commands.py @@ -23,11 +23,13 @@ from snowflake.cli.api.commands.flags import ( IfNotExistsOption, ReplaceOption, + identifier_argument, like_option, ) from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.console import cli_console from snowflake.cli.api.constants import ObjectType +from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.output.types import ( CollectionResult, MessageResult, @@ -48,18 +50,18 @@ ) -def _repo_name_callback(name: str): - if not is_valid_object_name(name, max_depth=2, allow_quoted=False): +def _repo_name_callback(name: FQN): + if not is_valid_object_name(name.identifier, max_depth=2, allow_quoted=False): raise ClickException( f"'{name}' is not a valid image repository name. Note that image repository names must be unquoted identifiers. The same constraint also applies to database and schema names where you create an image repository." ) return name -REPO_NAME_ARGUMENT = typer.Argument( - help="Name of the image repository.", +REPO_NAME_ARGUMENT = identifier_argument( + sf_object="image repository", + example="my_repository", callback=_repo_name_callback, - show_default=False, ) add_object_command_aliases( @@ -76,7 +78,7 @@ def _repo_name_callback(name: str): @app.command(requires_connection=True) def create( - name: str = REPO_NAME_ARGUMENT, + name: FQN = REPO_NAME_ARGUMENT, replace: bool = ReplaceOption(), if_not_exists: bool = IfNotExistsOption(), **options, @@ -86,21 +88,21 @@ def create( """ return SingleQueryResult( ImageRepositoryManager().create( - name=name, replace=replace, if_not_exists=if_not_exists + name=name.identifier, replace=replace, if_not_exists=if_not_exists ) ) @app.command("list-images", requires_connection=True) def list_images( - name: str = REPO_NAME_ARGUMENT, + name: FQN = REPO_NAME_ARGUMENT, **options, ) -> CollectionResult: """Lists images in the given repository.""" repository_manager = ImageRepositoryManager() database = repository_manager.get_database() schema = repository_manager.get_schema() - url = repository_manager.get_repository_url(name) + url = repository_manager.get_repository_url(name.identifier) api_url = repository_manager.get_repository_api_url(url) bearer_login = RegistryManager().login_to_registry(api_url) repos = [] @@ -136,7 +138,7 @@ def list_images( @app.command("list-tags", requires_connection=True) def list_tags( - name: str = REPO_NAME_ARGUMENT, + name: FQN = REPO_NAME_ARGUMENT, image_name: str = typer.Option( ..., "--image-name", @@ -150,7 +152,7 @@ def list_tags( """Lists tags for the given image in a repository.""" repository_manager = ImageRepositoryManager() - url = repository_manager.get_repository_url(name) + url = repository_manager.get_repository_url(name.identifier) api_url = repository_manager.get_repository_api_url(url) bearer_login = RegistryManager().login_to_registry(api_url) @@ -187,10 +189,14 @@ def list_tags( @app.command("url", requires_connection=True) def repo_url( - name: str = REPO_NAME_ARGUMENT, + name: FQN = REPO_NAME_ARGUMENT, **options, ): """Returns the URL for the given repository.""" return MessageResult( - (ImageRepositoryManager().get_repository_url(repo_name=name, with_scheme=False)) + ( + ImageRepositoryManager().get_repository_url( + repo_name=name.identifier, with_scheme=False + ) + ) ) diff --git a/src/snowflake/cli/plugins/spcs/services/commands.py b/src/snowflake/cli/plugins/spcs/services/commands.py index 1b4729a43..ef7d28588 100644 --- a/src/snowflake/cli/plugins/spcs/services/commands.py +++ b/src/snowflake/cli/plugins/spcs/services/commands.py @@ -23,10 +23,12 @@ from snowflake.cli.api.commands.flags import ( IfNotExistsOption, OverrideableOption, + identifier_argument, like_option, ) from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.constants import ObjectType +from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.output.types import ( CommandResult, QueryJsonValueResult, @@ -52,19 +54,18 @@ ) -def _service_name_callback(name: str) -> str: - if not is_valid_object_name(name, max_depth=2, allow_quoted=False): +def _service_name_callback(name: FQN) -> FQN: + if not is_valid_object_name(name.identifier, max_depth=2, allow_quoted=False): raise ClickException( f"'{name}' is not a valid service name. Note service names must be unquoted identifiers. The same constraint also applies to database and schema names where you create a service." ) return name -ServiceNameArgument = typer.Argument( - ..., - help="Name of the service.", +ServiceNameArgument = identifier_argument( + sf_object="service pool", + example="my_service", callback=_service_name_callback, - show_default=False, ) SpecPathOption = typer.Option( @@ -116,7 +117,7 @@ def _service_name_callback(name: str) -> str: @app.command(requires_connection=True) def create( - name: str = ServiceNameArgument, + name: FQN = ServiceNameArgument, compute_pool: str = typer.Option( ..., "--compute-pool", @@ -145,7 +146,7 @@ def create( min_instances, max_instances, "instances" ) cursor = ServiceManager().create( - service_name=name, + service_name=name.identifier, min_instances=min_instances, max_instances=max_instances, compute_pool=compute_pool, @@ -161,17 +162,17 @@ def create( @app.command(requires_connection=True) -def status(name: str = ServiceNameArgument, **options) -> CommandResult: +def status(name: FQN = ServiceNameArgument, **options) -> CommandResult: """ Retrieves the status of a service. """ - cursor = ServiceManager().status(service_name=name) + cursor = ServiceManager().status(service_name=name.identifier) return QueryJsonValueResult(cursor) @app.command(requires_connection=True) def logs( - name: str = ServiceNameArgument, + name: FQN = ServiceNameArgument, container_name: str = typer.Option( ..., "--container-name", @@ -193,7 +194,7 @@ def logs( Retrieves local logs from a service container. """ results = ServiceManager().logs( - service_name=name, + service_name=name.identifier, instance_id=instance_id, container_name=container_name, num_lines=num_lines, @@ -205,7 +206,7 @@ def logs( @app.command(requires_connection=True) def upgrade( - name: str = ServiceNameArgument, + name: FQN = ServiceNameArgument, spec_path: Path = SpecPathOption, **options, ): @@ -213,20 +214,20 @@ def upgrade( Updates an existing service with a new specification file. """ return SingleQueryResult( - ServiceManager().upgrade_spec(service_name=name, spec_path=spec_path) + ServiceManager().upgrade_spec(service_name=name.identifier, spec_path=spec_path) ) @app.command("list-endpoints", requires_connection=True) -def list_endpoints(name: str = ServiceNameArgument, **options): +def list_endpoints(name: FQN = ServiceNameArgument, **options): """ Lists the endpoints in a service. """ - return QueryResult(ServiceManager().list_endpoints(service_name=name)) + return QueryResult(ServiceManager().list_endpoints(service_name=name.identifier)) @app.command(requires_connection=True) -def suspend(name: str = ServiceNameArgument, **options) -> CommandResult: +def suspend(name: FQN = ServiceNameArgument, **options) -> CommandResult: """ Suspends the service, shutting down and deleting all its containers. """ @@ -234,7 +235,7 @@ def suspend(name: str = ServiceNameArgument, **options) -> CommandResult: @app.command(requires_connection=True) -def resume(name: str = ServiceNameArgument, **options) -> CommandResult: +def resume(name: FQN = ServiceNameArgument, **options) -> CommandResult: """ Resumes the service from a SUSPENDED state. """ @@ -243,7 +244,7 @@ def resume(name: str = ServiceNameArgument, **options) -> CommandResult: @app.command("set", requires_connection=True) def set_property( - name: str = ServiceNameArgument, + name: FQN = ServiceNameArgument, min_instances: Optional[int] = MinInstancesOption(default=None, show_default=False), max_instances: Optional[int] = MaxInstancesOption(show_default=False), query_warehouse: Optional[str] = QueryWarehouseOption(show_default=False), @@ -255,7 +256,7 @@ def set_property( Sets one or more properties for the service. """ cursor = ServiceManager().set_property( - service_name=name, + service_name=name.identifier, min_instances=min_instances, max_instances=max_instances, query_warehouse=query_warehouse, @@ -267,7 +268,7 @@ def set_property( @app.command("unset", requires_connection=True) def unset_property( - name: str = ServiceNameArgument, + name: FQN = ServiceNameArgument, min_instances: bool = MinInstancesOption( default=False, help=f"Reset the MIN_INSTANCES property - {_MIN_INSTANCES_HELP}", @@ -301,7 +302,7 @@ def unset_property( Resets one or more properties for the service to their default value(s). """ cursor = ServiceManager().unset_property( - service_name=name, + service_name=name.identifier, min_instances=min_instances, max_instances=max_instances, query_warehouse=query_warehouse, diff --git a/src/snowflake/cli/plugins/stage/commands.py b/src/snowflake/cli/plugins/stage/commands.py index 781695b5b..b7cc573fc 100644 --- a/src/snowflake/cli/plugins/stage/commands.py +++ b/src/snowflake/cli/plugins/stage/commands.py @@ -26,11 +26,13 @@ ExecuteVariablesOption, OnErrorOption, PatternOption, + identifier_argument, like_option, ) from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.console import cli_console from snowflake.cli.api.constants import ObjectType +from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.output.formats import OutputFormat from snowflake.cli.api.output.types import ( CollectionResult, @@ -56,7 +58,7 @@ help="Manages stages.", ) -StageNameArgument = typer.Argument(..., help="Name of the stage.", show_default=False) +StageNameArgument = identifier_argument(sf_object="stage", example="@my_stage") add_object_command_aliases( app=app, @@ -142,17 +144,17 @@ def copy( @app.command("create", requires_connection=True) -def stage_create(stage_name: str = StageNameArgument, **options) -> CommandResult: +def stage_create(stage_name: FQN = StageNameArgument, **options) -> CommandResult: """ Creates a named stage if it does not already exist. """ - cursor = StageManager().create(stage_name=stage_name) + cursor = StageManager().create(fqn=stage_name) return SingleQueryResult(cursor) @app.command("remove", requires_connection=True) def stage_remove( - stage_name: str = StageNameArgument, + stage_name: FQN = StageNameArgument, file_name: str = typer.Argument( ..., help="Name of the file to remove.", @@ -164,7 +166,7 @@ def stage_remove( Removes a file from a stage. """ - cursor = StageManager().remove(stage_name=stage_name, path=file_name) + cursor = StageManager().remove(stage_name=stage_name.identifier, path=file_name) return SingleQueryResult(cursor) diff --git a/src/snowflake/cli/plugins/stage/manager.py b/src/snowflake/cli/plugins/stage/manager.py index dd594fad3..d067796c9 100644 --- a/src/snowflake/cli/plugins/stage/manager.py +++ b/src/snowflake/cli/plugins/stage/manager.py @@ -65,14 +65,21 @@ class StagePathParts: stage_name: str is_directory: bool - @staticmethod - def get_directory(stage_path: str) -> str: + @classmethod + def get_directory(cls, stage_path: str) -> str: return "/".join(Path(stage_path).parts[1:]) @property def path(self) -> str: raise NotImplementedError + @property + def full_path(self) -> str: + raise NotImplementedError + + def replace_stage_prefix(self, file_path: str) -> str: + raise NotImplementedError + def add_stage_prefix(self, file_path: str) -> str: raise NotImplementedError @@ -112,24 +119,27 @@ def __init__(self, stage_path: str): self.directory = self.get_directory(stage_path) self.stage = StageManager.get_stage_from_path(stage_path) stage_name = self.stage.split(".")[-1] - if stage_name.startswith("@"): - stage_name = stage_name[1:] + stage_name = stage_name[1:] if stage_name.startswith("@") else stage_name self.stage_name = stage_name self.is_directory = True if stage_path.endswith("/") else False @property def path(self) -> str: - return ( - f"{self.stage_name}{self.directory}" - if self.stage_name.endswith("/") - else f"{self.stage_name}/{self.directory}" - ) + return f"{self.stage_name.rstrip('/')}/{self.directory}" - def add_stage_prefix(self, file_path: str) -> str: + @property + def full_path(self) -> str: + return f"{self.stage.rstrip('/')}/{self.directory}" + + def replace_stage_prefix(self, file_path: str) -> str: stage = Path(self.stage).parts[0] file_path_without_prefix = Path(file_path).parts[1:] return f"{stage}/{'/'.join(file_path_without_prefix)}" + def add_stage_prefix(self, file_path: str) -> str: + stage = self.stage.rstrip("/") + return f"{stage}/{file_path.lstrip('/')}" + def get_directory_from_file_path(self, file_path: str) -> List[str]: stage_path_length = len(Path(self.directory).parts) return list(Path(file_path).parts[1 + stage_path_length : -1]) @@ -146,14 +156,29 @@ class UserStagePathParts(StagePathParts): def __init__(self, stage_path: str): self.directory = self.get_directory(stage_path) - self.stage = "@~" - self.stage_name = "@~" + self.stage = USER_STAGE_PREFIX + self.stage_name = USER_STAGE_PREFIX self.is_directory = True if stage_path.endswith("/") else False + @classmethod + def get_directory(cls, stage_path: str) -> str: + if Path(stage_path).parts[0] == USER_STAGE_PREFIX: + return super().get_directory(stage_path) + return stage_path + @property def path(self) -> str: return f"{self.directory}" + @property + def full_path(self) -> str: + return f"{self.stage}/{self.directory}" + + def replace_stage_prefix(self, file_path: str) -> str: + if Path(file_path).parts[0] == self.stage_name: + return file_path + return f"{self.stage}/{file_path}" + def add_stage_prefix(self, file_path: str) -> str: return f"{self.stage}/{file_path}" @@ -168,7 +193,9 @@ def __init__(self): self._python_exe_procedure = None @staticmethod - def get_standard_stage_prefix(name: str) -> str: + def get_standard_stage_prefix(name: str | FQN) -> str: + if isinstance(name, FQN): + name = name.identifier # Handle embedded stages if name.startswith("snow://") or name.startswith("@"): return name @@ -239,7 +266,7 @@ def get_recursive( self._assure_is_existing_directory(dest_directory) result = self._execute_query( - f"get {self.quote_stage_name(stage_path_parts.add_stage_prefix(file_path))} {self._to_uri(f'{dest_directory}/')} parallel={parallel}" + f"get {self.quote_stage_name(stage_path_parts.replace_stage_prefix(file_path))} {self._to_uri(f'{dest_directory}/')} parallel={parallel}" ) results.append(result) @@ -300,8 +327,8 @@ def remove( quoted_stage_name = self.quote_stage_name(f"{stage_name}{path}") return self._execute_query(f"remove {quoted_stage_name}") - def create(self, stage_name: str, comment: Optional[str] = None) -> SnowflakeCursor: - query = f"create stage if not exists {stage_name}" + def create(self, fqn: FQN, comment: Optional[str] = None) -> SnowflakeCursor: + query = f"create stage if not exists {fqn.sql_identifier}" if comment: query += f" comment='{comment}'" return self._execute_query(query) @@ -319,8 +346,14 @@ def execute( stage_path_parts = self._stage_path_part_factory(stage_path) all_files_list = self._get_files_list_from_stage(stage_path_parts) + all_files_with_stage_name_prefix = [ + stage_path_parts.get_directory(file) for file in all_files_list + ] + # filter files from stage if match stage_path pattern - filtered_file_list = self._filter_files_list(stage_path_parts, all_files_list) + filtered_file_list = self._filter_files_list( + stage_path_parts, all_files_with_stage_name_prefix + ) if not filtered_file_list: raise ClickException(f"No files matched pattern '{stage_path}'") @@ -376,7 +409,7 @@ def _filter_files_list( if not stage_path_parts.directory: return self._filter_supported_files(files_on_stage) - stage_path = stage_path_parts.path.lower() + stage_path = stage_path_parts.directory # Exact file path was provided if stage_path in file list if stage_path in files_on_stage: diff --git a/src/snowflake/cli/plugins/streamlit/commands.py b/src/snowflake/cli/plugins/streamlit/commands.py index 9d1e1aef0..759cf48f6 100644 --- a/src/snowflake/cli/plugins/streamlit/commands.py +++ b/src/snowflake/cli/plugins/streamlit/commands.py @@ -25,7 +25,11 @@ with_experimental_behaviour, with_project_definition, ) -from snowflake.cli.api.commands.flags import ReplaceOption, like_option +from snowflake.cli.api.commands.flags import ( + ReplaceOption, + identifier_argument, + like_option, +) from snowflake.cli.api.commands.project_initialisation import add_init_command from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.constants import ObjectType @@ -49,19 +53,8 @@ ) log = logging.getLogger(__name__) - -class IdentifierType(click.ParamType): - name = "TEXT" - - def convert(self, value, param, ctx): - return FQN.from_string(value) - - -StreamlitNameArgument = typer.Argument( - ..., - help="Name of the Streamlit app.", - show_default=False, - click_type=IdentifierType(), +StreamlitNameArgument = identifier_argument( + sf_object="Streamlit app", example="my_streamlit" ) OpenOption = typer.Option( False, diff --git a/src/snowflake/cli/plugins/streamlit/manager.py b/src/snowflake/cli/plugins/streamlit/manager.py index f78c96f90..653c6154c 100644 --- a/src/snowflake/cli/plugins/streamlit/manager.py +++ b/src/snowflake/cli/plugins/streamlit/manager.py @@ -185,7 +185,7 @@ def deploy( stage_name = stage_name or "streamlit" stage_name = FQN.from_string(stage_name).using_connection(self._conn) - stage_manager.create(stage_name=stage_name) + stage_manager.create(fqn=stage_name) root_location = stage_manager.get_standard_stage_prefix( f"{stage_name}/{streamlit_name_for_root_location}" diff --git a/tests/__snapshots__/test_help_messages.ambr b/tests/__snapshots__/test_help_messages.ambr index 17fd925de..93d4b069b 100644 --- a/tests/__snapshots__/test_help_messages.ambr +++ b/tests/__snapshots__/test_help_messages.ambr @@ -2620,7 +2620,7 @@ | * object_type TEXT Type of object. For example table, database, | | compute-pool. | | [required] | - | * object_name TEXT Name of the object [default: None] [required] | + | * object_name TEXT Name of the object. [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -2698,7 +2698,7 @@ | * object_type TEXT Type of object. For example table, database, | | compute-pool. | | [required] | - | * object_name TEXT Name of the object [default: None] [required] | + | * object_name TEXT Name of the object. [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -2952,7 +2952,8 @@ Creates a named stage if it does not already exist. +- Arguments ------------------------------------------------------------------+ - | * stage_name TEXT Name of the stage. [required] | + | * stage_name TEXT Identifier of the stage. For example: @my_stage | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -3102,7 +3103,8 @@ Lists the stage contents. +- Arguments ------------------------------------------------------------------+ - | * stage_name TEXT Name of the stage. [required] | + | * stage_name TEXT Identifier of the stage. For example: @my_stage | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --pattern TEXT Regex pattern for filtering files by name. For | @@ -3179,7 +3181,8 @@ Removes a file from a stage. +- Arguments ------------------------------------------------------------------+ - | * stage_name TEXT Name of the stage. [required] | + | * stage_name TEXT Identifier of the stage. For example: @my_stage | + | [required] | | * file_name TEXT Name of the file to remove. [default: None] | | [required] | +------------------------------------------------------------------------------+ @@ -4106,7 +4109,9 @@ Creates a new compute pool. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the compute pool. [required] | + | * name TEXT Identifier of the compute pool. For example: | + | my_compute_pool | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | * --family TEXT Name of the | @@ -4229,7 +4234,9 @@ Provides description of compute pool. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the compute pool. [required] | + | * name TEXT Identifier of the compute pool. For example: | + | my_compute_pool | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -4300,7 +4307,9 @@ Drops compute pool with given name. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the compute pool. [required] | + | * name TEXT Identifier of the compute pool. For example: | + | my_compute_pool | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -4443,7 +4452,9 @@ Resumes the compute pool from a SUSPENDED state. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the compute pool. [required] | + | * name TEXT Identifier of the compute pool. For example: | + | my_compute_pool | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -4514,7 +4525,9 @@ Sets one or more properties for the compute pool. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the compute pool. [required] | + | * name TEXT Identifier of the compute pool. For example: | + | my_compute_pool | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --min-nodes INTEGER RANGE Minimum number | @@ -4611,7 +4624,9 @@ exists. +- Arguments ------------------------------------------------------------------+ - | * pool_name TEXT Name of the compute pool. [required] | + | * pool_name TEXT Identifier of the compute pool. For example: | + | my_compute_pool | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -4682,7 +4697,9 @@ Deletes all services running on the compute pool. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the compute pool. [required] | + | * name TEXT Identifier of the compute pool. For example: | + | my_compute_pool | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -4754,7 +4771,9 @@ then releasing compute pool nodes. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the compute pool. [required] | + | * name TEXT Identifier of the compute pool. For example: | + | my_compute_pool | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -4825,7 +4844,9 @@ Resets one or more properties for the compute pool to their default value(s). +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the compute pool. [required] | + | * name TEXT Identifier of the compute pool. For example: | + | my_compute_pool | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --auto-resume Reset the AUTO_RESUME property - The compute | @@ -5173,7 +5194,9 @@ Creates a new image repository in the current schema. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the image repository. [required] | + | * name TEXT Identifier of the image repository. For example: | + | my_repository | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --replace Replace this object if it already exists. | @@ -5247,7 +5270,9 @@ Drops image repository with given name. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the image repository. [required] | + | * name TEXT Identifier of the image repository. For example: | + | my_repository | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -5318,7 +5343,9 @@ Lists images in the given repository. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the image repository. [required] | + | * name TEXT Identifier of the image repository. For example: | + | my_repository | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -5389,7 +5416,9 @@ Lists tags for the given image in a repository. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the image repository. [required] | + | * name TEXT Identifier of the image repository. For example: | + | my_repository | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | * --image-name,--image_name -i TEXT Fully qualified name of the | @@ -5541,7 +5570,9 @@ Returns the URL for the given repository. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the image repository. [required] | + | * name TEXT Identifier of the image repository. For example: | + | my_repository | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -5872,7 +5903,8 @@ Creates a new service in the current schema. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the service. [required] | + | * name TEXT Identifier of the service pool. For example: my_service | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | * --compute-pool TEXT Compute pool to | @@ -6005,7 +6037,8 @@ Provides description of service. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the service. [required] | + | * name TEXT Identifier of the service pool. For example: my_service | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -6076,7 +6109,8 @@ Drops service with given name. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the service. [required] | + | * name TEXT Identifier of the service pool. For example: my_service | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -6147,7 +6181,8 @@ Lists the endpoints in a service. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the service. [required] | + | * name TEXT Identifier of the service pool. For example: my_service | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -6294,7 +6329,8 @@ Retrieves local logs from a service container. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the service. [required] | + | * name TEXT Identifier of the service pool. For example: my_service | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | * --container-name TEXT Name of the container. [required] | @@ -6371,7 +6407,8 @@ Resumes the service from a SUSPENDED state. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the service. [required] | + | * name TEXT Identifier of the service pool. For example: my_service | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -6442,7 +6479,8 @@ Sets one or more properties for the service. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the service. [required] | + | * name TEXT Identifier of the service pool. For example: my_service | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --min-instances INTEGER RANGE Minimum number | @@ -6542,7 +6580,8 @@ Retrieves the status of a service. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the service. [required] | + | * name TEXT Identifier of the service pool. For example: my_service | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -6613,7 +6652,8 @@ Suspends the service, shutting down and deleting all its containers. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the service. [required] | + | * name TEXT Identifier of the service pool. For example: my_service | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -6684,7 +6724,8 @@ Resets one or more properties for the service to their default value(s). +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the service. [required] | + | * name TEXT Identifier of the service pool. For example: my_service | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --min-instances Reset the MIN_INSTANCES property - Minimum | @@ -6768,7 +6809,8 @@ Updates an existing service with a new specification file. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the service. [required] | + | * name TEXT Identifier of the service pool. For example: my_service | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | * --spec-path FILE Path to service specification file. [required] | @@ -7076,7 +7118,8 @@ Creates a named stage if it does not already exist. +- Arguments ------------------------------------------------------------------+ - | * stage_name TEXT Name of the stage. [required] | + | * stage_name TEXT Identifier of the stage. For example: @my_stage | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -7147,7 +7190,8 @@ Provides description of stage. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the stage. [required] | + | * name TEXT Identifier of the stage. For example: @my_stage | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -7290,7 +7334,8 @@ Drops stage with given name. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the stage. [required] | + | * name TEXT Identifier of the stage. For example: @my_stage | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -7451,7 +7496,8 @@ Lists the stage contents. +- Arguments ------------------------------------------------------------------+ - | * stage_name TEXT Name of the stage. [required] | + | * stage_name TEXT Identifier of the stage. For example: @my_stage | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --pattern TEXT Regex pattern for filtering files by name. For | @@ -7601,7 +7647,8 @@ Removes a file from a stage. +- Arguments ------------------------------------------------------------------+ - | * stage_name TEXT Name of the stage. [required] | + | * stage_name TEXT Identifier of the stage. For example: @my_stage | + | [required] | | * file_name TEXT Name of the file to remove. [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ @@ -7780,7 +7827,9 @@ Provides description of streamlit. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the Streamlit app. [required] | + | * name TEXT Identifier of the Streamlit app. For example: | + | my_streamlit | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -7851,7 +7900,9 @@ Drops streamlit with given name. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the Streamlit app. [required] | + | * name TEXT Identifier of the Streamlit app. For example: | + | my_streamlit | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --help -h Show this message and exit. | @@ -7922,7 +7973,9 @@ Returns a URL to the specified Streamlit app +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the Streamlit app. [required] | + | * name TEXT Identifier of the Streamlit app. For example: | + | my_streamlit | + | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ | --open Whether to open the Streamlit app in a browser. | @@ -8103,7 +8156,9 @@ Shares a Streamlit app with another role. +- Arguments ------------------------------------------------------------------+ - | * name TEXT Name of the Streamlit app. [required] | + | * name TEXT Identifier of the Streamlit app. For example: | + | my_streamlit | + | [required] | | * to_role TEXT Role with which to share the Streamlit app. | | [default: None] | | [required] | diff --git a/tests/api/test_fqn.py b/tests/api/test_fqn.py index 546512e42..df951002e 100644 --- a/tests/api/test_fqn.py +++ b/tests/api/test_fqn.py @@ -84,17 +84,19 @@ def test_set_schema(): # Callables ( "db.schema.function(string, int, variant)", - "db.schema.function(string, int, variant)", + "db.schema.function", ), ( 'db.schema."fun tion"(string, int, variant)', - 'db.schema."fun tion"(string, int, variant)', + 'db.schema."fun tion"', ), ], ) def test_from_string(fqn_str, identifier): fqn = FQN.from_string(fqn_str) assert fqn.identifier == identifier + if fqn.signature: + assert fqn.signature == "(string, int, variant)" @pytest.mark.parametrize( diff --git a/tests/api/utils/test_naming_utils.py b/tests/api/utils/test_naming_utils.py index dfd271834..390e08672 100644 --- a/tests/api/utils/test_naming_utils.py +++ b/tests/api/utils/test_naming_utils.py @@ -19,7 +19,7 @@ @pytest.mark.parametrize( "qualified_name, expected", [ - ("func(number, number)", ("func(number, number)", None, None)), + ("func(number, number)", ("func", None, None)), ("name", ("name", None, None)), ("schema.name", ("name", "schema", None)), ("db.schema.name", ("name", "schema", "db")), @@ -31,3 +31,5 @@ def test_from_fully_qualified_name(qualified_name, expected): assert fqn.name == name assert fqn.schema == schema assert fqn.database == database + if fqn.signature: + assert fqn.signature == "(number, number)" diff --git a/tests/git/__snapshots__/test_git_commands.ambr b/tests/git/__snapshots__/test_git_commands.ambr index 0f6c6b8b5..ab159786d 100644 --- a/tests/git/__snapshots__/test_git_commands.ambr +++ b/tests/git/__snapshots__/test_git_commands.ambr @@ -1,13 +1,35 @@ # serializer version: 1 +# name: test_execute[@DB.SCHEMA.REPO/branches/main/s1.sql-@DB.SCHEMA.REPO/branches/main/-expected_files4] + ''' + SUCCESS - @DB.SCHEMA.REPO/branches/main/s1.sql + +--------------------------------------------------------+ + | File | Status | Error | + |--------------------------------------+---------+-------| + | @DB.SCHEMA.REPO/branches/main/s1.sql | SUCCESS | None | + +--------------------------------------------------------+ + + ''' +# --- +# name: test_execute[@DB.schema.REPO/branches/main/a/S3.sql-@DB.schema.REPO/branches/main/-expected_files5] + ''' + SUCCESS - @DB.schema.REPO/branches/main/a/S3.sql + +----------------------------------------------------------+ + | File | Status | Error | + |----------------------------------------+---------+-------| + | @DB.schema.REPO/branches/main/a/S3.sql | SUCCESS | None | + +----------------------------------------------------------+ + + ''' +# --- # name: test_execute[@db.schema.repo/branches/main/-@db.schema.repo/branches/main/-expected_files2] ''' SUCCESS - @db.schema.repo/branches/main/s1.sql - SUCCESS - @db.schema.repo/branches/main/a/s3.sql + SUCCESS - @db.schema.repo/branches/main/a/S3.sql +----------------------------------------------------------+ | File | Status | Error | |----------------------------------------+---------+-------| | @db.schema.repo/branches/main/s1.sql | SUCCESS | None | - | @db.schema.repo/branches/main/a/s3.sql | SUCCESS | None | + | @db.schema.repo/branches/main/a/S3.sql | SUCCESS | None | +----------------------------------------------------------+ ''' @@ -26,17 +48,54 @@ # name: test_execute[@repo/branches/main/-@repo/branches/main/-expected_files0] ''' SUCCESS - @repo/branches/main/s1.sql + SUCCESS - @repo/branches/main/a/S3.sql + +------------------------------------------------+ + | File | Status | Error | + |------------------------------+---------+-------| + | @repo/branches/main/s1.sql | SUCCESS | None | + | @repo/branches/main/a/S3.sql | SUCCESS | None | + +------------------------------------------------+ + + ''' +# --- +# name: test_execute[@repo/branches/main/a-@repo/branches/main/-expected_files1] + ''' + SUCCESS - @repo/branches/main/a/S3.sql + +------------------------------------------------+ + | File | Status | Error | + |------------------------------+---------+-------| + | @repo/branches/main/a/S3.sql | SUCCESS | None | + +------------------------------------------------+ + + ''' +# --- +# name: test_execute_new_git_repository_list_files[@repo/branches/main/-@repo/branches/main/-expected_files0] + ''' + SUCCESS - @repo/branches/main/S2.sql + SUCCESS - @repo/branches/main/s1.sql SUCCESS - @repo/branches/main/a/s3.sql +------------------------------------------------+ | File | Status | Error | |------------------------------+---------+-------| + | @repo/branches/main/S2.sql | SUCCESS | None | | @repo/branches/main/s1.sql | SUCCESS | None | | @repo/branches/main/a/s3.sql | SUCCESS | None | +------------------------------------------------+ ''' # --- -# name: test_execute[@repo/branches/main/a-@repo/branches/main/-expected_files1] +# name: test_execute_new_git_repository_list_files[@repo/branches/main/S2.sql-@repo/branches/main/-expected_files2] + ''' + SUCCESS - @repo/branches/main/S2.sql + +----------------------------------------------+ + | File | Status | Error | + |----------------------------+---------+-------| + | @repo/branches/main/S2.sql | SUCCESS | None | + +----------------------------------------------+ + + ''' +# --- +# name: test_execute_new_git_repository_list_files[@repo/branches/main/a/s3.sql-@repo/branches/main/-expected_files3] ''' SUCCESS - @repo/branches/main/a/s3.sql +------------------------------------------------+ @@ -47,3 +106,14 @@ ''' # --- +# name: test_execute_new_git_repository_list_files[@repo/branches/main/s1.sql-@repo/branches/main/-expected_files1] + ''' + SUCCESS - @repo/branches/main/s1.sql + +----------------------------------------------+ + | File | Status | Error | + |----------------------------+---------+-------| + | @repo/branches/main/s1.sql | SUCCESS | None | + +----------------------------------------------+ + + ''' +# --- diff --git a/tests/git/test_git_commands.py b/tests/git/test_git_commands.py index bdcd21335..a8ca2d484 100644 --- a/tests/git/test_git_commands.py +++ b/tests/git/test_git_commands.py @@ -212,11 +212,15 @@ def test_setup_invalid_url_error(mock_om_describe, mock_connector, runner, mock_ @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli.plugins.snowpark.commands.ObjectManager.describe") +@mock.patch("snowflake.cli.plugins.snowpark.commands.ObjectManager.show") def test_setup_no_secret_existing_api( - mock_om_describe, mock_connector, runner, mock_ctx + mock_om_show, mock_om_describe, mock_connector, runner, mock_ctx, mock_cursor ): + mock_om_show.return_value = mock_cursor([], []) mock_om_describe.side_effect = [ + # repo does not exist ProgrammingError(errno=DOES_NOT_EXIST_OR_NOT_AUTHORIZED), + # api integration exists None, ] mock_om_describe.return_value = [None, {"object_details": "something"}] @@ -239,24 +243,45 @@ def test_setup_no_secret_existing_api( ) assert ctx.get_query() == dedent( """ - create git repository repo_name + create git repository IDENTIFIER('repo_name') api_integration = existing_api_integration origin = 'https://github.com/an-example-repo.git' """ ) +@pytest.mark.parametrize( + "repo_name, int_name, secret_name", + [ + ("db.schema.FooRepo", "FooRepo_api_integration", "db.schema.FooRepo_secret"), + ("schema.FooRepo", "FooRepo_api_integration", "schema.FooRepo_secret"), + ("FooRepo", "FooRepo_api_integration", "FooRepo_secret"), + ], +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli.plugins.snowpark.commands.ObjectManager.describe") -def test_setup_no_secret_create_api(mock_om_describe, mock_connector, runner, mock_ctx): +@mock.patch("snowflake.cli.plugins.snowpark.commands.ObjectManager.show") +def test_setup_no_secret_create_api( + mock_om_show, + mock_om_describe, + mock_connector, + runner, + mock_ctx, + mock_cursor, + repo_name, + int_name, + secret_name, +): + mock_om_show.return_value = mock_cursor([], []) mock_om_describe.side_effect = ProgrammingError( + # nothing exists errno=DOES_NOT_EXIST_OR_NOT_AUTHORIZED ) ctx = mock_ctx() mock_connector.return_value = ctx communication = "\n".join([EXAMPLE_URL, "n", "", ""]) - result = runner.invoke(["git", "setup", "repo_name"], input=communication) + result = runner.invoke(["git", "setup", repo_name], input=communication) assert result.exit_code == 0, result.output assert result.output.startswith( @@ -264,22 +289,22 @@ def test_setup_no_secret_create_api(mock_om_describe, mock_connector, runner, mo [ "Origin url: https://github.com/an-example-repo.git", "Use secret for authentication? [y/N]: n", - "API integration identifier (will be created if not exists) [repo_name_api_integration]: ", - "API integration 'repo_name_api_integration' successfully created.", + f"API integration identifier (will be created if not exists) [{int_name}]: ", + f"API integration '{int_name}' successfully created.", ] ) ) assert ctx.get_query() == dedent( - """ - create api integration repo_name_api_integration + f""" + create api integration IDENTIFIER('{int_name}') api_provider = git_https_api api_allowed_prefixes = ('https://github.com/an-example-repo.git') allowed_authentication_secrets = () enabled = true - create git repository repo_name - api_integration = repo_name_api_integration + create git repository IDENTIFIER('{repo_name}') + api_integration = {int_name} origin = 'https://github.com/an-example-repo.git' """ ) @@ -287,15 +312,26 @@ def test_setup_no_secret_create_api(mock_om_describe, mock_connector, runner, mo @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli.plugins.snowpark.commands.ObjectManager.describe") +@mock.patch("snowflake.cli.plugins.snowpark.commands.ObjectManager.show") def test_setup_existing_secret_existing_api( - mock_om_describe, mock_connector, runner, mock_ctx + mock_om_show, mock_om_describe, mock_connector, runner, mock_ctx, mock_cursor ): + mock_om_show.return_value = mock_cursor([], []) mock_om_describe.side_effect = [ + # repo does not exist ProgrammingError(errno=DOES_NOT_EXIST_OR_NOT_AUTHORIZED), + # secret exists None, + # api integration exists None, ] - mock_om_describe.return_value = [None, "integration_details", "secret_details"] + mock_om_describe.return_value = [ + None, + None, + "integration_details", + None, + "secret_details", + ] ctx = mock_ctx() mock_connector.return_value = ctx @@ -319,7 +355,7 @@ def test_setup_existing_secret_existing_api( ) assert ctx.get_query() == dedent( """ - create git repository repo_name + create git repository IDENTIFIER('repo_name') api_integration = existing_api_integration origin = 'https://github.com/an-example-repo.git' git_credentials = existing_secret @@ -327,22 +363,47 @@ def test_setup_existing_secret_existing_api( ) +@pytest.mark.parametrize( + "repo_name, int_name, existing_secret_name", + [ + ("db.schema.FooRepo", "FooRepo_api_integration", "db.schema.existing_secret"), + ( + "schema.FooRepo", + "FooRepo_api_integration", + "different_schema.existing_secret", + ), + ("FooRepo", "FooRepo_api_integration", "existing_secret"), + ], +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli.plugins.snowpark.commands.ObjectManager.describe") +@mock.patch("snowflake.cli.plugins.snowpark.commands.ObjectManager.show") def test_setup_existing_secret_create_api( - mock_om_describe, mock_connector, runner, mock_ctx + mock_om_show, + mock_om_describe, + mock_connector, + runner, + mock_ctx, + mock_cursor, + repo_name, + int_name, + existing_secret_name, ): + mock_om_show.return_value = mock_cursor([], []) mock_om_describe.side_effect = [ + # repo does not exists ProgrammingError(errno=DOES_NOT_EXIST_OR_NOT_AUTHORIZED), + # chosen secret exists None, + # chosen integration does not exist ProgrammingError(errno=DOES_NOT_EXIST_OR_NOT_AUTHORIZED), ] - mock_om_describe.return_value = [None, "secret_details", None] + mock_om_describe.return_value = [None, None, "secret_details", None, None, None] ctx = mock_ctx() mock_connector.return_value = ctx - communication = "\n".join([EXAMPLE_URL, "y", "existing_secret", "", ""]) - result = runner.invoke(["git", "setup", "repo_name"], input=communication) + communication = "\n".join([EXAMPLE_URL, "y", existing_secret_name, "", ""]) + result = runner.invoke(["git", "setup", repo_name], input=communication) assert result.exit_code == 0, result.output assert result.output.startswith( @@ -350,36 +411,39 @@ def test_setup_existing_secret_create_api( [ "Origin url: https://github.com/an-example-repo.git", "Use secret for authentication? [y/N]: y", - "Secret identifier (will be created if not exists) [repo_name_secret]: existing_secret", - "Using existing secret 'existing_secret'", - "API integration identifier (will be created if not exists) [repo_name_api_integration]: ", - "API integration 'repo_name_api_integration' successfully created.", + f"Secret identifier (will be created if not exists) [FooRepo_secret]: {existing_secret_name}", + f"Using existing secret '{existing_secret_name}'", + f"API integration identifier (will be created if not exists) [{int_name}]: ", + f"API integration '{int_name}' successfully created.", ] ) ) assert ctx.get_query() == dedent( - """ - create api integration repo_name_api_integration + f""" + create api integration IDENTIFIER('{int_name}') api_provider = git_https_api api_allowed_prefixes = ('https://github.com/an-example-repo.git') - allowed_authentication_secrets = (existing_secret) + allowed_authentication_secrets = ({existing_secret_name}) enabled = true - create git repository repo_name - api_integration = repo_name_api_integration + create git repository IDENTIFIER('{repo_name}') + api_integration = {int_name} origin = 'https://github.com/an-example-repo.git' - git_credentials = existing_secret + git_credentials = {existing_secret_name} """ ) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli.plugins.snowpark.commands.ObjectManager.describe") +@mock.patch("snowflake.cli.plugins.snowpark.commands.ObjectManager.show") def test_setup_create_secret_create_api( - mock_om_describe, mock_connector, runner, mock_ctx + mock_om_show, mock_om_describe, mock_connector, runner, mock_ctx, mock_cursor ): + mock_om_show.return_value = mock_cursor([], []) mock_om_describe.side_effect = ProgrammingError( + # nothing exists errno=DOES_NOT_EXIST_OR_NOT_AUTHORIZED ) ctx = mock_ctx() @@ -408,20 +472,20 @@ def test_setup_create_secret_create_api( ) assert ctx.get_query() == dedent( """ - create secret repo_name_secret + create secret IDENTIFIER('repo_name_secret') type = password username = 'john_doe' password = 'admin123' - create api integration new_integration + create api integration IDENTIFIER('new_integration') api_provider = git_https_api api_allowed_prefixes = ('https://github.com/an-example-repo.git') allowed_authentication_secrets = (repo_name_secret) enabled = true - create git repository repo_name + create git repository IDENTIFIER('repo_name') api_integration = new_integration origin = 'https://github.com/an-example-repo.git' git_credentials = repo_name_secret @@ -429,25 +493,69 @@ def test_setup_create_secret_create_api( ) +@mock.patch("snowflake.connector.connect") +@mock.patch("snowflake.cli.plugins.snowpark.commands.ObjectManager.describe") +@mock.patch("snowflake.cli.plugins.snowpark.commands.ObjectManager.show") +def test_api_integration_and_secrets_get_unique_names( + mock_om_show, mock_om_describe, mock_connector, runner, mock_ctx, mock_cursor +): + mock_om_show.return_value = mock_cursor( + [{"name": f"repo_name_secret{x}"} for x in range(1, 3)] + + [{"name": f"repo_name_api_integration{x}"} for x in range(1, 4)] + + [{"name": "repo_name_secret"}, {"name": "repo_name_api_integration"}], + [], + ) + mock_om_describe.side_effect = [ + # repo does not exist + ProgrammingError(errno=DOES_NOT_EXIST_OR_NOT_AUTHORIZED), + # chosen secret does not exist + ProgrammingError(errno=DOES_NOT_EXIST_OR_NOT_AUTHORIZED), + # chosen api integration does not exist + ProgrammingError(errno=DOES_NOT_EXIST_OR_NOT_AUTHORIZED), + ] + ctx = mock_ctx() + mock_connector.return_value = ctx + + communication = "\n".join([EXAMPLE_URL, "y", "", "john_doe", "admin123", "", ""]) + result = runner.invoke(["git", "setup", "repo_name"], input=communication) + + assert result.exit_code == 0, result.output + assert result.output.startswith( + "\n".join( + [ + "Origin url: https://github.com/an-example-repo.git", + "Use secret for authentication? [y/N]: y", + "Secret identifier (will be created if not exists) [repo_name_secret3]: ", + "Secret 'repo_name_secret3' will be created", + "username: john_doe", + "password/token: ", + "API integration identifier (will be created if not exists) [repo_name_api_integration4]: ", + "Secret 'repo_name_secret3' successfully created.", + "API integration 'repo_name_api_integration4' successfully created.", + ] + ) + ) + + @pytest.mark.parametrize( "repository_path, expected_stage, expected_files", [ ( "@repo/branches/main/", "@repo/branches/main/", - ["@repo/branches/main/s1.sql", "@repo/branches/main/a/s3.sql"], + ["@repo/branches/main/s1.sql", "@repo/branches/main/a/S3.sql"], ), ( "@repo/branches/main/a", "@repo/branches/main/", - ["@repo/branches/main/a/s3.sql"], + ["@repo/branches/main/a/S3.sql"], ), ( "@db.schema.repo/branches/main/", "@db.schema.repo/branches/main/", [ "@db.schema.repo/branches/main/s1.sql", - "@db.schema.repo/branches/main/a/s3.sql", + "@db.schema.repo/branches/main/a/S3.sql", ], ), ( @@ -455,6 +563,16 @@ def test_setup_create_secret_create_api( "@db.schema.repo/branches/main/", ["@db.schema.repo/branches/main/s1.sql"], ), + ( + "@DB.SCHEMA.REPO/branches/main/s1.sql", + "@DB.SCHEMA.REPO/branches/main/", + ["@DB.SCHEMA.REPO/branches/main/s1.sql"], + ), + ( + "@DB.schema.REPO/branches/main/a/S3.sql", + "@DB.schema.REPO/branches/main/", + ["@DB.schema.REPO/branches/main/a/S3.sql"], + ), ], ) @mock.patch(f"{STAGE_MANAGER}._execute_query") @@ -469,7 +587,7 @@ def test_execute( ): mock_execute.return_value = mock_cursor( [ - {"name": "repo/branches/main/a/s3.sql"}, + {"name": "repo/branches/main/a/S3.sql"}, {"name": "repo/branches/main/s1.sql"}, {"name": "repo/branches/main/s2"}, ], @@ -487,6 +605,65 @@ def test_execute( assert result.output == os_agnostic_snapshot +@pytest.mark.parametrize( + "repository_path, expected_stage, expected_files", + [ + ( + "@repo/branches/main/", + "@repo/branches/main/", + [ + "@repo/branches/main/S2.sql", + "@repo/branches/main/s1.sql", + "@repo/branches/main/a/s3.sql", + ], + ), + ( + "@repo/branches/main/s1.sql", + "@repo/branches/main/", + ["@repo/branches/main/s1.sql"], + ), + ( + "@repo/branches/main/S2.sql", + "@repo/branches/main/", + ["@repo/branches/main/S2.sql"], + ), + ( + "@repo/branches/main/a/s3.sql", + "@repo/branches/main/", + ["@repo/branches/main/a/s3.sql"], + ), + ], +) +@mock.patch(f"{STAGE_MANAGER}._execute_query") +def test_execute_new_git_repository_list_files( + mock_execute, + mock_cursor, + runner, + repository_path, + expected_stage, + expected_files, + os_agnostic_snapshot, +): + mock_execute.return_value = mock_cursor( + [ + {"name": "/branches/main/s1.sql"}, + {"name": "/branches/main/S2.sql"}, + {"name": "/branches/main/a/s3.sql"}, + ], + [], + ) + + result = runner.invoke(["git", "execute", repository_path]) + + assert result.exit_code == 0, result.output + ls_call, *execute_calls = mock_execute.mock_calls + assert ls_call == mock.call(f"ls {expected_stage}", cursor_class=DictCursor) + assert execute_calls == [ + mock.call(f"execute immediate from {p}") for p in expected_files + ] + assert result.output == os_agnostic_snapshot + + @mock.patch(f"{STAGE_MANAGER}._execute_query") def test_execute_with_variables(mock_execute, mock_cursor, runner): mock_execute.return_value = mock_cursor([{"name": "repo/branches/main/s1.sql"}], []) diff --git a/tests/notebook/test_notebook_commands.py b/tests/notebook/test_notebook_commands.py index 5caa95dbd..ffa993bcd 100644 --- a/tests/notebook/test_notebook_commands.py +++ b/tests/notebook/test_notebook_commands.py @@ -15,6 +15,7 @@ from unittest import mock import typer +from snowflake.cli.api.identifiers import FQN from snowflake.cli.plugins.notebook.manager import NotebookManager @@ -24,7 +25,7 @@ def test_execute(mock_execute, runner): assert result.exit_code == 0, result.output assert result.output == "Notebook my_notebook executed.\n" - mock_execute.assert_called_once_with(notebook_name="my_notebook") + mock_execute.assert_called_once_with(notebook_name=FQN.from_string("my_notebook")) @mock.patch.object(NotebookManager, "get_url") @@ -34,7 +35,7 @@ def test_get_url(mock_url, runner): assert result.exit_code == 0, result.output assert result.output == "http://my.url\n" - mock_url.assert_called_once_with(notebook_name="my_notebook") + mock_url.assert_called_once_with(notebook_name=FQN.from_string("my_notebook")) @mock.patch.object(NotebookManager, "get_url") @@ -45,7 +46,7 @@ def test_open(mock_launch, mock_url, runner): assert result.exit_code == 0, result.output assert result.output == "http://my.url\n" - mock_url.assert_called_once_with(notebook_name="my_notebook") + mock_url.assert_called_once_with(notebook_name=FQN.from_string("my_notebook")) mock_launch.assert_called_once_with("http://my.url") @@ -61,6 +62,6 @@ def test_create(mock_create, runner): assert result.exit_code == 0, result.output mock_create.assert_called_once_with( - notebook_name=notebook_name, + notebook_name=FQN.from_string("my_notebook"), notebook_file=notebook_file, ) diff --git a/tests/notebook/test_notebook_manager.py b/tests/notebook/test_notebook_manager.py index f0eaf3cd8..d365fc931 100644 --- a/tests/notebook/test_notebook_manager.py +++ b/tests/notebook/test_notebook_manager.py @@ -17,14 +17,17 @@ from unittest.mock import MagicMock, PropertyMock import pytest +from snowflake.cli.api.identifiers import FQN from snowflake.cli.plugins.notebook.exceptions import NotebookStagePathError from snowflake.cli.plugins.notebook.manager import NotebookManager @mock.patch.object(NotebookManager, "_execute_query") def test_execute(mock_execute): - _ = NotebookManager().execute(notebook_name="MY_NOTEBOOK") - mock_execute.assert_called_once_with(query="EXECUTE NOTEBOOK MY_NOTEBOOK()") + _ = NotebookManager().execute(notebook_name=FQN.from_string("MY_NOTEBOOK")) + mock_execute.assert_called_once_with( + query="EXECUTE NOTEBOOK IDENTIFIER('MY_NOTEBOOK')()" + ) @mock.patch("snowflake.cli.plugins.notebook.manager.make_snowsight_url") @@ -32,7 +35,7 @@ def test_get_url(mock_url): mock_url.return_value = "my_url" conn_mock = MagicMock(database="nb_database", schema="nb_schema") with mock.patch.object(NotebookManager, "_conn", conn_mock): - result = NotebookManager().get_url(notebook_name="MY_NOTEBOOK") + result = NotebookManager().get_url(notebook_name=FQN.from_string("MY_NOTEBOOK")) assert result == "my_url" mock_url.assert_called_once_with( @@ -50,17 +53,17 @@ def test_create(mock_ctx, mock_execute, mock_url): with mock.patch.object(NotebookManager, "_conn", cn_mock): _ = NotebookManager().create( - notebook_name="my_notebook", + notebook_name=FQN.from_string("MY_NOTEBOOK"), notebook_file="@stage/nb file.ipynb", ) expected_query = dedent( """ - CREATE OR REPLACE NOTEBOOK nb_db.nb_schema.my_notebook + CREATE OR REPLACE NOTEBOOK IDENTIFIER('nb_db.nb_schema.MY_NOTEBOOK') FROM '@stage' QUERY_WAREHOUSE = 'MY_WH' MAIN_FILE = 'nb file.ipynb'; - - ALTER NOTEBOOK nb_db.nb_schema.my_notebook ADD LIVE VERSION FROM LAST; + // Cannot use IDENTIFIER(...) + ALTER NOTEBOOK nb_db.nb_schema.MY_NOTEBOOK ADD LIVE VERSION FROM LAST; """ ) mock_execute.assert_called_once_with(queries=expected_query) diff --git a/tests/snowpark/test_function.py b/tests/snowpark/test_function.py index d43a21ee5..9c5620a08 100644 --- a/tests/snowpark/test_function.py +++ b/tests/snowpark/test_function.py @@ -52,7 +52,7 @@ def test_deploy_function( assert result.exit_code == 0, result.output assert ctx.get_queries() == [ - "create stage if not exists MockDatabase.MockSchema.dev_deployment comment='deployments managed by Snowflake CLI'", + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.dev_deployment') comment='deployments managed by Snowflake CLI'", f"put file://{Path(project_dir).resolve()}/app.zip @MockDatabase.MockSchema.dev_deployment/my_snowpark_project" f" auto_compress=false parallel=4 overwrite=True", dedent( @@ -100,7 +100,7 @@ def test_deploy_function_with_external_access( assert result.exit_code == 0, result.output assert ctx.get_queries() == [ - "create stage if not exists MockDatabase.MockSchema.dev_deployment comment='deployments managed by Snowflake CLI'", + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.dev_deployment') comment='deployments managed by Snowflake CLI'", f"put file://{Path(project_dir).resolve()}/app.zip @MockDatabase.MockSchema.dev_deployment/my_snowpark_project" f" auto_compress=false parallel=4 overwrite=True", dedent( @@ -184,7 +184,7 @@ def test_deploy_function_no_changes( } ] assert queries == [ - "create stage if not exists MockDatabase.MockSchema.dev_deployment comment='deployments managed by Snowflake CLI'", + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.dev_deployment') comment='deployments managed by Snowflake CLI'", f"put file://{Path(project_dir).resolve()}/app.zip @MockDatabase.MockSchema.dev_deployment/my_snowpark_project auto_compress=false parallel=4 overwrite=True", ] @@ -222,7 +222,7 @@ def test_deploy_function_needs_update_because_packages_changes( } ] assert queries == [ - "create stage if not exists MockDatabase.MockSchema.dev_deployment comment='deployments managed by Snowflake CLI'", + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.dev_deployment') comment='deployments managed by Snowflake CLI'", f"put file://{Path(project_dir).resolve()}/app.zip @MockDatabase.MockSchema.dev_deployment/my_snowpark_project auto_compress=false parallel=4 overwrite=True", dedent( """\ @@ -272,7 +272,7 @@ def test_deploy_function_needs_update_because_handler_changes( } ] assert queries == [ - "create stage if not exists MockDatabase.MockSchema.dev_deployment comment='deployments managed by Snowflake CLI'", + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.dev_deployment') comment='deployments managed by Snowflake CLI'", f"put file://{Path(project_dir).resolve()}/app.zip @MockDatabase.MockSchema.dev_deployment/my_snowpark_project" f" auto_compress=false parallel=4 overwrite=True", dedent( diff --git a/tests/snowpark/test_models.py b/tests/snowpark/test_models.py index b48a3c5be..1378661fb 100644 --- a/tests/snowpark/test_models.py +++ b/tests/snowpark/test_models.py @@ -13,7 +13,11 @@ # limitations under the License. import pytest -from snowflake.cli.plugins.snowpark.models import Requirement, get_package_name +from snowflake.cli.plugins.snowpark.models import ( + Requirement, + WheelMetadata, + get_package_name, +) @pytest.mark.parametrize( @@ -58,3 +62,27 @@ def test_requirement_is_parsed_correctly(line, name, extras): ) def test_get_package_name(line, name): assert get_package_name(line) == name + + +def test_wheel_metadata_parsing(test_root_path): + from snowflake.cli.api.secure_path import SecurePath + from snowflake.cli.plugins.snowpark.zipper import zip_dir + + with SecurePath.temporary_directory() as tmpdir: + wheel_path = tmpdir / "Zendesk-1.1.1-py3-none-any.whl" + + # prepare .whl package + package_dir = tmpdir / "ZendeskWhl" + package_dir.mkdir() + package_src = ( + SecurePath(test_root_path) / "test_data" / "local_packages" / ".packages" + ) + for srcdir in ["zendesk", "Zendesk-1.1.1.dist-info"]: + (package_src / srcdir).copy(package_dir.path) + zip_dir(source=package_dir.path, dest_zip=wheel_path.path) + + # check metadata + meta = WheelMetadata.from_wheel(wheel_path.path) + assert meta.name == "zendesk" + assert meta.wheel_path == wheel_path.path + assert meta.dependencies == ["httplib2", "simplejson"] diff --git a/tests/snowpark/test_package.py b/tests/snowpark/test_package.py index f3b6564c9..60ab0bf50 100644 --- a/tests/snowpark/test_package.py +++ b/tests/snowpark/test_package.py @@ -89,10 +89,12 @@ def test_package_create( assert os.path.isfile("totally-awesome-package.zip"), result.output @mock.patch("snowflake.cli.plugins.snowpark.package.manager.StageManager") + @mock.patch("snowflake.cli.plugins.snowpark.package.manager.FQN.from_string") @mock.patch("snowflake.connector.connect") def test_package_upload( self, mock_connector, + _, mock_stage_manager, package_file: str, runner, @@ -140,7 +142,9 @@ def test_package_upload_to_path( assert result.exit_code == 0 assert mock_execute_queries.call_count == 2 create, put = mock_execute_queries.call_args_list - assert create.args[0] == "create stage if not exists db.schema.stage" + assert ( + create.args[0] == "create stage if not exists IDENTIFIER('db.schema.stage')" + ) assert "db.schema.stage/path/to/file" in put.args[0] @pytest.mark.parametrize( diff --git a/tests/snowpark/test_procedure.py b/tests/snowpark/test_procedure.py index 885421afc..affeb528f 100644 --- a/tests/snowpark/test_procedure.py +++ b/tests/snowpark/test_procedure.py @@ -21,6 +21,7 @@ import pytest from snowflake.cli.api.constants import ObjectType from snowflake.cli.api.errno import DOES_NOT_EXIST_OR_NOT_AUTHORIZED +from snowflake.cli.api.identifiers import FQN from snowflake.connector import ProgrammingError from tests_common import IS_WINDOWS @@ -75,7 +76,7 @@ def test_deploy_procedure( ] ) assert ctx.get_queries() == [ - "create stage if not exists MockDatabase.MockSchema.dev_deployment comment='deployments managed by Snowflake CLI'", + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.dev_deployment') comment='deployments managed by Snowflake CLI'", f"put file://{Path(tmp).resolve()}/app.zip @MockDatabase.MockSchema.dev_deployment/my_snowpark_project auto_compress=false parallel=4 overwrite=True", dedent( """\ @@ -139,12 +140,12 @@ def test_deploy_procedure_with_external_access( [ call( object_type=str(ObjectType.PROCEDURE), - name="MockDatabase.MockSchema.procedureName(string)", + fqn=FQN.from_string("MockDatabase.MockSchema.procedureName(string)"), ), ] ) assert ctx.get_queries() == [ - "create stage if not exists MockDatabase.MockSchema.dev_deployment comment='deployments managed by Snowflake CLI'", + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.dev_deployment') comment='deployments managed by Snowflake CLI'", f"put file://{Path(project_dir).resolve()}/app.zip @MockDatabase.MockSchema.dev_deployment/my_snowpark_project" f" auto_compress=false parallel=4 overwrite=True", dedent( diff --git a/tests/spcs/test_compute_pool.py b/tests/spcs/test_compute_pool.py index 382393c87..e7469b724 100644 --- a/tests/spcs/test_compute_pool.py +++ b/tests/spcs/test_compute_pool.py @@ -18,6 +18,7 @@ import pytest from click import ClickException from snowflake.cli.api.constants import ObjectType +from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.project.util import to_string_literal from snowflake.cli.plugins.spcs.common import ( NoPropertiesProvidedError, @@ -284,7 +285,8 @@ def test_resume_cli(mock_resume, mock_cursor, runner): def test_compute_pool_name_callback(mock_is_valid): name = "test_pool" mock_is_valid.return_value = True - assert _compute_pool_name_callback(name) == name + fqn = FQN.from_string(name) + assert _compute_pool_name_callback(fqn) == fqn @patch("snowflake.cli.plugins.spcs.compute_pool.commands.is_valid_object_name") @@ -292,7 +294,7 @@ def test_compute_pool_name_callback_invalid(mock_is_valid): name = "test_pool" mock_is_valid.return_value = False with pytest.raises(ClickException) as e: - _compute_pool_name_callback(name) + _compute_pool_name_callback(FQN.from_string(name)) assert "is not a valid compute pool name." in e.value.message diff --git a/tests/spcs/test_services.py b/tests/spcs/test_services.py index 44b4ce71a..f6ac7550b 100644 --- a/tests/spcs/test_services.py +++ b/tests/spcs/test_services.py @@ -20,6 +20,7 @@ import pytest from click import ClickException from snowflake.cli.api.constants import ObjectType +from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.project.util import to_string_literal from snowflake.cli.plugins.object.common import Tag from snowflake.cli.plugins.spcs.common import NoPropertiesProvidedError @@ -617,14 +618,15 @@ def test_invalid_service_name(runner): invalid_service_name = "account.db.schema.name" result = runner.invoke(["spcs", "service", "status", invalid_service_name]) assert result.exit_code == 1 - assert f"'{invalid_service_name}' is not a valid service name." in result.output + assert f"'{invalid_service_name}' is not valid" in result.output @patch("snowflake.cli.plugins.spcs.services.commands.is_valid_object_name") def test_service_name_parser(mock_is_valid_object_name): service_name = "db.schema.test_service" mock_is_valid_object_name.return_value = True - assert _service_name_callback(service_name) == service_name + fqn = FQN.from_string(service_name) + assert _service_name_callback(fqn) == fqn mock_is_valid_object_name.assert_called_once_with( service_name, max_depth=2, allow_quoted=False ) @@ -632,10 +634,10 @@ def test_service_name_parser(mock_is_valid_object_name): @patch("snowflake.cli.plugins.spcs.services.commands.is_valid_object_name") def test_service_name_parser_invalid_object_name(mock_is_valid_object_name): - invalid_service_name = "account.db.schema.test_service" + invalid_service_name = '"db.schema.test_service"' mock_is_valid_object_name.return_value = False with pytest.raises(ClickException) as e: - _service_name_callback(invalid_service_name) + _service_name_callback(FQN.from_string(invalid_service_name)) assert f"'{invalid_service_name}' is not a valid service name." in e.value.message diff --git a/tests/stage/__snapshots__/test_stage.ambr b/tests/stage/__snapshots__/test_stage.ambr index 823f08b8c..af8887291 100644 --- a/tests/stage/__snapshots__/test_stage.ambr +++ b/tests/stage/__snapshots__/test_stage.ambr @@ -23,21 +23,76 @@ ''' # --- +# name: test_execute[@DB.SCHEMA.EXE/s1.sql-@DB.SCHEMA.EXE-expected_files19] + ''' + SUCCESS - @DB.SCHEMA.EXE/s1.sql + +-----------------------------------------+ + | File | Status | Error | + |-----------------------+---------+-------| + | @DB.SCHEMA.EXE/s1.sql | SUCCESS | None | + +-----------------------------------------+ + + ''' +# --- +# name: test_execute[@DB.SCHEMA.EXE/s1.sql-@db.schema.exe-expected_files19] + ''' + SUCCESS - @db.schema.exe/s1.sql + +-----------------------------------------+ + | File | Status | Error | + |-----------------------+---------+-------| + | @db.schema.exe/s1.sql | SUCCESS | None | + +-----------------------------------------+ + + ''' +# --- +# name: test_execute[@DB.schema.EXE/a/S3.sql-@DB.schema.EXE-expected_files20] + ''' + SUCCESS - @DB.schema.EXE/a/S3.sql + +-------------------------------------------+ + | File | Status | Error | + |-------------------------+---------+-------| + | @DB.schema.EXE/a/S3.sql | SUCCESS | None | + +-------------------------------------------+ + + ''' +# --- +# name: test_execute[@DB.schema.EXE/a/S3.sql-@db.schema.exe-expected_files20] + ''' + SUCCESS - @db.schema.exe/a/S3.sql + +-------------------------------------------+ + | File | Status | Error | + |-------------------------+---------+-------| + | @db.schema.exe/a/S3.sql | SUCCESS | None | + +-------------------------------------------+ + + ''' +# --- # name: test_execute[@db.schema.exe-@db.schema.exe-expected_files15] ''' SUCCESS - @db.schema.exe/s1.sql - SUCCESS - @db.schema.exe/a/s3.sql + SUCCESS - @db.schema.exe/a/S3.sql SUCCESS - @db.schema.exe/a/b/s4.sql +---------------------------------------------+ | File | Status | Error | |---------------------------+---------+-------| | @db.schema.exe/s1.sql | SUCCESS | None | - | @db.schema.exe/a/s3.sql | SUCCESS | None | + | @db.schema.exe/a/S3.sql | SUCCESS | None | | @db.schema.exe/a/b/s4.sql | SUCCESS | None | +---------------------------------------------+ ''' # --- +# name: test_execute[@db.schema.exe/a/S3.sql-@db.schema.exe-expected_files18] + ''' + SUCCESS - @db.schema.exe/a/S3.sql + +-------------------------------------------+ + | File | Status | Error | + |-------------------------+---------+-------| + | @db.schema.exe/a/S3.sql | SUCCESS | None | + +-------------------------------------------+ + + ''' +# --- # name: test_execute[@db.schema.exe/s1.sql-@db.schema.exe-expected_files17] ''' SUCCESS - @db.schema.exe/s1.sql @@ -52,13 +107,13 @@ # name: test_execute[@exe-@exe-expected_files0] ''' SUCCESS - @exe/s1.sql - SUCCESS - @exe/a/s3.sql + SUCCESS - @exe/a/S3.sql SUCCESS - @exe/a/b/s4.sql +-----------------------------------+ | File | Status | Error | |-----------------+---------+-------| | @exe/s1.sql | SUCCESS | None | - | @exe/a/s3.sql | SUCCESS | None | + | @exe/a/S3.sql | SUCCESS | None | | @exe/a/b/s4.sql | SUCCESS | None | +-----------------------------------+ @@ -67,13 +122,13 @@ # name: test_execute[db.schema.exe-@db.schema.exe-expected_files16] ''' SUCCESS - @db.schema.exe/s1.sql - SUCCESS - @db.schema.exe/a/s3.sql + SUCCESS - @db.schema.exe/a/S3.sql SUCCESS - @db.schema.exe/a/b/s4.sql +---------------------------------------------+ | File | Status | Error | |---------------------------+---------+-------| | @db.schema.exe/s1.sql | SUCCESS | None | - | @db.schema.exe/a/s3.sql | SUCCESS | None | + | @db.schema.exe/a/S3.sql | SUCCESS | None | | @db.schema.exe/a/b/s4.sql | SUCCESS | None | +---------------------------------------------+ @@ -82,13 +137,13 @@ # name: test_execute[exe-@exe-expected_files1] ''' SUCCESS - @exe/s1.sql - SUCCESS - @exe/a/s3.sql + SUCCESS - @exe/a/S3.sql SUCCESS - @exe/a/b/s4.sql +-----------------------------------+ | File | Status | Error | |-----------------+---------+-------| | @exe/s1.sql | SUCCESS | None | - | @exe/a/s3.sql | SUCCESS | None | + | @exe/a/S3.sql | SUCCESS | None | | @exe/a/b/s4.sql | SUCCESS | None | +-----------------------------------+ @@ -97,13 +152,13 @@ # name: test_execute[exe/*-@exe-expected_files3] ''' SUCCESS - @exe/s1.sql - SUCCESS - @exe/a/s3.sql + SUCCESS - @exe/a/S3.sql SUCCESS - @exe/a/b/s4.sql +-----------------------------------+ | File | Status | Error | |-----------------+---------+-------| | @exe/s1.sql | SUCCESS | None | - | @exe/a/s3.sql | SUCCESS | None | + | @exe/a/S3.sql | SUCCESS | None | | @exe/a/b/s4.sql | SUCCESS | None | +-----------------------------------+ @@ -112,13 +167,13 @@ # name: test_execute[exe/*.sql-@exe-expected_files4] ''' SUCCESS - @exe/s1.sql - SUCCESS - @exe/a/s3.sql + SUCCESS - @exe/a/S3.sql SUCCESS - @exe/a/b/s4.sql +-----------------------------------+ | File | Status | Error | |-----------------+---------+-------| | @exe/s1.sql | SUCCESS | None | - | @exe/a/s3.sql | SUCCESS | None | + | @exe/a/S3.sql | SUCCESS | None | | @exe/a/b/s4.sql | SUCCESS | None | +-----------------------------------+ @@ -127,13 +182,13 @@ # name: test_execute[exe/-@exe-expected_files2] ''' SUCCESS - @exe/s1.sql - SUCCESS - @exe/a/s3.sql + SUCCESS - @exe/a/S3.sql SUCCESS - @exe/a/b/s4.sql +-----------------------------------+ | File | Status | Error | |-----------------+---------+-------| | @exe/s1.sql | SUCCESS | None | - | @exe/a/s3.sql | SUCCESS | None | + | @exe/a/S3.sql | SUCCESS | None | | @exe/a/b/s4.sql | SUCCESS | None | +-----------------------------------+ @@ -141,12 +196,12 @@ # --- # name: test_execute[exe/a-@exe-expected_files5] ''' - SUCCESS - @exe/a/s3.sql + SUCCESS - @exe/a/S3.sql SUCCESS - @exe/a/b/s4.sql +-----------------------------------+ | File | Status | Error | |-----------------+---------+-------| - | @exe/a/s3.sql | SUCCESS | None | + | @exe/a/S3.sql | SUCCESS | None | | @exe/a/b/s4.sql | SUCCESS | None | +-----------------------------------+ @@ -154,12 +209,12 @@ # --- # name: test_execute[exe/a/*-@exe-expected_files7] ''' - SUCCESS - @exe/a/s3.sql + SUCCESS - @exe/a/S3.sql SUCCESS - @exe/a/b/s4.sql +-----------------------------------+ | File | Status | Error | |-----------------+---------+-------| - | @exe/a/s3.sql | SUCCESS | None | + | @exe/a/S3.sql | SUCCESS | None | | @exe/a/b/s4.sql | SUCCESS | None | +-----------------------------------+ @@ -167,12 +222,12 @@ # --- # name: test_execute[exe/a/*.sql-@exe-expected_files8] ''' - SUCCESS - @exe/a/s3.sql + SUCCESS - @exe/a/S3.sql SUCCESS - @exe/a/b/s4.sql +-----------------------------------+ | File | Status | Error | |-----------------+---------+-------| - | @exe/a/s3.sql | SUCCESS | None | + | @exe/a/S3.sql | SUCCESS | None | | @exe/a/b/s4.sql | SUCCESS | None | +-----------------------------------+ @@ -180,12 +235,12 @@ # --- # name: test_execute[exe/a/-@exe-expected_files6] ''' - SUCCESS - @exe/a/s3.sql + SUCCESS - @exe/a/S3.sql SUCCESS - @exe/a/b/s4.sql +-----------------------------------+ | File | Status | Error | |-----------------+---------+-------| - | @exe/a/s3.sql | SUCCESS | None | + | @exe/a/S3.sql | SUCCESS | None | | @exe/a/b/s4.sql | SUCCESS | None | +-----------------------------------+ diff --git a/tests/stage/test_stage.py b/tests/stage/test_stage.py index 395a2f291..10876564d 100644 --- a/tests/stage/test_stage.py +++ b/tests/stage/test_stage.py @@ -499,7 +499,9 @@ def test_stage_create(mock_execute, runner, mock_cursor): mock_execute.return_value = mock_cursor(["row"], []) result = runner.invoke(["stage", "create", "-c", "empty", "stageName"]) assert result.exit_code == 0, result.output - mock_execute.assert_called_once_with("create stage if not exists stageName") + mock_execute.assert_called_once_with( + "create stage if not exists IDENTIFIER('stageName')" + ) @mock.patch(f"{STAGE_MANAGER}._execute_query") @@ -507,7 +509,9 @@ def test_stage_create_quoted(mock_execute, runner, mock_cursor): mock_execute.return_value = mock_cursor(["row"], []) result = runner.invoke(["stage", "create", "-c", "empty", '"stage name"']) assert result.exit_code == 0, result.output - mock_execute.assert_called_once_with('create stage if not exists "stage name"') + mock_execute.assert_called_once_with( + """create stage if not exists IDENTIFIER('"stage name"')""" + ) @mock.patch("snowflake.cli.plugins.object.commands.ObjectManager._execute_query") @@ -515,7 +519,7 @@ def test_stage_drop(mock_execute, runner, mock_cursor): mock_execute.return_value = mock_cursor(["row"], []) result = runner.invoke(["object", "drop", "stage", "stageName", "-c", "empty"]) assert result.exit_code == 0, result.output - mock_execute.assert_called_once_with("drop stage stageName") + mock_execute.assert_called_once_with("drop stage IDENTIFIER('stageName')") @mock.patch("snowflake.cli.plugins.object.commands.ObjectManager._execute_query") @@ -523,7 +527,7 @@ def test_stage_drop_quoted(mock_execute, runner, mock_cursor): mock_execute.return_value = mock_cursor(["row"], []) result = runner.invoke(["object", "drop", "stage", '"stage name"', "-c", "empty"]) assert result.exit_code == 0, result.output - mock_execute.assert_called_once_with('drop stage "stage name"') + mock_execute.assert_called_once_with("""drop stage IDENTIFIER('"stage name"')""") @mock.patch(f"{STAGE_MANAGER}._execute_query") @@ -744,15 +748,15 @@ def test_stage_internal_put_quoted_path( @pytest.mark.parametrize( "stage_path, expected_stage, expected_files", [ - ("@exe", "@exe", ["@exe/s1.sql", "@exe/a/s3.sql", "@exe/a/b/s4.sql"]), - ("exe", "@exe", ["@exe/s1.sql", "@exe/a/s3.sql", "@exe/a/b/s4.sql"]), - ("exe/", "@exe", ["@exe/s1.sql", "@exe/a/s3.sql", "@exe/a/b/s4.sql"]), - ("exe/*", "@exe", ["@exe/s1.sql", "@exe/a/s3.sql", "@exe/a/b/s4.sql"]), - ("exe/*.sql", "@exe", ["@exe/s1.sql", "@exe/a/s3.sql", "@exe/a/b/s4.sql"]), - ("exe/a", "@exe", ["@exe/a/s3.sql", "@exe/a/b/s4.sql"]), - ("exe/a/", "@exe", ["@exe/a/s3.sql", "@exe/a/b/s4.sql"]), - ("exe/a/*", "@exe", ["@exe/a/s3.sql", "@exe/a/b/s4.sql"]), - ("exe/a/*.sql", "@exe", ["@exe/a/s3.sql", "@exe/a/b/s4.sql"]), + ("@exe", "@exe", ["@exe/s1.sql", "@exe/a/S3.sql", "@exe/a/b/s4.sql"]), + ("exe", "@exe", ["@exe/s1.sql", "@exe/a/S3.sql", "@exe/a/b/s4.sql"]), + ("exe/", "@exe", ["@exe/s1.sql", "@exe/a/S3.sql", "@exe/a/b/s4.sql"]), + ("exe/*", "@exe", ["@exe/s1.sql", "@exe/a/S3.sql", "@exe/a/b/s4.sql"]), + ("exe/*.sql", "@exe", ["@exe/s1.sql", "@exe/a/S3.sql", "@exe/a/b/s4.sql"]), + ("exe/a", "@exe", ["@exe/a/S3.sql", "@exe/a/b/s4.sql"]), + ("exe/a/", "@exe", ["@exe/a/S3.sql", "@exe/a/b/s4.sql"]), + ("exe/a/*", "@exe", ["@exe/a/S3.sql", "@exe/a/b/s4.sql"]), + ("exe/a/*.sql", "@exe", ["@exe/a/S3.sql", "@exe/a/b/s4.sql"]), ("exe/a/b", "@exe", ["@exe/a/b/s4.sql"]), ("exe/a/b/", "@exe", ["@exe/a/b/s4.sql"]), ("exe/a/b/*", "@exe", ["@exe/a/b/s4.sql"]), @@ -764,7 +768,7 @@ def test_stage_internal_put_quoted_path( "@db.schema.exe", [ "@db.schema.exe/s1.sql", - "@db.schema.exe/a/s3.sql", + "@db.schema.exe/a/S3.sql", "@db.schema.exe/a/b/s4.sql", ], ), @@ -773,11 +777,14 @@ def test_stage_internal_put_quoted_path( "@db.schema.exe", [ "@db.schema.exe/s1.sql", - "@db.schema.exe/a/s3.sql", + "@db.schema.exe/a/S3.sql", "@db.schema.exe/a/b/s4.sql", ], ), ("@db.schema.exe/s1.sql", "@db.schema.exe", ["@db.schema.exe/s1.sql"]), + ("@db.schema.exe/a/S3.sql", "@db.schema.exe", ["@db.schema.exe/a/S3.sql"]), + ("@DB.SCHEMA.EXE/s1.sql", "@DB.SCHEMA.EXE", ["@DB.SCHEMA.EXE/s1.sql"]), + ("@DB.schema.EXE/a/S3.sql", "@DB.schema.EXE", ["@DB.schema.EXE/a/S3.sql"]), ], ) @mock.patch(f"{STAGE_MANAGER}._execute_query") @@ -792,7 +799,7 @@ def test_execute( ): mock_execute.return_value = mock_cursor( [ - {"name": "exe/a/s3.sql"}, + {"name": "exe/a/S3.sql"}, {"name": "exe/a/b/s4.sql"}, {"name": "exe/s1.sql"}, {"name": "exe/s2"}, diff --git a/tests/streamlit/test_commands.py b/tests/streamlit/test_commands.py index 27047c91d..cf1dae239 100644 --- a/tests/streamlit/test_commands.py +++ b/tests/streamlit/test_commands.py @@ -44,7 +44,7 @@ def test_describe_streamlit(mock_connector, runner, mock_ctx): assert result.exit_code == 0, result.output assert ctx.get_queries() == [ - f"describe streamlit {STREAMLIT_NAME}", + f"describe streamlit IDENTIFIER('{STREAMLIT_NAME}')", ] @@ -86,7 +86,7 @@ def test_deploy_only_streamlit_file( assert result.exit_code == 0, result.output assert ctx.get_queries() == [ - "create stage if not exists MockDatabase.MockSchema.streamlit", + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.streamlit')", _put_query( "streamlit_app.py", "@MockDatabase.MockSchema.streamlit/test_streamlit" ), @@ -137,7 +137,7 @@ def test_deploy_only_streamlit_file_no_stage( assert result.exit_code == 0, result.output assert ctx.get_queries() == [ - "create stage if not exists MockDatabase.MockSchema.streamlit", + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.streamlit')", _put_query( "streamlit_app.py", "@MockDatabase.MockSchema.streamlit/test_streamlit" ), @@ -187,7 +187,7 @@ def test_deploy_only_streamlit_file_replace( assert result.exit_code == 0, result.output assert ctx.get_queries() == [ - "create stage if not exists MockDatabase.MockSchema.streamlit", + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.streamlit')", _put_query( "streamlit_app.py", "@MockDatabase.MockSchema.streamlit/test_streamlit" ), @@ -256,7 +256,7 @@ def test_deploy_streamlit_and_environment_files( root_path = f"@MockDatabase.MockSchema.streamlit/{STREAMLIT_NAME}" assert result.exit_code == 0, result.output assert ctx.get_queries() == [ - "create stage if not exists MockDatabase.MockSchema.streamlit", + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.streamlit')", _put_query("streamlit_app.py", root_path), _put_query("environment.yml", root_path), dedent( @@ -297,7 +297,7 @@ def test_deploy_streamlit_and_pages_files( root_path = f"@MockDatabase.MockSchema.streamlit/{STREAMLIT_NAME}" assert result.exit_code == 0, result.output assert ctx.get_queries() == [ - "create stage if not exists MockDatabase.MockSchema.streamlit", + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.streamlit')", _put_query("streamlit_app.py", root_path), _put_query("pages/*.py", f"{root_path}/pages"), dedent( @@ -337,7 +337,7 @@ def test_deploy_all_streamlit_files( root_path = f"@MockDatabase.MockSchema.streamlit/{STREAMLIT_NAME}" assert result.exit_code == 0, result.output assert ctx.get_queries() == [ - "create stage if not exists MockDatabase.MockSchema.streamlit", + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.streamlit')", _put_query("streamlit_app.py", root_path), _put_query("environment.yml", root_path), _put_query("pages/*.py", f"{root_path}/pages"), @@ -382,7 +382,7 @@ def test_deploy_put_files_on_stage( root_path = f"@MockDatabase.MockSchema.streamlit_stage/{STREAMLIT_NAME}" assert result.exit_code == 0, result.output assert ctx.get_queries() == [ - "create stage if not exists MockDatabase.MockSchema.streamlit_stage", + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.streamlit_stage')", _put_query("streamlit_app.py", root_path), _put_query("environment.yml", root_path), _put_query("pages/*.py", f"{root_path}/pages"), @@ -423,7 +423,7 @@ def test_deploy_all_streamlit_files_not_defaults( root_path = f"@MockDatabase.MockSchema.streamlit_stage/{STREAMLIT_NAME}" assert result.exit_code == 0, result.output assert ctx.get_queries() == [ - "create stage if not exists MockDatabase.MockSchema.streamlit_stage", + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.streamlit_stage')", _put_query("main.py", root_path), _put_query("streamlit_environment.yml", root_path), _put_query("streamlit_pages/*.py", f"{root_path}/pages"), @@ -726,7 +726,7 @@ def test_drop_streamlit(mock_connector, runner, mock_ctx): result = runner.invoke(["object", "drop", "streamlit", STREAMLIT_NAME]) assert result.exit_code == 0, result.output - assert ctx.get_query() == f"drop streamlit {STREAMLIT_NAME}" + assert ctx.get_query() == f"drop streamlit IDENTIFIER('{STREAMLIT_NAME}')" @mock.patch( diff --git a/tests/test_connection.py b/tests/test_connection.py index 26d4ad8d1..cc1085ff8 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -353,7 +353,7 @@ def test_connection_test(mock_connect, mock_om, runner): temporary_connection=False, mfa_passcode=None, enable_diag=False, - diag_log_path="/tmp", + diag_log_path=Path("/tmp"), diag_allowlist_path=None, connection_name="full", account=None, @@ -896,7 +896,7 @@ def test_connection_test_diag_report(mock_connect, mock_om, runner): temporary_connection=False, mfa_passcode=None, enable_diag=True, - diag_log_path="/tmp", + diag_log_path=Path("/tmp"), diag_allowlist_path=None, connection_name="full", account=None, @@ -912,3 +912,16 @@ def test_connection_test_diag_report(mock_connect, mock_om, runner): role=None, warehouse=None, ) + + +@mock.patch("snowflake.cli.plugins.connection.commands.ObjectManager") +@mock.patch("snowflake.cli.app.snow_connector.connect_to_snowflake") +def test_diag_log_path_default_is_actual_tempdir(mock_connect, mock_om, runner): + from snowflake.cli.api.commands.flags import _DIAG_LOG_DEFAULT_VALUE + + result = runner.invoke(["connection", "test", "-c", "full", "--enable-diag"]) + assert result.exit_code == 0, result.output + assert mock_connect.call_args.kwargs["diag_log_path"] not in [ + _DIAG_LOG_DEFAULT_VALUE, + Path(_DIAG_LOG_DEFAULT_VALUE), + ] diff --git a/tests/test_sql.py b/tests/test_sql.py index bd57bb21a..682cdc5c4 100644 --- a/tests/test_sql.py +++ b/tests/test_sql.py @@ -143,7 +143,7 @@ def test_sql_overrides_connection_configuration(mock_conn, runner, mock_cursor): temporary_connection=False, mfa_passcode=None, enable_diag=False, - diag_log_path="/tmp", + diag_log_path=Path("/tmp"), diag_allowlist_path=None, connection_name="connectionName", account="accountnameValue", diff --git a/tests_integration/__snapshots__/test_git.ambr b/tests_integration/__snapshots__/test_git.ambr index 984e3e5a1..a3903cf7e 100644 --- a/tests_integration/__snapshots__/test_git.ambr +++ b/tests_integration/__snapshots__/test_git.ambr @@ -8,3 +8,12 @@ }), ]) # --- +# name: test_execute_with_name_in_pascal_case + list([ + dict({ + 'Error': None, + 'File': '@SNOWCLI_TESTING_REPO/branches/main/tests_integration/test_data/projects/stage_execute/ScriptInPascalCase.sql', + 'Status': 'SUCCESS', + }), + ]) +# --- diff --git a/tests_integration/test_git.py b/tests_integration/test_git.py index 2352f432a..def13c6a7 100644 --- a/tests_integration/test_git.py +++ b/tests_integration/test_git.py @@ -258,6 +258,22 @@ def test_copy_error(runner, sf_git_repository): ) +@pytest.mark.integration +def test_execute_with_name_in_pascal_case( + runner, test_database, sf_git_repository, snapshot +): + result = runner.invoke_with_connection_json( + [ + "git", + "execute", + f"@{sf_git_repository}/branches/main/tests_integration/test_data/projects/stage_execute/ScriptInPascalCase.sql", + ] + ) + + assert result.exit_code == 0 + assert result.json == snapshot + + @pytest.mark.integration def test_execute(runner, test_database, sf_git_repository, snapshot): result = runner.invoke_with_connection_json( diff --git a/tests_integration/test_object.py b/tests_integration/test_object.py index 1a8bc005d..5fae070a6 100644 --- a/tests_integration/test_object.py +++ b/tests_integration/test_object.py @@ -314,7 +314,9 @@ def test_create_error_schema_not_exist(runner, test_database): @mock.patch.dict(os.environ, os.environ, clear=True) def test_create_error_undefined_database(runner): # undefined database - del os.environ["SNOWFLAKE_CONNECTIONS_INTEGRATION_DATABASE"] + database_environment_variable = "SNOWFLAKE_CONNECTIONS_INTEGRATION_DATABASE" + if database_environment_variable in os.environ: + del os.environ[database_environment_variable] result = runner.invoke_with_connection( ["object", "create", "schema", f"name=test_schema"] diff --git a/tests_integration/test_package.py b/tests_integration/test_package.py index a6c219b3a..d7f3a0029 100644 --- a/tests_integration/test_package.py +++ b/tests_integration/test_package.py @@ -210,6 +210,30 @@ def test_package_with_native_libraries(self, directory_for_test, runner): assert result.exit_code == 1 assert "at https://support.anaconda.com/" in result.output + @pytest.mark.integration + def test_package_with_capital_letters(self, directory_for_test, runner): + # TODO: change to package controlled by SF, for example dummy-package-with-Capital-Letters + package_name = "Zendesk" + result = runner.invoke( + [ + "snowpark", + "package", + "create", + package_name, + "--ignore-anaconda", + "--allow-shared-libraries", + ] + ) + assert result.exit_code == 0 + zipfile = f"{package_name.lower()}.zip" + assert Path(zipfile).exists() + files = self._get_filenames_from_zip(zipfile) + assert any( + file.startswith(package_name) and file.endswith("dist-info/") + for file in files + ) + + @pytest.mark.integration @pytest.mark.integration def test_incorrect_input(self, runner): with pytest.raises(InvalidRequirement) as err: