Skip to content

Commit

Permalink
Merge branch 'main' into jsikorski/create_getter_for_artifacts
Browse files Browse the repository at this point in the history
# Conflicts:
#	RELEASE-NOTES.md
  • Loading branch information
sfc-gh-jsikorski committed Sep 18, 2024
2 parents 06f1393 + d77ddb6 commit 721b458
Show file tree
Hide file tree
Showing 60 changed files with 2,380 additions and 1,210 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
* Added templates expansion of arbitrary files for Native Apps through `templates` processor.
* Added `SNOWFLAKE_..._PRIVATE_KEY_RAW` environment variable to pass private key as a raw string.
* Snowpark and Streamlit entities in `snowflake.yml` now accept glob patterns for listing artifacts
* Added periodic check for newest version of Snowflake CLI. When new version is available, user will be notified.

## Fixes and improvements
* Fixed problem with whitespaces in `snow connection add` command.
Expand Down
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ dependencies = [
"pluggy==1.5.0",
"PyYAML==6.0.2",
"packaging",
"rich==13.8.0",
"rich==13.8.1",
"requests==2.32.3",
"requirements-parser==0.11.0",
"setuptools==74.1.2",
'snowflake.core==0.8.0; python_version < "3.12"',
"snowflake-connector-python[secure-local-storage]==3.12.1",
"setuptools==75.1.0",
'snowflake.core==0.12.1; python_version < "3.12"',
"snowflake-connector-python[secure-local-storage]==3.12.2",
'snowflake-snowpark-python>=1.15.0;python_version < "3.12"',
"tomlkit==0.13.2",
"typer==0.12.5",
Expand All @@ -59,7 +59,7 @@ classifiers = [
development = [
"coverage==7.6.1",
"pre-commit>=3.5.0",
"pytest==8.3.2",
"pytest==8.3.3",
"pytest-randomly==3.15.0",
"syrupy==4.7.1",
]
Expand Down
10 changes: 5 additions & 5 deletions snyk/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ jinja2==3.1.4
pluggy==1.5.0
PyYAML==6.0.2
packaging
rich==13.8.0
rich==13.8.1
requests==2.32.3
requirements-parser==0.11.0
setuptools==74.1.2
snowflake.core==0.8.0; python_version < "3.12"
snowflake-connector-python[secure-local-storage]==3.12.1
setuptools==75.1.0
snowflake.core==0.12.1; python_version < "3.12"
snowflake-connector-python[secure-local-storage]==3.12.2
snowflake-snowpark-python>=1.15.0;python_version < "3.12"
tomlkit==0.13.2
typer==0.12.5
Expand All @@ -17,6 +17,6 @@ pip
pydantic==2.9.1
coverage==7.6.1
pre-commit>=3.5.0
pytest==8.3.2
pytest==8.3.3
pytest-randomly==3.15.0
syrupy==4.7.1
2 changes: 1 addition & 1 deletion src/snowflake/cli/__about__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@

from __future__ import annotations

VERSION = "3.0.0.dev0"
VERSION = "3.0.0rc2"
11 changes: 10 additions & 1 deletion src/snowflake/cli/_app/cli_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
)
from snowflake.cli._app.main_typer import SnowCliMainTyper
from snowflake.cli._app.printing import MessageResult, print_result
from snowflake.cli._app.version_check import (
get_new_version_msg,
show_new_version_banner_callback,
)
from snowflake.cli.api import Api, api_provider
from snowflake.cli.api.config import config_init
from snowflake.cli.api.output.formats import OutputFormat
Expand Down Expand Up @@ -145,8 +149,13 @@ def _info_callback(value: bool):

def app_factory() -> SnowCliMainTyper:
app = SnowCliMainTyper()
new_version_msg = get_new_version_msg()

@app.callback(invoke_without_command=True)
@app.callback(
invoke_without_command=True,
epilog=new_version_msg,
result_callback=show_new_version_banner_callback(new_version_msg),
)
def default(
ctx: typer.Context,
version: bool = typer.Option(
Expand Down
74 changes: 74 additions & 0 deletions src/snowflake/cli/_app/version_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import json
import time

import requests
from packaging.version import Version
from snowflake.cli.__about__ import VERSION
from snowflake.cli.api.console import cli_console
from snowflake.cli.api.secure_path import SecurePath
from snowflake.connector.config_manager import CONFIG_MANAGER


def get_new_version_msg() -> str | None:
last = _VersionCache().get_last_version()
current = Version(VERSION)
if last and last > current:
return f"\nNew version of Snowflake CLI available. Newest: {last}, current: {VERSION}\n"
return None


def show_new_version_banner_callback(msg):
def _callback(*args, **kwargs):
if msg:
cli_console.message(msg)

return _callback


class _VersionCache:
_last_time = "last_time_check"
_version = "version"
_version_cache_file = SecurePath(
CONFIG_MANAGER.file_path.parent / ".cli_version.cache"
)

def __init__(self):
self._cache_file = _VersionCache._version_cache_file

def _save_latest_version(self, version: str):
data = {
_VersionCache._last_time: time.time(),
_VersionCache._version: str(version),
}
self._cache_file.write_text(json.dumps(data))

@staticmethod
def _get_version_from_pypi() -> str | None:
headers = {"Content-Type": "application/vnd.pypi.simple.v1+json"}
response = requests.get(
"https://pypi.org/pypi/snowflake-cli-labs/json", headers=headers, timeout=3
)
response.raise_for_status()
return response.json()["info"]["version"]

def _update_latest_version(self) -> Version | None:
version = self._get_version_from_pypi()
if version is None:
return None
self._save_latest_version(version)
return Version(version)

def _read_latest_version(self) -> Version | None:
if self._cache_file.exists():
data = json.loads(self._cache_file.read_text())
now = time.time()
if data[_VersionCache._last_time] > now - 60 * 60:
return Version(data[_VersionCache._version])

return self._update_latest_version()

def get_last_version(self) -> Version | None:
try:
return self._read_latest_version()
except: # anything, this it not crucial feature
return None
117 changes: 38 additions & 79 deletions src/snowflake/cli/_plugins/nativeapp/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,16 @@

import time
from abc import ABC, abstractmethod
from contextlib import contextmanager
from datetime import datetime
from functools import cached_property
from pathlib import Path
from textwrap import dedent
from typing import Generator, List, Optional, TypedDict
from typing import Generator, List, Optional

from click import ClickException
from snowflake.cli._plugins.connection.util import make_snowsight_url
from snowflake.cli._plugins.nativeapp.artifacts import (
BundleMap,
)
from snowflake.cli._plugins.nativeapp.constants import (
NAME_COL,
)
from snowflake.cli._plugins.nativeapp.exceptions import (
NoEventTableForAccount,
)
Expand All @@ -41,6 +36,10 @@
DiffResult,
)
from snowflake.cli.api.console import cli_console as cc
from snowflake.cli.api.entities.application_entity import (
ApplicationEntity,
ApplicationOwnedObject,
)
from snowflake.cli.api.entities.application_package_entity import (
ApplicationPackageEntity,
)
Expand All @@ -59,8 +58,6 @@
from snowflake.cli.api.sql_execution import SqlExecutionMixin
from snowflake.connector import DictCursor, ProgrammingError

ApplicationOwnedObject = TypedDict("ApplicationOwnedObject", {"name": str, "type": str})


class NativeAppCommandProcessor(ABC):
@abstractmethod
Expand Down Expand Up @@ -137,20 +134,8 @@ def use_package_warehouse(self):
def application_warehouse(self) -> Optional[str]:
return self.na_project.application_warehouse

@contextmanager
def use_application_warehouse(self):
if self.application_warehouse:
with self.use_warehouse(self.application_warehouse):
yield
else:
raise ClickException(
dedent(
f"""\
Application warehouse cannot be empty.
Please provide a value for it in your connection information or your project definition file.
"""
)
)
return ApplicationEntity.use_application_warehouse(self.application_warehouse)

@property
def project_identifier(self) -> str:
Expand Down Expand Up @@ -249,47 +234,30 @@ def sync_deploy_root_with_stage(
)

def get_existing_app_info(self) -> Optional[dict]:
"""
Check for an existing application object by the same name as in project definition, in account.
It executes a 'show applications like' query and returns the result as single row, if one exists.
"""
with self.use_role(self.app_role):
return self.show_specific_object(
"applications", self.app_name, name_col=NAME_COL
)
return ApplicationEntity.get_existing_app_info(
app_name=self.app_name,
app_role=self.app_role,
)

def get_existing_app_pkg_info(self) -> Optional[dict]:
return ApplicationPackageEntity.get_existing_app_pkg_info(
package_name=self.package_name,
package_role=self.package_role,
)

def get_objects_owned_by_application(self) -> List[ApplicationOwnedObject]:
"""
Returns all application objects owned by this application.
"""
with self.use_role(self.app_role):
results = self._execute_query(
f"show objects owned by application {self.app_name}"
).fetchall()
return [{"name": row[1], "type": row[2]} for row in results]
def get_objects_owned_by_application(self):
return ApplicationEntity.get_objects_owned_by_application(
app_name=self.app_name,
app_role=self.app_role,
)

def _application_objects_to_str(
self, application_objects: list[ApplicationOwnedObject]
) -> str:
"""
Returns a list in an "(Object Type) Object Name" format. Database-level and schema-level object names are fully qualified:
(COMPUTE_POOL) POOL_NAME
(DATABASE) DB_NAME
(SCHEMA) DB_NAME.PUBLIC
...
"""
return "\n".join(
[self._application_object_to_str(obj) for obj in application_objects]
)
return ApplicationEntity.application_objects_to_str(application_objects)

def _application_object_to_str(self, obj: ApplicationOwnedObject) -> str:
return f"({obj['type']}) {obj['name']}"
def _application_object_to_str(self, obj: ApplicationOwnedObject):
return ApplicationEntity.application_object_to_str(obj)

def get_snowsight_url(self) -> str:
"""Returns the URL that can be used to visit this app via Snowsight."""
Expand Down Expand Up @@ -343,35 +311,26 @@ def deploy(
validate: bool = True,
print_diff: bool = True,
) -> DiffResult:
"""app deploy process"""

# 1. Create an empty application package, if none exists
self.create_app_package()

with self.use_role(self.package_role):
# 2. now that the application package exists, create shared data
self._apply_package_scripts()

# 3. Upload files from deploy root local folder to the above stage
stage_fqn = stage_fqn or self.stage_fqn
diff = self.sync_deploy_root_with_stage(
bundle_map=bundle_map,
role=self.package_role,
prune=prune,
recursive=recursive,
stage_fqn=stage_fqn,
local_paths_to_sync=local_paths_to_sync,
print_diff=print_diff,
)

# 4. Execute post-deploy hooks
with self.use_package_warehouse():
self.execute_package_post_deploy_hooks()

if validate:
self.validate(use_scratch_stage=False)

return diff
return ApplicationPackageEntity.deploy(
console=cc,
project_root=self.project_root,
deploy_root=self.deploy_root,
bundle_root=self.bundle_root,
generated_root=self.generated_root,
artifacts=self.artifacts,
package_name=self.package_name,
package_role=self.package_role,
package_distribution=self.package_distribution,
prune=prune,
recursive=recursive,
paths=local_paths_to_sync,
print_diff=print_diff,
validate=validate,
stage_fqn=stage_fqn or self.stage_fqn,
package_warehouse=self.package_warehouse,
post_deploy_hooks=self.package_post_deploy_hooks,
package_scripts=self.package_scripts,
)

def deploy_to_scratch_stage_fn(self):
bundle_map = self.build_bundle()
Expand Down
Loading

0 comments on commit 721b458

Please sign in to comment.