snowflake-cli 3.2.2__py3-none-any.whl → 3.4.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.
Files changed (97) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/__main__.py +2 -2
  3. snowflake/cli/_app/cli_app.py +224 -192
  4. snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +1 -27
  5. snowflake/cli/_app/constants.py +4 -0
  6. snowflake/cli/_app/snow_connector.py +12 -0
  7. snowflake/cli/_app/telemetry.py +10 -3
  8. snowflake/cli/_plugins/connection/util.py +12 -19
  9. snowflake/cli/_plugins/cortex/commands.py +2 -4
  10. snowflake/cli/_plugins/git/manager.py +1 -1
  11. snowflake/cli/_plugins/helpers/commands.py +207 -1
  12. snowflake/cli/_plugins/nativeapp/artifacts.py +16 -628
  13. snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
  14. snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
  15. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +42 -20
  16. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +9 -2
  17. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +6 -3
  18. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +44 -34
  19. snowflake/cli/_plugins/nativeapp/commands.py +113 -21
  20. snowflake/cli/_plugins/nativeapp/constants.py +5 -0
  21. snowflake/cli/_plugins/nativeapp/entities/application.py +226 -296
  22. snowflake/cli/_plugins/nativeapp/entities/application_package.py +911 -141
  23. snowflake/cli/_plugins/nativeapp/entities/application_package_child_interface.py +43 -0
  24. snowflake/cli/_plugins/nativeapp/feature_flags.py +5 -1
  25. snowflake/cli/_plugins/nativeapp/release_channel/__init__.py +13 -0
  26. snowflake/cli/_plugins/nativeapp/release_channel/commands.py +246 -0
  27. snowflake/cli/_plugins/nativeapp/release_directive/__init__.py +13 -0
  28. snowflake/cli/_plugins/nativeapp/release_directive/commands.py +243 -0
  29. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +9 -17
  30. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +80 -0
  31. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +1184 -80
  32. snowflake/cli/_plugins/nativeapp/utils.py +11 -0
  33. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +7 -3
  34. snowflake/cli/_plugins/nativeapp/version/commands.py +32 -5
  35. snowflake/cli/_plugins/notebook/commands.py +55 -2
  36. snowflake/cli/_plugins/notebook/exceptions.py +1 -1
  37. snowflake/cli/_plugins/notebook/manager.py +7 -5
  38. snowflake/cli/_plugins/notebook/notebook_entity.py +120 -0
  39. snowflake/cli/_plugins/notebook/notebook_entity_model.py +42 -0
  40. snowflake/cli/_plugins/notebook/notebook_project_paths.py +15 -0
  41. snowflake/cli/_plugins/notebook/types.py +3 -0
  42. snowflake/cli/_plugins/snowpark/commands.py +48 -30
  43. snowflake/cli/_plugins/snowpark/common.py +47 -2
  44. snowflake/cli/_plugins/snowpark/snowpark_entity.py +247 -4
  45. snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +18 -30
  46. snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +156 -23
  47. snowflake/cli/_plugins/snowpark/zipper.py +33 -1
  48. snowflake/cli/_plugins/spcs/common.py +129 -0
  49. snowflake/cli/_plugins/spcs/services/commands.py +131 -14
  50. snowflake/cli/_plugins/spcs/services/manager.py +169 -1
  51. snowflake/cli/_plugins/stage/commands.py +2 -1
  52. snowflake/cli/_plugins/stage/diff.py +60 -39
  53. snowflake/cli/_plugins/stage/manager.py +34 -13
  54. snowflake/cli/_plugins/stage/utils.py +1 -1
  55. snowflake/cli/_plugins/streamlit/commands.py +10 -1
  56. snowflake/cli/_plugins/streamlit/manager.py +70 -22
  57. snowflake/cli/_plugins/streamlit/streamlit_entity.py +131 -1
  58. snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +14 -24
  59. snowflake/cli/_plugins/streamlit/streamlit_project_paths.py +30 -0
  60. snowflake/cli/_plugins/workspace/commands.py +6 -5
  61. snowflake/cli/_plugins/workspace/manager.py +9 -5
  62. snowflake/cli/api/artifacts/__init__.py +13 -0
  63. snowflake/cli/api/artifacts/bundle_map.py +500 -0
  64. snowflake/cli/api/artifacts/common.py +78 -0
  65. snowflake/cli/api/artifacts/utils.py +82 -0
  66. snowflake/cli/api/cli_global_context.py +36 -2
  67. snowflake/cli/api/commands/flags.py +10 -4
  68. snowflake/cli/api/commands/utils.py +28 -2
  69. snowflake/cli/api/config.py +6 -2
  70. snowflake/cli/api/connections.py +12 -1
  71. snowflake/cli/api/constants.py +10 -1
  72. snowflake/cli/api/entities/common.py +81 -14
  73. snowflake/cli/api/entities/resolver.py +160 -0
  74. snowflake/cli/api/entities/utils.py +65 -23
  75. snowflake/cli/api/errno.py +63 -3
  76. snowflake/cli/api/feature_flags.py +19 -4
  77. snowflake/cli/api/metrics.py +21 -27
  78. snowflake/cli/api/project/definition_conversion.py +4 -4
  79. snowflake/cli/api/project/project_paths.py +28 -0
  80. snowflake/cli/api/project/schemas/entities/common.py +130 -1
  81. snowflake/cli/api/project/schemas/entities/entities.py +4 -0
  82. snowflake/cli/api/project/schemas/project_definition.py +54 -6
  83. snowflake/cli/api/project/schemas/updatable_model.py +2 -2
  84. snowflake/cli/api/project/schemas/v1/native_app/native_app.py +5 -7
  85. snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +1 -1
  86. snowflake/cli/api/project/util.py +45 -0
  87. snowflake/cli/api/secure_path.py +6 -0
  88. snowflake/cli/api/sql_execution.py +5 -1
  89. snowflake/cli/api/stage_path.py +7 -2
  90. snowflake/cli/api/utils/graph.py +3 -0
  91. snowflake/cli/api/utils/path_utils.py +24 -0
  92. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/METADATA +14 -15
  93. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/RECORD +96 -82
  94. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/WHEEL +1 -1
  95. snowflake/cli/api/project/schemas/v1/native_app/path_mapping.py +0 -65
  96. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/entry_points.txt +0 -0
  97. {snowflake_cli-3.2.2.dist-info → snowflake_cli-3.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -13,38 +13,110 @@
13
13
  # limitations under the License.
14
14
  from __future__ import annotations
15
15
 
16
+ import json
16
17
  import logging
17
18
  from contextlib import contextmanager
19
+ from datetime import datetime
20
+ from functools import cache
18
21
  from textwrap import dedent
19
- from typing import Any, Dict, List
22
+ from typing import Any, Dict, List, TypedDict
20
23
 
24
+ from snowflake.cli._plugins.connection.util import UIParameter, get_ui_parameter
25
+ from snowflake.cli._plugins.nativeapp.constants import (
26
+ AUTHORIZE_TELEMETRY_COL,
27
+ CHANNEL_COL,
28
+ DEFAULT_CHANNEL,
29
+ DEFAULT_DIRECTIVE,
30
+ NAME_COL,
31
+ SPECIAL_COMMENT,
32
+ )
33
+ from snowflake.cli._plugins.nativeapp.same_account_install_method import (
34
+ SameAccountInstallMethod,
35
+ )
21
36
  from snowflake.cli._plugins.nativeapp.sf_facade_constants import UseObjectType
22
37
  from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import (
38
+ CREATE_OR_UPGRADE_APPLICATION_EXPECTED_USER_ERROR_CODES,
39
+ UPGRADE_RESTRICTION_CODES,
23
40
  CouldNotUseObjectError,
24
41
  InsufficientPrivilegesError,
25
42
  UnexpectedResultError,
43
+ UpgradeApplicationRestrictionError,
44
+ UserInputError,
26
45
  UserScriptError,
27
46
  handle_unclassified_error,
28
47
  )
48
+ from snowflake.cli._plugins.stage.manager import StageManager
49
+ from snowflake.cli.api.cli_global_context import get_cli_context
50
+ from snowflake.cli.api.constants import ObjectType
29
51
  from snowflake.cli.api.errno import (
52
+ ACCOUNT_DOES_NOT_EXIST,
53
+ ACCOUNT_HAS_TOO_MANY_QUALIFIERS,
54
+ APPLICATION_PACKAGE_MAX_VERSIONS_HIT,
55
+ APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS,
56
+ APPLICATION_REQUIRES_TELEMETRY_SHARING,
57
+ CANNOT_ADD_PATCH_WITH_NON_INCREASING_PATCH_NUMBER,
58
+ CANNOT_CREATE_VERSION_WITH_NON_ZERO_PATCH,
59
+ CANNOT_DEREGISTER_VERSION_ASSOCIATED_WITH_CHANNEL,
60
+ CANNOT_DISABLE_MANDATORY_TELEMETRY,
61
+ CANNOT_DISABLE_RELEASE_CHANNELS,
62
+ CANNOT_MODIFY_RELEASE_CHANNEL_ACCOUNTS,
30
63
  DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED,
64
+ DOES_NOT_EXIST_OR_NOT_AUTHORIZED,
31
65
  INSUFFICIENT_PRIVILEGES,
66
+ MAX_UNBOUND_VERSIONS_REACHED,
67
+ MAX_VERSIONS_IN_RELEASE_CHANNEL_REACHED,
32
68
  NO_WAREHOUSE_SELECTED_IN_SESSION,
69
+ RELEASE_DIRECTIVE_DOES_NOT_EXIST,
70
+ RELEASE_DIRECTIVE_UNAPPROVED_VERSION_OR_PATCH,
71
+ RELEASE_DIRECTIVES_VERSION_PATCH_NOT_FOUND,
72
+ SQL_COMPILATION_ERROR,
73
+ TARGET_ACCOUNT_USED_BY_OTHER_RELEASE_DIRECTIVE,
74
+ VERSION_ALREADY_ADDED_TO_RELEASE_CHANNEL,
75
+ VERSION_DOES_NOT_EXIST,
76
+ VERSION_NOT_ADDED_TO_RELEASE_CHANNEL,
77
+ VERSION_NOT_IN_RELEASE_CHANNEL,
78
+ VERSION_REFERENCED_BY_RELEASE_DIRECTIVE,
33
79
  )
34
80
  from snowflake.cli.api.identifiers import FQN
81
+ from snowflake.cli.api.metrics import CLICounterField
82
+ from snowflake.cli.api.project.schemas.v1.native_app.package import DistributionOptions
35
83
  from snowflake.cli.api.project.util import (
36
84
  identifier_to_show_like_pattern,
37
- is_valid_unquoted_identifier,
85
+ same_identifiers,
38
86
  to_identifier,
39
- to_quoted_identifier,
40
87
  to_string_literal,
88
+ unquote_identifier,
41
89
  )
42
- from snowflake.cli.api.sql_execution import BaseSqlExecutor, SqlExecutor
90
+ from snowflake.cli.api.sql_execution import BaseSqlExecutor
91
+ from snowflake.cli.api.utils.cursor import find_first_row
43
92
  from snowflake.connector import DictCursor, ProgrammingError
44
93
 
94
+ ReleaseChannel = TypedDict(
95
+ "ReleaseChannel",
96
+ {
97
+ "name": str,
98
+ "description": str,
99
+ "created_on": datetime,
100
+ "updated_on": datetime,
101
+ "targets": dict[str, Any],
102
+ "versions": list[str],
103
+ },
104
+ )
105
+
106
+ Version = TypedDict(
107
+ "Version",
108
+ {
109
+ "version": str,
110
+ "patch": int,
111
+ "label": str | None,
112
+ "created_on": datetime,
113
+ "review_status": str,
114
+ },
115
+ )
116
+
45
117
 
46
118
  class SnowflakeSQLFacade:
47
- def __init__(self, sql_executor: SqlExecutor | None = None):
119
+ def __init__(self, sql_executor: BaseSqlExecutor | None = None):
48
120
  self._sql_executor = (
49
121
  sql_executor if sql_executor is not None else BaseSqlExecutor()
50
122
  )
@@ -58,12 +130,10 @@ class SnowflakeSQLFacade:
58
130
  """
59
131
  try:
60
132
  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
133
  except Exception as err:
134
+ if isinstance(err, ProgrammingError):
135
+ if err.errno == DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED:
136
+ raise CouldNotUseObjectError(object_type, name) from err
67
137
  handle_unclassified_error(err, f"Failed to use {object_type} {name}.")
68
138
 
69
139
  @contextmanager
@@ -91,7 +161,7 @@ class SnowflakeSQLFacade:
91
161
  except IndexError:
92
162
  prev_obj = None
93
163
 
94
- if prev_obj is not None and _same_identifier(prev_obj, name):
164
+ if prev_obj is not None and same_identifiers(prev_obj, name):
95
165
  yield
96
166
  return
97
167
 
@@ -136,6 +206,37 @@ class SnowflakeSQLFacade:
136
206
  """
137
207
  return self._use_object_optional(UseObjectType.SCHEMA, schema_name)
138
208
 
209
+ def grant_privileges_to_role(
210
+ self,
211
+ privileges: list[str],
212
+ object_type: ObjectType,
213
+ object_identifier: str,
214
+ role_to_grant: str,
215
+ role_to_use: str | None = None,
216
+ ) -> None:
217
+ """
218
+ Grants one or more access privileges on a securable object to a role
219
+
220
+ @param privileges: List of privileges to grant to a role
221
+ @param object_type: Type of snowflake object to grant to a role
222
+ @param object_identifier: Valid identifier of the snowflake object to grant to a role
223
+ @param role_to_grant: Name of the role to grant privileges to
224
+ @param [Optional] role_to_use: Name of the role to use to grant privileges
225
+ """
226
+ comma_separated_privileges = ", ".join(privileges)
227
+ object_type_and_name = f"{object_type.value.sf_name} {object_identifier}"
228
+
229
+ with self._use_role_optional(role_to_use):
230
+ try:
231
+ self._sql_executor.execute_query(
232
+ f"grant {comma_separated_privileges} on {object_type_and_name} to role {role_to_grant}"
233
+ )
234
+ except Exception as err:
235
+ handle_unclassified_error(
236
+ err,
237
+ f"Failed to grant {comma_separated_privileges} on {object_type_and_name} to role {role_to_grant}.",
238
+ )
239
+
139
240
  def execute_user_script(
140
241
  self,
141
242
  queries: str,
@@ -192,7 +293,7 @@ class SnowflakeSQLFacade:
192
293
  def create_version_in_package(
193
294
  self,
194
295
  package_name: str,
195
- stage_fqn: str,
296
+ path_to_version_directory: str,
196
297
  version: str,
197
298
  label: str | None = None,
198
299
  role: str | None = None,
@@ -200,39 +301,106 @@ class SnowflakeSQLFacade:
200
301
  """
201
302
  Creates a new version in an existing application package.
202
303
  @param package_name: Name of the application package to alter.
203
- @param stage_fqn: Stage fully qualified name.
304
+ @param path_to_version_directory: Path to artifacts on the stage to create a version from.
204
305
  @param version: Version name to create.
205
306
  @param [Optional] role: Switch to this role while executing create version.
206
307
  @param [Optional] label: Label for this version, visible to consumers.
207
308
  """
208
309
 
209
- # Make the version a valid identifier, adding quotes if necessary
210
310
  version = to_identifier(version)
311
+ package_name = to_identifier(package_name)
312
+
313
+ available_release_channels = self.show_release_channels(package_name, role)
211
314
 
212
315
  # Label must be a string literal
213
- with_label_cause = (
214
- f"\nlabel={to_string_literal(label)}" if label is not None else ""
316
+ with_label_clause = (
317
+ f"label={to_string_literal(label)}" if label is not None else ""
215
318
  )
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
- """
319
+ using_clause = (
320
+ f"using {StageManager.quote_stage_name(path_to_version_directory)}"
222
321
  )
322
+
323
+ action = "register" if available_release_channels else "add"
324
+
325
+ query = dedent(
326
+ _strip_empty_lines(
327
+ f"""\
328
+ alter application package {package_name}
329
+ {action} version {version}
330
+ {using_clause}
331
+ {with_label_clause}
332
+ """
333
+ )
334
+ )
335
+
223
336
  with self._use_role_optional(role):
224
337
  try:
225
- self._sql_executor.execute_query(add_version_query)
338
+ self._sql_executor.execute_query(query)
226
339
  except Exception as err:
340
+ if isinstance(err, ProgrammingError):
341
+ if err.errno == MAX_UNBOUND_VERSIONS_REACHED:
342
+ raise UserInputError(
343
+ f"Maximum unbound versions reached for application package {package_name}. "
344
+ "Please drop other unbound versions first, or add them to a release channel. "
345
+ "Use `snow app version list` to view all versions.",
346
+ ) from err
347
+ if err.errno == APPLICATION_PACKAGE_MAX_VERSIONS_HIT:
348
+ raise UserInputError(
349
+ f"Maximum versions reached for application package {package_name}. "
350
+ "Please drop the other versions first."
351
+ ) from err
352
+ if err.errno == CANNOT_CREATE_VERSION_WITH_NON_ZERO_PATCH:
353
+ raise UserInputError(
354
+ "Cannot create a new version with a non-zero patch in the manifest file."
355
+ ) from err
227
356
  handle_unclassified_error(
228
357
  err,
229
- f"Failed to add version {version} to application package {package_name}.",
358
+ f"Failed to {action} version {version} to application package {package_name}.",
359
+ )
360
+
361
+ def drop_version_from_package(
362
+ self, package_name: str, version: str, role: str | None = None
363
+ ):
364
+ """
365
+ Drops a version from an existing application package.
366
+ @param package_name: Name of the application package to alter.
367
+ @param version: Version name to drop.
368
+ @param [Optional] role: Switch to this role while executing drop version.
369
+ """
370
+
371
+ version = to_identifier(version)
372
+ package_name = to_identifier(package_name)
373
+
374
+ release_channels = self.show_release_channels(package_name, role)
375
+ action = "deregister" if release_channels else "drop"
376
+
377
+ query = f"alter application package {package_name} {action} version {version}"
378
+ with self._use_role_optional(role):
379
+ try:
380
+ self._sql_executor.execute_query(query)
381
+ except Exception as err:
382
+ if isinstance(err, ProgrammingError):
383
+ if err.errno == VERSION_REFERENCED_BY_RELEASE_DIRECTIVE:
384
+ raise UserInputError(
385
+ f"Cannot drop version {version} from application package {package_name} because it is in use by one or more release directives."
386
+ ) from err
387
+ if err.errno == CANNOT_DEREGISTER_VERSION_ASSOCIATED_WITH_CHANNEL:
388
+ raise UserInputError(
389
+ f"Cannot drop version {version} from application package {package_name} because it is associated with a release channel."
390
+ ) from err
391
+ if err.errno == VERSION_DOES_NOT_EXIST:
392
+ raise UserInputError(
393
+ f"Version {version} does not exist in application package {package_name}."
394
+ ) from err
395
+ handle_unclassified_error(
396
+ err,
397
+ f"Failed to {action} version {version} from application package {package_name}.",
230
398
  )
231
399
 
232
400
  def add_patch_to_package_version(
233
401
  self,
234
402
  package_name: str,
235
- stage_fqn: str,
403
+ path_to_version_directory: str,
236
404
  version: str,
237
405
  patch: int | None = None,
238
406
  label: str | None = None,
@@ -241,7 +409,7 @@ class SnowflakeSQLFacade:
241
409
  """
242
410
  Add a new patch, optionally a custom one, to an existing version in an application package.
243
411
  @param package_name: Name of the application package to alter.
244
- @param stage_fqn: Stage fully qualified name.
412
+ @param path_to_version_directory: Path to artifacts on the stage to create a version from.
245
413
  @param version: Version name to create.
246
414
  @param [Optional] patch: Patch number to create.
247
415
  @param [Optional] label: Label for this patch, visible to consumers.
@@ -257,12 +425,15 @@ class SnowflakeSQLFacade:
257
425
  with_label_clause = (
258
426
  f"\nlabel={to_string_literal(label)}" if label is not None else ""
259
427
  )
260
- patch_query = f"{patch}" if patch else ""
428
+
429
+ patch_query = f" {patch}" if patch is not None else ""
430
+ using_clause = StageManager.quote_stage_name(path_to_version_directory)
431
+ # No space between patch and patch{patch_query} to avoid extra space when patch is None
261
432
  add_patch_query = dedent(
262
433
  f"""\
263
434
  alter application package {package_name}
264
- add patch {patch_query} for version {version}
265
- using @{stage_fqn}{with_label_clause}
435
+ add patch{patch_query} for version {version}
436
+ using {using_clause}{with_label_clause}
266
437
  """
267
438
  )
268
439
  with self._use_role_optional(role):
@@ -271,9 +442,23 @@ class SnowflakeSQLFacade:
271
442
  add_patch_query, cursor_class=DictCursor
272
443
  ).fetchall()
273
444
  except Exception as err:
445
+ if isinstance(err, ProgrammingError):
446
+ if err.errno == APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS:
447
+ extra_message = (
448
+ " Check the manifest file for any hard-coded patch value."
449
+ if patch is None
450
+ else ""
451
+ )
452
+ raise UserInputError(
453
+ f"Patch{patch_query} already exists for version {version} in application package {package_name}.{extra_message}"
454
+ ) from err
455
+ if err.errno == CANNOT_ADD_PATCH_WITH_NON_INCREASING_PATCH_NUMBER:
456
+ raise UserInputError(
457
+ f"Cannot add a patch with a non-increasing patch number to version {version} in application package {package_name}."
458
+ ) from err
274
459
  handle_unclassified_error(
275
460
  err,
276
- f"Failed to create patch {patch_query} for version {version} in application package {package_name}.",
461
+ f"Failed to create patch{patch_query} for version {version} in application package {package_name}.",
277
462
  )
278
463
  try:
279
464
  show_row = result_cursor[0]
@@ -370,13 +555,14 @@ class SnowflakeSQLFacade:
370
555
  self._sql_executor.execute_query(
371
556
  f"create schema if not exists {identifier}"
372
557
  )
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
558
+ except Exception as err:
559
+ if isinstance(err, ProgrammingError):
560
+ if err.errno == INSUFFICIENT_PRIVILEGES:
561
+ raise InsufficientPrivilegesError(
562
+ f"Insufficient privileges to create schema {name}",
563
+ role=role,
564
+ database=database,
565
+ ) from err
380
566
  handle_unclassified_error(err, f"Failed to create schema {name}.")
381
567
 
382
568
  def stage_exists(
@@ -414,16 +600,17 @@ class SnowflakeSQLFacade:
414
600
  results = self._sql_executor.execute_query(
415
601
  f"show stages like {pattern}{in_schema_clause}",
416
602
  )
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
603
+ except Exception as err:
604
+ if isinstance(err, ProgrammingError):
605
+ if err.errno == DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED:
606
+ return False
607
+ if err.errno == INSUFFICIENT_PRIVILEGES:
608
+ raise InsufficientPrivilegesError(
609
+ f"Insufficient privileges to check if stage {name} exists",
610
+ role=role,
611
+ database=database,
612
+ schema=schema,
613
+ ) from err
427
614
  handle_unclassified_error(
428
615
  err, f"Failed to check if stage {name} exists."
429
616
  )
@@ -466,18 +653,22 @@ class SnowflakeSQLFacade:
466
653
  ):
467
654
  try:
468
655
  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
656
+ except Exception as err:
657
+ if isinstance(err, ProgrammingError):
658
+ if err.errno == INSUFFICIENT_PRIVILEGES:
659
+ raise InsufficientPrivilegesError(
660
+ f"Insufficient privileges to create stage {name}",
661
+ role=role,
662
+ database=database,
663
+ schema=schema,
664
+ ) from err
477
665
  handle_unclassified_error(err, f"Failed to create stage {name}.")
478
666
 
479
667
  def show_release_directives(
480
- self, package_name: str, role: str | None = None
668
+ self,
669
+ package_name: str,
670
+ release_channel: str | None = None,
671
+ role: str | None = None,
481
672
  ) -> list[dict[str, Any]]:
482
673
  """
483
674
  Show release directives for a package
@@ -485,41 +676,954 @@ class SnowflakeSQLFacade:
485
676
  @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
486
677
  """
487
678
  package_identifier = to_identifier(package_name)
679
+
680
+ query = f"show release directives in application package {package_identifier}"
681
+ if release_channel:
682
+ query += f" for release channel {to_identifier(release_channel)}"
683
+
488
684
  with self._use_role_optional(role):
489
685
  try:
490
686
  cursor = self._sql_executor.execute_query(
491
- f"show release directives in application package {package_identifier}",
687
+ query,
492
688
  cursor_class=DictCursor,
493
689
  )
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
690
+ except Exception as err:
691
+ if isinstance(err, ProgrammingError):
692
+ if err.errno == INSUFFICIENT_PRIVILEGES:
693
+ raise InsufficientPrivilegesError(
694
+ f"Insufficient privileges to show release directives for application package {package_name}",
695
+ role=role,
696
+ ) from err
697
+ if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED:
698
+ raise UserInputError(
699
+ f"Application package {package_name} does not exist or you are not authorized to access it."
700
+ ) from err
500
701
  handle_unclassified_error(
501
702
  err,
502
- f"Failed to show release directives for package {package_name}.",
703
+ f"Failed to show release directives for application package {package_name}.",
503
704
  )
504
705
  return cursor.fetchall()
505
706
 
707
+ def get_existing_app_info(self, name: str, role: str) -> dict | None:
708
+ """
709
+ Check for an existing application object by the same name as in project definition, in account.
710
+ It executes a 'show applications like' query and returns the result as single row, if one exists.
711
+ """
712
+ with self._use_role_optional(role):
713
+ try:
714
+ object_type_plural = ObjectType.APPLICATION.value.sf_plural_name
715
+ show_obj_query = f"show {object_type_plural} like {identifier_to_show_like_pattern(name)}".strip()
506
716
 
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.
717
+ show_obj_cursor = self._sql_executor.execute_query(
718
+ show_obj_query, cursor_class=DictCursor
719
+ )
720
+
721
+ show_obj_row = find_first_row(
722
+ # row[NAME_COL] is not an identifier. It is the unquoted internal representation
723
+ show_obj_cursor,
724
+ lambda row: row[NAME_COL] == unquote_identifier(name),
725
+ )
726
+ except Exception as err:
727
+ handle_unclassified_error(
728
+ err, f"Unable to fetch information on application {name}."
729
+ )
730
+ return show_obj_row
512
731
 
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
732
+ def upgrade_application(
733
+ self,
734
+ name: str,
735
+ install_method: SameAccountInstallMethod,
736
+ path_to_version_directory: str,
737
+ role: str,
738
+ warehouse: str,
739
+ debug_mode: bool | None,
740
+ should_authorize_event_sharing: bool | None,
741
+ release_channel: str | None = None,
742
+ ) -> list[tuple[str]]:
743
+ """
744
+ Upgrades an application object using the provided clauses
745
+
746
+ @param name: Name of the application object
747
+ @param install_method: Method of installing the application
748
+ @param path_to_version_directory: Path to directory in stage housing the application artifacts
749
+ @param role: Role to use when creating the application and provider-side objects
750
+ @param warehouse: Warehouse which is required to create an application object
751
+ @param debug_mode: Whether to enable debug mode; None means not explicitly enabled or disabled
752
+ @param should_authorize_event_sharing: Whether to enable event sharing; None means not explicitly enabled or disabled
753
+ @param release_channel [Optional]: Release channel to use when upgrading the application
754
+ """
755
+
756
+ name = to_identifier(name)
757
+ release_channel = to_identifier(release_channel or DEFAULT_CHANNEL)
758
+
759
+ install_method.ensure_app_usable(
760
+ app_name=name,
761
+ app_role=role,
762
+ show_app_row=self.get_existing_app_info(name, role),
763
+ )
764
+
765
+ # If all the above checks are in order, proceed to upgrade
766
+
767
+ @cache # only cache within the scope of this method
768
+ def get_app_properties():
769
+ return self.get_app_properties(name, role)
770
+
771
+ with self._use_role_optional(role), self._use_warehouse_optional(warehouse):
772
+ try:
773
+ using_clause = install_method.using_clause(path_to_version_directory)
774
+
775
+ current_release_channel = (
776
+ get_app_properties().get(CHANNEL_COL) or DEFAULT_CHANNEL
777
+ )
778
+ if unquote_identifier(release_channel) != current_release_channel:
779
+ raise UpgradeApplicationRestrictionError(
780
+ f"Application {name} is currently on release channel {current_release_channel}. Cannot upgrade to release channel {release_channel}."
781
+ )
782
+
783
+ upgrade_cursor = self._sql_executor.execute_query(
784
+ f"alter application {name} upgrade {using_clause}",
785
+ )
786
+
787
+ # if debug_mode is present (controlled), ensure it is up-to-date
788
+ if install_method.is_dev_mode:
789
+ if debug_mode is not None:
790
+ self._sql_executor.execute_query(
791
+ f"alter application {name} set debug_mode = {debug_mode}"
792
+ )
793
+
794
+ except UpgradeApplicationRestrictionError as err:
795
+ raise err
796
+ except Exception as err:
797
+ if isinstance(err, ProgrammingError):
798
+ if err.errno in UPGRADE_RESTRICTION_CODES:
799
+ raise UpgradeApplicationRestrictionError(err.msg) from err
800
+ if (
801
+ err.errno
802
+ in CREATE_OR_UPGRADE_APPLICATION_EXPECTED_USER_ERROR_CODES
803
+ ):
804
+ raise UserInputError(
805
+ f"Failed to upgrade application {name} with the following error message:\n"
806
+ f"{err.msg}"
807
+ ) from err
808
+ handle_unclassified_error(err, f"Failed to upgrade application {name}.")
809
+
810
+ try:
811
+ # Only update event sharing if the current value is different as the one we want to set
812
+ if should_authorize_event_sharing is not None:
813
+ current_authorize_event_sharing = (
814
+ get_app_properties()
815
+ .get(AUTHORIZE_TELEMETRY_COL, "false")
816
+ .lower()
817
+ == "true"
818
+ )
819
+ if (
820
+ current_authorize_event_sharing
821
+ != should_authorize_event_sharing
822
+ ):
823
+ self._log.info(
824
+ "Setting telemetry sharing authorization to %s",
825
+ should_authorize_event_sharing,
826
+ )
827
+ self._sql_executor.execute_query(
828
+ f"alter application {name} set AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(should_authorize_event_sharing).upper()}"
829
+ )
830
+ except Exception as err:
831
+ if isinstance(err, ProgrammingError):
832
+ if err.errno == CANNOT_DISABLE_MANDATORY_TELEMETRY:
833
+ get_cli_context().metrics.set_counter(
834
+ CLICounterField.EVENT_SHARING_ERROR, 1
835
+ )
836
+ raise UserInputError(
837
+ "Could not disable telemetry event sharing for the application because it contains mandatory events. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file."
838
+ ) from err
839
+ handle_unclassified_error(
840
+ err,
841
+ f"Failed to set AUTHORIZE_TELEMETRY_EVENT_SHARING when upgrading application {name}.",
842
+ )
843
+
844
+ return upgrade_cursor.fetchall()
845
+
846
+ def create_application(
847
+ self,
848
+ name: str,
849
+ package_name: str,
850
+ install_method: SameAccountInstallMethod,
851
+ path_to_version_directory: str,
852
+ role: str,
853
+ warehouse: str,
854
+ debug_mode: bool | None,
855
+ should_authorize_event_sharing: bool | None,
856
+ release_channel: str | None = None,
857
+ ) -> list[tuple[str]]:
858
+ """
859
+ Creates a new application object using an application package,
860
+ running the setup script of the application package
861
+
862
+ @param name: Name of the application object
863
+ @param package_name: Name of the application package to install the application from
864
+ @param install_method: Method of installing the application
865
+ @param path_to_version_directory: Path to directory in stage housing the application artifacts
866
+ @param role: Role to use when creating the application and provider-side objects
867
+ @param warehouse: Warehouse which is required to create an application object
868
+ @param debug_mode: Whether to enable debug mode; None means not explicitly enabled or disabled
869
+ @param should_authorize_event_sharing: Whether to enable event sharing; None means not explicitly enabled or disabled
870
+ @param release_channel [Optional]: Release channel to use when creating the application
871
+ """
872
+ package_name = to_identifier(package_name)
873
+ name = to_identifier(name)
874
+ release_channel = to_identifier(release_channel) if release_channel else None
875
+
876
+ # by default, applications are created in debug mode when possible;
877
+ # this can be overridden in the project definition
878
+ debug_mode_clause = ""
879
+ if install_method.is_dev_mode:
880
+ initial_debug_mode = debug_mode if debug_mode is not None else True
881
+ debug_mode_clause = f"debug_mode = {initial_debug_mode}"
882
+
883
+ authorize_telemetry_clause = ""
884
+ if should_authorize_event_sharing is not None:
885
+ self._log.info(
886
+ "Setting AUTHORIZE_TELEMETRY_EVENT_SHARING to %s",
887
+ should_authorize_event_sharing,
888
+ )
889
+ authorize_telemetry_clause = f"AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(should_authorize_event_sharing).upper()}"
890
+
891
+ using_clause = install_method.using_clause(path_to_version_directory)
892
+ release_channel_clause = (
893
+ f"using release channel {release_channel}" if release_channel else ""
894
+ )
895
+
896
+ with self._use_role_optional(role), self._use_warehouse_optional(warehouse):
897
+ try:
898
+ create_cursor = self._sql_executor.execute_query(
899
+ dedent(
900
+ _strip_empty_lines(
901
+ f"""\
902
+ create application {name}
903
+ from application package {package_name}
904
+ {using_clause}
905
+ {release_channel_clause}
906
+ {debug_mode_clause}
907
+ {authorize_telemetry_clause}
908
+ comment = {SPECIAL_COMMENT}
909
+ """
910
+ )
911
+ ),
912
+ )
913
+ except Exception as err:
914
+ if isinstance(err, ProgrammingError):
915
+ if err.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING:
916
+ get_cli_context().metrics.set_counter(
917
+ CLICounterField.EVENT_SHARING_ERROR, 1
918
+ )
919
+ raise UserInputError(
920
+ "The application package requires event sharing to be authorized. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file."
921
+ ) from err
922
+ if (
923
+ err.errno
924
+ in CREATE_OR_UPGRADE_APPLICATION_EXPECTED_USER_ERROR_CODES
925
+ ):
926
+ raise UserInputError(
927
+ f"Failed to create application {name} with the following error message:\n"
928
+ f"{err.msg}"
929
+ ) from err
930
+ handle_unclassified_error(err, f"Failed to create application {name}.")
931
+
932
+ return create_cursor.fetchall()
933
+
934
+ def create_application_package(
935
+ self,
936
+ package_name: str,
937
+ distribution: DistributionOptions,
938
+ enable_release_channels: bool | None = None,
939
+ role: str | None = None,
940
+ ) -> None:
941
+ """
942
+ Creates a new application package.
943
+ @param package_name: Name of the application package to create.
944
+ @param [Optional] enable_release_channels: Enable/Disable release channels if not None.
945
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
946
+ """
947
+ package_name = to_identifier(package_name)
948
+
949
+ enable_release_channels_clause = ""
950
+ if enable_release_channels is not None:
951
+ enable_release_channels_clause = (
952
+ f"enable_release_channels = {str(enable_release_channels).lower()}"
953
+ )
954
+
955
+ with self._use_role_optional(role):
956
+ try:
957
+ self._sql_executor.execute_query(
958
+ dedent(
959
+ _strip_empty_lines(
960
+ f"""\
961
+ create application package {package_name}
962
+ comment = {SPECIAL_COMMENT}
963
+ distribution = {distribution}
964
+ {enable_release_channels_clause}
965
+ """
966
+ )
967
+ )
968
+ )
969
+ except Exception as err:
970
+ if isinstance(err, ProgrammingError):
971
+ if err.errno == INSUFFICIENT_PRIVILEGES:
972
+ raise InsufficientPrivilegesError(
973
+ f"Insufficient privileges to create application package {package_name}",
974
+ role=role,
975
+ ) from err
976
+ handle_unclassified_error(
977
+ err, f"Failed to create application package {package_name}."
978
+ )
979
+
980
+ def alter_application_package_properties(
981
+ self,
982
+ package_name: str,
983
+ enable_release_channels: bool | None = None,
984
+ role: str | None = None,
985
+ ) -> None:
986
+ """
987
+ Alters the properties of an existing application package.
988
+ @param package_name: Name of the application package to alter.
989
+ @param [Optional] enable_release_channels: Enable/Disable release channels if not None.
990
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
991
+ """
992
+
993
+ package_name = to_identifier(package_name)
994
+
995
+ if enable_release_channels is not None:
996
+ with self._use_role_optional(role):
997
+ try:
998
+ self._sql_executor.execute_query(
999
+ dedent(
1000
+ f"""\
1001
+ alter application package {package_name}
1002
+ set enable_release_channels = {str(enable_release_channels).lower()}
1003
+ """
1004
+ )
1005
+ )
1006
+ except Exception as err:
1007
+ if isinstance(err, ProgrammingError):
1008
+ if err.errno == INSUFFICIENT_PRIVILEGES:
1009
+ raise InsufficientPrivilegesError(
1010
+ f"Insufficient privileges to update enable_release_channels for application package {package_name}",
1011
+ role=role,
1012
+ ) from err
1013
+ if err.errno == CANNOT_DISABLE_RELEASE_CHANNELS:
1014
+ raise UserInputError(
1015
+ f"Cannot disable release channels for application package {package_name} after it is enabled. Try recreating the application package."
1016
+ ) from err
1017
+ handle_unclassified_error(
1018
+ err,
1019
+ f"Failed to update enable_release_channels for application package {package_name}.",
1020
+ )
1021
+
1022
+ def get_ui_parameter(self, parameter: UIParameter, default: Any) -> Any:
1023
+ """
1024
+ Returns the value of a single UI parameter.
1025
+ If the parameter is not found, the default value is returned.
1026
+
1027
+ @param parameter: UIParameter, the parameter to get the value of.
1028
+ @param default: Default value to return if the parameter is not found.
1029
+ """
1030
+ connection = self._sql_executor._conn # noqa SLF001
1031
+
1032
+ return get_ui_parameter(connection, parameter, default)
1033
+
1034
+ def set_release_directive(
1035
+ self,
1036
+ package_name: str,
1037
+ release_directive: str,
1038
+ release_channel: str | None,
1039
+ target_accounts: List[str] | None,
1040
+ version: str,
1041
+ patch: int,
1042
+ role: str | None = None,
1043
+ ):
1044
+ """
1045
+ Sets a release directive for an application package.
1046
+ Default release directive does not support target accounts.
1047
+ Non-default release directives require target accounts to be specified.
1048
+
1049
+ @param package_name: Name of the application package to alter.
1050
+ @param release_directive: Name of the release directive to set.
1051
+ @param release_channel: Name of the release channel to set the release directive for.
1052
+ @param target_accounts: List of target accounts for the release directive.
1053
+ @param version: Version to set the release directive for.
1054
+ @param patch: Patch number to set the release directive for.
1055
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1056
+ """
1057
+
1058
+ package_name = to_identifier(package_name)
1059
+ release_channel = to_identifier(release_channel) if release_channel else None
1060
+ release_directive = to_identifier(release_directive)
1061
+ version = to_identifier(version)
1062
+
1063
+ if same_identifiers(release_directive, DEFAULT_DIRECTIVE):
1064
+ if target_accounts:
1065
+ raise UserInputError(
1066
+ "Default release directive does not support target accounts."
1067
+ )
1068
+ release_directive_statement = "set default release directive"
1069
+ else:
1070
+ if target_accounts:
1071
+ release_directive_statement = (
1072
+ f"set release directive {release_directive}"
1073
+ )
1074
+ else:
1075
+ release_directive_statement = (
1076
+ f"modify release directive {release_directive}"
1077
+ )
1078
+
1079
+ release_channel_statement = (
1080
+ f"modify release channel {release_channel}" if release_channel else ""
1081
+ )
1082
+
1083
+ accounts_statement = (
1084
+ f"accounts = ({','.join(target_accounts)})" if target_accounts else ""
1085
+ )
1086
+
1087
+ full_query = dedent(
1088
+ _strip_empty_lines(
1089
+ f"""\
1090
+ alter application package {package_name}
1091
+ {release_channel_statement}
1092
+ {release_directive_statement}
1093
+ {accounts_statement}
1094
+ version = {version} patch = {patch}
1095
+ """
1096
+ )
1097
+ )
1098
+
1099
+ with self._use_role_optional(role):
1100
+ try:
1101
+ self._sql_executor.execute_query(full_query)
1102
+ except Exception as err:
1103
+ if isinstance(err, ProgrammingError):
1104
+ if (
1105
+ err.errno == ACCOUNT_DOES_NOT_EXIST
1106
+ or err.errno == ACCOUNT_HAS_TOO_MANY_QUALIFIERS
1107
+ ):
1108
+ raise UserInputError(
1109
+ f"Invalid account passed in.\n{str(err.msg)}"
1110
+ ) from err
1111
+ if err.errno == RELEASE_DIRECTIVE_DOES_NOT_EXIST:
1112
+ raise UserInputError(
1113
+ f"Release directive {release_directive} does not exist in application package {package_name}. Please create it first by specifying --target-accounts with the `snow app release-directive set` command."
1114
+ ) from err
1115
+ if err.errno == TARGET_ACCOUNT_USED_BY_OTHER_RELEASE_DIRECTIVE:
1116
+ raise UserInputError(
1117
+ f"Some target accounts are already referenced by other release directives in application package {package_name}.\n{str(err.msg)}"
1118
+ ) from err
1119
+ if err.errno == VERSION_NOT_ADDED_TO_RELEASE_CHANNEL:
1120
+ raise UserInputError(
1121
+ f"Version {version} is not added to release channel {release_channel}. Please add it to the release channel first."
1122
+ ) from err
1123
+ if err.errno == RELEASE_DIRECTIVES_VERSION_PATCH_NOT_FOUND:
1124
+ raise UserInputError(
1125
+ f"Patch {patch} for version {version} not found in application package {package_name}."
1126
+ ) from err
1127
+ if err.errno == RELEASE_DIRECTIVE_UNAPPROVED_VERSION_OR_PATCH:
1128
+ raise UserInputError(
1129
+ f"Version {version}, patch {patch} has not yet been approved to release to accounts outside of this organization."
1130
+ ) from err
1131
+ if err.errno == VERSION_DOES_NOT_EXIST:
1132
+ raise UserInputError(
1133
+ f"Version {version} does not exist in application package {package_name}."
1134
+ ) from err
1135
+ handle_unclassified_error(
1136
+ err,
1137
+ f"Failed to set release directive {release_directive} for application package {package_name}.",
1138
+ )
1139
+
1140
+ def unset_release_directive(
1141
+ self,
1142
+ package_name: str,
1143
+ release_directive: str,
1144
+ release_channel: str | None,
1145
+ role: str | None = None,
1146
+ ):
1147
+ """
1148
+ Unsets a release directive for an application package.
1149
+ Release directive must already exist in the application package.
1150
+ Does not accept default release directive.
1151
+
1152
+ @param package_name: Name of the application package to alter.
1153
+ @param release_directive: Name of the release directive to unset.
1154
+ @param release_channel: Name of the release channel to unset the release directive for.
1155
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1156
+ """
1157
+ package_name = to_identifier(package_name)
1158
+ release_channel = to_identifier(release_channel) if release_channel else None
1159
+ release_directive = to_identifier(release_directive)
1160
+
1161
+ if same_identifiers(release_directive, DEFAULT_DIRECTIVE):
1162
+ raise UserInputError(
1163
+ "Cannot unset default release directive. Please specify a non-default release directive."
1164
+ )
1165
+
1166
+ release_channel_statement = ""
1167
+ if release_channel:
1168
+ release_channel_statement = f" modify release channel {release_channel}"
1169
+
1170
+ with self._use_role_optional(role):
1171
+ try:
1172
+ self._sql_executor.execute_query(
1173
+ f"alter application package {package_name}{release_channel_statement} unset release directive {release_directive}"
1174
+ )
1175
+ except Exception as err:
1176
+ if isinstance(err, ProgrammingError):
1177
+ if err.errno == RELEASE_DIRECTIVE_DOES_NOT_EXIST:
1178
+ raise UserInputError(
1179
+ f"Release directive {release_directive} does not exist in application package {package_name}."
1180
+ ) from err
1181
+ handle_unclassified_error(
1182
+ err,
1183
+ f"Failed to unset release directive {release_directive} for application package {package_name}.",
1184
+ )
1185
+
1186
+ def add_accounts_to_release_directive(
1187
+ self,
1188
+ package_name: str,
1189
+ release_directive: str,
1190
+ release_channel: str | None,
1191
+ target_accounts: List[str],
1192
+ role: str | None = None,
1193
+ ):
1194
+ """
1195
+ Adds target accounts to a release directive of a release channel in an application package.
1196
+ Release directive must already exist in the application package.
1197
+ Default release directive does not support target accounts.
1198
+
1199
+ @param package_name: Name of the application package to alter.
1200
+ @param release_directive: Name of the release directive to add target accounts to.
1201
+ @param release_channel: Name of the release channel where the release directive belongs to.
1202
+ @param target_accounts: List of target accounts to add to the release directive.
1203
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1204
+ """
1205
+ package_name = to_identifier(package_name)
1206
+ release_channel = to_identifier(release_channel) if release_channel else None
1207
+ release_directive = to_identifier(release_directive)
1208
+
1209
+ if same_identifiers(release_directive, DEFAULT_DIRECTIVE):
1210
+ raise UserInputError(
1211
+ "Default release directive does not support adding accounts. Please specify a non-default release directive."
1212
+ )
1213
+
1214
+ release_channel_statement = ""
1215
+ if release_channel:
1216
+ release_channel_statement = f"modify release channel {release_channel}"
1217
+
1218
+ with self._use_role_optional(role):
1219
+ try:
1220
+ self._sql_executor.execute_query(
1221
+ dedent(
1222
+ _strip_empty_lines(
1223
+ f"""\
1224
+ alter application package {package_name}
1225
+ {release_channel_statement}
1226
+ modify release directive {release_directive}
1227
+ add accounts = ({','.join(target_accounts)})
1228
+ """
1229
+ )
1230
+ )
1231
+ )
1232
+ except Exception as err:
1233
+ if isinstance(err, ProgrammingError):
1234
+ if (
1235
+ err.errno == ACCOUNT_DOES_NOT_EXIST
1236
+ or err.errno == ACCOUNT_HAS_TOO_MANY_QUALIFIERS
1237
+ ):
1238
+ raise UserInputError(
1239
+ f"Invalid account passed in.\n{str(err.msg)}"
1240
+ ) from err
1241
+ if err.errno == RELEASE_DIRECTIVE_DOES_NOT_EXIST:
1242
+ raise UserInputError(
1243
+ f"Release directive {release_directive} does not exist in application package {package_name}."
1244
+ ) from err
1245
+ if err.errno == TARGET_ACCOUNT_USED_BY_OTHER_RELEASE_DIRECTIVE:
1246
+ raise UserInputError(
1247
+ f"Some target accounts are already referenced by other release directives in application package {package_name}.\n{str(err.msg)}"
1248
+ ) from err
1249
+ handle_unclassified_error(
1250
+ err,
1251
+ f"Failed to add accounts to release directive {release_directive} for application package {package_name}.",
1252
+ )
1253
+
1254
+ def remove_accounts_from_release_directive(
1255
+ self,
1256
+ package_name: str,
1257
+ release_directive: str,
1258
+ release_channel: str | None,
1259
+ target_accounts: List[str],
1260
+ role: str | None = None,
1261
+ ):
1262
+ """
1263
+ Removes target accounts from a release directive of a release channel in an application package.
1264
+ Release directive must already exist in the application package.
1265
+ Default release directive does not support target accounts.
1266
+
1267
+ @param package_name: Name of the application package to alter.
1268
+ @param release_directive: Name of the release directive to remove target accounts from.
1269
+ @param release_channel: Name of the release channel where the release directive belongs to.
1270
+ @param target_accounts: List of target accounts to remove from the release directive.
1271
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1272
+ """
1273
+ package_name = to_identifier(package_name)
1274
+ release_channel = to_identifier(release_channel) if release_channel else None
1275
+ release_directive = to_identifier(release_directive)
1276
+
1277
+ if same_identifiers(release_directive, DEFAULT_DIRECTIVE):
1278
+ raise UserInputError(
1279
+ "Default release directive does not support removing accounts. Please specify a non-default release directive."
1280
+ )
1281
+
1282
+ release_channel_statement = ""
1283
+ if release_channel:
1284
+ release_channel_statement = f"modify release channel {release_channel}"
1285
+
1286
+ with self._use_role_optional(role):
1287
+ try:
1288
+ self._sql_executor.execute_query(
1289
+ dedent(
1290
+ _strip_empty_lines(
1291
+ f"""\
1292
+ alter application package {package_name}
1293
+ {release_channel_statement}
1294
+ modify release directive {release_directive}
1295
+ remove accounts = ({','.join(target_accounts)})
1296
+ """
1297
+ )
1298
+ )
1299
+ )
1300
+ except Exception as err:
1301
+ if isinstance(err, ProgrammingError):
1302
+ if (
1303
+ err.errno == ACCOUNT_DOES_NOT_EXIST
1304
+ or err.errno == ACCOUNT_HAS_TOO_MANY_QUALIFIERS
1305
+ ):
1306
+ raise UserInputError(
1307
+ f"Invalid account passed in.\n{str(err.msg)}"
1308
+ ) from err
1309
+ if err.errno == RELEASE_DIRECTIVE_DOES_NOT_EXIST:
1310
+ raise UserInputError(
1311
+ f"Release directive {release_directive} does not exist in application package {package_name}."
1312
+ ) from err
1313
+ handle_unclassified_error(
1314
+ err,
1315
+ f"Failed to remove accounts from release directive {release_directive} for application package {package_name}.",
1316
+ )
1317
+
1318
+ def show_release_channels(
1319
+ self, package_name: str, role: str | None = None
1320
+ ) -> list[ReleaseChannel]:
1321
+ """
1322
+ Show release channels in a package.
1323
+
1324
+ @param package_name: Name of the application package
1325
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1326
+ """
1327
+
1328
+ if (
1329
+ self.get_ui_parameter(UIParameter.NA_FEATURE_RELEASE_CHANNELS, True)
1330
+ is False
1331
+ ):
1332
+ return []
1333
+
1334
+ package_identifier = to_identifier(package_name)
1335
+ results = []
1336
+ with self._use_role_optional(role):
1337
+ try:
1338
+ cursor = self._sql_executor.execute_query(
1339
+ f"show release channels in application package {package_identifier}",
1340
+ cursor_class=DictCursor,
1341
+ )
1342
+ except Exception as err:
1343
+ if isinstance(err, ProgrammingError):
1344
+ # TODO: Temporary check for syntax until UI Parameter is available in production
1345
+ if err.errno == SQL_COMPILATION_ERROR:
1346
+ # Release not out yet and param not out yet
1347
+ return []
1348
+ if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED:
1349
+ raise UserInputError(
1350
+ f"Application package {package_name} does not exist or you are not authorized to access it."
1351
+ ) from err
1352
+ handle_unclassified_error(
1353
+ err,
1354
+ f"Failed to show release channels for application package {package_name}.",
1355
+ )
1356
+
1357
+ rows = cursor.fetchall()
1358
+
1359
+ for row in rows:
1360
+ targets = json.loads(row["targets"]) if row.get("targets") else {}
1361
+ versions = json.loads(row["versions"]) if row.get("versions") else []
1362
+ results.append(
1363
+ ReleaseChannel(
1364
+ name=row["name"],
1365
+ description=row["description"],
1366
+ created_on=row["created_on"],
1367
+ updated_on=row["updated_on"],
1368
+ targets=targets,
1369
+ versions=versions,
1370
+ )
1371
+ )
1372
+
1373
+ return results
1374
+
1375
+ def add_accounts_to_release_channel(
1376
+ self,
1377
+ package_name: str,
1378
+ release_channel: str,
1379
+ target_accounts: List[str],
1380
+ role: str | None = None,
1381
+ ):
1382
+ """
1383
+ Adds accounts to a release channel.
1384
+
1385
+ @param package_name: Name of the application package
1386
+ @param release_channel: Name of the release channel
1387
+ @param target_accounts: List of target accounts to add to the release channel
1388
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1389
+ """
1390
+
1391
+ package_name = to_identifier(package_name)
1392
+ release_channel = to_identifier(release_channel)
1393
+
1394
+ with self._use_role_optional(role):
1395
+ try:
1396
+ self._sql_executor.execute_query(
1397
+ f"alter application package {package_name} modify release channel {release_channel} add accounts = ({','.join(target_accounts)})"
1398
+ )
1399
+ except Exception as err:
1400
+ if isinstance(err, ProgrammingError):
1401
+ if (
1402
+ err.errno == ACCOUNT_DOES_NOT_EXIST
1403
+ or err.errno == ACCOUNT_HAS_TOO_MANY_QUALIFIERS
1404
+ ):
1405
+ raise UserInputError(
1406
+ f"Invalid account passed in.\n{str(err.msg)}"
1407
+ ) from err
1408
+ if err.errno == CANNOT_MODIFY_RELEASE_CHANNEL_ACCOUNTS:
1409
+ raise UserInputError(
1410
+ f"Cannot modify accounts for release channel {release_channel} in application package {package_name}."
1411
+ ) from err
1412
+ handle_unclassified_error(
1413
+ err,
1414
+ f"Failed to add accounts to release channel {release_channel} in application package {package_name}.",
1415
+ )
1416
+
1417
+ def remove_accounts_from_release_channel(
1418
+ self,
1419
+ package_name: str,
1420
+ release_channel: str,
1421
+ target_accounts: List[str],
1422
+ role: str | None = None,
1423
+ ):
1424
+ """
1425
+ Removes accounts from a release channel.
1426
+
1427
+ @param package_name: Name of the application package
1428
+ @param release_channel: Name of the release channel
1429
+ @param target_accounts: List of target accounts to remove from the release channel
1430
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1431
+ """
1432
+
1433
+ package_name = to_identifier(package_name)
1434
+ release_channel = to_identifier(release_channel)
1435
+
1436
+ with self._use_role_optional(role):
1437
+ try:
1438
+ self._sql_executor.execute_query(
1439
+ f"alter application package {package_name} modify release channel {release_channel} remove accounts = ({','.join(target_accounts)})"
1440
+ )
1441
+ except Exception as err:
1442
+ if isinstance(err, ProgrammingError):
1443
+ if (
1444
+ err.errno == ACCOUNT_DOES_NOT_EXIST
1445
+ or err.errno == ACCOUNT_HAS_TOO_MANY_QUALIFIERS
1446
+ ):
1447
+ raise UserInputError(
1448
+ f"Invalid account passed in.\n{str(err.msg)}"
1449
+ ) from err
1450
+ if err.errno == CANNOT_MODIFY_RELEASE_CHANNEL_ACCOUNTS:
1451
+ raise UserInputError(
1452
+ f"Cannot modify accounts for release channel {release_channel} in application package {package_name}."
1453
+ ) from err
1454
+ handle_unclassified_error(
1455
+ err,
1456
+ f"Failed to remove accounts from release channel {release_channel} in application package {package_name}.",
1457
+ )
1458
+
1459
+ def set_accounts_for_release_channel(
1460
+ self,
1461
+ package_name: str,
1462
+ release_channel: str,
1463
+ target_accounts: List[str],
1464
+ role: str | None = None,
1465
+ ):
1466
+ """
1467
+ Sets accounts for a release channel.
1468
+
1469
+ @param package_name: Name of the application package
1470
+ @param release_channel: Name of the release channel
1471
+ @param target_accounts: List of target accounts to set for the release channel
1472
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1473
+ """
1474
+
1475
+ package_name = to_identifier(package_name)
1476
+ release_channel = to_identifier(release_channel)
1477
+
1478
+ with self._use_role_optional(role):
1479
+ try:
1480
+ self._sql_executor.execute_query(
1481
+ f"alter application package {package_name} modify release channel {release_channel} set accounts = ({','.join(target_accounts)})"
1482
+ )
1483
+ except Exception as err:
1484
+ if isinstance(err, ProgrammingError):
1485
+ if (
1486
+ err.errno == ACCOUNT_DOES_NOT_EXIST
1487
+ or err.errno == ACCOUNT_HAS_TOO_MANY_QUALIFIERS
1488
+ ):
1489
+ raise UserInputError(
1490
+ f"Invalid account passed in.\n{str(err.msg)}"
1491
+ ) from err
1492
+ if err.errno == CANNOT_MODIFY_RELEASE_CHANNEL_ACCOUNTS:
1493
+ raise UserInputError(
1494
+ f"Cannot modify accounts for release channel {release_channel} in application package {package_name}."
1495
+ ) from err
1496
+ handle_unclassified_error(
1497
+ err,
1498
+ f"Failed to set accounts for release channel {release_channel} in application package {package_name}.",
1499
+ )
1500
+
1501
+ def add_version_to_release_channel(
1502
+ self,
1503
+ package_name: str,
1504
+ release_channel: str,
1505
+ version: str,
1506
+ role: str | None = None,
1507
+ ):
1508
+ """
1509
+ Adds a version to a release channel.
1510
+
1511
+ @param package_name: Name of the application package
1512
+ @param release_channel: Name of the release channel
1513
+ @param version: Version to add to the release channel
1514
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1515
+ """
1516
+
1517
+ package_name = to_identifier(package_name)
1518
+ release_channel = to_identifier(release_channel)
1519
+ version = to_identifier(version)
1520
+
1521
+ with self._use_role_optional(role):
1522
+ try:
1523
+ self._sql_executor.execute_query(
1524
+ f"alter application package {package_name} modify release channel {release_channel} add version {version}"
1525
+ )
1526
+ except Exception as err:
1527
+ if isinstance(err, ProgrammingError):
1528
+ if err.errno == VERSION_DOES_NOT_EXIST:
1529
+ raise UserInputError(
1530
+ f"Version {version} does not exist in application package {package_name}."
1531
+ ) from err
1532
+ if err.errno == VERSION_ALREADY_ADDED_TO_RELEASE_CHANNEL:
1533
+ raise UserInputError(
1534
+ f"Version {version} is already added to release channel {release_channel}."
1535
+ ) from err
1536
+ if err.errno == MAX_VERSIONS_IN_RELEASE_CHANNEL_REACHED:
1537
+ raise UserInputError(
1538
+ f"Maximum number of versions allowed in release channel {release_channel} has been reached."
1539
+ ) from err
1540
+ handle_unclassified_error(
1541
+ err,
1542
+ f"Failed to add version {version} to release channel {release_channel} in application package {package_name}.",
1543
+ )
1544
+
1545
+ def remove_version_from_release_channel(
1546
+ self,
1547
+ package_name: str,
1548
+ release_channel: str,
1549
+ version: str,
1550
+ role: str | None = None,
1551
+ ):
1552
+ """
1553
+ Removes a version from a release channel.
1554
+
1555
+ @param package_name: Name of the application package
1556
+ @param release_channel: Name of the release channel
1557
+ @param version: Version to remove from the release channel
1558
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1559
+ """
1560
+
1561
+ package_name = to_identifier(package_name)
1562
+ release_channel = to_identifier(release_channel)
1563
+ version = to_identifier(version)
1564
+
1565
+ with self._use_role_optional(role):
1566
+ try:
1567
+ self._sql_executor.execute_query(
1568
+ f"alter application package {package_name} modify release channel {release_channel} drop version {version}"
1569
+ )
1570
+ except Exception as err:
1571
+ if isinstance(err, ProgrammingError):
1572
+ if err.errno == VERSION_NOT_IN_RELEASE_CHANNEL:
1573
+ raise UserInputError(
1574
+ f"Version {version} is not found in release channel {release_channel}."
1575
+ ) from err
1576
+ if err.errno == VERSION_REFERENCED_BY_RELEASE_DIRECTIVE:
1577
+ raise UserInputError(
1578
+ f"Cannot remove version {version} from release channel {release_channel} as it is referenced by a release directive."
1579
+ ) from err
1580
+ handle_unclassified_error(
1581
+ err,
1582
+ f"Failed to remove version {version} from release channel {release_channel} in application package {package_name}.",
1583
+ )
1584
+
1585
+ def show_versions(
1586
+ self,
1587
+ package_name: str,
1588
+ role: str | None = None,
1589
+ ) -> list[Version]:
1590
+ """
1591
+ Show all versions in an application package.
1592
+
1593
+ @param package_name: Name of the application package
1594
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1595
+ """
1596
+ package_name = to_identifier(package_name)
1597
+
1598
+ with self._use_role_optional(role):
1599
+ try:
1600
+ cursor = self._sql_executor.execute_query(
1601
+ f"show versions in application package {package_name}",
1602
+ cursor_class=DictCursor,
1603
+ )
1604
+ except Exception as err:
1605
+ if isinstance(err, ProgrammingError):
1606
+ if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED:
1607
+ raise UserInputError(
1608
+ f"Application package {package_name} does not exist or you are not authorized to access it."
1609
+ ) from err
1610
+ handle_unclassified_error(
1611
+ err,
1612
+ f"Failed to show versions for application package {package_name}.",
1613
+ )
1614
+
1615
+ return cursor.fetchall()
1616
+
1617
+
1618
+ def _strip_empty_lines(text: str) -> str:
1619
+ """
1620
+ Strips empty lines from the input string.
1621
+ Preserves the new line at the end of the string if it exists.
518
1622
  """
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
1623
+ all_lines = text.splitlines()
1624
+
1625
+ # join all non-empty lines, but preserve the new line at the end if it exists
1626
+ last_line = all_lines[-1]
1627
+ other_lines = [line for line in all_lines[:-1] if line.strip()]
522
1628
 
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)
1629
+ return "\n".join(other_lines) + "\n" + last_line