snowflake-cli 3.1.0__py3-none-any.whl → 3.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +1 -1
- snowflake/cli/_plugins/connection/commands.py +124 -109
- snowflake/cli/_plugins/connection/util.py +54 -9
- snowflake/cli/_plugins/cortex/manager.py +1 -1
- snowflake/cli/_plugins/git/manager.py +4 -4
- snowflake/cli/_plugins/nativeapp/artifacts.py +64 -10
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
- snowflake/cli/_plugins/nativeapp/commands.py +10 -3
- snowflake/cli/_plugins/nativeapp/constants.py +1 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +501 -440
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +563 -885
- snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
- snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
- snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
- snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
- snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +1 -89
- snowflake/cli/_plugins/nativeapp/version/commands.py +6 -3
- snowflake/cli/_plugins/notebook/manager.py +2 -2
- snowflake/cli/_plugins/object/commands.py +10 -1
- snowflake/cli/_plugins/object/manager.py +13 -5
- snowflake/cli/_plugins/snowpark/common.py +3 -3
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -1
- snowflake/cli/_plugins/spcs/common.py +29 -0
- snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
- snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
- snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
- snowflake/cli/_plugins/spcs/services/commands.py +64 -13
- snowflake/cli/_plugins/spcs/services/manager.py +75 -15
- snowflake/cli/_plugins/sql/commands.py +9 -1
- snowflake/cli/_plugins/sql/manager.py +9 -4
- snowflake/cli/_plugins/stage/commands.py +20 -16
- snowflake/cli/_plugins/stage/diff.py +1 -1
- snowflake/cli/_plugins/stage/manager.py +140 -11
- snowflake/cli/_plugins/streamlit/manager.py +5 -5
- snowflake/cli/_plugins/workspace/commands.py +6 -3
- snowflake/cli/api/cli_global_context.py +1 -0
- snowflake/cli/api/config.py +23 -5
- snowflake/cli/api/console/console.py +4 -19
- snowflake/cli/api/entities/utils.py +19 -32
- snowflake/cli/api/errno.py +2 -0
- snowflake/cli/api/exceptions.py +9 -0
- snowflake/cli/api/metrics.py +223 -7
- snowflake/cli/api/output/types.py +1 -1
- snowflake/cli/api/project/definition_conversion.py +179 -62
- snowflake/cli/api/rest_api.py +26 -4
- snowflake/cli/api/secure_utils.py +1 -1
- snowflake/cli/api/sql_execution.py +35 -22
- snowflake/cli/api/stage_path.py +5 -2
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +7 -8
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +56 -55
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
- snowflake/cli/_plugins/nativeapp/manager.py +0 -392
- snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
- snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
- snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -56
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -17,17 +17,21 @@ from __future__ import annotations
|
|
|
17
17
|
import fnmatch
|
|
18
18
|
import glob
|
|
19
19
|
import logging
|
|
20
|
+
import os
|
|
20
21
|
import re
|
|
22
|
+
import shutil
|
|
21
23
|
import sys
|
|
22
24
|
import time
|
|
25
|
+
from collections import deque
|
|
23
26
|
from contextlib import nullcontext
|
|
24
27
|
from dataclasses import dataclass
|
|
25
28
|
from os import path
|
|
26
29
|
from pathlib import Path
|
|
30
|
+
from tempfile import TemporaryDirectory
|
|
27
31
|
from textwrap import dedent
|
|
28
|
-
from typing import Dict, List, Optional, Union
|
|
32
|
+
from typing import Deque, Dict, Generator, List, Optional, Union
|
|
29
33
|
|
|
30
|
-
from click import ClickException
|
|
34
|
+
from click import ClickException, UsageError
|
|
31
35
|
from snowflake.cli._plugins.snowpark.package_utils import parse_requirements
|
|
32
36
|
from snowflake.cli.api.commands.common import (
|
|
33
37
|
OnErrorType,
|
|
@@ -260,7 +264,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
260
264
|
query = f"ls {stage_path}"
|
|
261
265
|
if pattern is not None:
|
|
262
266
|
query += f" pattern = '{pattern}'"
|
|
263
|
-
return self.
|
|
267
|
+
return self.execute_query(query, cursor_class=DictCursor)
|
|
264
268
|
|
|
265
269
|
@staticmethod
|
|
266
270
|
def _assure_is_existing_directory(path: Path) -> None:
|
|
@@ -275,7 +279,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
275
279
|
spath = self.build_path(stage_path)
|
|
276
280
|
self._assure_is_existing_directory(dest_path)
|
|
277
281
|
dest_directory = f"{dest_path}/"
|
|
278
|
-
return self.
|
|
282
|
+
return self.execute_query(
|
|
279
283
|
f"get {spath.path_for_sql()} {self._to_uri(dest_directory)} parallel={parallel}"
|
|
280
284
|
)
|
|
281
285
|
|
|
@@ -291,7 +295,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
291
295
|
)
|
|
292
296
|
self._assure_is_existing_directory(local_dir)
|
|
293
297
|
|
|
294
|
-
result = self.
|
|
298
|
+
result = self.execute_query(
|
|
295
299
|
f"get {file_path.path_for_sql()} {self._to_uri(f'{local_dir}/')} parallel={parallel}"
|
|
296
300
|
)
|
|
297
301
|
results.append(result)
|
|
@@ -306,6 +310,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
306
310
|
overwrite: bool = False,
|
|
307
311
|
role: Optional[str] = None,
|
|
308
312
|
auto_compress: bool = False,
|
|
313
|
+
use_dict_cursor: bool = False,
|
|
309
314
|
) -> SnowflakeCursor:
|
|
310
315
|
"""
|
|
311
316
|
This method will take a file path from the user's system and put it into a Snowflake stage,
|
|
@@ -313,16 +318,140 @@ class StageManager(SqlExecutionMixin):
|
|
|
313
318
|
If provided with a role, then temporarily use this role to perform the operation above,
|
|
314
319
|
and switch back to the original role for the next commands to run.
|
|
315
320
|
"""
|
|
321
|
+
if "*" not in str(local_path):
|
|
322
|
+
local_path = (
|
|
323
|
+
os.path.join(local_path, "*")
|
|
324
|
+
if Path(local_path).is_dir()
|
|
325
|
+
else str(local_path)
|
|
326
|
+
)
|
|
316
327
|
with self.use_role(role) if role else nullcontext():
|
|
317
328
|
spath = self.build_path(stage_path)
|
|
318
329
|
local_resolved_path = path_resolver(str(local_path))
|
|
319
330
|
log.info("Uploading %s to %s", local_resolved_path, stage_path)
|
|
320
|
-
cursor = self.
|
|
331
|
+
cursor = self.execute_query(
|
|
321
332
|
f"put {self._to_uri(local_resolved_path)} {spath.path_for_sql()} "
|
|
322
|
-
f"auto_compress={str(auto_compress).lower()} parallel={parallel} overwrite={overwrite}"
|
|
333
|
+
f"auto_compress={str(auto_compress).lower()} parallel={parallel} overwrite={overwrite}",
|
|
334
|
+
cursor_class=DictCursor if use_dict_cursor else SnowflakeCursor,
|
|
323
335
|
)
|
|
324
336
|
return cursor
|
|
325
337
|
|
|
338
|
+
@staticmethod
|
|
339
|
+
def _symlink_or_copy(source_root: Path, source_file_or_dir: Path, dest_dir: Path):
|
|
340
|
+
from snowflake.cli._plugins.nativeapp.artifacts import resolve_without_follow
|
|
341
|
+
|
|
342
|
+
absolute_src = resolve_without_follow(source_file_or_dir)
|
|
343
|
+
dest_path = dest_dir / source_file_or_dir.relative_to(source_root)
|
|
344
|
+
|
|
345
|
+
if absolute_src.is_file():
|
|
346
|
+
try:
|
|
347
|
+
os.symlink(absolute_src, dest_path)
|
|
348
|
+
except OSError:
|
|
349
|
+
if not dest_path.parent.exists():
|
|
350
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
351
|
+
shutil.copyfile(absolute_src, dest_path)
|
|
352
|
+
else:
|
|
353
|
+
dest_path.mkdir(exist_ok=True, parents=True)
|
|
354
|
+
|
|
355
|
+
def put_recursive(
|
|
356
|
+
self,
|
|
357
|
+
local_path: Path,
|
|
358
|
+
stage_path: str,
|
|
359
|
+
parallel: int = 4,
|
|
360
|
+
overwrite: bool = False,
|
|
361
|
+
role: Optional[str] = None,
|
|
362
|
+
auto_compress: bool = False,
|
|
363
|
+
) -> Generator[dict, None, None]:
|
|
364
|
+
if local_path.is_file():
|
|
365
|
+
raise UsageError("Cannot use recursive upload with a single file.")
|
|
366
|
+
|
|
367
|
+
if local_path.is_dir():
|
|
368
|
+
root = local_path
|
|
369
|
+
glob_pattern = str(local_path / "**/*")
|
|
370
|
+
else:
|
|
371
|
+
root = Path([p for p in local_path.parents if p.is_dir()][0])
|
|
372
|
+
glob_pattern = str(local_path)
|
|
373
|
+
|
|
374
|
+
with TemporaryDirectory() as tmp:
|
|
375
|
+
temp_dir_with_copy = Path(tmp)
|
|
376
|
+
|
|
377
|
+
# Create a symlink or copy the file to the temp directory
|
|
378
|
+
for file_or_dir in glob.iglob(glob_pattern, recursive=True):
|
|
379
|
+
self._symlink_or_copy(
|
|
380
|
+
source_root=root,
|
|
381
|
+
source_file_or_dir=Path(file_or_dir),
|
|
382
|
+
dest_dir=temp_dir_with_copy,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Find the deepest directories, we will be iterating from bottom to top
|
|
386
|
+
deepest_dirs_list = self._find_deepest_directories(temp_dir_with_copy)
|
|
387
|
+
|
|
388
|
+
while deepest_dirs_list:
|
|
389
|
+
# Remove as visited
|
|
390
|
+
directory = deepest_dirs_list.pop(0)
|
|
391
|
+
|
|
392
|
+
# We reached root but there are still directories to process
|
|
393
|
+
if directory == temp_dir_with_copy and deepest_dirs_list:
|
|
394
|
+
continue
|
|
395
|
+
|
|
396
|
+
# Upload the directory content, at this moment the directory has only files
|
|
397
|
+
if list(directory.iterdir()):
|
|
398
|
+
destination = StagePath.from_stage_str(
|
|
399
|
+
stage_path
|
|
400
|
+
) / directory.relative_to(temp_dir_with_copy)
|
|
401
|
+
results: list[dict] = self.put(
|
|
402
|
+
local_path=directory,
|
|
403
|
+
stage_path=destination,
|
|
404
|
+
parallel=parallel,
|
|
405
|
+
overwrite=overwrite,
|
|
406
|
+
role=role,
|
|
407
|
+
auto_compress=auto_compress,
|
|
408
|
+
use_dict_cursor=True,
|
|
409
|
+
).fetchall()
|
|
410
|
+
|
|
411
|
+
# Rewrite results to have resolved paths for better UX
|
|
412
|
+
for item in results:
|
|
413
|
+
item["source"] = (directory / item["source"]).relative_to(
|
|
414
|
+
temp_dir_with_copy
|
|
415
|
+
)
|
|
416
|
+
item["target"] = str(destination / item["target"])
|
|
417
|
+
yield item
|
|
418
|
+
|
|
419
|
+
# We end if we reach the root directory
|
|
420
|
+
if directory == temp_dir_with_copy:
|
|
421
|
+
break
|
|
422
|
+
|
|
423
|
+
# Add parent directory to the list if it's not already there
|
|
424
|
+
if directory.parent not in deepest_dirs_list:
|
|
425
|
+
deepest_dirs_list.append(directory.parent)
|
|
426
|
+
|
|
427
|
+
# Remove the directory so the parent directory will contain only files
|
|
428
|
+
shutil.rmtree(directory)
|
|
429
|
+
|
|
430
|
+
@staticmethod
|
|
431
|
+
def _find_deepest_directories(root_directory: Path) -> list[Path]:
|
|
432
|
+
"""
|
|
433
|
+
BFS to find the deepest directories. Build a tree of directories
|
|
434
|
+
structure and return leaves.
|
|
435
|
+
"""
|
|
436
|
+
deepest_dirs: list[Path] = list()
|
|
437
|
+
|
|
438
|
+
queue: Deque[Path] = deque()
|
|
439
|
+
queue.append(root_directory)
|
|
440
|
+
while queue:
|
|
441
|
+
current_dir = queue.popleft()
|
|
442
|
+
# Sorted to have deterministic order
|
|
443
|
+
children_directories = sorted(
|
|
444
|
+
list(d for d in current_dir.iterdir() if d.is_dir())
|
|
445
|
+
)
|
|
446
|
+
if not children_directories and current_dir not in deepest_dirs:
|
|
447
|
+
deepest_dirs.append(current_dir)
|
|
448
|
+
else:
|
|
449
|
+
queue.extend([c for c in children_directories if c not in deepest_dirs])
|
|
450
|
+
deepest_dirs_list = sorted(
|
|
451
|
+
list(deepest_dirs), key=lambda d: len(d.parts), reverse=True
|
|
452
|
+
)
|
|
453
|
+
return deepest_dirs_list
|
|
454
|
+
|
|
326
455
|
def copy_files(self, source_path: str, destination_path: str) -> SnowflakeCursor:
|
|
327
456
|
source_stage_path = self.build_path(source_path)
|
|
328
457
|
# We copy only into stage
|
|
@@ -339,7 +468,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
339
468
|
# Destination needs to end with /
|
|
340
469
|
dest = destination_stage_path.absolute_path().rstrip("/") + "/"
|
|
341
470
|
query = f"copy files into {dest} from {source_stage_path}"
|
|
342
|
-
return self.
|
|
471
|
+
return self.execute_query(query)
|
|
343
472
|
|
|
344
473
|
def remove(
|
|
345
474
|
self, stage_name: str, path: str, role: Optional[str] = None
|
|
@@ -352,7 +481,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
352
481
|
"""
|
|
353
482
|
with self.use_role(role) if role else nullcontext():
|
|
354
483
|
stage_path = self.build_path(stage_name) / path
|
|
355
|
-
return self.
|
|
484
|
+
return self.execute_query(f"remove {stage_path.path_for_sql()}")
|
|
356
485
|
|
|
357
486
|
def create(
|
|
358
487
|
self, fqn: FQN, comment: Optional[str] = None, temporary: bool = False
|
|
@@ -361,7 +490,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
361
490
|
query = f"create {temporary_str}stage if not exists {fqn.sql_identifier}"
|
|
362
491
|
if comment:
|
|
363
492
|
query += f" comment='{comment}'"
|
|
364
|
-
return self.
|
|
493
|
+
return self.execute_query(query)
|
|
365
494
|
|
|
366
495
|
def iter_stage(self, stage_path: StagePath):
|
|
367
496
|
for file in self.list_files(stage_path.absolute_path()).fetchall():
|
|
@@ -557,7 +686,7 @@ class StageManager(SqlExecutionMixin):
|
|
|
557
686
|
query = f"execute immediate from {self.quote_stage_name(file_stage_path)}"
|
|
558
687
|
if variables:
|
|
559
688
|
query += variables
|
|
560
|
-
self.
|
|
689
|
+
self.execute_query(query)
|
|
561
690
|
return StageManager._success_result(file=original_file)
|
|
562
691
|
except ProgrammingError as e:
|
|
563
692
|
StageManager._handle_execution_exception(on_error=on_error, exception=e)
|
|
@@ -45,10 +45,10 @@ log = logging.getLogger(__name__)
|
|
|
45
45
|
class StreamlitManager(SqlExecutionMixin):
|
|
46
46
|
def execute(self, app_name: FQN):
|
|
47
47
|
query = f"EXECUTE STREAMLIT {app_name.sql_identifier}()"
|
|
48
|
-
return self.
|
|
48
|
+
return self.execute_query(query=query)
|
|
49
49
|
|
|
50
50
|
def share(self, streamlit_name: FQN, to_role: str) -> SnowflakeCursor:
|
|
51
|
-
return self.
|
|
51
|
+
return self.execute_query(
|
|
52
52
|
f"grant usage on streamlit {streamlit_name.sql_identifier} to role {to_role}"
|
|
53
53
|
)
|
|
54
54
|
|
|
@@ -118,7 +118,7 @@ class StreamlitManager(SqlExecutionMixin):
|
|
|
118
118
|
if streamlit.secrets:
|
|
119
119
|
query.append(streamlit.get_secrets_sql())
|
|
120
120
|
|
|
121
|
-
self.
|
|
121
|
+
self.execute_query("\n".join(query))
|
|
122
122
|
|
|
123
123
|
def deploy(self, streamlit: StreamlitEntityModel, replace: bool = False):
|
|
124
124
|
streamlit_id = streamlit.fqn.using_connection(self._conn)
|
|
@@ -152,11 +152,11 @@ class StreamlitManager(SqlExecutionMixin):
|
|
|
152
152
|
)
|
|
153
153
|
try:
|
|
154
154
|
if use_versioned_stage:
|
|
155
|
-
self.
|
|
155
|
+
self.execute_query(
|
|
156
156
|
f"ALTER STREAMLIT {streamlit_id.identifier} ADD LIVE VERSION FROM LAST"
|
|
157
157
|
)
|
|
158
158
|
elif not FeatureFlag.ENABLE_STREAMLIT_NO_CHECKOUTS.is_enabled():
|
|
159
|
-
self.
|
|
159
|
+
self.execute_query(
|
|
160
160
|
f"ALTER streamlit {streamlit_id.identifier} CHECKOUT"
|
|
161
161
|
)
|
|
162
162
|
except ProgrammingError as e:
|
|
@@ -22,7 +22,6 @@ from typing import List, Optional
|
|
|
22
22
|
|
|
23
23
|
import typer
|
|
24
24
|
import yaml
|
|
25
|
-
from click import MissingParameter
|
|
26
25
|
from snowflake.cli._plugins.nativeapp.artifacts import BundleMap
|
|
27
26
|
from snowflake.cli._plugins.nativeapp.common_flags import (
|
|
28
27
|
ForceOption,
|
|
@@ -263,6 +262,11 @@ def version_create(
|
|
|
263
262
|
help=f"""The patch number you want to create for an existing version.
|
|
264
263
|
Defaults to undefined if it is not set, which means the Snowflake CLI either uses the patch specified in the `manifest.yml` file or automatically generates a new patch number.""",
|
|
265
264
|
),
|
|
265
|
+
label: Optional[str] = typer.Option(
|
|
266
|
+
None,
|
|
267
|
+
"--label",
|
|
268
|
+
help="A label for the version that is displayed to consumers. If unset, the version label specified in `manifest.yml` file is used.",
|
|
269
|
+
),
|
|
266
270
|
skip_git_check: Optional[bool] = typer.Option(
|
|
267
271
|
False,
|
|
268
272
|
"--skip-git-check",
|
|
@@ -274,8 +278,6 @@ def version_create(
|
|
|
274
278
|
**options,
|
|
275
279
|
):
|
|
276
280
|
"""Creates a new version for the specified entity."""
|
|
277
|
-
if version is None and patch is not None:
|
|
278
|
-
raise MissingParameter("Cannot provide a patch without version!")
|
|
279
281
|
|
|
280
282
|
cli_context = get_cli_context()
|
|
281
283
|
ws = WorkspaceManager(
|
|
@@ -287,6 +289,7 @@ def version_create(
|
|
|
287
289
|
EntityActions.VERSION_CREATE,
|
|
288
290
|
version=version,
|
|
289
291
|
patch=patch,
|
|
292
|
+
label=label,
|
|
290
293
|
skip_git_check=skip_git_check,
|
|
291
294
|
interactive=interactive,
|
|
292
295
|
force=force,
|
snowflake/cli/api/config.py
CHANGED
|
@@ -181,7 +181,7 @@ def _read_config_file():
|
|
|
181
181
|
)
|
|
182
182
|
warnings.warn(
|
|
183
183
|
f"Unauthorized users ({users}) have access to configuration file {CONFIG_MANAGER.file_path}.\n"
|
|
184
|
-
f'Run `icacls "{CONFIG_MANAGER.file_path}" /
|
|
184
|
+
f'Run `icacls "{CONFIG_MANAGER.file_path}" /remove:g <USER_ID>` on those users to restrict permissions.'
|
|
185
185
|
)
|
|
186
186
|
|
|
187
187
|
try:
|
|
@@ -340,9 +340,24 @@ def _get_envs_for_path(*path) -> dict:
|
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
|
|
343
|
-
def _dump_config(
|
|
343
|
+
def _dump_config(config_and_connections: Dict):
|
|
344
|
+
config_toml_dict = config_and_connections.copy()
|
|
345
|
+
|
|
346
|
+
if CONNECTIONS_FILE.exists():
|
|
347
|
+
# update connections in connections.toml
|
|
348
|
+
# it will add only connections (maybe updated) which were originally read from connections.toml
|
|
349
|
+
# it won't add connections from config.toml
|
|
350
|
+
# because config manager doesn't have connections from config.toml if connections.toml exists
|
|
351
|
+
_update_connections_toml(config_and_connections.get("connections") or {})
|
|
352
|
+
# to config.toml save only connections from config.toml
|
|
353
|
+
connections_to_save_in_config_toml = _read_config_file_toml().get("connections")
|
|
354
|
+
if connections_to_save_in_config_toml:
|
|
355
|
+
config_toml_dict["connections"] = connections_to_save_in_config_toml
|
|
356
|
+
else:
|
|
357
|
+
config_toml_dict.pop("connections", None)
|
|
358
|
+
|
|
344
359
|
with SecurePath(CONFIG_MANAGER.file_path).open("w+") as fh:
|
|
345
|
-
dump(
|
|
360
|
+
dump(config_toml_dict, fh)
|
|
346
361
|
|
|
347
362
|
|
|
348
363
|
def _check_default_config_files_permissions() -> None:
|
|
@@ -373,9 +388,12 @@ def get_feature_flags_section() -> Dict[str, bool | Literal["UNKNOWN"]]:
|
|
|
373
388
|
return {k: _bool_or_unknown(v) for k, v in flags.items()}
|
|
374
389
|
|
|
375
390
|
|
|
391
|
+
def _read_config_file_toml() -> dict:
|
|
392
|
+
return tomlkit.loads(CONFIG_MANAGER.file_path.read_text()).unwrap()
|
|
393
|
+
|
|
394
|
+
|
|
376
395
|
def _read_connections_toml() -> dict:
|
|
377
|
-
|
|
378
|
-
return tomlkit.loads(f.read()).unwrap()
|
|
396
|
+
return tomlkit.loads(CONNECTIONS_FILE.read_text()).unwrap()
|
|
379
397
|
|
|
380
398
|
|
|
381
399
|
def _update_connections_toml(connections: dict):
|
|
@@ -29,10 +29,6 @@ IMPORTANT_STYLE: Style = Style(bold=True, italic=True)
|
|
|
29
29
|
INDENTATION_LEVEL: int = 2
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
class CliConsoleNestingProhibitedError(RuntimeError):
|
|
33
|
-
"""CliConsole phase nesting not allowed."""
|
|
34
|
-
|
|
35
|
-
|
|
36
32
|
class CliConsole(AbstractConsole):
|
|
37
33
|
"""An utility for displaying intermediate output.
|
|
38
34
|
|
|
@@ -70,28 +66,21 @@ class CliConsole(AbstractConsole):
|
|
|
70
66
|
@contextmanager
|
|
71
67
|
def phase(self, enter_message: str, exit_message: Optional[str] = None):
|
|
72
68
|
"""A context manager for organising steps into logical group."""
|
|
73
|
-
if self.in_phase:
|
|
74
|
-
raise CliConsoleNestingProhibitedError("Only one phase allowed at a time.")
|
|
75
|
-
if self._extra_indent > 0:
|
|
76
|
-
raise CliConsoleNestingProhibitedError(
|
|
77
|
-
"Phase cannot be used in an indented block."
|
|
78
|
-
)
|
|
79
|
-
|
|
80
69
|
self._print(self._format_message(enter_message, Output.PHASE))
|
|
81
|
-
self.
|
|
70
|
+
self._extra_indent += 1
|
|
82
71
|
|
|
83
72
|
try:
|
|
84
73
|
yield self.step
|
|
85
74
|
finally:
|
|
86
|
-
self.
|
|
75
|
+
self._extra_indent -= 1
|
|
87
76
|
if exit_message:
|
|
88
77
|
self._print(self._format_message(exit_message, Output.PHASE))
|
|
89
78
|
|
|
90
79
|
@contextmanager
|
|
91
80
|
def indented(self):
|
|
92
81
|
"""
|
|
93
|
-
A context manager for temporarily indenting messages and warnings.
|
|
94
|
-
|
|
82
|
+
A context manager for temporarily indenting messages and warnings.
|
|
83
|
+
Multiple indented blocks can be nested (use sparingly).
|
|
95
84
|
"""
|
|
96
85
|
self._extra_indent += 1
|
|
97
86
|
try:
|
|
@@ -104,10 +93,6 @@ class CliConsole(AbstractConsole):
|
|
|
104
93
|
|
|
105
94
|
If called within a phase, the output will be indented.
|
|
106
95
|
"""
|
|
107
|
-
if self._extra_indent > 0:
|
|
108
|
-
raise CliConsoleNestingProhibitedError(
|
|
109
|
-
"Step cannot be used in an indented block."
|
|
110
|
-
)
|
|
111
96
|
text = self._format_message(message, Output.STEP)
|
|
112
97
|
self._print(text)
|
|
113
98
|
|
|
@@ -12,6 +12,7 @@ from snowflake.cli._plugins.nativeapp.exceptions import (
|
|
|
12
12
|
InvalidTemplateInFileError,
|
|
13
13
|
MissingScriptError,
|
|
14
14
|
)
|
|
15
|
+
from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade
|
|
15
16
|
from snowflake.cli._plugins.nativeapp.utils import verify_exists, verify_no_directories
|
|
16
17
|
from snowflake.cli._plugins.stage.diff import (
|
|
17
18
|
DiffResult,
|
|
@@ -106,22 +107,15 @@ def sync_deploy_root_with_stage(
|
|
|
106
107
|
A `DiffResult` instance describing the changes that were performed.
|
|
107
108
|
"""
|
|
108
109
|
|
|
109
|
-
|
|
110
|
+
sql_facade = get_snowflake_facade()
|
|
110
111
|
# Does a stage already exist within the application package, or we need to create one?
|
|
111
112
|
# Using "if not exists" should take care of either case.
|
|
112
113
|
console.step(
|
|
113
114
|
f"Checking if stage {stage_fqn} exists, or creating a new one if none exists."
|
|
114
115
|
)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
)
|
|
119
|
-
sql_executor.execute_query(
|
|
120
|
-
f"""
|
|
121
|
-
create stage if not exists {stage_fqn}
|
|
122
|
-
encryption = (TYPE = 'SNOWFLAKE_SSE')
|
|
123
|
-
DIRECTORY = (ENABLE = TRUE)"""
|
|
124
|
-
)
|
|
116
|
+
if not sql_facade.stage_exists(stage_fqn):
|
|
117
|
+
sql_facade.create_schema(stage_schema, database=package_name)
|
|
118
|
+
sql_facade.create_stage(stage_fqn)
|
|
125
119
|
|
|
126
120
|
# Perform a diff operation and display results to the user for informational purposes
|
|
127
121
|
if print_diff:
|
|
@@ -195,36 +189,24 @@ def sync_deploy_root_with_stage(
|
|
|
195
189
|
return diff
|
|
196
190
|
|
|
197
191
|
|
|
198
|
-
def _execute_sql_script(
|
|
199
|
-
script_content: str,
|
|
200
|
-
database_name: Optional[str] = None,
|
|
201
|
-
) -> None:
|
|
202
|
-
"""
|
|
203
|
-
Executing the provided SQL script content.
|
|
204
|
-
This assumes that a relevant warehouse is already active.
|
|
205
|
-
If database_name is passed in, it will be used first.
|
|
206
|
-
"""
|
|
207
|
-
try:
|
|
208
|
-
sql_executor = get_sql_executor()
|
|
209
|
-
if database_name is not None:
|
|
210
|
-
sql_executor.execute_query(f"use database {database_name}")
|
|
211
|
-
sql_executor.execute_queries(script_content)
|
|
212
|
-
except ProgrammingError as err:
|
|
213
|
-
generic_sql_error_handler(err)
|
|
214
|
-
|
|
215
|
-
|
|
216
192
|
def execute_post_deploy_hooks(
|
|
217
193
|
console: AbstractConsole,
|
|
218
194
|
project_root: Path,
|
|
219
195
|
post_deploy_hooks: Optional[List[PostDeployHook]],
|
|
220
196
|
deployed_object_type: str,
|
|
197
|
+
role_name: str,
|
|
221
198
|
database_name: str,
|
|
199
|
+
warehouse_name: str,
|
|
222
200
|
) -> None:
|
|
223
201
|
"""
|
|
224
202
|
Executes post-deploy hooks for the given object type.
|
|
225
203
|
While executing SQL post deploy hooks, it first switches to the database provided in the input.
|
|
226
204
|
All post deploy scripts templates will first be expanded using the global template context.
|
|
227
205
|
"""
|
|
206
|
+
get_cli_context().metrics.set_counter_default(
|
|
207
|
+
CLICounterField.POST_DEPLOY_SCRIPTS, 0
|
|
208
|
+
)
|
|
209
|
+
|
|
228
210
|
if not post_deploy_hooks:
|
|
229
211
|
return
|
|
230
212
|
|
|
@@ -248,11 +230,16 @@ def execute_post_deploy_hooks(
|
|
|
248
230
|
sql_scripts_paths,
|
|
249
231
|
)
|
|
250
232
|
|
|
233
|
+
sql_facade = get_snowflake_facade()
|
|
234
|
+
|
|
251
235
|
for index, sql_script_path in enumerate(display_paths):
|
|
252
236
|
console.step(f"Executing SQL script: {sql_script_path}")
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
237
|
+
sql_facade.execute_user_script(
|
|
238
|
+
queries=scripts_content_list[index],
|
|
239
|
+
script_name=sql_script_path,
|
|
240
|
+
role=role_name,
|
|
241
|
+
warehouse=warehouse_name,
|
|
242
|
+
database=database_name,
|
|
256
243
|
)
|
|
257
244
|
|
|
258
245
|
|
snowflake/cli/api/errno.py
CHANGED
|
@@ -26,3 +26,5 @@ ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS = 93046
|
|
|
26
26
|
NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS = 93055
|
|
27
27
|
APPLICATION_NO_LONGER_AVAILABLE = 93079
|
|
28
28
|
APPLICATION_OWNS_EXTERNAL_OBJECTS = 93128
|
|
29
|
+
APPLICATION_REQUIRES_TELEMETRY_SHARING = 93321
|
|
30
|
+
CANNOT_DISABLE_MANDATORY_TELEMETRY = 93329
|
snowflake/cli/api/exceptions.py
CHANGED
|
@@ -190,6 +190,15 @@ class IncompatibleParametersError(UsageError):
|
|
|
190
190
|
)
|
|
191
191
|
|
|
192
192
|
|
|
193
|
+
class UnmetParametersError(UsageError):
|
|
194
|
+
def __init__(self, options: list[str]):
|
|
195
|
+
options_with_quotes = [f"'{option}'" for option in options]
|
|
196
|
+
comma_separated_options = ", ".join(options_with_quotes[:-1])
|
|
197
|
+
super().__init__(
|
|
198
|
+
f"Parameters {comma_separated_options} and {options_with_quotes[-1]} must be used simultaneously."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
193
202
|
class NoWarehouseSelectedInSessionError(ClickException):
|
|
194
203
|
def __init__(self, msg: str):
|
|
195
204
|
super().__init__(
|