snowflake-cli 3.1.0__py3-none-any.whl → 3.2.1__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.1.dist-info}/METADATA +7 -8
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.1.dist-info}/RECORD +56 -55
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.1.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.1.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,525 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from textwrap import dedent
|
|
19
|
+
from typing import Any, Dict, List
|
|
20
|
+
|
|
21
|
+
from snowflake.cli._plugins.nativeapp.sf_facade_constants import UseObjectType
|
|
22
|
+
from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import (
|
|
23
|
+
CouldNotUseObjectError,
|
|
24
|
+
InsufficientPrivilegesError,
|
|
25
|
+
UnexpectedResultError,
|
|
26
|
+
UserScriptError,
|
|
27
|
+
handle_unclassified_error,
|
|
28
|
+
)
|
|
29
|
+
from snowflake.cli.api.errno import (
|
|
30
|
+
DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED,
|
|
31
|
+
INSUFFICIENT_PRIVILEGES,
|
|
32
|
+
NO_WAREHOUSE_SELECTED_IN_SESSION,
|
|
33
|
+
)
|
|
34
|
+
from snowflake.cli.api.identifiers import FQN
|
|
35
|
+
from snowflake.cli.api.project.util import (
|
|
36
|
+
identifier_to_show_like_pattern,
|
|
37
|
+
is_valid_unquoted_identifier,
|
|
38
|
+
to_identifier,
|
|
39
|
+
to_quoted_identifier,
|
|
40
|
+
to_string_literal,
|
|
41
|
+
)
|
|
42
|
+
from snowflake.cli.api.sql_execution import BaseSqlExecutor, SqlExecutor
|
|
43
|
+
from snowflake.connector import DictCursor, ProgrammingError
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SnowflakeSQLFacade:
|
|
47
|
+
def __init__(self, sql_executor: SqlExecutor | None = None):
|
|
48
|
+
self._sql_executor = (
|
|
49
|
+
sql_executor if sql_executor is not None else BaseSqlExecutor()
|
|
50
|
+
)
|
|
51
|
+
self._log = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
def _use_object(self, object_type: UseObjectType, name: str):
|
|
54
|
+
"""
|
|
55
|
+
Call sql to use snowflake object with error handling
|
|
56
|
+
@param object_type: ObjectType, type of snowflake object to use
|
|
57
|
+
@param name: object name, has to be a valid snowflake identifier.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
self._sql_executor.execute_query(f"use {object_type} {name}")
|
|
61
|
+
except ProgrammingError as err:
|
|
62
|
+
if err.errno == DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED:
|
|
63
|
+
raise CouldNotUseObjectError(object_type, name) from err
|
|
64
|
+
else:
|
|
65
|
+
handle_unclassified_error(err, f"Failed to use {object_type} {name}.")
|
|
66
|
+
except Exception as err:
|
|
67
|
+
handle_unclassified_error(err, f"Failed to use {object_type} {name}.")
|
|
68
|
+
|
|
69
|
+
@contextmanager
|
|
70
|
+
def _use_object_optional(self, object_type: UseObjectType, name: str | None):
|
|
71
|
+
"""
|
|
72
|
+
Call sql to use snowflake object with error handling
|
|
73
|
+
@param object_type: ObjectType, type of snowflake object to use
|
|
74
|
+
@param name: object name, will be cast to a valid snowflake identifier.
|
|
75
|
+
"""
|
|
76
|
+
if name is None:
|
|
77
|
+
yield
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
current_obj_result_row = self._sql_executor.execute_query(
|
|
82
|
+
f"select current_{object_type}()"
|
|
83
|
+
).fetchone()
|
|
84
|
+
except Exception as err:
|
|
85
|
+
return handle_unclassified_error(
|
|
86
|
+
err, f"Failed to select current {object_type}."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
prev_obj = current_obj_result_row[0]
|
|
91
|
+
except IndexError:
|
|
92
|
+
prev_obj = None
|
|
93
|
+
|
|
94
|
+
if prev_obj is not None and _same_identifier(prev_obj, name):
|
|
95
|
+
yield
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
self._log.debug(f"Switching to {object_type}: {name}")
|
|
99
|
+
self._use_object(object_type, to_identifier(name))
|
|
100
|
+
try:
|
|
101
|
+
yield
|
|
102
|
+
finally:
|
|
103
|
+
if prev_obj is not None:
|
|
104
|
+
self._log.debug(f"Switching back to {object_type}: {prev_obj}")
|
|
105
|
+
self._use_object(object_type, prev_obj)
|
|
106
|
+
|
|
107
|
+
def _use_warehouse_optional(self, new_wh: str | None):
|
|
108
|
+
"""
|
|
109
|
+
Switches to a different warehouse for a while, then switches back.
|
|
110
|
+
This is a no-op if the requested warehouse is already active or if no warehouse is passed in.
|
|
111
|
+
@param new_wh: Name of the warehouse to use. If not a valid Snowflake identifier, will be converted before use.
|
|
112
|
+
"""
|
|
113
|
+
return self._use_object_optional(UseObjectType.WAREHOUSE, new_wh)
|
|
114
|
+
|
|
115
|
+
def _use_role_optional(self, new_role: str | None):
|
|
116
|
+
"""
|
|
117
|
+
Switches to a different role for a while, then switches back.
|
|
118
|
+
This is a no-op if the requested role is already active or if no role is passed in.
|
|
119
|
+
@param new_role: Name of the role to use. If not a valid Snowflake identifier, will be converted before use.
|
|
120
|
+
"""
|
|
121
|
+
return self._use_object_optional(UseObjectType.ROLE, new_role)
|
|
122
|
+
|
|
123
|
+
def _use_database_optional(self, database_name: str | None):
|
|
124
|
+
"""
|
|
125
|
+
Switch to database `database_name`, then switches back.
|
|
126
|
+
This is a no-op if the requested database is already selected or if no database_name is passed in.
|
|
127
|
+
@param database_name: Name of the database to use. If not a valid Snowflake identifier, will be converted before use.
|
|
128
|
+
"""
|
|
129
|
+
return self._use_object_optional(UseObjectType.DATABASE, database_name)
|
|
130
|
+
|
|
131
|
+
def _use_schema_optional(self, schema_name: str | None):
|
|
132
|
+
"""
|
|
133
|
+
Switch to schema `schema_name`, then switches back.
|
|
134
|
+
This is a no-op if the requested schema is already selected or if no schema_name is passed in.
|
|
135
|
+
@param schema_name: Name of the schema to use. If not a valid Snowflake identifier, will be converted before use.
|
|
136
|
+
"""
|
|
137
|
+
return self._use_object_optional(UseObjectType.SCHEMA, schema_name)
|
|
138
|
+
|
|
139
|
+
def execute_user_script(
|
|
140
|
+
self,
|
|
141
|
+
queries: str,
|
|
142
|
+
script_name: str,
|
|
143
|
+
role: str | None = None,
|
|
144
|
+
warehouse: str | None = None,
|
|
145
|
+
database: str | None = None,
|
|
146
|
+
):
|
|
147
|
+
"""
|
|
148
|
+
Runs the user-provided sql script.
|
|
149
|
+
@param queries: Queries to run in this script
|
|
150
|
+
@param script_name: Name of the file containing the script. Used to show logs to the user.
|
|
151
|
+
@param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
|
|
152
|
+
@param [Optional] warehouse: Warehouse to use while running this script.
|
|
153
|
+
@param [Optional] database: Database to use while running this script.
|
|
154
|
+
"""
|
|
155
|
+
with (
|
|
156
|
+
self._use_role_optional(role),
|
|
157
|
+
self._use_warehouse_optional(warehouse),
|
|
158
|
+
self._use_database_optional(database),
|
|
159
|
+
):
|
|
160
|
+
try:
|
|
161
|
+
self._sql_executor.execute_queries(queries)
|
|
162
|
+
except ProgrammingError as err:
|
|
163
|
+
if err.errno == NO_WAREHOUSE_SELECTED_IN_SESSION:
|
|
164
|
+
raise UserScriptError(
|
|
165
|
+
script_name,
|
|
166
|
+
f"{err.msg}. Please provide a warehouse in your project definition file, config.toml file, or via command line",
|
|
167
|
+
) from err
|
|
168
|
+
else:
|
|
169
|
+
raise UserScriptError(script_name, err.msg) from err
|
|
170
|
+
except Exception as err:
|
|
171
|
+
handle_unclassified_error(err, f"Failed to run script {script_name}.")
|
|
172
|
+
|
|
173
|
+
def get_account_event_table(self, role: str | None = None) -> str | None:
|
|
174
|
+
"""
|
|
175
|
+
Returns the name of the event table for the account.
|
|
176
|
+
If the account has no event table set up or the event table is set to NONE, returns None.
|
|
177
|
+
@param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
|
|
178
|
+
"""
|
|
179
|
+
query = "show parameters like 'event_table' in account"
|
|
180
|
+
with self._use_role_optional(role):
|
|
181
|
+
try:
|
|
182
|
+
results = self._sql_executor.execute_query(
|
|
183
|
+
query, cursor_class=DictCursor
|
|
184
|
+
)
|
|
185
|
+
except Exception as err:
|
|
186
|
+
handle_unclassified_error(err, f"Failed to get event table.")
|
|
187
|
+
table = next((r["value"] for r in results if r["key"] == "EVENT_TABLE"), None)
|
|
188
|
+
if table is None or table == "NONE":
|
|
189
|
+
return None
|
|
190
|
+
return table
|
|
191
|
+
|
|
192
|
+
def create_version_in_package(
|
|
193
|
+
self,
|
|
194
|
+
package_name: str,
|
|
195
|
+
stage_fqn: str,
|
|
196
|
+
version: str,
|
|
197
|
+
label: str | None = None,
|
|
198
|
+
role: str | None = None,
|
|
199
|
+
):
|
|
200
|
+
"""
|
|
201
|
+
Creates a new version in an existing application package.
|
|
202
|
+
@param package_name: Name of the application package to alter.
|
|
203
|
+
@param stage_fqn: Stage fully qualified name.
|
|
204
|
+
@param version: Version name to create.
|
|
205
|
+
@param [Optional] role: Switch to this role while executing create version.
|
|
206
|
+
@param [Optional] label: Label for this version, visible to consumers.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
# Make the version a valid identifier, adding quotes if necessary
|
|
210
|
+
version = to_identifier(version)
|
|
211
|
+
|
|
212
|
+
# Label must be a string literal
|
|
213
|
+
with_label_cause = (
|
|
214
|
+
f"\nlabel={to_string_literal(label)}" if label is not None else ""
|
|
215
|
+
)
|
|
216
|
+
add_version_query = dedent(
|
|
217
|
+
f"""\
|
|
218
|
+
alter application package {package_name}
|
|
219
|
+
add version {version}
|
|
220
|
+
using @{stage_fqn}{with_label_cause}
|
|
221
|
+
"""
|
|
222
|
+
)
|
|
223
|
+
with self._use_role_optional(role):
|
|
224
|
+
try:
|
|
225
|
+
self._sql_executor.execute_query(add_version_query)
|
|
226
|
+
except Exception as err:
|
|
227
|
+
handle_unclassified_error(
|
|
228
|
+
err,
|
|
229
|
+
f"Failed to add version {version} to application package {package_name}.",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def add_patch_to_package_version(
|
|
233
|
+
self,
|
|
234
|
+
package_name: str,
|
|
235
|
+
stage_fqn: str,
|
|
236
|
+
version: str,
|
|
237
|
+
patch: int | None = None,
|
|
238
|
+
label: str | None = None,
|
|
239
|
+
role: str | None = None,
|
|
240
|
+
) -> int:
|
|
241
|
+
"""
|
|
242
|
+
Add a new patch, optionally a custom one, to an existing version in an application package.
|
|
243
|
+
@param package_name: Name of the application package to alter.
|
|
244
|
+
@param stage_fqn: Stage fully qualified name.
|
|
245
|
+
@param version: Version name to create.
|
|
246
|
+
@param [Optional] patch: Patch number to create.
|
|
247
|
+
@param [Optional] label: Label for this patch, visible to consumers.
|
|
248
|
+
@param [Optional] role: Switch to this role while executing create version.
|
|
249
|
+
|
|
250
|
+
@return patch number created for the version.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
# Make the version a valid identifier, adding quotes if necessary
|
|
254
|
+
version = to_identifier(version)
|
|
255
|
+
|
|
256
|
+
# Label must be a string literal
|
|
257
|
+
with_label_clause = (
|
|
258
|
+
f"\nlabel={to_string_literal(label)}" if label is not None else ""
|
|
259
|
+
)
|
|
260
|
+
patch_query = f"{patch}" if patch else ""
|
|
261
|
+
add_patch_query = dedent(
|
|
262
|
+
f"""\
|
|
263
|
+
alter application package {package_name}
|
|
264
|
+
add patch {patch_query} for version {version}
|
|
265
|
+
using @{stage_fqn}{with_label_clause}
|
|
266
|
+
"""
|
|
267
|
+
)
|
|
268
|
+
with self._use_role_optional(role):
|
|
269
|
+
try:
|
|
270
|
+
result_cursor = self._sql_executor.execute_query(
|
|
271
|
+
add_patch_query, cursor_class=DictCursor
|
|
272
|
+
).fetchall()
|
|
273
|
+
except Exception as err:
|
|
274
|
+
handle_unclassified_error(
|
|
275
|
+
err,
|
|
276
|
+
f"Failed to create patch {patch_query} for version {version} in application package {package_name}.",
|
|
277
|
+
)
|
|
278
|
+
try:
|
|
279
|
+
show_row = result_cursor[0]
|
|
280
|
+
except IndexError as err:
|
|
281
|
+
raise UnexpectedResultError(
|
|
282
|
+
f"Expected to receive the new patch but the result is empty"
|
|
283
|
+
) from err
|
|
284
|
+
new_patch = show_row["patch"]
|
|
285
|
+
|
|
286
|
+
return new_patch
|
|
287
|
+
|
|
288
|
+
def get_event_definitions(
|
|
289
|
+
self, app_name: str, role: str | None = None
|
|
290
|
+
) -> list[dict]:
|
|
291
|
+
"""
|
|
292
|
+
Retrieves event definitions for the specified application.
|
|
293
|
+
@param app_name: Name of the application to get event definitions for.
|
|
294
|
+
@return: A list of dictionaries containing event definitions.
|
|
295
|
+
"""
|
|
296
|
+
query = (
|
|
297
|
+
f"show telemetry event definitions in application {to_identifier(app_name)}"
|
|
298
|
+
)
|
|
299
|
+
with self._use_role_optional(role):
|
|
300
|
+
try:
|
|
301
|
+
results = self._sql_executor.execute_query(
|
|
302
|
+
query, cursor_class=DictCursor
|
|
303
|
+
).fetchall()
|
|
304
|
+
except Exception as err:
|
|
305
|
+
handle_unclassified_error(
|
|
306
|
+
err,
|
|
307
|
+
f"Failed to get event definitions for application {to_identifier(app_name)}.",
|
|
308
|
+
)
|
|
309
|
+
return [dict(row) for row in results]
|
|
310
|
+
|
|
311
|
+
def get_app_properties(
|
|
312
|
+
self, app_name: str, role: str | None = None
|
|
313
|
+
) -> Dict[str, str]:
|
|
314
|
+
"""
|
|
315
|
+
Retrieve the properties of the specified application.
|
|
316
|
+
@param app_name: Name of the application.
|
|
317
|
+
@return: A dictionary containing the properties of the application.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
query = f"desc application {to_identifier(app_name)}"
|
|
321
|
+
with self._use_role_optional(role):
|
|
322
|
+
try:
|
|
323
|
+
results = self._sql_executor.execute_query(
|
|
324
|
+
query, cursor_class=DictCursor
|
|
325
|
+
).fetchall()
|
|
326
|
+
except Exception as err:
|
|
327
|
+
handle_unclassified_error(
|
|
328
|
+
err, f"Failed to describe application {to_identifier(app_name)}."
|
|
329
|
+
)
|
|
330
|
+
return {row["property"]: row["value"] for row in results}
|
|
331
|
+
|
|
332
|
+
def share_telemetry_events(
|
|
333
|
+
self, app_name: str, event_names: List[str], role: str | None = None
|
|
334
|
+
):
|
|
335
|
+
"""
|
|
336
|
+
Shares the specified events from the specified application to the application package provider.
|
|
337
|
+
@param app_name: Name of the application to share events from.
|
|
338
|
+
@param events: List of event names to share.
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
self._log.info("sharing events %s", event_names)
|
|
342
|
+
query = f"alter application {to_identifier(app_name)} set shared telemetry events ({', '.join([to_string_literal(x) for x in event_names])})"
|
|
343
|
+
|
|
344
|
+
with self._use_role_optional(role):
|
|
345
|
+
try:
|
|
346
|
+
self._sql_executor.execute_query(query)
|
|
347
|
+
except Exception as err:
|
|
348
|
+
handle_unclassified_error(
|
|
349
|
+
err,
|
|
350
|
+
f"Failed to share telemetry events for application {to_identifier(app_name)}.",
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def create_schema(
|
|
354
|
+
self, name: str, role: str | None = None, database: str | None = None
|
|
355
|
+
):
|
|
356
|
+
"""
|
|
357
|
+
Creates a schema.
|
|
358
|
+
@param name: Name of the schema to create. Can be a database-qualified name or just the schema name, in which case the current database or the database passed in will be used.
|
|
359
|
+
@param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
|
|
360
|
+
@param [Optional] database: Database to use while running this query, unless the schema name is database-qualified.
|
|
361
|
+
"""
|
|
362
|
+
fqn = FQN.from_string(name)
|
|
363
|
+
identifier = to_identifier(fqn.name)
|
|
364
|
+
database = fqn.prefix or database
|
|
365
|
+
with (
|
|
366
|
+
self._use_role_optional(role),
|
|
367
|
+
self._use_database_optional(database),
|
|
368
|
+
):
|
|
369
|
+
try:
|
|
370
|
+
self._sql_executor.execute_query(
|
|
371
|
+
f"create schema if not exists {identifier}"
|
|
372
|
+
)
|
|
373
|
+
except ProgrammingError as err:
|
|
374
|
+
if err.errno == INSUFFICIENT_PRIVILEGES:
|
|
375
|
+
raise InsufficientPrivilegesError(
|
|
376
|
+
f"Insufficient privileges to create schema {name}",
|
|
377
|
+
role=role,
|
|
378
|
+
database=database,
|
|
379
|
+
) from err
|
|
380
|
+
handle_unclassified_error(err, f"Failed to create schema {name}.")
|
|
381
|
+
|
|
382
|
+
def stage_exists(
|
|
383
|
+
self,
|
|
384
|
+
name: str,
|
|
385
|
+
role: str | None = None,
|
|
386
|
+
database: str | None = None,
|
|
387
|
+
schema: str | None = None,
|
|
388
|
+
) -> bool:
|
|
389
|
+
"""
|
|
390
|
+
Checks if a stage exists.
|
|
391
|
+
@param name: Name of the stage to check for. Can be a fully qualified name or just the stage name, in which case the current database and schema or the database and schema passed in will be used.
|
|
392
|
+
@param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
|
|
393
|
+
@param [Optional] database: Database to use while running this script, unless the stage name is database-qualified.
|
|
394
|
+
@param [Optional] schema: Schema to use while running this script, unless the stage name is schema-qualified.
|
|
395
|
+
"""
|
|
396
|
+
fqn = FQN.from_string(name)
|
|
397
|
+
identifier = to_identifier(fqn.name)
|
|
398
|
+
database = fqn.database or database
|
|
399
|
+
schema = fqn.schema or schema
|
|
400
|
+
|
|
401
|
+
pattern = identifier_to_show_like_pattern(identifier)
|
|
402
|
+
if schema and database:
|
|
403
|
+
in_schema_clause = f" in schema {database}.{schema}"
|
|
404
|
+
elif schema:
|
|
405
|
+
in_schema_clause = f" in schema {schema}"
|
|
406
|
+
elif database:
|
|
407
|
+
in_schema_clause = f" in database {database}"
|
|
408
|
+
else:
|
|
409
|
+
in_schema_clause = ""
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
with self._use_role_optional(role):
|
|
413
|
+
try:
|
|
414
|
+
results = self._sql_executor.execute_query(
|
|
415
|
+
f"show stages like {pattern}{in_schema_clause}",
|
|
416
|
+
)
|
|
417
|
+
except ProgrammingError as err:
|
|
418
|
+
if err.errno == DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED:
|
|
419
|
+
return False
|
|
420
|
+
if err.errno == INSUFFICIENT_PRIVILEGES:
|
|
421
|
+
raise InsufficientPrivilegesError(
|
|
422
|
+
f"Insufficient privileges to check if stage {name} exists",
|
|
423
|
+
role=role,
|
|
424
|
+
database=database,
|
|
425
|
+
schema=schema,
|
|
426
|
+
) from err
|
|
427
|
+
handle_unclassified_error(
|
|
428
|
+
err, f"Failed to check if stage {name} exists."
|
|
429
|
+
)
|
|
430
|
+
return results.rowcount > 0
|
|
431
|
+
except CouldNotUseObjectError:
|
|
432
|
+
return False
|
|
433
|
+
|
|
434
|
+
def create_stage(
|
|
435
|
+
self,
|
|
436
|
+
name: str,
|
|
437
|
+
encryption_type: str = "SNOWFLAKE_SSE",
|
|
438
|
+
enable_directory: bool = True,
|
|
439
|
+
role: str | None = None,
|
|
440
|
+
database: str | None = None,
|
|
441
|
+
schema: str | None = None,
|
|
442
|
+
):
|
|
443
|
+
"""
|
|
444
|
+
Creates a stage.
|
|
445
|
+
@param name: Name of the stage to create. Can be a fully qualified name or just the stage name, in which case the current database and schema or the database and schema passed in will be used.
|
|
446
|
+
@param [Optional] encryption_type: Encryption type for the stage. Default is Snowflake SSE. Pass an empty string to disable encryption.
|
|
447
|
+
@param [Optional] enable_directory: Directory settings for the stage. Default is enabled.
|
|
448
|
+
@param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
|
|
449
|
+
@param [Optional] database: Database to use while running this script, unless the stage name is database-qualified.
|
|
450
|
+
@param [Optional] schema: Schema to use while running this script, unless the stage name is schema-qualified.
|
|
451
|
+
"""
|
|
452
|
+
fqn = FQN.from_string(name)
|
|
453
|
+
identifier = to_identifier(fqn.name)
|
|
454
|
+
database = fqn.database or database
|
|
455
|
+
schema = fqn.schema or schema
|
|
456
|
+
|
|
457
|
+
query = f"create stage if not exists {identifier}"
|
|
458
|
+
if encryption_type:
|
|
459
|
+
query += f" encryption = (type = '{encryption_type}')"
|
|
460
|
+
if enable_directory:
|
|
461
|
+
query += f" directory = (enable = {str(enable_directory)})"
|
|
462
|
+
with (
|
|
463
|
+
self._use_role_optional(role),
|
|
464
|
+
self._use_database_optional(database),
|
|
465
|
+
self._use_schema_optional(schema),
|
|
466
|
+
):
|
|
467
|
+
try:
|
|
468
|
+
self._sql_executor.execute_query(query)
|
|
469
|
+
except ProgrammingError as err:
|
|
470
|
+
if err.errno == INSUFFICIENT_PRIVILEGES:
|
|
471
|
+
raise InsufficientPrivilegesError(
|
|
472
|
+
f"Insufficient privileges to create stage {name}",
|
|
473
|
+
role=role,
|
|
474
|
+
database=database,
|
|
475
|
+
schema=schema,
|
|
476
|
+
) from err
|
|
477
|
+
handle_unclassified_error(err, f"Failed to create stage {name}.")
|
|
478
|
+
|
|
479
|
+
def show_release_directives(
|
|
480
|
+
self, package_name: str, role: str | None = None
|
|
481
|
+
) -> list[dict[str, Any]]:
|
|
482
|
+
"""
|
|
483
|
+
Show release directives for a package
|
|
484
|
+
@param package_name: Name of the package
|
|
485
|
+
@param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
|
|
486
|
+
"""
|
|
487
|
+
package_identifier = to_identifier(package_name)
|
|
488
|
+
with self._use_role_optional(role):
|
|
489
|
+
try:
|
|
490
|
+
cursor = self._sql_executor.execute_query(
|
|
491
|
+
f"show release directives in application package {package_identifier}",
|
|
492
|
+
cursor_class=DictCursor,
|
|
493
|
+
)
|
|
494
|
+
except ProgrammingError as err:
|
|
495
|
+
if err.errno == INSUFFICIENT_PRIVILEGES:
|
|
496
|
+
raise InsufficientPrivilegesError(
|
|
497
|
+
f"Insufficient privileges to show release directives for package {package_name}",
|
|
498
|
+
role=role,
|
|
499
|
+
) from err
|
|
500
|
+
handle_unclassified_error(
|
|
501
|
+
err,
|
|
502
|
+
f"Failed to show release directives for package {package_name}.",
|
|
503
|
+
)
|
|
504
|
+
return cursor.fetchall()
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# TODO move this to src/snowflake/cli/api/project/util.py in a separate
|
|
508
|
+
# PR since it's codeowned by the CLI team
|
|
509
|
+
def _same_identifier(id1: str, id2: str) -> bool:
|
|
510
|
+
"""
|
|
511
|
+
Returns whether two identifiers refer to the same object.
|
|
512
|
+
|
|
513
|
+
Two unquoted identifiers are considered the same if they are equal when both are converted to uppercase
|
|
514
|
+
Two quoted identifiers are considered the same if they are exactly equal
|
|
515
|
+
An unquoted identifier and a quoted identifier are considered the same
|
|
516
|
+
if the quoted identifier is equal to the unquoted identifier
|
|
517
|
+
when the unquoted identifier is converted to uppercase and quoted
|
|
518
|
+
"""
|
|
519
|
+
# Canonicalize the identifiers by converting unquoted identifiers to uppercase and leaving quoted identifiers as is
|
|
520
|
+
canonical_id1 = id1.upper() if is_valid_unquoted_identifier(id1) else id1
|
|
521
|
+
canonical_id2 = id2.upper() if is_valid_unquoted_identifier(id2) else id2
|
|
522
|
+
|
|
523
|
+
# The canonical identifiers are equal if they are equal when both are quoted
|
|
524
|
+
# (if they are already quoted, this is a no-op)
|
|
525
|
+
return to_quoted_identifier(canonical_id1) == to_quoted_identifier(canonical_id2)
|
|
@@ -16,7 +16,7 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import inspect
|
|
18
18
|
from functools import wraps
|
|
19
|
-
from typing import
|
|
19
|
+
from typing import Optional, Type, TypeVar
|
|
20
20
|
|
|
21
21
|
import typer
|
|
22
22
|
from click import ClickException
|
|
@@ -34,13 +34,10 @@ from snowflake.cli.api.project.definition_conversion import (
|
|
|
34
34
|
)
|
|
35
35
|
from snowflake.cli.api.project.schemas.entities.common import EntityModelBase
|
|
36
36
|
from snowflake.cli.api.project.schemas.project_definition import (
|
|
37
|
-
DefinitionV11,
|
|
38
37
|
DefinitionV20,
|
|
39
38
|
ProjectDefinition,
|
|
40
39
|
ProjectDefinitionV1,
|
|
41
40
|
)
|
|
42
|
-
from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import PathMapping
|
|
43
|
-
from snowflake.cli.api.utils.definition_rendering import render_definition_template
|
|
44
41
|
|
|
45
42
|
APP_AND_PACKAGE_OPTIONS = [
|
|
46
43
|
inspect.Parameter(
|
|
@@ -64,91 +61,6 @@ APP_AND_PACKAGE_OPTIONS = [
|
|
|
64
61
|
]
|
|
65
62
|
|
|
66
63
|
|
|
67
|
-
def _convert_v2_artifact_to_v1_dict(
|
|
68
|
-
v2_artifact: Union[PathMapping, str]
|
|
69
|
-
) -> Union[Dict, str]:
|
|
70
|
-
if isinstance(v2_artifact, PathMapping):
|
|
71
|
-
return {
|
|
72
|
-
"src": v2_artifact.src,
|
|
73
|
-
"dest": v2_artifact.dest,
|
|
74
|
-
"processors": v2_artifact.processors,
|
|
75
|
-
}
|
|
76
|
-
return v2_artifact
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _pdf_v2_to_v1(
|
|
80
|
-
v2_definition: DefinitionV20,
|
|
81
|
-
package_entity_id: str = "",
|
|
82
|
-
app_entity_id: str = "",
|
|
83
|
-
app_required: bool = False,
|
|
84
|
-
) -> DefinitionV11:
|
|
85
|
-
pdfv1: Dict[str, Any] = {"definition_version": "1.1", "native_app": {}}
|
|
86
|
-
|
|
87
|
-
app_definition, app_package_definition = _find_app_and_package_entities(
|
|
88
|
-
v2_definition=v2_definition,
|
|
89
|
-
package_entity_id=package_entity_id,
|
|
90
|
-
app_entity_id=app_entity_id,
|
|
91
|
-
app_required=app_required,
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
# NativeApp
|
|
95
|
-
if app_definition and app_definition.fqn.identifier:
|
|
96
|
-
pdfv1["native_app"]["name"] = app_definition.fqn.identifier
|
|
97
|
-
else:
|
|
98
|
-
pdfv1["native_app"]["name"] = app_package_definition.fqn.identifier.split(
|
|
99
|
-
"_pkg_"
|
|
100
|
-
)[0]
|
|
101
|
-
pdfv1["native_app"]["artifacts"] = [
|
|
102
|
-
_convert_v2_artifact_to_v1_dict(a) for a in app_package_definition.artifacts
|
|
103
|
-
]
|
|
104
|
-
pdfv1["native_app"]["source_stage"] = app_package_definition.stage
|
|
105
|
-
pdfv1["native_app"]["bundle_root"] = app_package_definition.bundle_root
|
|
106
|
-
pdfv1["native_app"]["generated_root"] = app_package_definition.generated_root
|
|
107
|
-
pdfv1["native_app"]["deploy_root"] = app_package_definition.deploy_root
|
|
108
|
-
pdfv1["native_app"]["scratch_stage"] = app_package_definition.scratch_stage
|
|
109
|
-
|
|
110
|
-
# Package
|
|
111
|
-
pdfv1["native_app"]["package"] = {}
|
|
112
|
-
pdfv1["native_app"]["package"]["name"] = app_package_definition.fqn.identifier
|
|
113
|
-
if app_package_definition.distribution:
|
|
114
|
-
pdfv1["native_app"]["package"][
|
|
115
|
-
"distribution"
|
|
116
|
-
] = app_package_definition.distribution
|
|
117
|
-
if app_package_definition.meta and app_package_definition.meta.post_deploy:
|
|
118
|
-
pdfv1["native_app"]["package"][
|
|
119
|
-
"post_deploy"
|
|
120
|
-
] = app_package_definition.meta.post_deploy
|
|
121
|
-
if app_package_definition.meta:
|
|
122
|
-
if app_package_definition.meta.role:
|
|
123
|
-
pdfv1["native_app"]["package"]["role"] = app_package_definition.meta.role
|
|
124
|
-
if app_package_definition.meta.warehouse:
|
|
125
|
-
pdfv1["native_app"]["package"][
|
|
126
|
-
"warehouse"
|
|
127
|
-
] = app_package_definition.meta.warehouse
|
|
128
|
-
|
|
129
|
-
# Application
|
|
130
|
-
if app_definition:
|
|
131
|
-
pdfv1["native_app"]["application"] = {}
|
|
132
|
-
pdfv1["native_app"]["application"]["name"] = app_definition.fqn.identifier
|
|
133
|
-
if app_definition.debug:
|
|
134
|
-
pdfv1["native_app"]["application"]["debug"] = app_definition.debug
|
|
135
|
-
if app_definition.meta:
|
|
136
|
-
if app_definition.meta.role:
|
|
137
|
-
pdfv1["native_app"]["application"]["role"] = app_definition.meta.role
|
|
138
|
-
if app_definition.meta.warehouse:
|
|
139
|
-
pdfv1["native_app"]["application"][
|
|
140
|
-
"warehouse"
|
|
141
|
-
] = app_definition.meta.warehouse
|
|
142
|
-
if app_definition.meta.post_deploy:
|
|
143
|
-
pdfv1["native_app"]["application"][
|
|
144
|
-
"post_deploy"
|
|
145
|
-
] = app_definition.meta.post_deploy
|
|
146
|
-
|
|
147
|
-
result = render_definition_template(pdfv1, {})
|
|
148
|
-
# Override the definition object in global context
|
|
149
|
-
return result.project_definition
|
|
150
|
-
|
|
151
|
-
|
|
152
64
|
def _find_app_and_package_entities(
|
|
153
65
|
v2_definition: DefinitionV20,
|
|
154
66
|
package_entity_id: str,
|
|
@@ -18,7 +18,6 @@ import logging
|
|
|
18
18
|
from typing import Optional
|
|
19
19
|
|
|
20
20
|
import typer
|
|
21
|
-
from click import MissingParameter
|
|
22
21
|
from snowflake.cli._plugins.nativeapp.common_flags import ForceOption, InteractiveOption
|
|
23
22
|
from snowflake.cli._plugins.nativeapp.v2_conversions.compat import (
|
|
24
23
|
force_project_definition_v2,
|
|
@@ -54,6 +53,11 @@ def create(
|
|
|
54
53
|
help=f"""The patch number you want to create for an existing version.
|
|
55
54
|
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.""",
|
|
56
55
|
),
|
|
56
|
+
label: Optional[str] = typer.Option(
|
|
57
|
+
None,
|
|
58
|
+
"--label",
|
|
59
|
+
help="A label for the version that is displayed to consumers. If unset, the version label specified in `manifest.yml` file is used.",
|
|
60
|
+
),
|
|
57
61
|
skip_git_check: Optional[bool] = typer.Option(
|
|
58
62
|
False,
|
|
59
63
|
"--skip-git-check",
|
|
@@ -67,8 +71,6 @@ def create(
|
|
|
67
71
|
"""
|
|
68
72
|
Adds a new patch to the provided version defined in your application package. If the version does not exist, creates a version with patch 0.
|
|
69
73
|
"""
|
|
70
|
-
if version is None and patch is not None:
|
|
71
|
-
raise MissingParameter("Cannot provide a patch without version!")
|
|
72
74
|
|
|
73
75
|
cli_context = get_cli_context()
|
|
74
76
|
ws = WorkspaceManager(
|
|
@@ -81,6 +83,7 @@ def create(
|
|
|
81
83
|
EntityActions.VERSION_CREATE,
|
|
82
84
|
version=version,
|
|
83
85
|
patch=patch,
|
|
86
|
+
label=label,
|
|
84
87
|
force=force,
|
|
85
88
|
interactive=interactive,
|
|
86
89
|
skip_git_check=skip_git_check,
|
|
@@ -26,7 +26,7 @@ from snowflake.cli.api.sql_execution import SqlExecutionMixin
|
|
|
26
26
|
class NotebookManager(SqlExecutionMixin):
|
|
27
27
|
def execute(self, notebook_name: FQN):
|
|
28
28
|
query = f"EXECUTE NOTEBOOK {notebook_name.sql_identifier}()"
|
|
29
|
-
return self.
|
|
29
|
+
return self.execute_query(query=query)
|
|
30
30
|
|
|
31
31
|
def get_url(self, notebook_name: FQN):
|
|
32
32
|
fqn = notebook_name.using_connection(self._conn)
|
|
@@ -64,7 +64,7 @@ class NotebookManager(SqlExecutionMixin):
|
|
|
64
64
|
ALTER NOTEBOOK {notebook_fqn.identifier} ADD LIVE VERSION FROM LAST;
|
|
65
65
|
"""
|
|
66
66
|
)
|
|
67
|
-
self.
|
|
67
|
+
self.execute_queries(queries=queries)
|
|
68
68
|
|
|
69
69
|
return make_snowsight_url(
|
|
70
70
|
self._conn, f"/#/notebooks/{notebook_fqn.url_identifier}"
|