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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/cli_app.py +3 -0
  3. snowflake/cli/_app/dev/docs/templates/overview.rst.jinja2 +1 -1
  4. snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +2 -2
  5. snowflake/cli/_app/telemetry.py +69 -4
  6. snowflake/cli/_plugins/connection/commands.py +152 -99
  7. snowflake/cli/_plugins/connection/util.py +54 -9
  8. snowflake/cli/_plugins/cortex/manager.py +1 -1
  9. snowflake/cli/_plugins/git/commands.py +6 -3
  10. snowflake/cli/_plugins/git/manager.py +9 -4
  11. snowflake/cli/_plugins/nativeapp/artifacts.py +77 -13
  12. snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
  13. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +7 -0
  14. snowflake/cli/_plugins/nativeapp/codegen/sandbox.py +10 -10
  15. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
  16. snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +1 -1
  17. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +8 -8
  18. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
  19. snowflake/cli/_plugins/nativeapp/commands.py +144 -188
  20. snowflake/cli/_plugins/nativeapp/constants.py +1 -0
  21. snowflake/cli/_plugins/nativeapp/entities/application.py +564 -351
  22. snowflake/cli/_plugins/nativeapp/entities/application_package.py +583 -929
  23. snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
  24. snowflake/cli/_plugins/nativeapp/exceptions.py +12 -0
  25. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
  26. snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
  27. snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
  28. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
  29. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
  30. snowflake/cli/_plugins/nativeapp/v2_conversions/{v2_to_v1_decorator.py → compat.py} +88 -117
  31. snowflake/cli/_plugins/nativeapp/version/commands.py +36 -32
  32. snowflake/cli/_plugins/notebook/manager.py +2 -2
  33. snowflake/cli/_plugins/object/commands.py +10 -1
  34. snowflake/cli/_plugins/object/manager.py +13 -5
  35. snowflake/cli/_plugins/snowpark/common.py +63 -21
  36. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +3 -3
  37. snowflake/cli/_plugins/spcs/common.py +29 -0
  38. snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
  39. snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
  40. snowflake/cli/_plugins/spcs/image_repository/commands.py +4 -37
  41. snowflake/cli/_plugins/spcs/image_repository/manager.py +4 -1
  42. snowflake/cli/_plugins/spcs/services/commands.py +100 -17
  43. snowflake/cli/_plugins/spcs/services/manager.py +108 -16
  44. snowflake/cli/_plugins/sql/commands.py +9 -1
  45. snowflake/cli/_plugins/sql/manager.py +9 -4
  46. snowflake/cli/_plugins/stage/commands.py +28 -19
  47. snowflake/cli/_plugins/stage/diff.py +17 -17
  48. snowflake/cli/_plugins/stage/manager.py +304 -84
  49. snowflake/cli/_plugins/stage/md5.py +1 -1
  50. snowflake/cli/_plugins/streamlit/manager.py +5 -5
  51. snowflake/cli/_plugins/workspace/commands.py +27 -4
  52. snowflake/cli/_plugins/workspace/context.py +38 -0
  53. snowflake/cli/_plugins/workspace/manager.py +23 -13
  54. snowflake/cli/api/cli_global_context.py +4 -3
  55. snowflake/cli/api/commands/flags.py +23 -7
  56. snowflake/cli/api/config.py +30 -9
  57. snowflake/cli/api/connections.py +12 -1
  58. snowflake/cli/api/console/console.py +4 -19
  59. snowflake/cli/api/entities/common.py +4 -2
  60. snowflake/cli/api/entities/utils.py +36 -69
  61. snowflake/cli/api/errno.py +2 -0
  62. snowflake/cli/api/exceptions.py +41 -0
  63. snowflake/cli/api/identifiers.py +8 -0
  64. snowflake/cli/api/metrics.py +223 -7
  65. snowflake/cli/api/output/types.py +1 -1
  66. snowflake/cli/api/project/definition_conversion.py +293 -77
  67. snowflake/cli/api/project/schemas/entities/common.py +11 -0
  68. snowflake/cli/api/project/schemas/project_definition.py +30 -25
  69. snowflake/cli/api/rest_api.py +26 -4
  70. snowflake/cli/api/secure_utils.py +1 -1
  71. snowflake/cli/api/sql_execution.py +40 -29
  72. snowflake/cli/api/stage_path.py +244 -0
  73. snowflake/cli/api/utils/definition_rendering.py +3 -5
  74. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +14 -15
  75. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +78 -77
  76. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
  77. snowflake/cli/_plugins/nativeapp/manager.py +0 -415
  78. snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
  79. snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
  80. snowflake/cli/_plugins/nativeapp/teardown_processor.py +0 -70
  81. snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -98
  82. snowflake/cli/_plugins/workspace/action_context.py +0 -18
  83. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
  84. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,16 +1,24 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import time
4
5
  from contextlib import contextmanager
5
6
  from datetime import datetime
6
7
  from pathlib import Path
7
8
  from textwrap import dedent
8
- from typing import Callable, Generator, List, Literal, Optional, TypedDict
9
+ from typing import Dict, Generator, List, Literal, Optional, TypedDict
9
10
 
10
11
  import typer
11
12
  from click import ClickException, UsageError
12
13
  from pydantic import Field, field_validator
13
- from snowflake.cli._plugins.connection.util import make_snowsight_url
14
+ from snowflake.cli._plugins.connection.util import (
15
+ UIParameter,
16
+ get_ui_parameter,
17
+ make_snowsight_url,
18
+ )
19
+ from snowflake.cli._plugins.nativeapp.artifacts import (
20
+ find_events_definitions_in_manifest_file,
21
+ )
14
22
  from snowflake.cli._plugins.nativeapp.common_flags import (
15
23
  ForceOption,
16
24
  InteractiveOption,
@@ -18,6 +26,7 @@ from snowflake.cli._plugins.nativeapp.common_flags import (
18
26
  )
19
27
  from snowflake.cli._plugins.nativeapp.constants import (
20
28
  ALLOWED_SPECIAL_COMMENTS,
29
+ AUTHORIZE_TELEMETRY_COL,
21
30
  COMMENT_COL,
22
31
  NAME_COL,
23
32
  OWNER_COL,
@@ -27,6 +36,9 @@ from snowflake.cli._plugins.nativeapp.entities.application_package import (
27
36
  ApplicationPackageEntity,
28
37
  ApplicationPackageEntityModel,
29
38
  )
39
+ from snowflake.cli._plugins.nativeapp.entities.models.event_sharing_telemetry import (
40
+ EventSharingTelemetry,
41
+ )
30
42
  from snowflake.cli._plugins.nativeapp.exceptions import (
31
43
  ApplicationPackageDoesNotExistError,
32
44
  NoEventTableForAccount,
@@ -40,8 +52,9 @@ from snowflake.cli._plugins.nativeapp.policy import (
40
52
  from snowflake.cli._plugins.nativeapp.same_account_install_method import (
41
53
  SameAccountInstallMethod,
42
54
  )
55
+ from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade
43
56
  from snowflake.cli._plugins.nativeapp.utils import needs_confirmation
44
- from snowflake.cli._plugins.workspace.action_context import ActionContext
57
+ from snowflake.cli._plugins.workspace.context import ActionContext
45
58
  from snowflake.cli.api.cli_global_context import get_cli_context
46
59
  from snowflake.cli.api.console.abc import AbstractConsole
47
60
  from snowflake.cli.api.entities.common import EntityBase, get_sql_executor
@@ -53,8 +66,12 @@ from snowflake.cli.api.entities.utils import (
53
66
  )
54
67
  from snowflake.cli.api.errno import (
55
68
  APPLICATION_NO_LONGER_AVAILABLE,
69
+ APPLICATION_OWNS_EXTERNAL_OBJECTS,
70
+ APPLICATION_REQUIRES_TELEMETRY_SHARING,
71
+ CANNOT_DISABLE_MANDATORY_TELEMETRY,
56
72
  CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION,
57
73
  CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES,
74
+ DOES_NOT_EXIST_OR_NOT_AUTHORIZED,
58
75
  NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS,
59
76
  ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS,
60
77
  )
@@ -70,10 +87,13 @@ from snowflake.cli.api.project.util import (
70
87
  append_test_resource_suffix,
71
88
  extract_schema,
72
89
  identifier_for_url,
90
+ to_identifier,
73
91
  unquote_identifier,
74
92
  )
75
93
  from snowflake.connector import DictCursor, ProgrammingError
76
94
 
95
+ log = logging.getLogger(__name__)
96
+
77
97
  # Reasons why an `alter application ... upgrade` might fail
78
98
  UPGRADE_RESTRICTION_CODES = {
79
99
  CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION,
@@ -86,6 +106,192 @@ UPGRADE_RESTRICTION_CODES = {
86
106
  ApplicationOwnedObject = TypedDict("ApplicationOwnedObject", {"name": str, "type": str})
87
107
 
88
108
 
109
+ class EventSharingHandler:
110
+ """
111
+ Handles the logic around event sharing for applications.
112
+ This class is used to determine whether event sharing should be authorized or not, and which events should be shared.
113
+ """
114
+
115
+ def __init__(
116
+ self,
117
+ *,
118
+ telemetry_definition: Optional[EventSharingTelemetry],
119
+ deploy_root: Path,
120
+ install_method: SameAccountInstallMethod,
121
+ console: AbstractConsole,
122
+ ):
123
+ """
124
+ Initializes the event sharing handler.
125
+ If telemetry_definition is not present or share_mandatory_events is not set,
126
+ we will default to sharing events if mandatory events are present in the manifest file and we are in dev mode.
127
+
128
+ :param telemetry_definition: The telemetry configuration for this application from the PDF if present.
129
+ :param deploy_root: The root directory of the application package.
130
+ :param install_method: The install method of the application.
131
+ :param console: The console to use for logging.
132
+ """
133
+
134
+ self._is_dev_mode = install_method.is_dev_mode
135
+ self._metrics = get_cli_context().metrics
136
+ self._console = console
137
+ connection = get_sql_executor()._conn # noqa: SLF001
138
+ self._event_sharing_enabled = (
139
+ get_ui_parameter(
140
+ connection, UIParameter.NA_EVENT_SHARING_V2, "true"
141
+ ).lower()
142
+ == "true"
143
+ )
144
+ self._event_sharing_enforced = (
145
+ get_ui_parameter(
146
+ connection, UIParameter.NA_ENFORCE_MANDATORY_FILTERS, "true"
147
+ ).lower()
148
+ == "true"
149
+ )
150
+
151
+ self._share_mandatory_events = (
152
+ telemetry_definition and telemetry_definition.share_mandatory_events
153
+ )
154
+ self._optional_shared_events = (
155
+ telemetry_definition and telemetry_definition.optional_shared_events
156
+ ) or []
157
+
158
+ self._metrics.set_counter(
159
+ CLICounterField.EVENT_SHARING, int(self._share_mandatory_events or False)
160
+ )
161
+ self._metrics.set_counter(CLICounterField.EVENT_SHARING_WARNING, 0)
162
+ self._metrics.set_counter(CLICounterField.EVENT_SHARING_ERROR, 0)
163
+
164
+ if not self._event_sharing_enabled:
165
+ # We cannot set AUTHORIZE_TELEMETRY_EVENT_SHARING to True or False if event sharing is not enabled,
166
+ # so we will ignore the field in both cases, but warn only if it is set to True
167
+ if self._share_mandatory_events or self._optional_shared_events:
168
+ self.event_sharing_warning(
169
+ "Same-account event sharing is not enabled in your account, therefore, application telemetry section will be ignored."
170
+ )
171
+ self._share_mandatory_events = None
172
+ self._optional_shared_events = []
173
+ return
174
+
175
+ if install_method.is_dev_mode:
176
+ mandatory_events_in_manifest = self._contains_mandatory_events(
177
+ find_events_definitions_in_manifest_file(deploy_root)
178
+ )
179
+
180
+ if mandatory_events_in_manifest and self._event_sharing_enforced:
181
+ if self._share_mandatory_events is None:
182
+ self.event_sharing_warning(
183
+ "Mandatory events are present in the manifest file. Automatically authorizing event sharing in dev mode. To suppress this warning, please add 'share_mandatory_events: true' in the application telemetry section."
184
+ )
185
+ self._share_mandatory_events = True
186
+
187
+ def _contains_mandatory_events(self, events_definitions: List[Dict[str, str]]):
188
+ return any(event["sharing"] == "MANDATORY" for event in events_definitions)
189
+
190
+ def should_authorize_event_sharing_during_create(
191
+ self,
192
+ ) -> Optional[bool]:
193
+ """
194
+ Determines whether event sharing should be authorized during the creation of the application object.
195
+
196
+ Outputs:
197
+ - None: Event sharing should not be updated or explicitly set.
198
+ - True: Event sharing should be authorized.
199
+ - False: Event sharing should be disabled.
200
+ """
201
+
202
+ if not self._event_sharing_enabled:
203
+ return None
204
+
205
+ return self._share_mandatory_events
206
+
207
+ def should_authorize_event_sharing_after_upgrade(
208
+ self,
209
+ upgraded_app_properties: Dict[str, str],
210
+ ) -> Optional[bool]:
211
+ """
212
+ Determines whether event sharing should be authorized after upgrading the application object.
213
+
214
+ :param upgraded_app_properties: The properties of the application after upgrading.
215
+
216
+ Outputs:
217
+ - None: Event sharing should not be updated or explicitly set.
218
+ - True: Event sharing should be authorized.
219
+ - False: Event sharing should be disabled.
220
+ """
221
+
222
+ if not self._event_sharing_enabled:
223
+ return None
224
+
225
+ current_app_authorization = (
226
+ upgraded_app_properties.get(AUTHORIZE_TELEMETRY_COL, "false").lower()
227
+ == "true"
228
+ )
229
+
230
+ # Skip the update if the current value is the same as the one we want to set
231
+ if current_app_authorization == self._share_mandatory_events:
232
+ return None
233
+
234
+ return self._share_mandatory_events
235
+
236
+ def event_sharing_warning(self, message: str):
237
+ """
238
+ Logs a warning message about event sharing, and emits an event sharing warning metric.
239
+ """
240
+
241
+ self._metrics.set_counter(CLICounterField.EVENT_SHARING_WARNING, 1)
242
+ self._console.warning(f"WARNING: {message}")
243
+
244
+ def event_sharing_error(self, message: str, err: Exception):
245
+ """
246
+ Raises an error about event sharing, and emits an event sharing error metric.
247
+ """
248
+
249
+ self._metrics.set_counter(CLICounterField.EVENT_SHARING_ERROR, 1)
250
+ raise ClickException(message) from err
251
+
252
+ def events_to_share(
253
+ self, events_definitions: List[Dict[str, str]]
254
+ ) -> Optional[List[str]]:
255
+ """
256
+ Determines which events should be shared based on the application events definitions.
257
+ It also combines the mandatory events with the optional shared events.
258
+
259
+ :param events_definitions: The events definitions of the application.
260
+
261
+ Outputs:
262
+ - None: No events should be updated.
263
+ - List[str]: The names of the events that should be shared.
264
+ """
265
+
266
+ # events definition has this format: [{'name': 'SNOWFLAKE$ERRORS_AND_WARNINGS', 'type': 'ERRORS_AND_WARNINGS', 'sharing': 'MANDATORY'...}]
267
+ events_map = {event["type"]: event for event in events_definitions}
268
+ events_names = []
269
+ for event_type in self._optional_shared_events:
270
+ if event_type not in events_map:
271
+ raise ClickException(
272
+ f"Event '{event_type}' from application 'telemetry' section in the project definition file could not be found in the application."
273
+ )
274
+ else:
275
+ events_names.append(events_map[event_type]["name"])
276
+
277
+ # add mandatory events to event_names list:
278
+ for event in events_definitions:
279
+ if event["sharing"] == "MANDATORY":
280
+ events_names.append(event["name"])
281
+
282
+ if not self._share_mandatory_events:
283
+ if (
284
+ self._contains_mandatory_events(events_definitions)
285
+ and not self._event_sharing_enforced
286
+ ):
287
+ self.event_sharing_warning(
288
+ "Mandatory events are present in the application, but event sharing is not authorized in the application telemetry field. This will soon be required to set in order to deploy this application."
289
+ )
290
+ return None
291
+
292
+ return sorted(list(set(events_names)))
293
+
294
+
89
295
  class ApplicationEntityModel(EntityModelBase):
90
296
  type: Literal["application"] = DiscriminatorField() # noqa A003
91
297
  from_: TargetField[ApplicationPackageEntityModel] = Field(
@@ -96,6 +302,10 @@ class ApplicationEntityModel(EntityModelBase):
96
302
  title="Whether to enable debug mode when using a named stage to create an application object",
97
303
  default=None,
98
304
  )
305
+ telemetry: Optional[EventSharingTelemetry] = Field(
306
+ title="Telemetry configuration for the application",
307
+ default=None,
308
+ )
99
309
 
100
310
  @field_validator("identifier")
101
311
  @classmethod
@@ -116,9 +326,38 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
116
326
  A Native App application object, created from an application package.
117
327
  """
118
328
 
329
+ @property
330
+ def project_root(self) -> Path:
331
+ return self._workspace_ctx.project_root
332
+
333
+ @property
334
+ def package_entity_id(self) -> str:
335
+ return self._entity_model.from_.target
336
+
337
+ @property
338
+ def name(self) -> str:
339
+ return to_identifier(self._entity_model.fqn.name)
340
+
341
+ @property
342
+ def role(self) -> str:
343
+ model = self._entity_model
344
+ return (model.meta and model.meta.role) or self._workspace_ctx.default_role
345
+
346
+ @property
347
+ def warehouse(self) -> str:
348
+ model = self._entity_model
349
+ return (
350
+ model.meta and model.meta.warehouse and to_identifier(model.meta.warehouse)
351
+ ) or to_identifier(self._workspace_ctx.default_warehouse)
352
+
353
+ @property
354
+ def post_deploy_hooks(self) -> list[PostDeployHook] | None:
355
+ model = self._entity_model
356
+ return model.meta and model.meta.post_deploy
357
+
119
358
  def action_deploy(
120
359
  self,
121
- ctx: ActionContext,
360
+ action_ctx: ActionContext,
122
361
  from_release_directive: bool,
123
362
  prune: bool,
124
363
  recursive: bool,
@@ -132,136 +371,109 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
132
371
  *args,
133
372
  **kwargs,
134
373
  ):
135
- model = self._entity_model
136
- app_name = model.fqn.identifier
137
- debug_mode = model.debug
138
- if model.meta:
139
- app_role = model.meta.role or ctx.default_role
140
- app_warehouse = model.meta.warehouse or ctx.default_warehouse
141
- post_deploy_hooks = model.meta.post_deploy
142
- else:
143
- app_role = ctx.default_role
144
- app_warehouse = ctx.default_warehouse
145
- post_deploy_hooks = None
146
-
147
- package_entity: ApplicationPackageEntity = ctx.get_entity(model.from_.target)
148
- package_model: ApplicationPackageEntityModel = (
149
- package_entity._entity_model # noqa: SLF001
374
+ """
375
+ Create or upgrade the application object using the given strategy
376
+ (unversioned dev, versioned dev, or same-account release directive).
377
+ """
378
+ package_entity: ApplicationPackageEntity = action_ctx.get_entity(
379
+ self.package_entity_id
150
380
  )
151
- package_name = package_model.fqn.identifier
152
- if package_model.meta and package_model.meta.role:
153
- package_role = package_model.meta.role
154
- else:
155
- package_role = ctx.default_role
381
+ stage_fqn = stage_fqn or package_entity.stage_fqn
156
382
 
157
- if not stage_fqn:
158
- stage_fqn = f"{package_name}.{package_model.stage}"
159
- stage_schema = extract_schema(stage_fqn)
160
-
161
- is_interactive = False
162
383
  if force:
163
384
  policy = AllowAlwaysPolicy()
164
385
  elif interactive:
165
- is_interactive = True
166
386
  policy = AskAlwaysPolicy()
167
387
  else:
168
388
  policy = DenyAlwaysPolicy()
169
389
 
170
- def deploy_package():
171
- package_entity.action_deploy(
172
- ctx=ctx,
173
- prune=True,
174
- recursive=True,
175
- paths=[],
176
- validate=validate,
390
+ # same-account release directive
391
+ if from_release_directive:
392
+ self.create_or_upgrade_app(
393
+ package=package_entity,
177
394
  stage_fqn=stage_fqn,
395
+ install_method=SameAccountInstallMethod.release_directive(),
396
+ policy=policy,
178
397
  interactive=interactive,
179
- force=force,
180
398
  )
399
+ return
181
400
 
182
- self.deploy(
183
- console=ctx.console,
184
- project_root=ctx.project_root,
185
- app_name=app_name,
186
- app_role=app_role,
187
- app_warehouse=app_warehouse,
188
- package_name=package_name,
189
- package_role=package_role,
190
- stage_schema=stage_schema,
191
- stage_fqn=stage_fqn,
192
- debug_mode=debug_mode,
401
+ # versioned dev
402
+ if version:
403
+ try:
404
+ version_exists = package_entity.get_existing_version_info(version)
405
+ if not version_exists:
406
+ raise UsageError(
407
+ f"Application package {package_entity.name} does not have any version {version} defined. Use 'snow app version create' to define a version in the application package first."
408
+ )
409
+ except ApplicationPackageDoesNotExistError as app_err:
410
+ raise UsageError(
411
+ f"Application package {package_entity.name} does not exist. Use 'snow app version create' to first create an application package and then define a version in it."
412
+ )
413
+
414
+ self.create_or_upgrade_app(
415
+ package=package_entity,
416
+ stage_fqn=stage_fqn,
417
+ install_method=SameAccountInstallMethod.versioned_dev(version, patch),
418
+ policy=policy,
419
+ interactive=interactive,
420
+ )
421
+ return
422
+
423
+ # unversioned dev
424
+ package_entity.action_deploy(
425
+ action_ctx=action_ctx,
426
+ prune=True,
427
+ recursive=True,
428
+ paths=[],
193
429
  validate=validate,
194
- from_release_directive=from_release_directive,
195
- is_interactive=is_interactive,
430
+ stage_fqn=stage_fqn,
431
+ interactive=interactive,
432
+ force=force,
433
+ )
434
+ self.create_or_upgrade_app(
435
+ package=package_entity,
436
+ stage_fqn=stage_fqn,
437
+ install_method=SameAccountInstallMethod.unversioned_dev(),
196
438
  policy=policy,
197
- version=version,
198
- patch=patch,
199
- post_deploy_hooks=post_deploy_hooks,
200
- deploy_package=deploy_package,
439
+ interactive=interactive,
201
440
  )
202
441
 
203
442
  def action_drop(
204
443
  self,
205
- ctx: ActionContext,
444
+ action_ctx: ActionContext,
206
445
  interactive: bool,
207
446
  force_drop: bool = False,
208
447
  cascade: Optional[bool] = None,
209
448
  *args,
210
449
  **kwargs,
211
- ):
212
- model = self._entity_model
213
- app_name = model.fqn.identifier
214
- if model.meta and model.meta.role:
215
- app_role = model.meta.role
216
- else:
217
- app_role = ctx.default_role
218
- self.drop(
219
- console=ctx.console,
220
- app_name=app_name,
221
- app_role=app_role,
222
- auto_yes=force_drop,
223
- interactive=interactive,
224
- cascade=cascade,
225
- )
226
-
227
- @classmethod
228
- def drop(
229
- cls,
230
- console: AbstractConsole,
231
- app_name: str,
232
- app_role: str,
233
- auto_yes: bool,
234
- interactive: bool = False,
235
- cascade: Optional[bool] = None,
236
450
  ):
237
451
  """
238
452
  Attempts to drop the application object if all validations and user prompts allow so.
239
453
  """
454
+ console = self._workspace_ctx.console
240
455
 
241
456
  needs_confirm = True
242
457
 
243
458
  # 1. If existing application is not found, exit gracefully
244
- show_obj_row = cls.get_existing_app_info(
245
- app_name=app_name,
246
- app_role=app_role,
247
- )
459
+ show_obj_row = self.get_existing_app_info()
248
460
  if show_obj_row is None:
249
461
  console.warning(
250
- f"Role {app_role} does not own any application object with the name {app_name}, or the application object does not exist."
462
+ f"Role {self.role} does not own any application object with the name {self.name}, or the application object does not exist."
251
463
  )
252
464
  return
253
465
 
254
466
  # 2. Check if created by the Snowflake CLI
255
467
  row_comment = show_obj_row[COMMENT_COL]
256
468
  if row_comment not in ALLOWED_SPECIAL_COMMENTS and needs_confirmation(
257
- needs_confirm, auto_yes
469
+ needs_confirm, force_drop
258
470
  ):
259
471
  should_drop_object = typer.confirm(
260
472
  dedent(
261
473
  f"""\
262
- Application object {app_name} was not created by Snowflake CLI.
474
+ Application object {self.name} was not created by Snowflake CLI.
263
475
  Application object details:
264
- Name: {app_name}
476
+ Name: {self.name}
265
477
  Created on: {show_obj_row["created_on"]}
266
478
  Source: {show_obj_row["source"]}
267
479
  Owner: {show_obj_row[OWNER_COL]}
@@ -273,7 +485,7 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
273
485
  )
274
486
  )
275
487
  if not should_drop_object:
276
- console.message(f"Did not drop application object {app_name}.")
488
+ console.message(f"Did not drop application object {self.name}.")
277
489
  # The user desires to keep the app, therefore we can't proceed since it would
278
490
  # leave behind an orphan app when we get to dropping the package
279
491
  raise typer.Abort()
@@ -287,13 +499,10 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
287
499
  interactive_prompt = ""
288
500
  non_interactive_abort = ""
289
501
  try:
290
- if application_objects := cls.get_objects_owned_by_application(
291
- app_name=app_name,
292
- app_role=app_role,
293
- ):
502
+ if application_objects := self.get_objects_owned_by_application():
294
503
  has_objects_to_drop = True
295
504
  message_prefix = (
296
- f"The following objects are owned by application {app_name}"
505
+ f"The following objects are owned by application {self.name}"
297
506
  )
298
507
  cascade_true_message = f"{message_prefix} and will be dropped:"
299
508
  cascade_false_message = f"{message_prefix} and will NOT be dropped:"
@@ -303,9 +512,7 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
303
512
  if e.errno != APPLICATION_NO_LONGER_AVAILABLE:
304
513
  raise
305
514
  application_objects = []
306
- message_prefix = (
307
- f"Could not determine which objects are owned by application {app_name}"
308
- )
515
+ message_prefix = f"Could not determine which objects are owned by application {self.name}"
309
516
  has_objects_to_drop = True # potentially, but we don't know what they are
310
517
  cascade_true_message = (
311
518
  f"{message_prefix}, an unknown number of objects will be dropped."
@@ -320,19 +527,19 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
320
527
  console.message(cascade_true_message)
321
528
  with console.indented():
322
529
  for obj in application_objects:
323
- console.message(cls.application_object_to_str(obj))
530
+ console.message(_application_object_to_str(obj))
324
531
  elif cascade is False:
325
532
  # If the user explicitly passed the --no-cascade flag
326
533
  console.message(cascade_false_message)
327
534
  with console.indented():
328
535
  for obj in application_objects:
329
- console.message(cls.application_object_to_str(obj))
536
+ console.message(_application_object_to_str(obj))
330
537
  elif interactive:
331
538
  # If the user didn't pass any cascade flag and the session is interactive
332
539
  console.message(message_prefix)
333
540
  with console.indented():
334
541
  for obj in application_objects:
335
- console.message(cls.application_object_to_str(obj))
542
+ console.message(_application_object_to_str(obj))
336
543
  user_response = typer.prompt(
337
544
  interactive_prompt,
338
545
  show_default=False,
@@ -349,7 +556,7 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
349
556
  console.message(message_prefix)
350
557
  with console.indented():
351
558
  for obj in application_objects:
352
- console.message(cls.application_object_to_str(obj))
559
+ console.message(_application_object_to_str(obj))
353
560
  console.message(non_interactive_abort)
354
561
  raise typer.Abort()
355
562
  elif cascade is None:
@@ -360,243 +567,185 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
360
567
  drop_generic_object(
361
568
  console=console,
362
569
  object_type="application",
363
- object_name=app_name,
364
- role=app_role,
570
+ object_name=self.name,
571
+ role=self.role,
365
572
  cascade=cascade,
366
573
  )
367
- return # The application object was successfully dropped, therefore exit gracefully
368
574
 
369
- @staticmethod
370
- def get_objects_owned_by_application(
371
- app_name: str,
372
- app_role: str,
373
- ) -> List[ApplicationOwnedObject]:
575
+ def action_events(
576
+ self,
577
+ action_ctx: ActionContext,
578
+ since: str | datetime | None = None,
579
+ until: str | datetime | None = None,
580
+ record_types: list[str] | None = None,
581
+ scopes: list[str] | None = None,
582
+ consumer_org: str = "",
583
+ consumer_account: str = "",
584
+ consumer_app_hash: str = "",
585
+ first: int = -1,
586
+ last: int = -1,
587
+ follow: bool = False,
588
+ interval_seconds: int = 10,
589
+ *args,
590
+ **kwargs,
591
+ ):
592
+ package_entity: ApplicationPackageEntity = action_ctx.get_entity(
593
+ self.package_entity_id
594
+ )
595
+ if follow:
596
+ return self.stream_events(
597
+ package_name=package_entity.name,
598
+ interval_seconds=interval_seconds,
599
+ since=since,
600
+ record_types=record_types,
601
+ scopes=scopes,
602
+ consumer_org=consumer_org,
603
+ consumer_account=consumer_account,
604
+ consumer_app_hash=consumer_app_hash,
605
+ last=last,
606
+ )
607
+ else:
608
+ return self.get_events(
609
+ package_name=package_entity.name,
610
+ since=since,
611
+ until=until,
612
+ record_types=record_types,
613
+ scopes=scopes,
614
+ consumer_org=consumer_org,
615
+ consumer_account=consumer_account,
616
+ consumer_app_hash=consumer_app_hash,
617
+ first=first,
618
+ last=last,
619
+ )
620
+
621
+ def get_objects_owned_by_application(self) -> List[ApplicationOwnedObject]:
374
622
  """
375
623
  Returns all application objects owned by this application.
376
624
  """
377
625
  sql_executor = get_sql_executor()
378
- with sql_executor.use_role(app_role):
626
+ with sql_executor.use_role(self.role):
379
627
  results = sql_executor.execute_query(
380
- f"show objects owned by application {app_name}"
628
+ f"show objects owned by application {self.name}"
381
629
  ).fetchall()
382
630
  return [{"name": row[1], "type": row[2]} for row in results]
383
631
 
384
- @classmethod
385
- def application_objects_to_str(
386
- cls, application_objects: list[ApplicationOwnedObject]
387
- ) -> str:
388
- """
389
- Returns a list in an "(Object Type) Object Name" format. Database-level and schema-level object names are fully qualified:
390
- (COMPUTE_POOL) POOL_NAME
391
- (DATABASE) DB_NAME
392
- (SCHEMA) DB_NAME.PUBLIC
393
- ...
394
- """
395
- return "\n".join(
396
- [cls.application_object_to_str(obj) for obj in application_objects]
397
- )
398
-
399
- @staticmethod
400
- def application_object_to_str(obj: ApplicationOwnedObject) -> str:
401
- return f"({obj['type']}) {obj['name']}"
402
-
403
- @classmethod
404
- def deploy(
405
- cls,
406
- console: AbstractConsole,
407
- project_root: Path,
408
- app_name: str,
409
- app_role: str,
410
- app_warehouse: str,
411
- package_name: str,
412
- package_role: str,
413
- stage_schema: str,
632
+ def create_or_upgrade_app(
633
+ self,
634
+ package: ApplicationPackageEntity,
414
635
  stage_fqn: str,
415
- debug_mode: bool,
416
- validate: bool,
417
- from_release_directive: bool,
418
- is_interactive: bool,
636
+ install_method: SameAccountInstallMethod,
419
637
  policy: PolicyBase,
420
- deploy_package: Callable,
421
- version: Optional[str] = None,
422
- patch: Optional[int] = None,
423
- post_deploy_hooks: Optional[List[PostDeployHook]] = None,
424
- drop_application_before_upgrade: Optional[Callable] = None,
638
+ interactive: bool,
425
639
  ):
426
- """
427
- Create or upgrade the application object using the given strategy
428
- (unversioned dev, versioned dev, or same-account release directive).
429
- """
430
-
431
- # same-account release directive
432
- if from_release_directive:
433
- cls.create_or_upgrade_app(
434
- console=console,
435
- project_root=project_root,
436
- package_name=package_name,
437
- package_role=package_role,
438
- app_name=app_name,
439
- app_role=app_role,
440
- app_warehouse=app_warehouse,
441
- stage_schema=stage_schema,
442
- stage_fqn=stage_fqn,
443
- debug_mode=debug_mode,
444
- policy=policy,
445
- install_method=SameAccountInstallMethod.release_directive(),
446
- is_interactive=is_interactive,
447
- post_deploy_hooks=post_deploy_hooks,
448
- drop_application_before_upgrade=drop_application_before_upgrade,
449
- )
450
- return
640
+ model = self._entity_model
641
+ console = self._workspace_ctx.console
642
+ debug_mode = model.debug
451
643
 
452
- # versioned dev
453
- if version:
454
- try:
455
- version_exists = ApplicationPackageEntity.get_existing_version_info(
456
- version=version,
457
- package_name=package_name,
458
- package_role=package_role,
459
- )
460
- if not version_exists:
461
- raise UsageError(
462
- f"Application package {package_name} does not have any version {version} defined. Use 'snow app version create' to define a version in the application package first."
463
- )
464
- except ApplicationPackageDoesNotExistError as app_err:
465
- raise UsageError(
466
- f"Application package {package_name} does not exist. Use 'snow app version create' to first create an application package and then define a version in it."
467
- )
644
+ stage_fqn = stage_fqn or package.stage_fqn
645
+ stage_schema = extract_schema(stage_fqn)
468
646
 
469
- cls.create_or_upgrade_app(
647
+ sql_executor = get_sql_executor()
648
+ with sql_executor.use_role(self.role):
649
+ event_sharing = EventSharingHandler(
650
+ telemetry_definition=model.telemetry,
651
+ deploy_root=package.deploy_root,
652
+ install_method=install_method,
470
653
  console=console,
471
- project_root=project_root,
472
- package_name=package_name,
473
- package_role=package_role,
474
- app_name=app_name,
475
- app_role=app_role,
476
- app_warehouse=app_warehouse,
477
- stage_schema=stage_schema,
478
- stage_fqn=stage_fqn,
479
- debug_mode=debug_mode,
480
- policy=policy,
481
- install_method=SameAccountInstallMethod.versioned_dev(version, patch),
482
- is_interactive=is_interactive,
483
- post_deploy_hooks=post_deploy_hooks,
484
- drop_application_before_upgrade=drop_application_before_upgrade,
485
654
  )
486
- return
487
-
488
- # unversioned dev
489
- deploy_package()
490
- cls.create_or_upgrade_app(
491
- console=console,
492
- project_root=project_root,
493
- package_name=package_name,
494
- package_role=package_role,
495
- app_name=app_name,
496
- app_role=app_role,
497
- app_warehouse=app_warehouse,
498
- stage_schema=stage_schema,
499
- stage_fqn=stage_fqn,
500
- debug_mode=debug_mode,
501
- policy=policy,
502
- install_method=SameAccountInstallMethod.unversioned_dev(),
503
- is_interactive=is_interactive,
504
- post_deploy_hooks=post_deploy_hooks,
505
- drop_application_before_upgrade=drop_application_before_upgrade,
506
- )
507
-
508
- @classmethod
509
- def create_or_upgrade_app(
510
- cls,
511
- console: AbstractConsole,
512
- project_root: Path,
513
- package_name: str,
514
- package_role: str,
515
- app_name: str,
516
- app_role: str,
517
- app_warehouse: Optional[str],
518
- stage_schema: Optional[str],
519
- stage_fqn: str,
520
- debug_mode: bool,
521
- policy: PolicyBase,
522
- install_method: SameAccountInstallMethod,
523
- is_interactive: bool = False,
524
- post_deploy_hooks: Optional[List[PostDeployHook]] = None,
525
- drop_application_before_upgrade: Optional[Callable] = None,
526
- ):
527
- sql_executor = get_sql_executor()
528
- with sql_executor.use_role(app_role):
529
655
 
530
656
  # 1. Need to use a warehouse to create an application object
531
- with sql_executor.use_warehouse(app_warehouse):
657
+ with sql_executor.use_warehouse(self.warehouse):
532
658
 
533
659
  # 2. Check for an existing application by the same name
534
- show_app_row = cls.get_existing_app_info(
535
- app_name=app_name,
536
- app_role=app_role,
537
- )
660
+ show_app_row = self.get_existing_app_info()
538
661
 
539
662
  # 3. If existing application is found, perform a few validations and upgrade the application object.
540
663
  if show_app_row:
541
-
542
664
  install_method.ensure_app_usable(
543
- app_name=app_name,
544
- app_role=app_role,
665
+ app_name=self.name,
666
+ app_role=self.role,
545
667
  show_app_row=show_app_row,
546
668
  )
547
669
 
548
670
  # If all the above checks are in order, proceed to upgrade
549
671
  try:
550
672
  console.step(
551
- f"Upgrading existing application object {app_name}."
673
+ f"Upgrading existing application object {self.name}."
552
674
  )
553
675
  using_clause = install_method.using_clause(stage_fqn)
554
676
  upgrade_cursor = sql_executor.execute_query(
555
- f"alter application {app_name} upgrade {using_clause}",
677
+ f"alter application {self.name} upgrade {using_clause}",
556
678
  )
557
679
  print_messages(console, upgrade_cursor)
558
680
 
681
+ events_definitions = (
682
+ get_snowflake_facade().get_event_definitions(
683
+ self.name, self.role
684
+ )
685
+ )
686
+
687
+ app_properties = get_snowflake_facade().get_app_properties(
688
+ self.name, self.role
689
+ )
690
+ new_authorize_event_sharing_value = (
691
+ event_sharing.should_authorize_event_sharing_after_upgrade(
692
+ app_properties,
693
+ )
694
+ )
695
+ if new_authorize_event_sharing_value is not None:
696
+ log.info(
697
+ "Setting telemetry sharing authorization to %s",
698
+ new_authorize_event_sharing_value,
699
+ )
700
+ sql_executor.execute_query(
701
+ f"alter application {self.name} set AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(new_authorize_event_sharing_value).upper()}"
702
+ )
703
+ events_to_share = event_sharing.events_to_share(
704
+ events_definitions
705
+ )
706
+ if events_to_share is not None:
707
+ get_snowflake_facade().share_telemetry_events(
708
+ self.name, events_to_share
709
+ )
710
+
559
711
  if install_method.is_dev_mode:
560
712
  # if debug_mode is present (controlled), ensure it is up-to-date
561
713
  if debug_mode is not None:
562
714
  sql_executor.execute_query(
563
- f"alter application {app_name} set debug_mode = {debug_mode}"
715
+ f"alter application {self.name} set debug_mode = {debug_mode}"
564
716
  )
565
717
 
566
718
  # hooks always executed after a create or upgrade
567
- cls.execute_post_deploy_hooks(
568
- console=console,
569
- project_root=project_root,
570
- post_deploy_hooks=post_deploy_hooks,
571
- app_name=app_name,
572
- app_warehouse=app_warehouse,
573
- )
719
+ self.execute_post_deploy_hooks()
574
720
  return
575
721
 
576
722
  except ProgrammingError as err:
577
- if err.errno not in UPGRADE_RESTRICTION_CODES:
578
- generic_sql_error_handler(err=err)
579
- else: # The existing application object was created from a different process.
723
+ if err.errno == CANNOT_DISABLE_MANDATORY_TELEMETRY:
724
+ event_sharing.event_sharing_error(
725
+ "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.",
726
+ err,
727
+ )
728
+ elif err.errno in UPGRADE_RESTRICTION_CODES:
580
729
  console.warning(err.msg)
581
- # TODO Drop the entity here instead of taking a callback once action_drop() is implemented
582
- if drop_application_before_upgrade:
583
- drop_application_before_upgrade()
584
- else:
585
- raise NotImplementedError
730
+ self.drop_application_before_upgrade(
731
+ policy=policy, interactive=interactive
732
+ )
733
+ else:
734
+ generic_sql_error_handler(err=err)
586
735
 
587
736
  # 4. With no (more) existing application objects, create an application object using the release directives
588
- console.step(f"Creating new application object {app_name} in account.")
737
+ console.step(f"Creating new application object {self.name} in account.")
589
738
 
590
- if app_role != package_role:
591
- with sql_executor.use_role(package_role):
739
+ if self.role != package.role:
740
+ with sql_executor.use_role(package.role):
592
741
  sql_executor.execute_query(
593
- f"grant install, develop on application package {package_name} to role {app_role}"
742
+ f"grant install, develop on application package {package.name} to role {self.role}"
594
743
  )
595
744
  sql_executor.execute_query(
596
- f"grant usage on schema {package_name}.{stage_schema} to role {app_role}"
745
+ f"grant usage on schema {package.name}.{stage_schema} to role {self.role}"
597
746
  )
598
747
  sql_executor.execute_query(
599
- f"grant read on stage {stage_fqn} to role {app_role}"
748
+ f"grant read on stage {stage_fqn} to role {self.role}"
600
749
  )
601
750
 
602
751
  try:
@@ -609,60 +758,64 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
609
758
  )
610
759
  debug_mode_clause = f"debug_mode = {initial_debug_mode}"
611
760
 
761
+ authorize_telemetry_clause = ""
762
+ new_authorize_event_sharing_value = (
763
+ event_sharing.should_authorize_event_sharing_during_create()
764
+ )
765
+ if new_authorize_event_sharing_value is not None:
766
+ log.info(
767
+ "Setting AUTHORIZE_TELEMETRY_EVENT_SHARING to %s",
768
+ new_authorize_event_sharing_value,
769
+ )
770
+ authorize_telemetry_clause = f" AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(new_authorize_event_sharing_value).upper()}"
771
+
612
772
  using_clause = install_method.using_clause(stage_fqn)
613
773
  create_cursor = sql_executor.execute_query(
614
774
  dedent(
615
775
  f"""\
616
- create application {app_name}
617
- from application package {package_name} {using_clause} {debug_mode_clause}
776
+ create application {self.name}
777
+ from application package {package.name} {using_clause} {debug_mode_clause}{authorize_telemetry_clause}
618
778
  comment = {SPECIAL_COMMENT}
619
779
  """
620
780
  ),
621
781
  )
622
782
  print_messages(console, create_cursor)
783
+ events_definitions = get_snowflake_facade().get_event_definitions(
784
+ self.name, self.role
785
+ )
786
+
787
+ events_to_share = event_sharing.events_to_share(events_definitions)
788
+ if events_to_share is not None:
789
+ get_snowflake_facade().share_telemetry_events(
790
+ self.name, events_to_share
791
+ )
623
792
 
624
793
  # hooks always executed after a create or upgrade
625
- cls.execute_post_deploy_hooks(
626
- console=console,
627
- project_root=project_root,
628
- post_deploy_hooks=post_deploy_hooks,
629
- app_name=app_name,
630
- app_warehouse=app_warehouse,
631
- )
794
+ self.execute_post_deploy_hooks()
632
795
 
633
796
  except ProgrammingError as err:
797
+ if err.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING:
798
+ event_sharing.event_sharing_error(
799
+ "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.",
800
+ err,
801
+ )
634
802
  generic_sql_error_handler(err)
635
803
 
636
- @classmethod
637
- def execute_post_deploy_hooks(
638
- cls,
639
- console: AbstractConsole,
640
- project_root: Path,
641
- post_deploy_hooks: Optional[List[PostDeployHook]],
642
- app_name: str,
643
- app_warehouse: Optional[str],
644
- ):
645
- get_cli_context().metrics.set_counter_default(
646
- CLICounterField.POST_DEPLOY_SCRIPTS, 0
804
+ def execute_post_deploy_hooks(self):
805
+ execute_post_deploy_hooks(
806
+ console=self._workspace_ctx.console,
807
+ project_root=self.project_root,
808
+ post_deploy_hooks=self.post_deploy_hooks,
809
+ deployed_object_type="application",
810
+ role_name=self.role,
811
+ warehouse_name=self.warehouse,
812
+ database_name=self.name,
647
813
  )
648
814
 
649
- if post_deploy_hooks:
650
- with cls.use_application_warehouse(app_warehouse):
651
- execute_post_deploy_hooks(
652
- console=console,
653
- project_root=project_root,
654
- post_deploy_hooks=post_deploy_hooks,
655
- deployed_object_type="application",
656
- database_name=app_name,
657
- )
658
-
659
- @staticmethod
660
815
  @contextmanager
661
- def use_application_warehouse(
662
- app_warehouse: Optional[str],
663
- ):
664
- if app_warehouse:
665
- with get_sql_executor().use_warehouse(app_warehouse):
816
+ def use_application_warehouse(self):
817
+ if self.warehouse:
818
+ with get_sql_executor().use_warehouse(self.warehouse):
666
819
  yield
667
820
  else:
668
821
  raise ClickException(
@@ -674,25 +827,72 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
674
827
  )
675
828
  )
676
829
 
677
- @staticmethod
678
- def get_existing_app_info(
679
- app_name: str,
680
- app_role: str,
681
- ) -> Optional[dict]:
830
+ def get_existing_app_info(self) -> Optional[dict]:
682
831
  """
683
832
  Check for an existing application object by the same name as in project definition, in account.
684
833
  It executes a 'show applications like' query and returns the result as single row, if one exists.
685
834
  """
686
835
  sql_executor = get_sql_executor()
687
- with sql_executor.use_role(app_role):
836
+ with sql_executor.use_role(self.role):
688
837
  return sql_executor.show_specific_object(
689
- "applications", app_name, name_col=NAME_COL
838
+ "applications", self.name, name_col=NAME_COL
690
839
  )
691
840
 
692
- @classmethod
841
+ def drop_application_before_upgrade(
842
+ self,
843
+ policy: PolicyBase,
844
+ interactive: bool,
845
+ cascade: bool = False,
846
+ ):
847
+ console = self._workspace_ctx.console
848
+
849
+ if cascade:
850
+ try:
851
+ if application_objects := self.get_objects_owned_by_application():
852
+ application_objects_str = _application_objects_to_str(
853
+ application_objects
854
+ )
855
+ console.message(
856
+ f"The following objects are owned by application {self.name} and need to be dropped:\n{application_objects_str}"
857
+ )
858
+ except ProgrammingError as err:
859
+ if err.errno != APPLICATION_NO_LONGER_AVAILABLE:
860
+ generic_sql_error_handler(err)
861
+ console.warning(
862
+ "The application owns other objects but they could not be determined."
863
+ )
864
+ user_prompt = "Do you want the Snowflake CLI to drop these objects, then drop the existing application object and recreate it?"
865
+ else:
866
+ user_prompt = "Do you want the Snowflake CLI to drop the existing application object and recreate it?"
867
+
868
+ if not policy.should_proceed(user_prompt):
869
+ if interactive:
870
+ console.message("Not upgrading the application object.")
871
+ raise typer.Exit(0)
872
+ else:
873
+ console.message(
874
+ "Cannot upgrade the application object non-interactively without --force."
875
+ )
876
+ raise typer.Exit(1)
877
+ try:
878
+ cascade_msg = " (cascade)" if cascade else ""
879
+ console.step(f"Dropping application object {self.name}{cascade_msg}.")
880
+ cascade_sql = " cascade" if cascade else ""
881
+ sql_executor = get_sql_executor()
882
+ sql_executor.execute_query(f"drop application {self.name}{cascade_sql}")
883
+ except ProgrammingError as err:
884
+ if err.errno == APPLICATION_OWNS_EXTERNAL_OBJECTS and not cascade:
885
+ # We need to cascade the deletion, let's try again (only if we didn't try with cascade already)
886
+ return self.drop_application_before_upgrade(
887
+ policy=policy,
888
+ interactive=interactive,
889
+ cascade=True,
890
+ )
891
+ else:
892
+ generic_sql_error_handler(err)
893
+
693
894
  def get_events(
694
- cls,
695
- app_name: str,
895
+ self,
696
896
  package_name: str,
697
897
  since: str | datetime | None = None,
698
898
  until: str | datetime | None = None,
@@ -704,19 +904,18 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
704
904
  first: int = -1,
705
905
  last: int = -1,
706
906
  ):
707
-
708
907
  record_types = record_types or []
709
908
  scopes = scopes or []
710
909
 
711
910
  if first >= 0 and last >= 0:
712
911
  raise ValueError("first and last cannot be used together")
713
912
 
714
- account_event_table = cls.get_account_event_table()
715
- if not account_event_table:
913
+ account_event_table = get_snowflake_facade().get_account_event_table()
914
+ if account_event_table is None:
716
915
  raise NoEventTableForAccount()
717
916
 
718
917
  # resource_attributes uses the unquoted/uppercase app and package name
719
- app_name = unquote_identifier(app_name)
918
+ app_name = unquote_identifier(self.name)
720
919
  package_name = unquote_identifier(package_name)
721
920
  org_name = unquote_identifier(consumer_org)
722
921
  account_name = unquote_identifier(consumer_account)
@@ -787,12 +986,19 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
787
986
  try:
788
987
  return sql_executor.execute_query(query, cursor_class=DictCursor).fetchall()
789
988
  except ProgrammingError as err:
790
- generic_sql_error_handler(err)
989
+ if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED:
990
+ raise ClickException(
991
+ dedent(
992
+ f"""\
993
+ Event table '{account_event_table}' does not exist or you are not authorized to perform this operation.
994
+ Please check your EVENT_TABLE parameter to ensure that it is set to a valid event table."""
995
+ )
996
+ ) from err
997
+ else:
998
+ generic_sql_error_handler(err)
791
999
 
792
- @classmethod
793
1000
  def stream_events(
794
- cls,
795
- app_name: str,
1001
+ self,
796
1002
  package_name: str,
797
1003
  interval_seconds: int,
798
1004
  since: str | datetime | None = None,
@@ -804,8 +1010,7 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
804
1010
  last: int = -1,
805
1011
  ) -> Generator[dict, None, None]:
806
1012
  try:
807
- events = cls.get_events(
808
- app_name=app_name,
1013
+ events = self.get_events(
809
1014
  package_name=package_name,
810
1015
  since=since,
811
1016
  record_types=record_types,
@@ -821,8 +1026,7 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
821
1026
  while True: # Then infinite poll for new events
822
1027
  time.sleep(interval_seconds)
823
1028
  previous_events = events
824
- events = cls.get_events(
825
- app_name=app_name,
1029
+ events = self.get_events(
826
1030
  package_name=package_name,
827
1031
  since=last_event_time,
828
1032
  record_types=record_types,
@@ -839,18 +1043,10 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
839
1043
  except KeyboardInterrupt:
840
1044
  return
841
1045
 
842
- @staticmethod
843
- def get_account_event_table():
844
- query = "show parameters like 'event_table' in account"
845
- sql_executor = get_sql_executor()
846
- results = sql_executor.execute_query(query, cursor_class=DictCursor)
847
- return next((r["value"] for r in results if r["key"] == "EVENT_TABLE"), "")
848
-
849
- @classmethod
850
- def get_snowsight_url(cls, app_name: str, app_warehouse: str | None) -> str:
1046
+ def get_snowsight_url(self) -> str:
851
1047
  """Returns the URL that can be used to visit this app via Snowsight."""
852
- name = identifier_for_url(app_name)
853
- with cls.use_application_warehouse(app_warehouse):
1048
+ name = identifier_for_url(self.name)
1049
+ with self.use_application_warehouse():
854
1050
  sql_executor = get_sql_executor()
855
1051
  return make_snowsight_url(
856
1052
  sql_executor._conn, f"/#/apps/application/{name}" # noqa: SLF001
@@ -876,3 +1072,20 @@ def _new_events_only(previous_events: list[dict], new_events: list[dict]) -> lis
876
1072
  # either be in both lists or in new_events only
877
1073
  new_events.remove(event)
878
1074
  return new_events
1075
+
1076
+
1077
+ def _application_objects_to_str(
1078
+ application_objects: list[ApplicationOwnedObject],
1079
+ ) -> str:
1080
+ """
1081
+ Returns a list in an "(Object Type) Object Name" format. Database-level and schema-level object names are fully qualified:
1082
+ (COMPUTE_POOL) POOL_NAME
1083
+ (DATABASE) DB_NAME
1084
+ (SCHEMA) DB_NAME.PUBLIC
1085
+ ...
1086
+ """
1087
+ return "\n".join([_application_object_to_str(obj) for obj in application_objects])
1088
+
1089
+
1090
+ def _application_object_to_str(obj: ApplicationOwnedObject) -> str:
1091
+ return f"({obj['type']}) {obj['name']}"