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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/constants.py +4 -0
- snowflake/cli/_app/snow_connector.py +12 -0
- snowflake/cli/_app/telemetry.py +10 -3
- snowflake/cli/_plugins/connection/util.py +12 -19
- snowflake/cli/_plugins/helpers/commands.py +207 -1
- snowflake/cli/_plugins/nativeapp/artifacts.py +10 -4
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +41 -17
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +7 -0
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +4 -1
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +42 -32
- snowflake/cli/_plugins/nativeapp/commands.py +92 -2
- snowflake/cli/_plugins/nativeapp/constants.py +5 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +221 -288
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +772 -89
- snowflake/cli/_plugins/nativeapp/entities/application_package_child_interface.py +43 -0
- snowflake/cli/_plugins/nativeapp/feature_flags.py +5 -1
- snowflake/cli/_plugins/nativeapp/release_channel/__init__.py +13 -0
- snowflake/cli/_plugins/nativeapp/release_channel/commands.py +212 -0
- snowflake/cli/_plugins/nativeapp/release_directive/__init__.py +13 -0
- snowflake/cli/_plugins/nativeapp/release_directive/commands.py +165 -0
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +9 -17
- snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +80 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +999 -75
- snowflake/cli/_plugins/nativeapp/utils.py +11 -0
- snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +5 -1
- snowflake/cli/_plugins/nativeapp/version/commands.py +31 -4
- snowflake/cli/_plugins/notebook/manager.py +4 -2
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +234 -4
- snowflake/cli/_plugins/spcs/common.py +129 -0
- snowflake/cli/_plugins/spcs/services/commands.py +134 -14
- snowflake/cli/_plugins/spcs/services/manager.py +169 -1
- snowflake/cli/_plugins/stage/manager.py +12 -4
- snowflake/cli/_plugins/streamlit/manager.py +8 -1
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +153 -2
- snowflake/cli/_plugins/workspace/commands.py +3 -2
- snowflake/cli/_plugins/workspace/manager.py +8 -4
- snowflake/cli/api/cli_global_context.py +22 -1
- snowflake/cli/api/config.py +6 -2
- snowflake/cli/api/connections.py +12 -1
- snowflake/cli/api/constants.py +9 -1
- snowflake/cli/api/entities/common.py +85 -0
- snowflake/cli/api/entities/utils.py +9 -8
- snowflake/cli/api/errno.py +60 -3
- snowflake/cli/api/feature_flags.py +20 -4
- snowflake/cli/api/metrics.py +21 -27
- snowflake/cli/api/project/definition_conversion.py +1 -2
- snowflake/cli/api/project/schemas/project_definition.py +27 -6
- snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +1 -1
- snowflake/cli/api/project/util.py +45 -0
- snowflake/cli/api/rest_api.py +3 -2
- {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/METADATA +13 -13
- {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/RECORD +56 -51
- {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/WHEEL +1 -1
- {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
214
|
-
f"
|
|
312
|
+
with_label_clause = (
|
|
313
|
+
f"label={to_string_literal(label)}" if label is not None else ""
|
|
215
314
|
)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
374
|
-
if err
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
418
|
-
if err
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
|
470
|
-
if err
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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,
|
|
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
|
-
|
|
667
|
+
query,
|
|
492
668
|
cursor_class=DictCursor,
|
|
493
669
|
)
|
|
494
|
-
except
|
|
495
|
-
if err
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|