snowflake-cli 3.2.1__py3-none-any.whl → 3.3.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 (56) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/constants.py +4 -0
  3. snowflake/cli/_app/snow_connector.py +12 -0
  4. snowflake/cli/_app/telemetry.py +10 -3
  5. snowflake/cli/_plugins/connection/util.py +12 -19
  6. snowflake/cli/_plugins/helpers/commands.py +207 -1
  7. snowflake/cli/_plugins/nativeapp/artifacts.py +10 -4
  8. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +41 -17
  9. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +7 -0
  10. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +4 -1
  11. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +42 -32
  12. snowflake/cli/_plugins/nativeapp/commands.py +92 -2
  13. snowflake/cli/_plugins/nativeapp/constants.py +5 -0
  14. snowflake/cli/_plugins/nativeapp/entities/application.py +221 -288
  15. snowflake/cli/_plugins/nativeapp/entities/application_package.py +772 -89
  16. snowflake/cli/_plugins/nativeapp/entities/application_package_child_interface.py +43 -0
  17. snowflake/cli/_plugins/nativeapp/feature_flags.py +5 -1
  18. snowflake/cli/_plugins/nativeapp/release_channel/__init__.py +13 -0
  19. snowflake/cli/_plugins/nativeapp/release_channel/commands.py +212 -0
  20. snowflake/cli/_plugins/nativeapp/release_directive/__init__.py +13 -0
  21. snowflake/cli/_plugins/nativeapp/release_directive/commands.py +165 -0
  22. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +9 -17
  23. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +80 -0
  24. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +999 -75
  25. snowflake/cli/_plugins/nativeapp/utils.py +11 -0
  26. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +5 -1
  27. snowflake/cli/_plugins/nativeapp/version/commands.py +31 -4
  28. snowflake/cli/_plugins/notebook/manager.py +4 -2
  29. snowflake/cli/_plugins/snowpark/snowpark_entity.py +234 -4
  30. snowflake/cli/_plugins/spcs/common.py +129 -0
  31. snowflake/cli/_plugins/spcs/services/commands.py +134 -14
  32. snowflake/cli/_plugins/spcs/services/manager.py +169 -1
  33. snowflake/cli/_plugins/stage/manager.py +12 -4
  34. snowflake/cli/_plugins/streamlit/manager.py +8 -1
  35. snowflake/cli/_plugins/streamlit/streamlit_entity.py +153 -2
  36. snowflake/cli/_plugins/workspace/commands.py +3 -2
  37. snowflake/cli/_plugins/workspace/manager.py +8 -4
  38. snowflake/cli/api/cli_global_context.py +22 -1
  39. snowflake/cli/api/config.py +6 -2
  40. snowflake/cli/api/connections.py +12 -1
  41. snowflake/cli/api/constants.py +9 -1
  42. snowflake/cli/api/entities/common.py +85 -0
  43. snowflake/cli/api/entities/utils.py +9 -8
  44. snowflake/cli/api/errno.py +60 -3
  45. snowflake/cli/api/feature_flags.py +20 -4
  46. snowflake/cli/api/metrics.py +21 -27
  47. snowflake/cli/api/project/definition_conversion.py +1 -2
  48. snowflake/cli/api/project/schemas/project_definition.py +27 -6
  49. snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +1 -1
  50. snowflake/cli/api/project/util.py +45 -0
  51. snowflake/cli/api/rest_api.py +3 -2
  52. {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/METADATA +13 -13
  53. {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/RECORD +56 -51
  54. {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/WHEEL +1 -1
  55. {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/entry_points.txt +0 -0
  56. {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -13,38 +13,106 @@
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.api.cli_global_context import get_cli_context
49
+ from snowflake.cli.api.constants import ObjectType
29
50
  from snowflake.cli.api.errno import (
51
+ ACCOUNT_DOES_NOT_EXIST,
52
+ ACCOUNT_HAS_TOO_MANY_QUALIFIERS,
53
+ APPLICATION_PACKAGE_MAX_VERSIONS_HIT,
54
+ APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS,
55
+ APPLICATION_REQUIRES_TELEMETRY_SHARING,
56
+ CANNOT_DEREGISTER_VERSION_ASSOCIATED_WITH_CHANNEL,
57
+ CANNOT_DISABLE_MANDATORY_TELEMETRY,
58
+ CANNOT_DISABLE_RELEASE_CHANNELS,
59
+ CANNOT_MODIFY_RELEASE_CHANNEL_ACCOUNTS,
30
60
  DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED,
61
+ DOES_NOT_EXIST_OR_NOT_AUTHORIZED,
31
62
  INSUFFICIENT_PRIVILEGES,
63
+ MAX_UNBOUND_VERSIONS_REACHED,
64
+ MAX_VERSIONS_IN_RELEASE_CHANNEL_REACHED,
32
65
  NO_WAREHOUSE_SELECTED_IN_SESSION,
66
+ RELEASE_DIRECTIVE_DOES_NOT_EXIST,
67
+ RELEASE_DIRECTIVES_VERSION_PATCH_NOT_FOUND,
68
+ SQL_COMPILATION_ERROR,
69
+ TARGET_ACCOUNT_USED_BY_OTHER_RELEASE_DIRECTIVE,
70
+ VERSION_ALREADY_ADDED_TO_RELEASE_CHANNEL,
71
+ VERSION_DOES_NOT_EXIST,
72
+ VERSION_NOT_ADDED_TO_RELEASE_CHANNEL,
73
+ VERSION_NOT_IN_RELEASE_CHANNEL,
74
+ VERSION_REFERENCED_BY_RELEASE_DIRECTIVE,
33
75
  )
34
76
  from snowflake.cli.api.identifiers import FQN
77
+ from snowflake.cli.api.metrics import CLICounterField
78
+ from snowflake.cli.api.project.schemas.v1.native_app.package import DistributionOptions
35
79
  from snowflake.cli.api.project.util import (
36
80
  identifier_to_show_like_pattern,
37
- is_valid_unquoted_identifier,
81
+ same_identifiers,
38
82
  to_identifier,
39
- to_quoted_identifier,
40
83
  to_string_literal,
84
+ unquote_identifier,
41
85
  )
42
- from snowflake.cli.api.sql_execution import BaseSqlExecutor, SqlExecutor
86
+ from snowflake.cli.api.sql_execution import BaseSqlExecutor
87
+ from snowflake.cli.api.utils.cursor import find_first_row
43
88
  from snowflake.connector import DictCursor, ProgrammingError
44
89
 
90
+ ReleaseChannel = TypedDict(
91
+ "ReleaseChannel",
92
+ {
93
+ "name": str,
94
+ "description": str,
95
+ "created_on": datetime,
96
+ "updated_on": datetime,
97
+ "targets": dict[str, Any],
98
+ "versions": list[str],
99
+ },
100
+ )
101
+
102
+ Version = TypedDict(
103
+ "Version",
104
+ {
105
+ "version": str,
106
+ "patch": int,
107
+ "label": str | None,
108
+ "created_on": datetime,
109
+ "review_status": str,
110
+ },
111
+ )
112
+
45
113
 
46
114
  class SnowflakeSQLFacade:
47
- def __init__(self, sql_executor: SqlExecutor | None = None):
115
+ def __init__(self, sql_executor: BaseSqlExecutor | None = None):
48
116
  self._sql_executor = (
49
117
  sql_executor if sql_executor is not None else BaseSqlExecutor()
50
118
  )
@@ -58,12 +126,10 @@ class SnowflakeSQLFacade:
58
126
  """
59
127
  try:
60
128
  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
129
  except Exception as err:
130
+ if isinstance(err, ProgrammingError):
131
+ if err.errno == DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED:
132
+ raise CouldNotUseObjectError(object_type, name) from err
67
133
  handle_unclassified_error(err, f"Failed to use {object_type} {name}.")
68
134
 
69
135
  @contextmanager
@@ -91,7 +157,7 @@ class SnowflakeSQLFacade:
91
157
  except IndexError:
92
158
  prev_obj = None
93
159
 
94
- if prev_obj is not None and _same_identifier(prev_obj, name):
160
+ if prev_obj is not None and same_identifiers(prev_obj, name):
95
161
  yield
96
162
  return
97
163
 
@@ -136,6 +202,37 @@ class SnowflakeSQLFacade:
136
202
  """
137
203
  return self._use_object_optional(UseObjectType.SCHEMA, schema_name)
138
204
 
205
+ def grant_privileges_to_role(
206
+ self,
207
+ privileges: list[str],
208
+ object_type: ObjectType,
209
+ object_identifier: str,
210
+ role_to_grant: str,
211
+ role_to_use: str | None = None,
212
+ ) -> None:
213
+ """
214
+ Grants one or more access privileges on a securable object to a role
215
+
216
+ @param privileges: List of privileges to grant to a role
217
+ @param object_type: Type of snowflake object to grant to a role
218
+ @param object_identifier: Valid identifier of the snowflake object to grant to a role
219
+ @param role_to_grant: Name of the role to grant privileges to
220
+ @param [Optional] role_to_use: Name of the role to use to grant privileges
221
+ """
222
+ comma_separated_privileges = ", ".join(privileges)
223
+ object_type_and_name = f"{object_type.value.sf_name} {object_identifier}"
224
+
225
+ with self._use_role_optional(role_to_use):
226
+ try:
227
+ self._sql_executor.execute_query(
228
+ f"grant {comma_separated_privileges} on {object_type_and_name} to role {role_to_grant}"
229
+ )
230
+ except Exception as err:
231
+ handle_unclassified_error(
232
+ err,
233
+ f"Failed to grant {comma_separated_privileges} on {object_type_and_name} to role {role_to_grant}.",
234
+ )
235
+
139
236
  def execute_user_script(
140
237
  self,
141
238
  queries: str,
@@ -206,27 +303,87 @@ class SnowflakeSQLFacade:
206
303
  @param [Optional] label: Label for this version, visible to consumers.
207
304
  """
208
305
 
209
- # Make the version a valid identifier, adding quotes if necessary
210
306
  version = to_identifier(version)
307
+ package_name = to_identifier(package_name)
308
+
309
+ available_release_channels = self.show_release_channels(package_name, role)
211
310
 
212
311
  # Label must be a string literal
213
- with_label_cause = (
214
- f"\nlabel={to_string_literal(label)}" if label is not None else ""
312
+ with_label_clause = (
313
+ f"label={to_string_literal(label)}" if label is not None else ""
215
314
  )
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
- """
315
+
316
+ action = "register" if available_release_channels else "add"
317
+
318
+ query = dedent(
319
+ _strip_empty_lines(
320
+ f"""\
321
+ alter application package {package_name}
322
+ {action} version {version}
323
+ using @{stage_fqn}
324
+ {with_label_clause}
325
+ """
326
+ )
222
327
  )
328
+
223
329
  with self._use_role_optional(role):
224
330
  try:
225
- self._sql_executor.execute_query(add_version_query)
331
+ self._sql_executor.execute_query(query)
226
332
  except Exception as err:
333
+ if isinstance(err, ProgrammingError):
334
+ if err.errno == MAX_UNBOUND_VERSIONS_REACHED:
335
+ raise UserInputError(
336
+ f"Maximum unbound versions reached for application package {package_name}. "
337
+ "Please drop other unbound versions first, or add them to a release channel. "
338
+ "Use `snow app version list` to view all versions.",
339
+ ) from err
340
+ if err.errno == APPLICATION_PACKAGE_MAX_VERSIONS_HIT:
341
+ raise UserInputError(
342
+ f"Maximum versions reached for application package {package_name}. "
343
+ "Please drop the other versions first."
344
+ ) from err
345
+ handle_unclassified_error(
346
+ err,
347
+ f"Failed to {action} version {version} to application package {package_name}.",
348
+ )
349
+
350
+ def drop_version_from_package(
351
+ self, package_name: str, version: str, role: str | None = None
352
+ ):
353
+ """
354
+ Drops a version from an existing application package.
355
+ @param package_name: Name of the application package to alter.
356
+ @param version: Version name to drop.
357
+ @param [Optional] role: Switch to this role while executing drop version.
358
+ """
359
+
360
+ version = to_identifier(version)
361
+ package_name = to_identifier(package_name)
362
+
363
+ release_channels = self.show_release_channels(package_name, role)
364
+ action = "deregister" if release_channels else "drop"
365
+
366
+ query = f"alter application package {package_name} {action} version {version}"
367
+ with self._use_role_optional(role):
368
+ try:
369
+ self._sql_executor.execute_query(query)
370
+ except Exception as err:
371
+ if isinstance(err, ProgrammingError):
372
+ if err.errno == VERSION_REFERENCED_BY_RELEASE_DIRECTIVE:
373
+ raise UserInputError(
374
+ f"Cannot drop version {version} from application package {package_name} because it is in use by one or more release directives."
375
+ ) from err
376
+ if err.errno == CANNOT_DEREGISTER_VERSION_ASSOCIATED_WITH_CHANNEL:
377
+ raise UserInputError(
378
+ f"Cannot drop version {version} from application package {package_name} because it is associated with a release channel."
379
+ ) from err
380
+ if err.errno == VERSION_DOES_NOT_EXIST:
381
+ raise UserInputError(
382
+ f"Version {version} does not exist in application package {package_name}."
383
+ ) from err
227
384
  handle_unclassified_error(
228
385
  err,
229
- f"Failed to add version {version} to application package {package_name}.",
386
+ f"Failed to {action} version {version} from application package {package_name}.",
230
387
  )
231
388
 
232
389
  def add_patch_to_package_version(
@@ -257,11 +414,14 @@ class SnowflakeSQLFacade:
257
414
  with_label_clause = (
258
415
  f"\nlabel={to_string_literal(label)}" if label is not None else ""
259
416
  )
260
- patch_query = f"{patch}" if patch else ""
417
+
418
+ patch_query = f" {patch}" if patch is not None else ""
419
+
420
+ # No space between patch and patch{patch_query} to avoid extra space when patch is None
261
421
  add_patch_query = dedent(
262
422
  f"""\
263
423
  alter application package {package_name}
264
- add patch {patch_query} for version {version}
424
+ add patch{patch_query} for version {version}
265
425
  using @{stage_fqn}{with_label_clause}
266
426
  """
267
427
  )
@@ -271,9 +431,14 @@ class SnowflakeSQLFacade:
271
431
  add_patch_query, cursor_class=DictCursor
272
432
  ).fetchall()
273
433
  except Exception as err:
434
+ if isinstance(err, ProgrammingError):
435
+ if err.errno == APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS:
436
+ raise UserInputError(
437
+ f"Patch {patch} already exists for version {version} in application package {package_name}."
438
+ ) from err
274
439
  handle_unclassified_error(
275
440
  err,
276
- f"Failed to create patch {patch_query} for version {version} in application package {package_name}.",
441
+ f"Failed to create patch{patch_query} for version {version} in application package {package_name}.",
277
442
  )
278
443
  try:
279
444
  show_row = result_cursor[0]
@@ -370,13 +535,14 @@ class SnowflakeSQLFacade:
370
535
  self._sql_executor.execute_query(
371
536
  f"create schema if not exists {identifier}"
372
537
  )
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
538
+ except Exception as err:
539
+ if isinstance(err, ProgrammingError):
540
+ if err.errno == INSUFFICIENT_PRIVILEGES:
541
+ raise InsufficientPrivilegesError(
542
+ f"Insufficient privileges to create schema {name}",
543
+ role=role,
544
+ database=database,
545
+ ) from err
380
546
  handle_unclassified_error(err, f"Failed to create schema {name}.")
381
547
 
382
548
  def stage_exists(
@@ -414,16 +580,17 @@ class SnowflakeSQLFacade:
414
580
  results = self._sql_executor.execute_query(
415
581
  f"show stages like {pattern}{in_schema_clause}",
416
582
  )
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
583
+ except Exception as err:
584
+ if isinstance(err, ProgrammingError):
585
+ if err.errno == DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED:
586
+ return False
587
+ if err.errno == INSUFFICIENT_PRIVILEGES:
588
+ raise InsufficientPrivilegesError(
589
+ f"Insufficient privileges to check if stage {name} exists",
590
+ role=role,
591
+ database=database,
592
+ schema=schema,
593
+ ) from err
427
594
  handle_unclassified_error(
428
595
  err, f"Failed to check if stage {name} exists."
429
596
  )
@@ -466,18 +633,22 @@ class SnowflakeSQLFacade:
466
633
  ):
467
634
  try:
468
635
  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
636
+ except Exception as err:
637
+ if isinstance(err, ProgrammingError):
638
+ if err.errno == INSUFFICIENT_PRIVILEGES:
639
+ raise InsufficientPrivilegesError(
640
+ f"Insufficient privileges to create stage {name}",
641
+ role=role,
642
+ database=database,
643
+ schema=schema,
644
+ ) from err
477
645
  handle_unclassified_error(err, f"Failed to create stage {name}.")
478
646
 
479
647
  def show_release_directives(
480
- self, package_name: str, role: str | None = None
648
+ self,
649
+ package_name: str,
650
+ release_channel: str | None = None,
651
+ role: str | None = None,
481
652
  ) -> list[dict[str, Any]]:
482
653
  """
483
654
  Show release directives for a package
@@ -485,41 +656,794 @@ class SnowflakeSQLFacade:
485
656
  @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
486
657
  """
487
658
  package_identifier = to_identifier(package_name)
659
+
660
+ query = f"show release directives in application package {package_identifier}"
661
+ if release_channel:
662
+ query += f" for release channel {to_identifier(release_channel)}"
663
+
488
664
  with self._use_role_optional(role):
489
665
  try:
490
666
  cursor = self._sql_executor.execute_query(
491
- f"show release directives in application package {package_identifier}",
667
+ query,
492
668
  cursor_class=DictCursor,
493
669
  )
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
670
+ except Exception as err:
671
+ if isinstance(err, ProgrammingError):
672
+ if err.errno == INSUFFICIENT_PRIVILEGES:
673
+ raise InsufficientPrivilegesError(
674
+ f"Insufficient privileges to show release directives for application package {package_name}",
675
+ role=role,
676
+ ) from err
677
+ if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED:
678
+ raise UserInputError(
679
+ f"Application package {package_name} does not exist or you are not authorized to access it."
680
+ ) from err
500
681
  handle_unclassified_error(
501
682
  err,
502
- f"Failed to show release directives for package {package_name}.",
683
+ f"Failed to show release directives for application package {package_name}.",
503
684
  )
504
685
  return cursor.fetchall()
505
686
 
687
+ def get_existing_app_info(self, name: str, role: str) -> dict | None:
688
+ """
689
+ Check for an existing application object by the same name as in project definition, in account.
690
+ It executes a 'show applications like' query and returns the result as single row, if one exists.
691
+ """
692
+ with self._use_role_optional(role):
693
+ try:
694
+ object_type_plural = ObjectType.APPLICATION.value.sf_plural_name
695
+ show_obj_query = f"show {object_type_plural} like {identifier_to_show_like_pattern(name)}".strip()
506
696
 
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.
697
+ show_obj_cursor = self._sql_executor.execute_query(
698
+ show_obj_query, cursor_class=DictCursor
699
+ )
512
700
 
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
701
+ show_obj_row = find_first_row(
702
+ # row[NAME_COL] is not an identifier. It is the unquoted internal representation
703
+ show_obj_cursor,
704
+ lambda row: row[NAME_COL] == unquote_identifier(name),
705
+ )
706
+ except Exception as err:
707
+ handle_unclassified_error(
708
+ err, f"Unable to fetch information on application {name}."
709
+ )
710
+ return show_obj_row
711
+
712
+ def upgrade_application(
713
+ self,
714
+ name: str,
715
+ install_method: SameAccountInstallMethod,
716
+ stage_fqn: str,
717
+ role: str,
718
+ warehouse: str,
719
+ debug_mode: bool | None,
720
+ should_authorize_event_sharing: bool | None,
721
+ release_channel: str | None = None,
722
+ ) -> list[tuple[str]]:
723
+ """
724
+ Upgrades an application object using the provided clauses
725
+
726
+ @param name: Name of the application object
727
+ @param install_method: Method of installing the application
728
+ @param stage_fqn: FQN of the stage housing the application artifacts
729
+ @param role: Role to use when creating the application and provider-side objects
730
+ @param warehouse: Warehouse which is required to create an application object
731
+ @param debug_mode: Whether to enable debug mode; None means not explicitly enabled or disabled
732
+ @param should_authorize_event_sharing: Whether to enable event sharing; None means not explicitly enabled or disabled
733
+ @param release_channel [Optional]: Release channel to use when upgrading the application
734
+ """
735
+
736
+ name = to_identifier(name)
737
+ release_channel = to_identifier(release_channel or DEFAULT_CHANNEL)
738
+
739
+ install_method.ensure_app_usable(
740
+ app_name=name,
741
+ app_role=role,
742
+ show_app_row=self.get_existing_app_info(name, role),
743
+ )
744
+
745
+ # If all the above checks are in order, proceed to upgrade
746
+
747
+ @cache # only cache within the scope of this method
748
+ def get_app_properties():
749
+ return self.get_app_properties(name, role)
750
+
751
+ with self._use_role_optional(role), self._use_warehouse_optional(warehouse):
752
+ try:
753
+ using_clause = install_method.using_clause(stage_fqn)
754
+
755
+ current_release_channel = (
756
+ get_app_properties().get(CHANNEL_COL) or DEFAULT_CHANNEL
757
+ )
758
+ if unquote_identifier(release_channel) != current_release_channel:
759
+ raise UpgradeApplicationRestrictionError(
760
+ f"Application {name} is currently on release channel {current_release_channel}. Cannot upgrade to release channel {release_channel}."
761
+ )
762
+
763
+ upgrade_cursor = self._sql_executor.execute_query(
764
+ f"alter application {name} upgrade {using_clause}",
765
+ )
766
+
767
+ # if debug_mode is present (controlled), ensure it is up-to-date
768
+ if install_method.is_dev_mode:
769
+ if debug_mode is not None:
770
+ self._sql_executor.execute_query(
771
+ f"alter application {name} set debug_mode = {debug_mode}"
772
+ )
773
+
774
+ except UpgradeApplicationRestrictionError as err:
775
+ raise err
776
+ except Exception as err:
777
+ if isinstance(err, ProgrammingError):
778
+ if err.errno in UPGRADE_RESTRICTION_CODES:
779
+ raise UpgradeApplicationRestrictionError(err.msg) from err
780
+ if (
781
+ err.errno
782
+ in CREATE_OR_UPGRADE_APPLICATION_EXPECTED_USER_ERROR_CODES
783
+ ):
784
+ raise UserInputError(
785
+ f"Failed to upgrade application {name} with the following error message:\n"
786
+ f"{err.msg}"
787
+ ) from err
788
+ handle_unclassified_error(err, f"Failed to upgrade application {name}.")
789
+
790
+ try:
791
+ # Only update event sharing if the current value is different as the one we want to set
792
+ if should_authorize_event_sharing is not None:
793
+ current_authorize_event_sharing = (
794
+ get_app_properties()
795
+ .get(AUTHORIZE_TELEMETRY_COL, "false")
796
+ .lower()
797
+ == "true"
798
+ )
799
+ if (
800
+ current_authorize_event_sharing
801
+ != should_authorize_event_sharing
802
+ ):
803
+ self._log.info(
804
+ "Setting telemetry sharing authorization to %s",
805
+ should_authorize_event_sharing,
806
+ )
807
+ self._sql_executor.execute_query(
808
+ f"alter application {name} set AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(should_authorize_event_sharing).upper()}"
809
+ )
810
+ except Exception as err:
811
+ if isinstance(err, ProgrammingError):
812
+ if err.errno == CANNOT_DISABLE_MANDATORY_TELEMETRY:
813
+ get_cli_context().metrics.set_counter(
814
+ CLICounterField.EVENT_SHARING_ERROR, 1
815
+ )
816
+ raise UserInputError(
817
+ "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."
818
+ ) from err
819
+ handle_unclassified_error(
820
+ err,
821
+ f"Failed to set AUTHORIZE_TELEMETRY_EVENT_SHARING when upgrading application {name}.",
822
+ )
823
+
824
+ return upgrade_cursor.fetchall()
825
+
826
+ def create_application(
827
+ self,
828
+ name: str,
829
+ package_name: str,
830
+ install_method: SameAccountInstallMethod,
831
+ stage_fqn: str,
832
+ role: str,
833
+ warehouse: str,
834
+ debug_mode: bool | None,
835
+ should_authorize_event_sharing: bool | None,
836
+ release_channel: str | None = None,
837
+ ) -> list[tuple[str]]:
838
+ """
839
+ Creates a new application object using an application package,
840
+ running the setup script of the application package
841
+
842
+ @param name: Name of the application object
843
+ @param package_name: Name of the application package to install the application from
844
+ @param install_method: Method of installing the application
845
+ @param stage_fqn: FQN of the stage housing the application artifacts
846
+ @param role: Role to use when creating the application and provider-side objects
847
+ @param warehouse: Warehouse which is required to create an application object
848
+ @param debug_mode: Whether to enable debug mode; None means not explicitly enabled or disabled
849
+ @param should_authorize_event_sharing: Whether to enable event sharing; None means not explicitly enabled or disabled
850
+ @param release_channel [Optional]: Release channel to use when creating the application
851
+ """
852
+ package_name = to_identifier(package_name)
853
+ name = to_identifier(name)
854
+ release_channel = to_identifier(release_channel) if release_channel else None
855
+
856
+ # by default, applications are created in debug mode when possible;
857
+ # this can be overridden in the project definition
858
+ debug_mode_clause = ""
859
+ if install_method.is_dev_mode:
860
+ initial_debug_mode = debug_mode if debug_mode is not None else True
861
+ debug_mode_clause = f"debug_mode = {initial_debug_mode}"
862
+
863
+ authorize_telemetry_clause = ""
864
+ if should_authorize_event_sharing is not None:
865
+ self._log.info(
866
+ "Setting AUTHORIZE_TELEMETRY_EVENT_SHARING to %s",
867
+ should_authorize_event_sharing,
868
+ )
869
+ authorize_telemetry_clause = f"AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(should_authorize_event_sharing).upper()}"
870
+
871
+ using_clause = install_method.using_clause(stage_fqn)
872
+ release_channel_clause = (
873
+ f"using release channel {release_channel}" if release_channel else ""
874
+ )
875
+
876
+ with self._use_role_optional(role), self._use_warehouse_optional(warehouse):
877
+ try:
878
+ create_cursor = self._sql_executor.execute_query(
879
+ dedent(
880
+ _strip_empty_lines(
881
+ f"""\
882
+ create application {name}
883
+ from application package {package_name}
884
+ {using_clause}
885
+ {release_channel_clause}
886
+ {debug_mode_clause}
887
+ {authorize_telemetry_clause}
888
+ comment = {SPECIAL_COMMENT}
889
+ """
890
+ )
891
+ ),
892
+ )
893
+ except Exception as err:
894
+ if isinstance(err, ProgrammingError):
895
+ if err.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING:
896
+ get_cli_context().metrics.set_counter(
897
+ CLICounterField.EVENT_SHARING_ERROR, 1
898
+ )
899
+ raise UserInputError(
900
+ "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."
901
+ ) from err
902
+ if (
903
+ err.errno
904
+ in CREATE_OR_UPGRADE_APPLICATION_EXPECTED_USER_ERROR_CODES
905
+ ):
906
+ raise UserInputError(
907
+ f"Failed to create application {name} with the following error message:\n"
908
+ f"{err.msg}"
909
+ ) from err
910
+ handle_unclassified_error(err, f"Failed to create application {name}.")
911
+
912
+ return create_cursor.fetchall()
913
+
914
+ def create_application_package(
915
+ self,
916
+ package_name: str,
917
+ distribution: DistributionOptions,
918
+ enable_release_channels: bool | None = None,
919
+ role: str | None = None,
920
+ ) -> None:
921
+ """
922
+ Creates a new application package.
923
+ @param package_name: Name of the application package to create.
924
+ @param [Optional] enable_release_channels: Enable/Disable release channels if not None.
925
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
926
+ """
927
+ package_name = to_identifier(package_name)
928
+
929
+ enable_release_channels_clause = ""
930
+ if enable_release_channels is not None:
931
+ enable_release_channels_clause = (
932
+ f"enable_release_channels = {str(enable_release_channels).lower()}"
933
+ )
934
+
935
+ with self._use_role_optional(role):
936
+ try:
937
+ self._sql_executor.execute_query(
938
+ dedent(
939
+ _strip_empty_lines(
940
+ f"""\
941
+ create application package {package_name}
942
+ comment = {SPECIAL_COMMENT}
943
+ distribution = {distribution}
944
+ {enable_release_channels_clause}
945
+ """
946
+ )
947
+ )
948
+ )
949
+ except Exception as err:
950
+ if isinstance(err, ProgrammingError):
951
+ if err.errno == INSUFFICIENT_PRIVILEGES:
952
+ raise InsufficientPrivilegesError(
953
+ f"Insufficient privileges to create application package {package_name}",
954
+ role=role,
955
+ ) from err
956
+ handle_unclassified_error(
957
+ err, f"Failed to create application package {package_name}."
958
+ )
959
+
960
+ def alter_application_package_properties(
961
+ self,
962
+ package_name: str,
963
+ enable_release_channels: bool | None = None,
964
+ role: str | None = None,
965
+ ) -> None:
966
+ """
967
+ Alters the properties of an existing application package.
968
+ @param package_name: Name of the application package to alter.
969
+ @param [Optional] enable_release_channels: Enable/Disable release channels if not None.
970
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
971
+ """
972
+
973
+ package_name = to_identifier(package_name)
974
+
975
+ if enable_release_channels is not None:
976
+ with self._use_role_optional(role):
977
+ try:
978
+ self._sql_executor.execute_query(
979
+ dedent(
980
+ f"""\
981
+ alter application package {package_name}
982
+ set enable_release_channels = {str(enable_release_channels).lower()}
983
+ """
984
+ )
985
+ )
986
+ except Exception as err:
987
+ if isinstance(err, ProgrammingError):
988
+ if err.errno == INSUFFICIENT_PRIVILEGES:
989
+ raise InsufficientPrivilegesError(
990
+ f"Insufficient privileges to update enable_release_channels for application package {package_name}",
991
+ role=role,
992
+ ) from err
993
+ if err.errno == CANNOT_DISABLE_RELEASE_CHANNELS:
994
+ raise UserInputError(
995
+ f"Cannot disable release channels for application package {package_name} after it is enabled. Try recreating the application package."
996
+ ) from err
997
+ handle_unclassified_error(
998
+ err,
999
+ f"Failed to update enable_release_channels for application package {package_name}.",
1000
+ )
1001
+
1002
+ def get_ui_parameter(self, parameter: UIParameter, default: Any) -> Any:
1003
+ """
1004
+ Returns the value of a single UI parameter.
1005
+ If the parameter is not found, the default value is returned.
1006
+
1007
+ @param parameter: UIParameter, the parameter to get the value of.
1008
+ @param default: Default value to return if the parameter is not found.
1009
+ """
1010
+ connection = self._sql_executor._conn # noqa SLF001
1011
+
1012
+ return get_ui_parameter(connection, parameter, default)
1013
+
1014
+ def set_release_directive(
1015
+ self,
1016
+ package_name: str,
1017
+ release_directive: str,
1018
+ release_channel: str | None,
1019
+ target_accounts: List[str] | None,
1020
+ version: str,
1021
+ patch: int,
1022
+ role: str | None = None,
1023
+ ):
1024
+ """
1025
+ Sets a release directive for an application package.
1026
+ Default release directive does not support target accounts.
1027
+ Non-default release directives require target accounts to be specified.
1028
+
1029
+ @param package_name: Name of the application package to alter.
1030
+ @param release_directive: Name of the release directive to set.
1031
+ @param release_channel: Name of the release channel to set the release directive for.
1032
+ @param target_accounts: List of target accounts for the release directive.
1033
+ @param version: Version to set the release directive for.
1034
+ @param patch: Patch number to set the release directive for.
1035
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1036
+ """
1037
+
1038
+ package_name = to_identifier(package_name)
1039
+ release_channel = to_identifier(release_channel) if release_channel else None
1040
+ release_directive = to_identifier(release_directive)
1041
+ version = to_identifier(version)
1042
+
1043
+ if same_identifiers(release_directive, DEFAULT_DIRECTIVE):
1044
+ if target_accounts:
1045
+ raise UserInputError(
1046
+ "Default release directive does not support target accounts."
1047
+ )
1048
+ release_directive_statement = "set default release directive"
1049
+ else:
1050
+ if target_accounts:
1051
+ release_directive_statement = (
1052
+ f"set release directive {release_directive}"
1053
+ )
1054
+ else:
1055
+ release_directive_statement = (
1056
+ f"modify release directive {release_directive}"
1057
+ )
1058
+
1059
+ release_channel_statement = (
1060
+ f"modify release channel {release_channel}" if release_channel else ""
1061
+ )
1062
+
1063
+ accounts_statement = (
1064
+ f"accounts = ({','.join(target_accounts)})" if target_accounts else ""
1065
+ )
1066
+
1067
+ full_query = dedent(
1068
+ _strip_empty_lines(
1069
+ f"""\
1070
+ alter application package {package_name}
1071
+ {release_channel_statement}
1072
+ {release_directive_statement}
1073
+ {accounts_statement}
1074
+ version = {version} patch = {patch}
1075
+ """
1076
+ )
1077
+ )
1078
+
1079
+ with self._use_role_optional(role):
1080
+ try:
1081
+ self._sql_executor.execute_query(full_query)
1082
+ except Exception as err:
1083
+ if isinstance(err, ProgrammingError):
1084
+ if (
1085
+ err.errno == ACCOUNT_DOES_NOT_EXIST
1086
+ or err.errno == ACCOUNT_HAS_TOO_MANY_QUALIFIERS
1087
+ ):
1088
+ raise UserInputError(
1089
+ f"Invalid account passed in.\n{str(err.msg)}"
1090
+ ) from err
1091
+ if err.errno == RELEASE_DIRECTIVE_DOES_NOT_EXIST:
1092
+ raise UserInputError(
1093
+ 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."
1094
+ ) from err
1095
+ if err.errno == TARGET_ACCOUNT_USED_BY_OTHER_RELEASE_DIRECTIVE:
1096
+ raise UserInputError(
1097
+ f"Some target accounts are already referenced by other release directives in application package {package_name}.\n{str(err.msg)}"
1098
+ ) from err
1099
+ _handle_release_directive_version_error(
1100
+ err,
1101
+ package_name=package_name,
1102
+ release_channel=release_channel,
1103
+ version=version,
1104
+ patch=patch,
1105
+ )
1106
+ handle_unclassified_error(
1107
+ err,
1108
+ f"Failed to set release directive {release_directive} for application package {package_name}.",
1109
+ )
1110
+
1111
+ def unset_release_directive(
1112
+ self,
1113
+ package_name: str,
1114
+ release_directive: str,
1115
+ release_channel: str | None,
1116
+ role: str | None = None,
1117
+ ):
1118
+ """
1119
+ Unsets a release directive for an application package.
1120
+ Release directive must already exist in the application package.
1121
+ Does not accept default release directive.
1122
+
1123
+ @param package_name: Name of the application package to alter.
1124
+ @param release_directive: Name of the release directive to unset.
1125
+ @param release_channel: Name of the release channel to unset the release directive for.
1126
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1127
+ """
1128
+ package_name = to_identifier(package_name)
1129
+ release_channel = to_identifier(release_channel) if release_channel else None
1130
+ release_directive = to_identifier(release_directive)
1131
+
1132
+ if same_identifiers(release_directive, DEFAULT_DIRECTIVE):
1133
+ raise UserInputError(
1134
+ "Cannot unset default release directive. Please specify a non-default release directive."
1135
+ )
1136
+
1137
+ release_channel_statement = ""
1138
+ if release_channel:
1139
+ release_channel_statement = f" modify release channel {release_channel}"
1140
+
1141
+ with self._use_role_optional(role):
1142
+ try:
1143
+ self._sql_executor.execute_query(
1144
+ f"alter application package {package_name}{release_channel_statement} unset release directive {release_directive}"
1145
+ )
1146
+ except Exception as err:
1147
+ if isinstance(err, ProgrammingError):
1148
+ if err.errno == RELEASE_DIRECTIVE_DOES_NOT_EXIST:
1149
+ raise UserInputError(
1150
+ f"Release directive {release_directive} does not exist in application package {package_name}."
1151
+ ) from err
1152
+ handle_unclassified_error(
1153
+ err,
1154
+ f"Failed to unset release directive {release_directive} for application package {package_name}.",
1155
+ )
1156
+
1157
+ def show_release_channels(
1158
+ self, package_name: str, role: str | None = None
1159
+ ) -> list[ReleaseChannel]:
1160
+ """
1161
+ Show release channels in a package.
1162
+
1163
+ @param package_name: Name of the application package
1164
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1165
+ """
1166
+
1167
+ if (
1168
+ self.get_ui_parameter(UIParameter.NA_FEATURE_RELEASE_CHANNELS, True)
1169
+ is False
1170
+ ):
1171
+ return []
1172
+
1173
+ package_identifier = to_identifier(package_name)
1174
+ results = []
1175
+ with self._use_role_optional(role):
1176
+ try:
1177
+ cursor = self._sql_executor.execute_query(
1178
+ f"show release channels in application package {package_identifier}",
1179
+ cursor_class=DictCursor,
1180
+ )
1181
+ except Exception as err:
1182
+ if isinstance(err, ProgrammingError):
1183
+ # TODO: Temporary check for syntax until UI Parameter is available in production
1184
+ if err.errno == SQL_COMPILATION_ERROR:
1185
+ # Release not out yet and param not out yet
1186
+ return []
1187
+ if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED:
1188
+ raise UserInputError(
1189
+ f"Application package {package_name} does not exist or you are not authorized to access it."
1190
+ ) from err
1191
+ handle_unclassified_error(
1192
+ err,
1193
+ f"Failed to show release channels for application package {package_name}.",
1194
+ )
1195
+
1196
+ rows = cursor.fetchall()
1197
+
1198
+ for row in rows:
1199
+ targets = json.loads(row["targets"]) if row.get("targets") else {}
1200
+ versions = json.loads(row["versions"]) if row.get("versions") else []
1201
+ results.append(
1202
+ ReleaseChannel(
1203
+ name=row["name"],
1204
+ description=row["description"],
1205
+ created_on=row["created_on"],
1206
+ updated_on=row["updated_on"],
1207
+ targets=targets,
1208
+ versions=versions,
1209
+ )
1210
+ )
1211
+
1212
+ return results
1213
+
1214
+ def add_accounts_to_release_channel(
1215
+ self,
1216
+ package_name: str,
1217
+ release_channel: str,
1218
+ target_accounts: List[str],
1219
+ role: str | None = None,
1220
+ ):
1221
+ """
1222
+ Adds accounts to a release channel.
1223
+
1224
+ @param package_name: Name of the application package
1225
+ @param release_channel: Name of the release channel
1226
+ @param target_accounts: List of target accounts to add to the release channel
1227
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1228
+ """
1229
+
1230
+ package_name = to_identifier(package_name)
1231
+ release_channel = to_identifier(release_channel)
1232
+
1233
+ with self._use_role_optional(role):
1234
+ try:
1235
+ self._sql_executor.execute_query(
1236
+ f"alter application package {package_name} modify release channel {release_channel} add accounts = ({','.join(target_accounts)})"
1237
+ )
1238
+ except Exception as err:
1239
+ if isinstance(err, ProgrammingError):
1240
+ if (
1241
+ err.errno == ACCOUNT_DOES_NOT_EXIST
1242
+ or err.errno == ACCOUNT_HAS_TOO_MANY_QUALIFIERS
1243
+ ):
1244
+ raise UserInputError(
1245
+ f"Invalid account passed in.\n{str(err.msg)}"
1246
+ ) from err
1247
+ if err.errno == CANNOT_MODIFY_RELEASE_CHANNEL_ACCOUNTS:
1248
+ raise UserInputError(
1249
+ f"Cannot modify accounts for release channel {release_channel} in application package {package_name}."
1250
+ ) from err
1251
+ handle_unclassified_error(
1252
+ err,
1253
+ f"Failed to add accounts to release channel {release_channel} in application package {package_name}.",
1254
+ )
1255
+
1256
+ def remove_accounts_from_release_channel(
1257
+ self,
1258
+ package_name: str,
1259
+ release_channel: str,
1260
+ target_accounts: List[str],
1261
+ role: str | None = None,
1262
+ ):
1263
+ """
1264
+ Removes accounts from a release channel.
1265
+
1266
+ @param package_name: Name of the application package
1267
+ @param release_channel: Name of the release channel
1268
+ @param target_accounts: List of target accounts to remove from the release channel
1269
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1270
+ """
1271
+
1272
+ package_name = to_identifier(package_name)
1273
+ release_channel = to_identifier(release_channel)
1274
+
1275
+ with self._use_role_optional(role):
1276
+ try:
1277
+ self._sql_executor.execute_query(
1278
+ f"alter application package {package_name} modify release channel {release_channel} remove accounts = ({','.join(target_accounts)})"
1279
+ )
1280
+ except Exception as err:
1281
+ if isinstance(err, ProgrammingError):
1282
+ if (
1283
+ err.errno == ACCOUNT_DOES_NOT_EXIST
1284
+ or err.errno == ACCOUNT_HAS_TOO_MANY_QUALIFIERS
1285
+ ):
1286
+ raise UserInputError(
1287
+ f"Invalid account passed in.\n{str(err.msg)}"
1288
+ ) from err
1289
+ if err.errno == CANNOT_MODIFY_RELEASE_CHANNEL_ACCOUNTS:
1290
+ raise UserInputError(
1291
+ f"Cannot modify accounts for release channel {release_channel} in application package {package_name}."
1292
+ ) from err
1293
+ handle_unclassified_error(
1294
+ err,
1295
+ f"Failed to remove accounts from release channel {release_channel} in application package {package_name}.",
1296
+ )
1297
+
1298
+ def add_version_to_release_channel(
1299
+ self,
1300
+ package_name: str,
1301
+ release_channel: str,
1302
+ version: str,
1303
+ role: str | None = None,
1304
+ ):
1305
+ """
1306
+ Adds a version to a release channel.
1307
+
1308
+ @param package_name: Name of the application package
1309
+ @param release_channel: Name of the release channel
1310
+ @param version: Version to add to the release channel
1311
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1312
+ """
1313
+
1314
+ package_name = to_identifier(package_name)
1315
+ release_channel = to_identifier(release_channel)
1316
+ version = to_identifier(version)
1317
+
1318
+ with self._use_role_optional(role):
1319
+ try:
1320
+ self._sql_executor.execute_query(
1321
+ f"alter application package {package_name} modify release channel {release_channel} add version {version}"
1322
+ )
1323
+ except Exception as err:
1324
+ if isinstance(err, ProgrammingError):
1325
+ if err.errno == VERSION_DOES_NOT_EXIST:
1326
+ raise UserInputError(
1327
+ f"Version {version} does not exist in application package {package_name}."
1328
+ ) from err
1329
+ if err.errno == VERSION_ALREADY_ADDED_TO_RELEASE_CHANNEL:
1330
+ raise UserInputError(
1331
+ f"Version {version} is already added to release channel {release_channel}."
1332
+ ) from err
1333
+ if err.errno == MAX_VERSIONS_IN_RELEASE_CHANNEL_REACHED:
1334
+ raise UserInputError(
1335
+ f"Maximum number of versions allowed in release channel {release_channel} has been reached."
1336
+ ) from err
1337
+ handle_unclassified_error(
1338
+ err,
1339
+ f"Failed to add version {version} to release channel {release_channel} in application package {package_name}.",
1340
+ )
1341
+
1342
+ def remove_version_from_release_channel(
1343
+ self,
1344
+ package_name: str,
1345
+ release_channel: str,
1346
+ version: str,
1347
+ role: str | None = None,
1348
+ ):
1349
+ """
1350
+ Removes a version from a release channel.
1351
+
1352
+ @param package_name: Name of the application package
1353
+ @param release_channel: Name of the release channel
1354
+ @param version: Version to remove from the release channel
1355
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1356
+ """
1357
+
1358
+ package_name = to_identifier(package_name)
1359
+ release_channel = to_identifier(release_channel)
1360
+ version = to_identifier(version)
1361
+
1362
+ with self._use_role_optional(role):
1363
+ try:
1364
+ self._sql_executor.execute_query(
1365
+ f"alter application package {package_name} modify release channel {release_channel} drop version {version}"
1366
+ )
1367
+ except Exception as err:
1368
+ if isinstance(err, ProgrammingError):
1369
+ if err.errno == VERSION_NOT_IN_RELEASE_CHANNEL:
1370
+ raise UserInputError(
1371
+ f"Version {version} is not found in release channel {release_channel}."
1372
+ ) from err
1373
+ if err.errno == VERSION_REFERENCED_BY_RELEASE_DIRECTIVE:
1374
+ raise UserInputError(
1375
+ f"Cannot remove version {version} from release channel {release_channel} as it is referenced by a release directive."
1376
+ ) from err
1377
+ handle_unclassified_error(
1378
+ err,
1379
+ f"Failed to remove version {version} from release channel {release_channel} in application package {package_name}.",
1380
+ )
1381
+
1382
+ def show_versions(
1383
+ self,
1384
+ package_name: str,
1385
+ role: str | None = None,
1386
+ ) -> list[Version]:
1387
+ """
1388
+ Show all versions in an application package.
1389
+
1390
+ @param package_name: Name of the application package
1391
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1392
+ """
1393
+ package_name = to_identifier(package_name)
1394
+
1395
+ with self._use_role_optional(role):
1396
+ try:
1397
+ cursor = self._sql_executor.execute_query(
1398
+ f"show versions in application package {package_name}",
1399
+ cursor_class=DictCursor,
1400
+ )
1401
+ except Exception as err:
1402
+ if isinstance(err, ProgrammingError):
1403
+ if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED:
1404
+ raise UserInputError(
1405
+ f"Application package {package_name} does not exist or you are not authorized to access it."
1406
+ ) from err
1407
+ handle_unclassified_error(
1408
+ err,
1409
+ f"Failed to show versions for application package {package_name}.",
1410
+ )
1411
+
1412
+ return cursor.fetchall()
1413
+
1414
+
1415
+ def _strip_empty_lines(text: str) -> str:
1416
+ """
1417
+ Strips empty lines from the input string.
1418
+ Preserves the new line at the end of the string if it exists.
518
1419
  """
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
1420
+ all_lines = text.splitlines()
1421
+
1422
+ # join all non-empty lines, but preserve the new line at the end if it exists
1423
+ last_line = all_lines[-1]
1424
+ other_lines = [line for line in all_lines[:-1] if line.strip()]
1425
+
1426
+ return "\n".join(other_lines) + "\n" + last_line
1427
+
1428
+
1429
+ def _handle_release_directive_version_error(
1430
+ err: ProgrammingError,
1431
+ *,
1432
+ package_name: str,
1433
+ release_channel: str | None,
1434
+ version: str,
1435
+ patch: int,
1436
+ ) -> None:
522
1437
 
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)
1438
+ if err.errno == VERSION_NOT_ADDED_TO_RELEASE_CHANNEL:
1439
+ raise UserInputError(
1440
+ f"Version {version} is not added to release channel {release_channel}. Please add it to the release channel first."
1441
+ ) from err
1442
+ if err.errno == RELEASE_DIRECTIVES_VERSION_PATCH_NOT_FOUND:
1443
+ raise UserInputError(
1444
+ f"Patch {patch} for version {version} not found in application package {package_name}."
1445
+ ) from err
1446
+ if err.errno == VERSION_DOES_NOT_EXIST:
1447
+ raise UserInputError(
1448
+ f"Version {version} does not exist in application package {package_name}."
1449
+ ) from err