snowflake-cli-labs 2.5.0rc2__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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/api/cli_global_context.py +31 -3
- snowflake/cli/api/commands/decorators.py +21 -6
- snowflake/cli/api/commands/flags.py +60 -51
- snowflake/cli/api/commands/snow_typer.py +24 -0
- snowflake/cli/api/commands/typer_pre_execute.py +26 -0
- snowflake/cli/api/console/abc.py +8 -0
- snowflake/cli/api/console/console.py +29 -4
- snowflake/cli/api/constants.py +3 -0
- snowflake/cli/api/project/definition.py +17 -35
- snowflake/cli/api/project/definition_manager.py +22 -19
- snowflake/cli/api/project/errors.py +9 -6
- snowflake/cli/api/project/schemas/identifier_model.py +1 -1
- snowflake/cli/api/project/schemas/native_app/application.py +15 -3
- snowflake/cli/api/project/schemas/native_app/native_app.py +5 -1
- snowflake/cli/api/project/schemas/native_app/path_mapping.py +14 -3
- snowflake/cli/api/project/schemas/project_definition.py +37 -6
- snowflake/cli/api/project/schemas/streamlit/streamlit.py +3 -0
- snowflake/cli/api/project/schemas/updatable_model.py +2 -6
- snowflake/cli/api/rest_api.py +113 -0
- snowflake/cli/api/sanitizers.py +43 -0
- snowflake/cli/api/sql_execution.py +7 -0
- snowflake/cli/api/utils/definition_rendering.py +95 -25
- snowflake/cli/api/utils/models.py +31 -26
- snowflake/cli/api/utils/rendering.py +24 -3
- snowflake/cli/app/cli_app.py +2 -0
- snowflake/cli/app/commands_registration/command_plugins_loader.py +8 -0
- snowflake/cli/app/dev/docs/commands_docs_generator.py +100 -0
- snowflake/cli/app/dev/docs/generator.py +8 -67
- snowflake/cli/app/dev/docs/project_definition_docs_generator.py +58 -0
- snowflake/cli/app/dev/docs/project_definition_generate_json_schema.py +227 -0
- snowflake/cli/app/dev/docs/template_utils.py +23 -0
- snowflake/cli/app/dev/docs/templates/definition_description.rst.jinja2 +38 -0
- snowflake/cli/app/dev/docs/templates/usage.rst.jinja2 +6 -1
- snowflake/cli/app/loggers.py +25 -0
- snowflake/cli/app/printing.py +10 -5
- snowflake/cli/app/telemetry.py +11 -0
- snowflake/cli/plugins/nativeapp/artifacts.py +78 -9
- snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +3 -11
- snowflake/cli/plugins/nativeapp/codegen/compiler.py +6 -24
- snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +27 -27
- snowflake/cli/plugins/nativeapp/commands.py +23 -12
- snowflake/cli/plugins/nativeapp/constants.py +2 -0
- snowflake/cli/plugins/nativeapp/errno.py +15 -0
- snowflake/cli/plugins/nativeapp/feature_flags.py +24 -0
- snowflake/cli/plugins/nativeapp/init.py +5 -0
- snowflake/cli/plugins/nativeapp/manager.py +101 -103
- snowflake/cli/plugins/nativeapp/project_model.py +181 -0
- snowflake/cli/plugins/nativeapp/run_processor.py +178 -110
- snowflake/cli/plugins/nativeapp/teardown_processor.py +89 -64
- snowflake/cli/plugins/nativeapp/utils.py +2 -2
- snowflake/cli/plugins/nativeapp/version/commands.py +3 -3
- snowflake/cli/plugins/object/commands.py +70 -4
- snowflake/cli/plugins/object/manager.py +44 -3
- snowflake/cli/plugins/snowpark/commands.py +2 -2
- snowflake/cli/plugins/sql/commands.py +2 -10
- snowflake/cli/plugins/sql/manager.py +4 -2
- snowflake/cli/plugins/stage/commands.py +23 -4
- snowflake/cli/plugins/stage/diff.py +81 -51
- snowflake/cli/plugins/stage/manager.py +2 -1
- snowflake/cli/plugins/streamlit/commands.py +2 -1
- snowflake/cli/plugins/streamlit/manager.py +6 -0
- {snowflake_cli_labs-2.5.0rc2.dist-info → snowflake_cli_labs-2.6.0.dist-info}/METADATA +15 -9
- {snowflake_cli_labs-2.5.0rc2.dist-info → snowflake_cli_labs-2.6.0.dist-info}/RECORD +67 -56
- {snowflake_cli_labs-2.5.0rc2.dist-info → snowflake_cli_labs-2.6.0.dist-info}/WHEEL +1 -1
- {snowflake_cli_labs-2.5.0rc2.dist-info → snowflake_cli_labs-2.6.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli_labs-2.5.0rc2.dist-info → snowflake_cli_labs-2.6.0.dist-info}/licenses/LICENSE +0 -0
snowflake/cli/app/loggers.py
CHANGED
|
@@ -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)
|
snowflake/cli/app/printing.py
CHANGED
|
@@ -34,19 +34,24 @@ 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
|
|
|
42
42
|
# ensure we do not break URLs that wrap lines
|
|
43
43
|
get_console().soft_wrap = True
|
|
44
44
|
|
|
45
|
+
# Disable markup to avoid escaping errors
|
|
46
|
+
get_console()._markup = False # noqa: SLF001
|
|
47
|
+
|
|
45
48
|
|
|
46
49
|
class CustomJSONEncoder(JSONEncoder):
|
|
47
50
|
"""Custom JSON encoder handling serialization of non-standard types"""
|
|
48
51
|
|
|
49
52
|
def default(self, o):
|
|
53
|
+
if isinstance(o, str):
|
|
54
|
+
return sanitize_for_terminal(o)
|
|
50
55
|
if isinstance(o, (ObjectResult, MessageResult)):
|
|
51
56
|
return o.result
|
|
52
57
|
if isinstance(o, (CollectionResult, MultipleResults)):
|
|
@@ -70,8 +75,6 @@ def _get_table():
|
|
|
70
75
|
|
|
71
76
|
|
|
72
77
|
def _print_multiple_table_results(obj: CollectionResult):
|
|
73
|
-
if isinstance(obj, QueryResult):
|
|
74
|
-
rich_print(obj.query)
|
|
75
78
|
items = obj.result
|
|
76
79
|
try:
|
|
77
80
|
first_item = next(items)
|
|
@@ -131,7 +134,7 @@ def print_unstructured(obj: CommandResult | None):
|
|
|
131
134
|
elif not obj.result:
|
|
132
135
|
rich_print("No data")
|
|
133
136
|
elif isinstance(obj, MessageResult):
|
|
134
|
-
rich_print(obj.message)
|
|
137
|
+
rich_print(sanitize_for_terminal(obj.message))
|
|
135
138
|
else:
|
|
136
139
|
if isinstance(obj, ObjectResult):
|
|
137
140
|
_print_single_table(obj)
|
|
@@ -146,7 +149,9 @@ def _print_single_table(obj):
|
|
|
146
149
|
table.add_column("key", overflow="fold")
|
|
147
150
|
table.add_column("value", overflow="fold")
|
|
148
151
|
for key, value in obj.result.items():
|
|
149
|
-
table.add_row(
|
|
152
|
+
table.add_row(
|
|
153
|
+
sanitize_for_terminal(str(key)), sanitize_for_terminal(str(value))
|
|
154
|
+
)
|
|
150
155
|
rich_print(table)
|
|
151
156
|
|
|
152
157
|
|
snowflake/cli/app/telemetry.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
181
|
+
self.__dest_to_src[dest] = src
|
|
183
182
|
self.__src_dest_pairs.append((src, dest))
|
|
184
183
|
|
|
185
|
-
def
|
|
184
|
+
def get_source(self, dest: Path) -> Optional[Path]:
|
|
186
185
|
"""
|
|
187
|
-
Returns
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
project_root: Path,
|
|
44
|
-
deploy_root: Path,
|
|
45
|
-
generated_root: Path,
|
|
46
|
-
**kwargs,
|
|
40
|
+
na_project: NativeAppProjectModel,
|
|
47
41
|
) -> None:
|
|
48
|
-
|
|
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
|
-
|
|
50
|
-
project_root: Path,
|
|
51
|
-
deploy_root: Path,
|
|
52
|
-
generated_root: Path,
|
|
46
|
+
na_project: NativeAppProjectModel,
|
|
53
47
|
):
|
|
54
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
project_root: Path,
|
|
168
|
-
deploy_root: Path,
|
|
169
|
-
generated_root: Path,
|
|
166
|
+
na_project: NativeAppProjectModel,
|
|
170
167
|
):
|
|
171
|
-
super().__init__(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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,
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
281
|
+
paths: Optional[List[Path]] = typer.Argument(
|
|
280
282
|
default=None,
|
|
281
283
|
show_default=False,
|
|
282
|
-
help=
|
|
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
|
-
|
|
292
|
-
if prune is None and recursive is None and not
|
|
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=
|
|
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
|