snowflake-cli 3.1.0__py3-none-any.whl → 3.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +1 -1
  3. snowflake/cli/_plugins/connection/commands.py +124 -109
  4. snowflake/cli/_plugins/connection/util.py +54 -9
  5. snowflake/cli/_plugins/cortex/manager.py +1 -1
  6. snowflake/cli/_plugins/git/manager.py +4 -4
  7. snowflake/cli/_plugins/nativeapp/artifacts.py +64 -10
  8. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
  9. snowflake/cli/_plugins/nativeapp/commands.py +10 -3
  10. snowflake/cli/_plugins/nativeapp/constants.py +1 -0
  11. snowflake/cli/_plugins/nativeapp/entities/application.py +501 -440
  12. snowflake/cli/_plugins/nativeapp/entities/application_package.py +563 -885
  13. snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
  14. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
  15. snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
  16. snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
  17. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
  18. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
  19. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +1 -89
  20. snowflake/cli/_plugins/nativeapp/version/commands.py +6 -3
  21. snowflake/cli/_plugins/notebook/manager.py +2 -2
  22. snowflake/cli/_plugins/object/commands.py +10 -1
  23. snowflake/cli/_plugins/object/manager.py +13 -5
  24. snowflake/cli/_plugins/snowpark/common.py +3 -3
  25. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -1
  26. snowflake/cli/_plugins/spcs/common.py +29 -0
  27. snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
  28. snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
  29. snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
  30. snowflake/cli/_plugins/spcs/services/commands.py +64 -13
  31. snowflake/cli/_plugins/spcs/services/manager.py +75 -15
  32. snowflake/cli/_plugins/sql/commands.py +9 -1
  33. snowflake/cli/_plugins/sql/manager.py +9 -4
  34. snowflake/cli/_plugins/stage/commands.py +20 -16
  35. snowflake/cli/_plugins/stage/diff.py +1 -1
  36. snowflake/cli/_plugins/stage/manager.py +140 -11
  37. snowflake/cli/_plugins/streamlit/manager.py +5 -5
  38. snowflake/cli/_plugins/workspace/commands.py +6 -3
  39. snowflake/cli/api/cli_global_context.py +1 -0
  40. snowflake/cli/api/config.py +23 -5
  41. snowflake/cli/api/console/console.py +4 -19
  42. snowflake/cli/api/entities/utils.py +19 -32
  43. snowflake/cli/api/errno.py +2 -0
  44. snowflake/cli/api/exceptions.py +9 -0
  45. snowflake/cli/api/metrics.py +223 -7
  46. snowflake/cli/api/output/types.py +1 -1
  47. snowflake/cli/api/project/definition_conversion.py +179 -62
  48. snowflake/cli/api/rest_api.py +26 -4
  49. snowflake/cli/api/secure_utils.py +1 -1
  50. snowflake/cli/api/sql_execution.py +35 -22
  51. snowflake/cli/api/stage_path.py +5 -2
  52. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +7 -8
  53. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +56 -55
  54. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
  55. snowflake/cli/_plugins/nativeapp/manager.py +0 -392
  56. snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
  57. snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
  58. snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -56
  59. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
  60. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.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 Any, Dict, Optional, Type, TypeVar, Union
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._execute_query(query=query)
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._execute_queries(queries=queries)
67
+ self.execute_queries(queries=queries)
68
68
 
69
69
  return make_snowsight_url(
70
70
  self._conn, f"/#/notebooks/{notebook_fqn.url_identifier}"