diff --git a/.github/workflows/test_integration_spcs.yaml b/.github/workflows/test_integration_spcs.yaml index 00973e2821..1f60f5b6ac 100644 --- a/.github/workflows/test_integration_spcs.yaml +++ b/.github/workflows/test_integration_spcs.yaml @@ -1,6 +1,9 @@ name: SPCS Integration testing on: + pull_request: + branches: + - release* push: tags: - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 0641ea823a..3a7e49f969 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -18,6 +18,35 @@ ## Deprecations +## New additions + +## Fixes and improvements + + +# v3.7.2 + +## Deprecations + +## New additions + +## Fixes and improvements +* Fix error appearing on help messages after click BCR update. + + +# v3.7.1 + +## Deprecations + +## New additions + +## Fixes and improvements +* Fix certificate connection issues. +* Fix `snow spcs image-registry login` slow query problem. + +# v3.7.0 + +## Deprecations + ## New additions * Added `--prune` flag to `deploy` commands, which removes files that exist in the stage, but not in the local filesystem. @@ -25,6 +54,7 @@ * Added `snow helper check-snowsql-env-vars` which reports environment variables from SnowSQL with replacements in CLI. ## Fixes and improvements +* Updated MacOS postinstall script to update PATH if snow not exist. # v3.6.0 diff --git a/pyproject.toml b/pyproject.toml index ad9868837d..0debbdfd6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ requires-python = ">=3.10" description = "Snowflake CLI" readme = "README.md" dependencies = [ + "click==8.1.8", "GitPython==3.1.44", "jinja2==3.1.6", "packaging", @@ -36,7 +37,7 @@ dependencies = [ "requirements-parser==0.11.0", "rich==14.0.0", "setuptools==78.1.0", - "snowflake-connector-python[secure-local-storage]==3.14.0", + "snowflake-connector-python[secure-local-storage]==3.15.0", 'snowflake-snowpark-python>=1.15.0,<1.26.0;python_version < "3.12"', 'snowflake.core==1.2.0; python_version < "3.12"', "tomlkit==0.13.2", @@ -108,7 +109,6 @@ features = ["development", "packaging"] build-isolated-binary = ["python scripts/packaging/build_isolated_binary_with_hatch.py"] build-binaries = ["./scripts/packaging/build_binaries.sh"] build-packages = ["./scripts/packaging/build_packages.sh"] -package-darwin-binaries = ["./scripts/packaging/build_darwin_package.sh"] build-all = [ "./scripts/packaging/build_binaries.sh", "./scripts/packaging/build_packages.sh", diff --git a/scripts/packaging/build_darwin_package.sh b/scripts/packaging/build_darwin_package.sh index b4cf4c9c1e..cbcbe3a81a 100755 --- a/scripts/packaging/build_darwin_package.sh +++ b/scripts/packaging/build_darwin_package.sh @@ -1,19 +1,44 @@ #!/usr/bin/env bash set -xeuo pipefail -git config --global --add safe.directory /snowflake-cli -brew install -q tree - -ROOT_DIR=$(git rev-parse --show-toplevel) -PACKAGING_DIR=$ROOT_DIR/scripts/packaging - SYSTEM=$(uname -s | tr '[:upper:]' '[:lower:]') MACHINE=$(uname -m | tr '[:upper:]' '[:lower:]') PLATFORM="${SYSTEM}-${MACHINE}" +echo "--- creating virtualenv ---" +python3.11 -m venv venv +. venv/bin/activate +python --version + +echo "--- installing dependencies ---" +pip install hatch + +# install cargo +if [[ ${MACHINE} == "arm64" ]]; then + echo "installing cargo on arm64" + curl https://sh.rustup.rs -sSf | bash -s -- -y +elif [[ ${MACHINE} == "x86_64" ]]; then + echo "installing cargo on x86_64" + curl https://sh.rustup.rs -sSf | bash -s -- -y --no-modify-path + source $HOME/.cargo/env +else + echo "Unsupported machine: ${MACHINE}" + exit 1 +fi +rustup default stable + + +echo "--- setup variables ---" +BRANCH=${branch} +REVISION=$(git rev-parse ${svnRevision}) CLI_VERSION=$(hatch version) +STAGE_URL="s3://sfc-eng-jenkins/repository/snowflake-cli/${releaseType}/${SYSTEM}_${MACHINE}/${REVISION}/" + +ROOT_DIR=$(git rev-parse --show-toplevel) +PACKAGING_DIR=$ROOT_DIR/scripts/packaging DIST_DIR=$ROOT_DIR/dist + BINARY_NAME="snow-${CLI_VERSION}" APP_NAME="SnowflakeCLI.app" APP_DIR=$DIST_DIR/app @@ -21,6 +46,7 @@ APP_SCRIPTS=$APP_DIR/scripts CODESIGN_IDENTITY="Developer ID Application: Snowflake Computing INC. (W4NT6CRQ7U)" PRODUCTSIGN_IDENTITY="Developer ID Installer: Snowflake Computing INC. (W4NT6CRQ7U)" + loginfo() { logger -s -p INFO -- $1 } @@ -29,26 +55,6 @@ clean_build_workspace() { rm -rf $DIST_DIR || true } -install_cargo() { - curl https://sh.rustup.rs -sSf > rustup-init.sh - - if [[ ${MACHINE} == "arm64" ]]; then - sudo bash rustup-init.sh -y - . $HOME/.cargo/env - elif [[ ${MACHINE} == "x86_64" ]]; then - export CARGO_HOME="$HOME/.cargo" - export RUSTUP_HOME="$HOME/.rustup" - bash -s rustup-init.sh -y - . $HOME/.cargo/env - rustup default stable - else - echo "Unsupported machine: ${MACHINE}" - exit 1 - fi - - rm rustup-init.sh -} - create_app_template() { rm -r ${APP_DIR}/${APP_NAME} || true mkdir -p ${APP_DIR}/${APP_NAME}/Contents/MacOS @@ -61,9 +67,9 @@ loginfo "---------------------------------" security find-identity -v -p codesigning loginfo "---------------------------------" -clean_build_workspace -install_cargo +echo "--- build binary ---" +clean_build_workspace hatch -e packaging run build-isolated-binary create_app_template mv $DIST_DIR/binary/${BINARY_NAME} ${APP_DIR}/${APP_NAME}/Contents/MacOS/snow @@ -118,7 +124,6 @@ prepare_postinstall_script() { prepare_postinstall_script ls -l $DIST_DIR -tree -d $DIST_DIR chmod +x $APP_SCRIPTS/postinstall @@ -209,3 +214,9 @@ validate_installation() { } validate_installation $DIST_DIR/snowflake-cli-${CLI_VERSION}-${SYSTEM}-${MACHINE}.pkg + +echo "--- Upload artifacts to AWS ---" +ls -la ./dist/ +echo "${STAGE_URL}" +command -v aws +aws s3 cp ./dist/ ${STAGE_URL} --recursive --exclude "*" --include="snowflake-cli-${CLI_VERSION}*.pkg" diff --git a/scripts/packaging/macos/postinstall b/scripts/packaging/macos/postinstall index 2f5b70b198..b42938bda4 100755 --- a/scripts/packaging/macos/postinstall +++ b/scripts/packaging/macos/postinstall @@ -3,6 +3,7 @@ # $2 is the install location # SNOWFLAKE_CLI_COMMENT="# added by Snowflake SnowflakeCLI installer v1.0" +RC_FILES=(~/.zprofile ~/.zshrc ~/.profile ~/.bash_profile ~/.bashrc) function add_dest_path_to_profile() { local dest=$1 @@ -18,19 +19,21 @@ export PATH=$dest:\$PATH" >>$profile echo "[DEBUG] Parameters: $1 $2" SNOWFLAKE_CLI_DEST=$2/SnowflakeCLI.app/Contents/MacOS/ -SNOWFLAKE_CLI_LOGIN_SHELL=~/.profile -if [[ -e ~/.zprofile ]]; then - SNOWFLAKE_CLI_LOGIN_SHELL=~/.zprofile -elif [[ -e ~/.zshrc ]]; then - SNOWFLAKE_CLI_LOGIN_SHELL=~/.zshrc -elif [[ -e ~/.profile ]]; then - SNOWFLAKE_CLI_LOGIN_SHELL=~/.profile -elif [[ -e ~/.bash_profile ]]; then - SNOWFLAKE_CLI_LOGIN_SHELL=~/.bash_profile -elif [[ -e ~/.bashrc ]]; then - SNOWFLAKE_CLI_LOGIN_SHELL=~/.bashrc -fi +# List of potential login shell RC files + +# Check if the path is already in the PATH variable +if [[ ":$PATH:" == *":$SNOWFLAKE_CLI_DEST:"* ]]; then + echo "[INFO] Path $SNOWFLAKE_CLI_DEST is already in PATH. No changes needed." +else + for rc_file in "${RC_FILES[@]}"; do + # Expand tilde (~) to the user's home directory + rc_file_expanded=$(eval echo "$rc_file") -if ! grep -q -E "^$SNOWFLAKE_CLI_COMMENT" $SNOWFLAKE_CLI_LOGIN_SHELL; then - add_dest_path_to_profile $SNOWFLAKE_CLI_DEST $SNOWFLAKE_CLI_LOGIN_SHELL + if [[ -e "$rc_file_expanded" ]]; then + # Add the PATH update to the file + add_dest_path_to_profile "$SNOWFLAKE_CLI_DEST" "$rc_file_expanded" + else + echo "[INFO] $rc_file_expanded does not exist, skipping..." + fi + done fi diff --git a/scripts/packaging/setup_darwin.sh b/scripts/packaging/setup_darwin.sh deleted file mode 100644 index 68b66f875d..0000000000 --- a/scripts/packaging/setup_darwin.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -set -o pipefail - -echo "Setting up the Snowflake CLI build environment" -MACHINE=$(uname -m) - -ensure_pyenv_installation() { - if ! command -v pyenv &>/dev/null; then - echo "pyenv not found, installing..." - arch -${MACHINE} brew install pyenv - else - echo "pyenv already installed" - fi -} - -activate_pyenv() { - export PYENV_ROOT="$HOME/.pyenv" - [[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH" - eval "$(pyenv init - bash)" -} - -ensure_hatch_installation() { - if ! command -v hatch &>/dev/null; then - echo "hatch not found, installing..." - arch -${MACHINE} brew install hatch - else - echo "hatch already installed" - arch -${MACHINE} brew upgrade hatch - fi -} - -ensure_python_installation() { - pyenv versions - pyenv install -s 3.10 - pyenv install -s 3.11 - pyenv global 3.11 - python --version - pip install -U pip uv hatch awscli -} diff --git a/snyk/requirements.txt b/snyk/requirements.txt index 7cd1828e5f..613d703352 100644 --- a/snyk/requirements.txt +++ b/snyk/requirements.txt @@ -1,3 +1,4 @@ +click==8.1.8 GitPython==3.1.44 jinja2==3.1.6 packaging @@ -9,7 +10,7 @@ requests==2.32.3 requirements-parser==0.11.0 rich==14.0.0 setuptools==78.1.0 -snowflake-connector-python[secure-local-storage]==3.14.0 +snowflake-connector-python[secure-local-storage]==3.15.0 snowflake-snowpark-python>=1.15.0,<1.26.0;python_version < "3.12" snowflake.core==1.2.0; python_version < "3.12" tomlkit==0.13.2 diff --git a/src/snowflake/cli/__about__.py b/src/snowflake/cli/__about__.py index 19520dac3c..a598692e20 100644 --- a/src/snowflake/cli/__about__.py +++ b/src/snowflake/cli/__about__.py @@ -16,7 +16,7 @@ from enum import Enum, unique -VERSION = "3.7.0.dev0" +VERSION = "3.8.0.dev+projects0" @unique diff --git a/src/snowflake/cli/_plugins/logs/commands.py b/src/snowflake/cli/_plugins/logs/commands.py index 67c7a5d820..eccfafefb3 100644 --- a/src/snowflake/cli/_plugins/logs/commands.py +++ b/src/snowflake/cli/_plugins/logs/commands.py @@ -4,9 +4,11 @@ import typer from click import ClickException -from snowflake.cli._plugins.logs.manager import LogsManager, LogsQueryRow +from snowflake.cli._plugins.logs.manager import LogsManager +from snowflake.cli._plugins.logs.utils import LOG_LEVELS, LogsQueryRow from snowflake.cli._plugins.object.commands import NameArgument, ObjectArgument from snowflake.cli.api.commands.snow_typer import SnowTyperFactory +from snowflake.cli.api.exceptions import CliArgumentError from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.output.types import ( CommandResult, @@ -41,11 +43,22 @@ def get_logs( "--table", help="The table to query for logs. If not provided, the default table will be used", ), + log_level: Optional[str] = typer.Option( + "INFO", + "--log-level", + help="The log level to filter by. If not provided, INFO will be used", + ), **options, ): """ Retrieves logs for a given object. """ + + if log_level and not log_level.upper() in LOG_LEVELS: + raise CliArgumentError( + f"Invalid log level. Please choose from {', '.join(LOG_LEVELS)}" + ) + if refresh_time and to: raise ClickException( "You cannot set both --refresh and --to parameters. Please check the values" @@ -61,6 +74,7 @@ def get_logs( from_time=from_time, refresh_time=refresh_time, event_table=event_table, + log_level=log_level, ) logs = itertools.chain( (MessageResult(log.log_message) for logs in logs_stream for log in logs) @@ -72,6 +86,7 @@ def get_logs( from_time=from_time, to_time=to_time, event_table=event_table, + log_level=log_level, ) logs = (MessageResult(log.log_message) for log in logs_iterable) # type: ignore diff --git a/src/snowflake/cli/_plugins/logs/manager.py b/src/snowflake/cli/_plugins/logs/manager.py index 5a2db4822d..d999dfb3c2 100644 --- a/src/snowflake/cli/_plugins/logs/manager.py +++ b/src/snowflake/cli/_plugins/logs/manager.py @@ -1,26 +1,19 @@ import time from datetime import datetime from textwrap import dedent -from typing import Iterable, List, NamedTuple, Optional, Tuple +from typing import Iterable, List, Optional -from click import ClickException +from snowflake.cli._plugins.logs.utils import ( + LogsQueryRow, + get_timestamp_query, + parse_log_levels_for_query, + sanitize_logs, +) from snowflake.cli._plugins.object.commands import NameArgument, ObjectArgument from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.sql_execution import SqlExecutionMixin from snowflake.connector.cursor import SnowflakeCursor -LogsQueryRow = NamedTuple( - "LogsQueryRow", - [ - ("timestamp", datetime), - ("database_name", str), - ("schema_name", str), - ("object_name", str), - ("log_level", str), - ("log_message", str), - ], -) - class LogsManager(SqlExecutionMixin): def stream_logs( @@ -30,6 +23,7 @@ def stream_logs( object_name: FQN = NameArgument, from_time: Optional[datetime] = None, event_table: Optional[str] = None, + log_level: Optional[str] = "INFO", ) -> Iterable[List[LogsQueryRow]]: try: previous_end = from_time @@ -41,6 +35,7 @@ def stream_logs( from_time=previous_end, to_time=None, event_table=event_table, + log_level=log_level, ).fetchall() if raw_logs: @@ -60,6 +55,7 @@ def get_logs( from_time: Optional[datetime] = None, to_time: Optional[datetime] = None, event_table: Optional[str] = None, + log_level: Optional[str] = "INFO", ) -> Iterable[LogsQueryRow]: """ Basic function to get a single batch of logs from the server @@ -71,9 +67,10 @@ def get_logs( from_time=from_time, to_time=to_time, event_table=event_table, + log_level=log_level, ) - return self.sanitize_logs(logs) + return sanitize_logs(logs) def get_raw_logs( self, @@ -82,6 +79,7 @@ def get_raw_logs( from_time: Optional[datetime] = None, to_time: Optional[datetime] = None, event_table: Optional[str] = None, + log_level: Optional[str] = "INFO", ) -> SnowflakeCursor: table = event_table if event_table else "SNOWFLAKE.TELEMETRY.EVENTS" @@ -97,9 +95,9 @@ def get_raw_logs( value::string as log_message FROM {table} WHERE record_type = 'LOG' - AND (record:severity_text = 'INFO' or record:severity_text is NULL ) + AND (record:severity_text IN ({parse_log_levels_for_query((log_level))}) or record:severity_text is NULL ) AND object_name = '{object_name}' - {self._get_timestamp_query(from_time, to_time)} + {get_timestamp_query(from_time, to_time)} ORDER BY timestamp; """ ).strip() @@ -107,32 +105,3 @@ def get_raw_logs( result = self.execute_query(query) return result - - def _get_timestamp_query( - self, from_time: Optional[datetime], to_time: Optional[datetime] - ): - if from_time and to_time and from_time > to_time: - raise ClickException( - "From_time cannot be later than to_time. Please check the values" - ) - query = [] - - if from_time is not None: - query.append( - f"AND timestamp >= TO_TIMESTAMP_LTZ('{from_time.isoformat()}')\n" - ) - - if to_time is not None: - query.append( - f"AND timestamp <= TO_TIMESTAMP_LTZ('{to_time.isoformat()}')\n" - ) - - return "".join(query) - - def sanitize_logs(self, logs: SnowflakeCursor | List[Tuple]) -> List[LogsQueryRow]: - try: - return [LogsQueryRow(*log) for log in logs] - except TypeError: - raise ClickException( - "Logs table has incorrect format. Please check the logs_table in your database" - ) diff --git a/src/snowflake/cli/_plugins/logs/utils.py b/src/snowflake/cli/_plugins/logs/utils.py new file mode 100644 index 0000000000..dbd45da433 --- /dev/null +++ b/src/snowflake/cli/_plugins/logs/utils.py @@ -0,0 +1,60 @@ +from datetime import datetime +from typing import List, NamedTuple, Optional, Tuple + +from snowflake.cli.api.exceptions import CliArgumentError, CliSqlError +from snowflake.connector.cursor import SnowflakeCursor + +LOG_LEVELS = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"] + +LogsQueryRow = NamedTuple( + "LogsQueryRow", + [ + ("timestamp", datetime), + ("database_name", str), + ("schema_name", str), + ("object_name", str), + ("log_level", str), + ("log_message", str), + ], +) + + +def sanitize_logs(logs: SnowflakeCursor | List[Tuple]) -> List[LogsQueryRow]: + try: + return [LogsQueryRow(*log) for log in logs] + except TypeError: + raise CliSqlError( + "Logs table has incorrect format. Please check the logs_table in your database" + ) + + +def get_timestamp_query(from_time: Optional[datetime], to_time: Optional[datetime]): + if from_time and to_time and from_time > to_time: + raise CliArgumentError( + "From_time cannot be later than to_time. Please check the values" + ) + query = [] + + if from_time is not None: + query.append(f"AND timestamp >= TO_TIMESTAMP_LTZ('{from_time.isoformat()}')\n") + + if to_time is not None: + query.append(f"AND timestamp <= TO_TIMESTAMP_LTZ('{to_time.isoformat()}')\n") + + return "".join(query) + + +def get_log_levels(log_level: str): + if log_level.upper() not in LOG_LEVELS and log_level != "": + raise CliArgumentError( + f"Invalid log level. Please choose from {', '.join(LOG_LEVELS)}" + ) + + if log_level == "": + log_level = "INFO" + + return LOG_LEVELS[LOG_LEVELS.index(log_level.upper()) :] + + +def parse_log_levels_for_query(log_level: str): + return ", ".join(f"'{level}'" for level in get_log_levels(log_level)) diff --git a/src/snowflake/cli/_plugins/spcs/image_registry/manager.py b/src/snowflake/cli/_plugins/spcs/image_registry/manager.py index fe051beec7..4d11287e47 100644 --- a/src/snowflake/cli/_plugins/spcs/image_registry/manager.py +++ b/src/snowflake/cli/_plugins/spcs/image_registry/manager.py @@ -74,12 +74,21 @@ def _has_url_scheme(self, url: str): return re.fullmatch(r"^.*//.+", url) is not None def get_registry_url(self) -> str: - repositories_query = "show image repositories in account" - result_set = self.execute_query(repositories_query, cursor_class=DictCursor) - results = result_set.fetchall() - if len(results) == 0: - raise NoImageRepositoriesFoundError() - sample_repository_url = results[0]["repository_url"] + images_query = "show image repositories in schema snowflake.images;" + images_result = self.execute_query(images_query, cursor_class=DictCursor) + + results = images_result.fetchone() + + if not results: + # fallback to account level query - slower one, so we try to avoid it if possible + repositories_query = "show image repositories in account" + result_set = self.execute_query(repositories_query, cursor_class=DictCursor) + results = result_set.fetchone() + + if not results: + raise NoImageRepositoriesFoundError() + + sample_repository_url = results["repository_url"] if not self._has_url_scheme(sample_repository_url): sample_repository_url = f"//{sample_repository_url}" return urlparse(sample_repository_url).netloc diff --git a/tests/__snapshots__/test_help_messages.ambr b/tests/__snapshots__/test_help_messages.ambr index 57f0e6eff5..ebf0de778c 100644 --- a/tests/__snapshots__/test_help_messages.ambr +++ b/tests/__snapshots__/test_help_messages.ambr @@ -5118,19 +5118,22 @@ | [required] | +------------------------------------------------------------------------------+ +- Options --------------------------------------------------------------------+ - | --from TEXT The start time of the logs to retrieve. Accepts | - | all ISO8061 formats | - | [default: None] | - | --to TEXT The end time of the logs to retrieve. Accepts | - | all ISO8061 formats | - | [default: None] | - | --refresh INTEGER If set, the logs will be streamed with the given | - | refresh time in seconds | - | [default: None] | - | --table TEXT The table to query for logs. If not provided, | - | the default table will be used | - | [default: None] | - | --help -h Show this message and exit. | + | --from TEXT The start time of the logs to retrieve. | + | Accepts all ISO8061 formats | + | [default: None] | + | --to TEXT The end time of the logs to retrieve. Accepts | + | all ISO8061 formats | + | [default: None] | + | --refresh INTEGER If set, the logs will be streamed with the | + | given refresh time in seconds | + | [default: None] | + | --table TEXT The table to query for logs. If not provided, | + | the default table will be used | + | [default: None] | + | --log-level TEXT The log level to filter by. If not provided, | + | INFO will be used | + | [default: INFO] | + | --help -h Show this message and exit. | +------------------------------------------------------------------------------+ +- Connection configuration ---------------------------------------------------+ | --connection,--environment -c TEXT Name of the connection, as | diff --git a/tests/logs/__snapshots__/test_logs.ambr b/tests/logs/__snapshots__/test_logs.ambr index c01ff40ebd..89517c6405 100644 --- a/tests/logs/__snapshots__/test_logs.ambr +++ b/tests/logs/__snapshots__/test_logs.ambr @@ -10,7 +10,7 @@ value::string as log_message FROM SNOWFLAKE.TELEMETRY.EVENTS WHERE record_type = 'LOG' - AND (record:severity_text = 'INFO' or record:severity_text is NULL ) + AND (record:severity_text IN ('INFO', 'WARN', 'ERROR', 'FATAL') or record:severity_text is NULL ) AND object_name = 'bar' AND timestamp >= TO_TIMESTAMP_LTZ('2022-02-02T02:02:02') AND timestamp <= TO_TIMESTAMP_LTZ('2022-02-03T02:02:02') @@ -29,7 +29,7 @@ value::string as log_message FROM bar WHERE record_type = 'LOG' - AND (record:severity_text = 'INFO' or record:severity_text is NULL ) + AND (record:severity_text IN ('INFO', 'WARN', 'ERROR', 'FATAL') or record:severity_text is NULL ) AND object_name = 'bar' AND timestamp >= TO_TIMESTAMP_LTZ('2022-02-02T02:02:02') AND timestamp <= TO_TIMESTAMP_LTZ('2022-02-03T02:02:02') @@ -48,7 +48,7 @@ value::string as log_message FROM foo WHERE record_type = 'LOG' - AND (record:severity_text = 'INFO' or record:severity_text is NULL ) + AND (record:severity_text IN ('INFO', 'WARN', 'ERROR', 'FATAL') or record:severity_text is NULL ) AND object_name = 'bar' AND timestamp >= TO_TIMESTAMP_LTZ('2022-02-02T02:02:02') AND timestamp <= TO_TIMESTAMP_LTZ('2022-02-03T02:02:02') @@ -56,6 +56,14 @@ ORDER BY timestamp; ''' # --- +# name: test_if_incorrect_log_level_causes_error + ''' + +- Error ----------------------------------------------------------------------+ + | Invalid log level. Please choose from TRACE, DEBUG, INFO, WARN, ERROR, FATAL | + +------------------------------------------------------------------------------+ + + ''' +# --- # name: test_providing_time_in_incorrect_format_causes_error[2024-11-03 12:00:00 UTC---from] ''' +- Error ----------------------------------------------------------------------+ diff --git a/tests/logs/test_logs.py b/tests/logs/test_logs.py index f8da712c96..718c1a0c98 100644 --- a/tests/logs/test_logs.py +++ b/tests/logs/test_logs.py @@ -77,3 +77,17 @@ def test_correct_query_is_constructed(mock_connect, mock_ctx, runner, snapshot, queries = ctx.get_queries() assert len(queries) == 1 assert queries[0] == snapshot + + +def test_if_incorrect_log_level_causes_error(runner, snapshot): + result = runner.invoke( + [ + "logs", + "table", + "test_table", + "--log-level", + "NOTALEVEL", + ] + ) + assert result.exit_code == 1 + assert result.output == snapshot diff --git a/tests/logs/test_logs_manager.py b/tests/logs/test_logs_utils.py similarity index 68% rename from tests/logs/test_logs_manager.py rename to tests/logs/test_logs_utils.py index bf98494d54..d6df2eec77 100644 --- a/tests/logs/test_logs_manager.py +++ b/tests/logs/test_logs_utils.py @@ -5,7 +5,11 @@ from snowflake.cli._plugins.logs.commands import ( get_datetime_from_string, ) -from snowflake.cli._plugins.logs.manager import LogsManager +from snowflake.cli._plugins.logs.utils import ( + get_log_levels, + get_timestamp_query, + parse_log_levels_for_query, +) DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" @@ -74,10 +78,45 @@ def test_if_passing_to_time_earlier_than_from_time_raiser_error(): to_time = from_time - timedelta(hours=1) with pytest.raises(ClickException) as e: - LogsManager()._get_timestamp_query(from_time=from_time, to_time=to_time) # noqa + get_timestamp_query(from_time=from_time, to_time=to_time) # noqa assert ( str(e.value) == "From_time cannot be later than to_time. Please check the values" ) assert e.value.exit_code == 1 + + +@pytest.mark.parametrize( + "log_level,expected", + [ + ("", ["INFO", "WARN", "ERROR", "FATAL"]), + ("TRACE", ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"]), + ("DEBUG", ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"]), + ("INFO", ["INFO", "WARN", "ERROR", "FATAL"]), + ("WARN", ["WARN", "ERROR", "FATAL"]), + ("ERROR", ["ERROR", "FATAL"]), + ("FATAL", ["FATAL"]), + ("fatal", ["FATAL"]), + ("eRrOr", ["ERROR", "FATAL"]), + ], +) +def test_if_log_levels_list_is_correctly_filtered(log_level, expected): + result = get_log_levels(log_level) + + assert result == expected + + +@pytest.mark.parametrize( + "level,expected", + [ + ("", "'INFO', 'WARN', 'ERROR', 'FATAL'"), + ("INFO", "'INFO', 'WARN', 'ERROR', 'FATAL'"), + ("DEBUG", "'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'"), + ("wArN", "'WARN', 'ERROR', 'FATAL'"), + ], +) +def test_if_log_level_gives_correct_query(level, expected): + result = parse_log_levels_for_query(level) + + assert result == expected diff --git a/tests/spcs/test_registry.py b/tests/spcs/test_registry.py index 816d1a7532..be8e14d06c 100644 --- a/tests/spcs/test_registry.py +++ b/tests/spcs/test_registry.py @@ -73,11 +73,40 @@ def test_get_registry_url(mock_execute, mock_conn, mock_cursor): ] mock_execute.return_value = mock_cursor( - rows=[{col: row for col, row in zip(MOCK_REPO_COLUMNS, mock_row)}], + rows=[{}, {col: row for col, row in zip(MOCK_REPO_COLUMNS, mock_row)}], columns=MOCK_REPO_COLUMNS, ) result = RegistryManager().get_registry_url() expected_query = "show image repositories in account" + assert mock_execute.call_count == 2 + mock_execute.assert_any_call(expected_query, cursor_class=DictCursor) + assert result == "orgname-alias.registry.snowflakecomputing.com" + + +@mock.patch("snowflake.cli._plugins.spcs.image_registry.manager.RegistryManager._conn") +@mock.patch( + "snowflake.cli._plugins.spcs.image_registry.manager.RegistryManager.execute_query" +) +def test_get_registry_url_with_schema_query(mock_execute, mock_conn, mock_cursor): + mock_row = [ + "2023-01-01 00:00:00", + "IMAGES", + "DB", + "SCHEMA", + "orgname-alias.registry.snowflakecomputing.com/DB/SCHEMA/IMAGES", + "TEST_ROLE", + "ROLE", + "", + ] + + mock_execute.return_value = mock_cursor( + rows=[{col: row for col, row in zip(MOCK_REPO_COLUMNS, mock_row)}], + columns=MOCK_REPO_COLUMNS, + ) + + result = RegistryManager().get_registry_url() + expected_query = "show image repositories in schema snowflake.images;" + mock_execute.assert_called_once_with(expected_query, cursor_class=DictCursor) assert result == "orgname-alias.registry.snowflakecomputing.com" @@ -88,14 +117,17 @@ def test_get_registry_url(mock_execute, mock_conn, mock_cursor): ) def test_get_registry_url_no_repositories(mock_execute, mock_conn, mock_cursor): mock_execute.return_value = mock_cursor( - rows=[], + rows=[{}, {}], columns=MOCK_REPO_COLUMNS, ) with pytest.raises(NoImageRepositoriesFoundError): RegistryManager().get_registry_url() - expected_query = "show image repositories in account" - mock_execute.assert_called_once_with(expected_query, cursor_class=DictCursor) + expected_query1 = "show image repositories in schema snowflake.images;" + expected_query2 = "show image repositories in account" + assert mock_execute.call_count == 2 + mock_execute.assert_any_call(expected_query1, cursor_class=DictCursor) + mock_execute.assert_any_call(expected_query2, cursor_class=DictCursor) @mock.patch( diff --git a/tests_integration/test_data/projects/snowpark_v2/c.py b/tests_integration/test_data/projects/snowpark_v2/c.py index 3ab4a6d6cc..14aed7edba 100644 --- a/tests_integration/test_data/projects/snowpark_v2/c.py +++ b/tests_integration/test_data/projects/snowpark_v2/c.py @@ -3,7 +3,14 @@ # test import import syrupy +import logging + +log = logging.getLogger("SnowCLI_Logs_Test") def hello_function(name: str) -> str: + log.debug("This is a debug message") + log.info("This is an info message") + log.warning("This is a warning message") + log.error("This is an error message") return f"Hello {name}!"