snowflake-cli-labs 2.5.0rc3__py3-none-any.whl → 2.6.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.
Files changed (67) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/api/cli_global_context.py +31 -3
  3. snowflake/cli/api/commands/decorators.py +21 -6
  4. snowflake/cli/api/commands/flags.py +60 -51
  5. snowflake/cli/api/commands/snow_typer.py +24 -0
  6. snowflake/cli/api/commands/typer_pre_execute.py +26 -0
  7. snowflake/cli/api/console/abc.py +8 -0
  8. snowflake/cli/api/console/console.py +29 -4
  9. snowflake/cli/api/constants.py +3 -0
  10. snowflake/cli/api/project/definition.py +17 -35
  11. snowflake/cli/api/project/definition_manager.py +22 -19
  12. snowflake/cli/api/project/errors.py +9 -6
  13. snowflake/cli/api/project/schemas/identifier_model.py +1 -1
  14. snowflake/cli/api/project/schemas/native_app/application.py +15 -3
  15. snowflake/cli/api/project/schemas/native_app/native_app.py +5 -1
  16. snowflake/cli/api/project/schemas/native_app/path_mapping.py +14 -3
  17. snowflake/cli/api/project/schemas/project_definition.py +37 -6
  18. snowflake/cli/api/project/schemas/streamlit/streamlit.py +3 -0
  19. snowflake/cli/api/project/schemas/updatable_model.py +2 -6
  20. snowflake/cli/api/rest_api.py +113 -0
  21. snowflake/cli/api/sanitizers.py +43 -0
  22. snowflake/cli/api/sql_execution.py +7 -0
  23. snowflake/cli/api/utils/definition_rendering.py +95 -25
  24. snowflake/cli/api/utils/models.py +31 -26
  25. snowflake/cli/api/utils/rendering.py +24 -3
  26. snowflake/cli/app/cli_app.py +2 -0
  27. snowflake/cli/app/commands_registration/command_plugins_loader.py +8 -0
  28. snowflake/cli/app/dev/docs/commands_docs_generator.py +100 -0
  29. snowflake/cli/app/dev/docs/generator.py +8 -67
  30. snowflake/cli/app/dev/docs/project_definition_docs_generator.py +58 -0
  31. snowflake/cli/app/dev/docs/project_definition_generate_json_schema.py +227 -0
  32. snowflake/cli/app/dev/docs/template_utils.py +23 -0
  33. snowflake/cli/app/dev/docs/templates/definition_description.rst.jinja2 +38 -0
  34. snowflake/cli/app/dev/docs/templates/usage.rst.jinja2 +6 -1
  35. snowflake/cli/app/loggers.py +25 -0
  36. snowflake/cli/app/printing.py +7 -5
  37. snowflake/cli/app/telemetry.py +11 -0
  38. snowflake/cli/plugins/nativeapp/artifacts.py +78 -9
  39. snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +3 -11
  40. snowflake/cli/plugins/nativeapp/codegen/compiler.py +6 -24
  41. snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +27 -27
  42. snowflake/cli/plugins/nativeapp/commands.py +23 -12
  43. snowflake/cli/plugins/nativeapp/constants.py +2 -0
  44. snowflake/cli/plugins/nativeapp/errno.py +15 -0
  45. snowflake/cli/plugins/nativeapp/feature_flags.py +24 -0
  46. snowflake/cli/plugins/nativeapp/init.py +5 -0
  47. snowflake/cli/plugins/nativeapp/manager.py +101 -103
  48. snowflake/cli/plugins/nativeapp/project_model.py +181 -0
  49. snowflake/cli/plugins/nativeapp/run_processor.py +178 -110
  50. snowflake/cli/plugins/nativeapp/teardown_processor.py +89 -64
  51. snowflake/cli/plugins/nativeapp/utils.py +2 -2
  52. snowflake/cli/plugins/nativeapp/version/commands.py +3 -3
  53. snowflake/cli/plugins/object/commands.py +70 -4
  54. snowflake/cli/plugins/object/manager.py +44 -3
  55. snowflake/cli/plugins/snowpark/commands.py +2 -2
  56. snowflake/cli/plugins/sql/commands.py +2 -10
  57. snowflake/cli/plugins/sql/manager.py +4 -2
  58. snowflake/cli/plugins/stage/commands.py +23 -4
  59. snowflake/cli/plugins/stage/diff.py +81 -51
  60. snowflake/cli/plugins/stage/manager.py +2 -1
  61. snowflake/cli/plugins/streamlit/commands.py +2 -1
  62. snowflake/cli/plugins/streamlit/manager.py +6 -0
  63. {snowflake_cli_labs-2.5.0rc3.dist-info → snowflake_cli_labs-2.6.0.dist-info}/METADATA +15 -9
  64. {snowflake_cli_labs-2.5.0rc3.dist-info → snowflake_cli_labs-2.6.0.dist-info}/RECORD +67 -56
  65. {snowflake_cli_labs-2.5.0rc3.dist-info → snowflake_cli_labs-2.6.0.dist-info}/WHEEL +1 -1
  66. {snowflake_cli_labs-2.5.0rc3.dist-info → snowflake_cli_labs-2.6.0.dist-info}/entry_points.txt +0 -0
  67. {snowflake_cli_labs-2.5.0rc3.dist-info → snowflake_cli_labs-2.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -25,6 +25,7 @@ from snowflake.cli.api.config import (
25
25
  )
26
26
  from snowflake.cli.api.exceptions import InvalidLogsConfiguration
27
27
  from snowflake.cli.api.secure_path import SecurePath
28
+ from snowflake.connector.errors import ConfigSourceError
28
29
 
29
30
  _DEFAULT_LOG_FILENAME = "snowflake-cli.log"
30
31
 
@@ -82,6 +83,16 @@ class DefaultLoggingConfig:
82
83
  )
83
84
 
84
85
 
86
+ @dataclass
87
+ class InitialLoggingConfig(DefaultLoggingConfig):
88
+ loggers: Dict[str, Any] = field(
89
+ default_factory=lambda: {
90
+ "snowflake.cli": LoggerConfig(level=logging.INFO, handlers=["file"]),
91
+ "snowflake": LoggerConfig(),
92
+ }
93
+ )
94
+
95
+
85
96
  def _remove_underscore_prefixes_from_keys(d: Dict[str, Any]) -> None:
86
97
  for k, v in list(d.items()):
87
98
  if k.startswith("_"):
@@ -127,6 +138,16 @@ class FileLogsConfig:
127
138
  return self.path.path / _DEFAULT_LOG_FILENAME
128
139
 
129
140
 
141
+ def create_initial_loggers():
142
+ config = InitialLoggingConfig()
143
+ try:
144
+ file_logs_config = FileLogsConfig(debug=False)
145
+ config.handlers["file"]["filename"] = file_logs_config.filename
146
+ _configurate_logging(config)
147
+ except ConfigSourceError:
148
+ pass
149
+
150
+
130
151
  def create_loggers(verbose: bool, debug: bool):
131
152
  """Creates a logger depending on the SnowCLI parameters and config file.
132
153
  verbose == True - print info and higher logs in default format
@@ -165,6 +186,10 @@ def create_loggers(verbose: bool, debug: bool):
165
186
  config.loggers["snowflake.cli"].level = global_log_level
166
187
  config.loggers["snowflake"].level = global_log_level
167
188
 
189
+ _configurate_logging(config)
190
+
191
+
192
+ def _configurate_logging(config: DefaultLoggingConfig | InitialLoggingConfig) -> None:
168
193
  dict_config = asdict(config)
169
194
  _remove_underscore_prefixes_from_keys(dict_config)
170
195
  logging.config.dictConfig(dict_config)
@@ -34,8 +34,8 @@ from snowflake.cli.api.output.types import (
34
34
  MessageResult,
35
35
  MultipleResults,
36
36
  ObjectResult,
37
- QueryResult,
38
37
  )
38
+ from snowflake.cli.api.sanitizers import sanitize_for_terminal
39
39
 
40
40
  NO_ITEMS_FOUND: str = "No data"
41
41
 
@@ -50,6 +50,8 @@ class CustomJSONEncoder(JSONEncoder):
50
50
  """Custom JSON encoder handling serialization of non-standard types"""
51
51
 
52
52
  def default(self, o):
53
+ if isinstance(o, str):
54
+ return sanitize_for_terminal(o)
53
55
  if isinstance(o, (ObjectResult, MessageResult)):
54
56
  return o.result
55
57
  if isinstance(o, (CollectionResult, MultipleResults)):
@@ -73,8 +75,6 @@ def _get_table():
73
75
 
74
76
 
75
77
  def _print_multiple_table_results(obj: CollectionResult):
76
- if isinstance(obj, QueryResult):
77
- rich_print(obj.query)
78
78
  items = obj.result
79
79
  try:
80
80
  first_item = next(items)
@@ -134,7 +134,7 @@ def print_unstructured(obj: CommandResult | None):
134
134
  elif not obj.result:
135
135
  rich_print("No data")
136
136
  elif isinstance(obj, MessageResult):
137
- rich_print(obj.message)
137
+ rich_print(sanitize_for_terminal(obj.message))
138
138
  else:
139
139
  if isinstance(obj, ObjectResult):
140
140
  _print_single_table(obj)
@@ -149,7 +149,9 @@ def _print_single_table(obj):
149
149
  table.add_column("key", overflow="fold")
150
150
  table.add_column("value", overflow="fold")
151
151
  for key, value in obj.result.items():
152
- table.add_row(str(key), str(value))
152
+ table.add_row(
153
+ sanitize_for_terminal(str(key)), sanitize_for_terminal(str(value))
154
+ )
153
155
  rich_print(table)
154
156
 
155
157
 
@@ -51,6 +51,8 @@ class CLITelemetryField(Enum):
51
51
  EVENT = "event"
52
52
  ERROR_MSG = "error_msg"
53
53
  ERROR_TYPE = "error_type"
54
+ # Project context
55
+ PROJECT_DEFINITION_VERSION = "project_definition_version"
54
56
 
55
57
 
56
58
  class TelemetryEvent(Enum):
@@ -74,9 +76,18 @@ def _find_command_info() -> TelemetryDict:
74
76
  CLITelemetryField.COMMAND_OUTPUT_TYPE: ctx.params.get(
75
77
  "format", OutputFormat.TABLE
76
78
  ).value,
79
+ CLITelemetryField.PROJECT_DEFINITION_VERSION: str(_get_definition_version()),
77
80
  }
78
81
 
79
82
 
83
+ def _get_definition_version() -> str | None:
84
+ from snowflake.cli.api.cli_global_context import cli_context
85
+
86
+ if cli_context.project_definition:
87
+ return cli_context.project_definition.definition_version
88
+ return None
89
+
90
+
80
91
  def command_info() -> str:
81
92
  info = _find_command_info()
82
93
  command = ".".join(info[CLITelemetryField.COMMAND])
@@ -116,7 +116,7 @@ class _ArtifactPathMap:
116
116
  self.__src_dest_pairs: List[Tuple[Path, Path]] = []
117
117
  # built-in dict instances are ordered as of Python 3.7
118
118
  self.__src_to_dest: Dict[Path, List[Path]] = {}
119
- self.__dest_to_src: Dict[Path, List[Path]] = {}
119
+ self.__dest_to_src: Dict[Path, Optional[Path]] = {}
120
120
 
121
121
  # This dictionary accumulates keys for each directory or file to be created in
122
122
  # the deploy root for any artifact mapping rule being processed. This includes
@@ -142,7 +142,7 @@ class _ArtifactPathMap:
142
142
 
143
143
  absolute_src = self._project_root / src
144
144
 
145
- current_sources = self.__dest_to_src.get(dest, [])
145
+ current_source = self.__dest_to_src.get(dest)
146
146
  src_is_dir = absolute_src.is_dir()
147
147
  if dest_is_dir:
148
148
  assert src_is_dir # file -> directory is not possible here given how rules are processed
@@ -156,7 +156,7 @@ class _ArtifactPathMap:
156
156
  else:
157
157
  # file -> file
158
158
  # Check that there is no previous mapping for the same file.
159
- if current_sources and src not in current_sources:
159
+ if current_source is not None and current_source != src:
160
160
  # There is already a different source mapping to this destination
161
161
  raise TooManyFilesError(dest)
162
162
 
@@ -176,17 +176,16 @@ class _ArtifactPathMap:
176
176
  self._update_dest_is_dir(dest, dest_is_dir)
177
177
 
178
178
  dests = self.__src_to_dest.setdefault(src, [])
179
- srcs = self.__dest_to_src.setdefault(dest, [])
180
179
  if dest not in dests:
181
180
  dests.append(dest)
182
- srcs.append(src)
181
+ self.__dest_to_src[dest] = src
183
182
  self.__src_dest_pairs.append((src, dest))
184
183
 
185
- def get_sources(self, dest: Path) -> Iterable[Path]:
184
+ def get_source(self, dest: Path) -> Optional[Path]:
186
185
  """
187
- Returns all source paths associated with the provided destination path, in insertion order.
186
+ Returns the source path associated with the provided destination path, if any.
188
187
  """
189
- return self.__dest_to_src.get(dest, [])
188
+ return self.__dest_to_src.get(dest)
190
189
 
191
190
  def get_destinations(self, src: Path) -> Iterable[Path]:
192
191
  """
@@ -194,6 +193,12 @@ class _ArtifactPathMap:
194
193
  """
195
194
  return self.__src_to_dest.get(src, [])
196
195
 
196
+ def all_sources(self) -> Iterable[Path]:
197
+ """
198
+ Returns all source paths associated with this map, in insertion order.
199
+ """
200
+ return self.__src_to_dest.keys()
201
+
197
202
  def __iter__(self) -> Iterator[Tuple[Path, Path]]:
198
203
  """
199
204
  Returns all (source, destination) pairs known to this map, in insertion order.
@@ -401,7 +406,14 @@ class BundleMap:
401
406
  def to_deploy_paths(self, src: Path) -> List[Path]:
402
407
  """
403
408
  Converts a source path to its corresponding deploy root path. If the input path is relative to the project root,
404
- a path relative to the deploy root is returned. If the input path is absolute, an absolute path is returned.
409
+ paths relative to the deploy root are returned. If the input path is absolute, absolute paths are returned.
410
+
411
+ Note that the provided source path must be part of a mapping. If the source path is not part of any mapping,
412
+ an empty list is returned. For example, if `app/*` is specified as the source of a mapping,
413
+ `to_deploy_paths(Path("app"))` will not yield any result.
414
+
415
+ Arguments:
416
+ src {Path} -- the source path within the project root, in canonical or absolute form.
405
417
 
406
418
  Returns:
407
419
  The deploy root paths for the given source path, or an empty list if no such path exists.
@@ -419,11 +431,14 @@ class BundleMap:
419
431
 
420
432
  output_destinations: List[Path] = []
421
433
 
434
+ # 1. Check for exact rule matches for this path
422
435
  canonical_dests = self._artifact_map.get_destinations(canonical_src)
423
436
  if canonical_dests:
424
437
  for d in canonical_dests:
425
438
  output_destinations.append(self._to_output_dest(d, is_absolute))
426
439
 
440
+ # 2. Check for any matches to parent directories for this path that would
441
+ # cause this path to be part of the recursive copy
427
442
  canonical_parent = canonical_src.parent
428
443
  canonical_parent_dests = self.to_deploy_paths(canonical_parent)
429
444
  if canonical_parent_dests:
@@ -435,6 +450,60 @@ class BundleMap:
435
450
 
436
451
  return output_destinations
437
452
 
453
+ def all_sources(self, absolute: bool = False) -> Iterator[Path]:
454
+ """
455
+ Yields each registered artifact source in the project.
456
+
457
+ Arguments:
458
+ self: this instance
459
+ absolute (bool): Specifies whether the yielded paths should be joined with the absolute project root.
460
+ Returns:
461
+ An iterator over all artifact mapping source paths.
462
+ """
463
+ for src in self._artifact_map.all_sources():
464
+ yield self._to_output_src(src, absolute)
465
+
466
+ def to_project_path(self, dest: Path) -> Optional[Path]:
467
+ """
468
+ Converts a deploy root path to its corresponding project source path. If the input path is relative to the
469
+ deploy root, a path relative to the project root is returned. If the input path is absolute, an absolute path is
470
+ returned.
471
+
472
+ Arguments:
473
+ dest {Path} -- the destination path within the deploy root, in canonical or absolute form.
474
+
475
+ Returns:
476
+ The project root path for the given deploy root path, or None if no such path exists.
477
+ """
478
+ is_absolute = dest.is_absolute()
479
+ try:
480
+ canonical_dest = self._canonical_dest(dest)
481
+ except NotInDeployRootError:
482
+ # No mapping possible for the dest path
483
+ return None
484
+
485
+ # 1. Look for an exact rule matching this path. If we find any, then
486
+ # stop searching. This is because each destination path can only originate
487
+ # from a single source (however, one source can be copied to multiple destinations).
488
+ canonical_src = self._artifact_map.get_source(canonical_dest)
489
+ if canonical_src is not None:
490
+ return self._to_output_src(canonical_src, is_absolute)
491
+
492
+ # 2. No exact match was found, look for a match for parent directories of this
493
+ # path, recursively. Stop when a match is found
494
+ canonical_parent = canonical_dest.parent
495
+ if canonical_parent == canonical_dest:
496
+ return None
497
+ canonical_parent_src = self.to_project_path(canonical_parent)
498
+ if canonical_parent_src is not None:
499
+ canonical_child = canonical_dest.relative_to(canonical_parent)
500
+ canonical_child_candidate = canonical_parent_src / canonical_child
501
+ if self._absolute_src(canonical_child_candidate).exists():
502
+ return self._to_output_src(canonical_child_candidate, is_absolute)
503
+
504
+ # No mapping for this destination path
505
+ return None
506
+
438
507
  def _absolute_src(self, src: Path) -> Path:
439
508
  if src.is_absolute():
440
509
  resolved_src = resolve_without_follow(src)
@@ -15,15 +15,14 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  from abc import ABC, abstractmethod
18
- from pathlib import Path
19
18
  from typing import Optional
20
19
 
21
20
  from click import ClickException
22
- from snowflake.cli.api.project.schemas.native_app.native_app import NativeApp
23
21
  from snowflake.cli.api.project.schemas.native_app.path_mapping import (
24
22
  PathMapping,
25
23
  ProcessorMapping,
26
24
  )
25
+ from snowflake.cli.plugins.nativeapp.project_model import NativeAppProjectModel
27
26
 
28
27
 
29
28
  class UnsupportedArtifactProcessorError(ClickException):
@@ -36,18 +35,11 @@ class UnsupportedArtifactProcessorError(ClickException):
36
35
 
37
36
 
38
37
  class ArtifactProcessor(ABC):
39
- @abstractmethod
40
38
  def __init__(
41
39
  self,
42
- project_definition: NativeApp,
43
- project_root: Path,
44
- deploy_root: Path,
45
- generated_root: Path,
46
- **kwargs,
40
+ na_project: NativeAppProjectModel,
47
41
  ) -> None:
48
- assert project_root.is_absolute()
49
- assert deploy_root.is_absolute()
50
- assert generated_root.is_absolute()
42
+ self._na_project = na_project
51
43
 
52
44
  @abstractmethod
53
45
  def process(
@@ -14,16 +14,12 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- from pathlib import Path
18
17
  from typing import Dict, Optional
19
18
 
20
19
  from snowflake.cli.api.console import cli_console as cc
21
- from snowflake.cli.api.project.schemas.native_app.native_app import NativeApp
22
20
  from snowflake.cli.api.project.schemas.native_app.path_mapping import (
23
- PathMapping,
24
21
  ProcessorMapping,
25
22
  )
26
- from snowflake.cli.plugins.nativeapp.artifacts import resolve_without_follow
27
23
  from snowflake.cli.plugins.nativeapp.codegen.artifact_processor import (
28
24
  ArtifactProcessor,
29
25
  UnsupportedArtifactProcessorError,
@@ -31,6 +27,7 @@ from snowflake.cli.plugins.nativeapp.codegen.artifact_processor import (
31
27
  from snowflake.cli.plugins.nativeapp.codegen.snowpark.python_processor import (
32
28
  SnowparkAnnotationProcessor,
33
29
  )
30
+ from snowflake.cli.plugins.nativeapp.project_model import NativeAppProjectModel
34
31
 
35
32
  SNOWPARK_PROCESSOR = "snowpark"
36
33
 
@@ -46,21 +43,9 @@ class NativeAppCompiler:
46
43
 
47
44
  def __init__(
48
45
  self,
49
- project_definition: NativeApp,
50
- project_root: Path,
51
- deploy_root: Path,
52
- generated_root: Path,
46
+ na_project: NativeAppProjectModel,
53
47
  ):
54
- self.project_definition = project_definition
55
- self.project_root = project_root
56
- self.deploy_root = deploy_root
57
- self.generated_root = generated_root
58
-
59
- self.artifacts = [
60
- artifact
61
- for artifact in project_definition.artifacts
62
- if isinstance(artifact, PathMapping)
63
- ]
48
+ self._na_project = na_project
64
49
  # dictionary of all processors created and shared between different artifact objects.
65
50
  self.cached_processors: Dict[str, ArtifactProcessor] = {}
66
51
 
@@ -70,7 +55,7 @@ class NativeAppCompiler:
70
55
  May have side-effects on the filesystem by either directly editing source files or the deploy root.
71
56
  """
72
57
  should_proceed = False
73
- for artifact in self.artifacts:
58
+ for artifact in self._na_project.artifacts:
74
59
  if artifact.processors:
75
60
  should_proceed = True
76
61
  break
@@ -78,7 +63,7 @@ class NativeAppCompiler:
78
63
  return
79
64
 
80
65
  with cc.phase("Invoking artifact processors"):
81
- for artifact in self.artifacts:
66
+ for artifact in self._na_project.artifacts:
82
67
  for processor in artifact.processors:
83
68
  artifact_processor = self._try_create_processor(
84
69
  processor_mapping=processor,
@@ -107,10 +92,7 @@ class NativeAppCompiler:
107
92
  return curr_processor
108
93
  else:
109
94
  curr_processor = SnowparkAnnotationProcessor(
110
- project_definition=self.project_definition,
111
- project_root=resolve_without_follow(self.project_root),
112
- deploy_root=resolve_without_follow(self.deploy_root),
113
- generated_root=resolve_without_follow(self.generated_root),
95
+ na_project=self._na_project,
114
96
  )
115
97
  self.cached_processors[SNOWPARK_PROCESSOR] = curr_processor
116
98
  return curr_processor
@@ -21,9 +21,8 @@ from textwrap import dedent
21
21
  from typing import Any, Dict, List, Optional, Set
22
22
 
23
23
  from click import ClickException
24
+ from pydantic import ValidationError
24
25
  from snowflake.cli.api.console import cli_console as cc
25
- from snowflake.cli.api.project.errors import SchemaValidationError
26
- from snowflake.cli.api.project.schemas.native_app.native_app import NativeApp
27
26
  from snowflake.cli.api.project.schemas.native_app.path_mapping import (
28
27
  PathMapping,
29
28
  ProcessorMapping,
@@ -52,6 +51,7 @@ from snowflake.cli.plugins.nativeapp.codegen.snowpark.models import (
52
51
  ExtensionFunctionTypeEnum,
53
52
  NativeAppExtensionFunction,
54
53
  )
54
+ from snowflake.cli.plugins.nativeapp.project_model import NativeAppProjectModel
55
55
  from snowflake.cli.plugins.stage.diff import to_stage_path
56
56
 
57
57
  DEFAULT_TIMEOUT = 30
@@ -163,25 +163,18 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
163
163
 
164
164
  def __init__(
165
165
  self,
166
- project_definition: NativeApp,
167
- project_root: Path,
168
- deploy_root: Path,
169
- generated_root: Path,
166
+ na_project: NativeAppProjectModel,
170
167
  ):
171
- super().__init__(
172
- project_definition=project_definition,
173
- project_root=project_root,
174
- deploy_root=deploy_root,
175
- generated_root=generated_root,
176
- )
177
- self.project_definition = project_definition
178
- self.project_root = project_root
179
- self.deploy_root = deploy_root
180
- self.generated_root = generated_root
168
+ super().__init__(na_project=na_project)
169
+
170
+ assert self._na_project.bundle_root.is_absolute()
171
+ assert self._na_project.deploy_root.is_absolute()
172
+ assert self._na_project.generated_root.is_absolute()
173
+ assert self._na_project.project_root.is_absolute()
181
174
 
182
- if self.generated_root.exists():
175
+ if self._na_project.generated_root.exists():
183
176
  raise ClickException(
184
- f"Path {self.generated_root} already exists. Please choose a different name for your generated directory in the project definition file."
177
+ f"Path {self._na_project.generated_root} already exists. Please choose a different name for your generated directory in the project definition file."
185
178
  )
186
179
 
187
180
  def process(
@@ -196,7 +189,8 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
196
189
  """
197
190
 
198
191
  bundle_map = BundleMap(
199
- project_root=self.project_root, deploy_root=self.deploy_root
192
+ project_root=self._na_project.project_root,
193
+ deploy_root=self._na_project.deploy_root,
200
194
  )
201
195
  bundle_map.add(artifact_to_process)
202
196
 
@@ -242,7 +236,7 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
242
236
  edit_setup_script_with_exec_imm_sql(
243
237
  collected_sql_files=collected_sql_files,
244
238
  deploy_root=bundle_map.deploy_root(),
245
- generated_root=self.generated_root,
239
+ generated_root=self._na_project.generated_root,
246
240
  )
247
241
 
248
242
  def _normalize_imports(
@@ -323,7 +317,7 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
323
317
  self, bundle_map: BundleMap, processor_mapping: Optional[ProcessorMapping]
324
318
  ) -> Dict[Path, List[NativeAppExtensionFunction]]:
325
319
  kwargs = (
326
- _determine_virtual_env(self.project_root, processor_mapping)
320
+ _determine_virtual_env(self._na_project.project_root, processor_mapping)
327
321
  if processor_mapping is not None
328
322
  else {}
329
323
  )
@@ -342,7 +336,7 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
342
336
  )
343
337
  collected_extension_function_json = _execute_in_sandbox(
344
338
  py_file=str(dest_file.resolve()),
345
- deploy_root=self.deploy_root,
339
+ deploy_root=self._na_project.deploy_root,
346
340
  kwargs=kwargs,
347
341
  )
348
342
 
@@ -359,7 +353,7 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
359
353
  deploy_root=bundle_map.deploy_root(),
360
354
  )
361
355
  collected_extension_functions.append(extension_fn)
362
- except SchemaValidationError:
356
+ except ValidationError:
363
357
  cc.warning("Invalid extension function definition")
364
358
 
365
359
  if collected_extension_functions:
@@ -373,8 +367,10 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
373
367
  """
374
368
  Generates a SQL filename for the generated root from the python file, and creates its parent directories.
375
369
  """
376
- relative_py_file = py_file.relative_to(self.deploy_root)
377
- sql_file = Path(self.generated_root, relative_py_file.with_suffix(".sql"))
370
+ relative_py_file = py_file.relative_to(self._na_project.deploy_root)
371
+ sql_file = Path(
372
+ self._na_project.generated_root, relative_py_file.with_suffix(".sql")
373
+ )
378
374
  if sql_file.exists():
379
375
  cc.warning(
380
376
  f"""\
@@ -510,7 +506,9 @@ def edit_setup_script_with_exec_imm_sql(
510
506
  sql_file_relative_path = sql_file.relative_to(
511
507
  deploy_root
512
508
  ) # Path on stage, without the leading slash
513
- file.write(f"EXECUTE IMMEDIATE FROM '/{sql_file_relative_path}';\n")
509
+ file.write(
510
+ f"EXECUTE IMMEDIATE FROM '/{to_stage_path(sql_file_relative_path)}';\n"
511
+ )
514
512
 
515
513
  # Find the setup script in the deploy root.
516
514
  setup_file_path = find_setup_script_file(deploy_root=deploy_root)
@@ -524,5 +522,7 @@ def edit_setup_script_with_exec_imm_sql(
524
522
  generated_file_relative_path = generated_file_path.relative_to(deploy_root)
525
523
  with open(setup_file_path, "w", encoding="utf-8") as file:
526
524
  file.write(code)
527
- file.write(f"\nEXECUTE IMMEDIATE FROM '/{generated_file_relative_path}';")
525
+ file.write(
526
+ f"\nEXECUTE IMMEDIATE FROM '/{to_stage_path(generated_file_relative_path)}';"
527
+ )
528
528
  file.write(f"\n")
@@ -16,9 +16,11 @@ from __future__ import annotations
16
16
 
17
17
  import logging
18
18
  from pathlib import Path
19
+ from textwrap import dedent
19
20
  from typing import List, Optional
20
21
 
21
22
  import typer
23
+ from click import ClickException
22
24
  from snowflake.cli.api.cli_global_context import cli_context
23
25
  from snowflake.cli.api.commands.decorators import (
24
26
  with_project_definition,
@@ -151,7 +153,7 @@ def app_bundle(
151
153
  Prepares a local folder with configured app artifacts.
152
154
  """
153
155
  manager = NativeAppManager(
154
- project_definition=cli_context.project_definition,
156
+ project_definition=cli_context.project_definition.native_app,
155
157
  project_root=cli_context.project_root,
156
158
  )
157
159
  manager.build_bundle()
@@ -199,7 +201,7 @@ def app_run(
199
201
  policy = DenyAlwaysPolicy()
200
202
 
201
203
  processor = NativeAppRunProcessor(
202
- project_definition=cli_context.project_definition,
204
+ project_definition=cli_context.project_definition.native_app,
203
205
  project_root=cli_context.project_root,
204
206
  )
205
207
  bundle_map = processor.build_bundle()
@@ -228,7 +230,7 @@ def app_open(
228
230
  once it has been installed in your account.
229
231
  """
230
232
  manager = NativeAppManager(
231
- project_definition=cli_context.project_definition,
233
+ project_definition=cli_context.project_definition.native_app,
232
234
  project_root=cli_context.project_root,
233
235
  )
234
236
  if manager.get_existing_app_info():
@@ -256,7 +258,7 @@ def app_teardown(
256
258
  Attempts to drop both the application object and application package as defined in the project definition file.
257
259
  """
258
260
  processor = NativeAppTeardownProcessor(
259
- project_definition=cli_context.project_definition,
261
+ project_definition=cli_context.project_definition.native_app,
260
262
  project_root=cli_context.project_root,
261
263
  )
262
264
  processor.process(interactive, force, cascade)
@@ -268,7 +270,7 @@ def app_teardown(
268
270
  def app_deploy(
269
271
  prune: Optional[bool] = typer.Option(
270
272
  default=None,
271
- help=f"""Whether to delete specified files from the stage if they don't exist locally. If set, the command deletes files that exist in the stage, but not in the local filesystem.""",
273
+ help=f"""Whether to delete specified files from the stage if they don't exist locally. If set, the command deletes files that exist in the stage, but not in the local filesystem. This option cannot be used when paths are specified.""",
272
274
  ),
273
275
  recursive: Optional[bool] = typer.Option(
274
276
  None,
@@ -276,10 +278,16 @@ def app_deploy(
276
278
  "-r",
277
279
  help=f"""Whether to traverse and deploy files from subdirectories. If set, the command deploys all files and subdirectories; otherwise, only files in the current directory are deployed.""",
278
280
  ),
279
- files: Optional[List[Path]] = typer.Argument(
281
+ paths: Optional[List[Path]] = typer.Argument(
280
282
  default=None,
281
283
  show_default=False,
282
- help=f"""Paths, relative to the the project root, of files you want to upload to a stage. The paths must match one of the artifacts src pattern entries in snowflake.yml. If unspecified, the command syncs all local changes to the stage.""",
284
+ help=dedent(
285
+ f"""
286
+ Paths, relative to the the project root, of files or directories you want to upload to a stage. If a file is
287
+ specified, it must match one of the artifacts src pattern entries in snowflake.yml. If a directory is
288
+ specified, it will be searched for subfolders or files to deploy based on artifacts src pattern entries. If
289
+ unspecified, the command syncs all local changes to the stage."""
290
+ ).strip(),
283
291
  ),
284
292
  validate: bool = ValidateOption,
285
293
  **options,
@@ -288,8 +296,8 @@ def app_deploy(
288
296
  Creates an application package in your Snowflake account and syncs the local changes to the stage without creating or updating the application.
289
297
  Running this command with no arguments at all, as in `snow app deploy`, is a shorthand for `snow app deploy --prune --recursive`.
290
298
  """
291
- has_files = files is not None and len(files) > 0
292
- if prune is None and recursive is None and not has_files:
299
+ has_paths = paths is not None and len(paths) > 0
300
+ if prune is None and recursive is None and not has_paths:
293
301
  prune = True
294
302
  recursive = True
295
303
  else:
@@ -298,8 +306,11 @@ def app_deploy(
298
306
  if recursive is None:
299
307
  recursive = False
300
308
 
309
+ if has_paths and prune:
310
+ raise ClickException("--prune cannot be used when paths are also specified")
311
+
301
312
  manager = NativeAppManager(
302
- project_definition=cli_context.project_definition,
313
+ project_definition=cli_context.project_definition.native_app,
303
314
  project_root=cli_context.project_root,
304
315
  )
305
316
 
@@ -308,7 +319,7 @@ def app_deploy(
308
319
  bundle_map=bundle_map,
309
320
  prune=prune,
310
321
  recursive=recursive,
311
- local_paths_to_sync=files,
322
+ local_paths_to_sync=paths,
312
323
  validate=validate,
313
324
  )
314
325
 
@@ -324,7 +335,7 @@ def app_validate(**options):
324
335
  Validates a deployed Snowflake Native App's setup script.
325
336
  """
326
337
  manager = NativeAppManager(
327
- project_definition=cli_context.project_definition,
338
+ project_definition=cli_context.project_definition.native_app,
328
339
  project_root=cli_context.project_root,
329
340
  )
330
341
  if cli_context.output_format == OutputFormat.JSON:
@@ -29,3 +29,5 @@ EXTERNAL_DISTRIBUTION = "external"
29
29
  ERROR_MESSAGE_2003 = "does not exist or not authorized"
30
30
  ERROR_MESSAGE_2043 = "Object does not exist, or operation cannot be performed."
31
31
  ERROR_MESSAGE_606 = "No active warehouse selected in the current session."
32
+ ERROR_MESSAGE_093079 = "Application is no longer available for use"
33
+ ERROR_MESSAGE_093128 = "The application owns one or more objects within the account"
@@ -0,0 +1,15 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ APPLICATION_NO_LONGER_AVAILABLE = 93079