snowflake-cli-labs 2.3.0rc1__py3-none-any.whl → 2.4.0rc0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/api/__init__.py +2 -0
  3. snowflake/cli/api/cli_global_context.py +8 -1
  4. snowflake/cli/api/commands/decorators.py +2 -2
  5. snowflake/cli/api/commands/flags.py +49 -4
  6. snowflake/cli/api/commands/snow_typer.py +2 -0
  7. snowflake/cli/api/console/abc.py +2 -0
  8. snowflake/cli/api/console/console.py +6 -5
  9. snowflake/cli/api/constants.py +5 -0
  10. snowflake/cli/api/exceptions.py +12 -0
  11. snowflake/cli/api/identifiers.py +123 -0
  12. snowflake/cli/api/plugins/command/__init__.py +2 -0
  13. snowflake/cli/api/plugins/plugin_config.py +2 -0
  14. snowflake/cli/api/project/definition.py +2 -0
  15. snowflake/cli/api/project/errors.py +3 -3
  16. snowflake/cli/api/project/schemas/identifier_model.py +35 -0
  17. snowflake/cli/api/project/schemas/native_app/native_app.py +4 -0
  18. snowflake/cli/api/project/schemas/native_app/path_mapping.py +21 -3
  19. snowflake/cli/api/project/schemas/project_definition.py +58 -6
  20. snowflake/cli/api/project/schemas/snowpark/argument.py +2 -0
  21. snowflake/cli/api/project/schemas/snowpark/callable.py +8 -17
  22. snowflake/cli/api/project/schemas/streamlit/streamlit.py +2 -2
  23. snowflake/cli/api/project/schemas/updatable_model.py +2 -0
  24. snowflake/cli/api/project/util.py +2 -0
  25. snowflake/cli/api/secure_path.py +2 -0
  26. snowflake/cli/api/sql_execution.py +14 -54
  27. snowflake/cli/api/utils/cursor.py +2 -0
  28. snowflake/cli/api/utils/models.py +23 -0
  29. snowflake/cli/api/utils/naming_utils.py +0 -27
  30. snowflake/cli/api/utils/rendering.py +178 -23
  31. snowflake/cli/app/api_impl/plugin/plugin_config_provider_impl.py +2 -0
  32. snowflake/cli/app/cli_app.py +4 -1
  33. snowflake/cli/app/commands_registration/builtin_plugins.py +8 -0
  34. snowflake/cli/app/commands_registration/command_plugins_loader.py +2 -0
  35. snowflake/cli/app/commands_registration/commands_registration_with_callbacks.py +2 -0
  36. snowflake/cli/app/commands_registration/typer_registration.py +2 -0
  37. snowflake/cli/app/dev/pycharm_remote_debug.py +2 -0
  38. snowflake/cli/app/loggers.py +2 -0
  39. snowflake/cli/app/main_typer.py +1 -1
  40. snowflake/cli/app/printing.py +3 -1
  41. snowflake/cli/app/snow_connector.py +2 -2
  42. snowflake/cli/plugins/connection/commands.py +5 -14
  43. snowflake/cli/plugins/connection/util.py +1 -1
  44. snowflake/cli/plugins/cortex/__init__.py +0 -0
  45. snowflake/cli/plugins/cortex/commands.py +312 -0
  46. snowflake/cli/plugins/cortex/constants.py +3 -0
  47. snowflake/cli/plugins/cortex/manager.py +175 -0
  48. snowflake/cli/plugins/cortex/plugin_spec.py +16 -0
  49. snowflake/cli/plugins/cortex/types.py +8 -0
  50. snowflake/cli/plugins/git/commands.py +15 -0
  51. snowflake/cli/plugins/nativeapp/artifacts.py +368 -123
  52. snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +45 -0
  53. snowflake/cli/plugins/nativeapp/codegen/compiler.py +104 -0
  54. snowflake/cli/plugins/nativeapp/codegen/sandbox.py +2 -0
  55. snowflake/cli/plugins/nativeapp/codegen/snowpark/callback_source.py.jinja +181 -0
  56. snowflake/cli/plugins/nativeapp/codegen/snowpark/extension_function_utils.py +196 -0
  57. snowflake/cli/plugins/nativeapp/codegen/snowpark/models.py +47 -0
  58. snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +489 -0
  59. snowflake/cli/plugins/nativeapp/commands.py +11 -4
  60. snowflake/cli/plugins/nativeapp/common_flags.py +12 -5
  61. snowflake/cli/plugins/nativeapp/constants.py +1 -0
  62. snowflake/cli/plugins/nativeapp/manager.py +49 -16
  63. snowflake/cli/plugins/nativeapp/policy.py +2 -0
  64. snowflake/cli/plugins/nativeapp/run_processor.py +28 -10
  65. snowflake/cli/plugins/nativeapp/teardown_processor.py +78 -8
  66. snowflake/cli/plugins/nativeapp/utils.py +7 -6
  67. snowflake/cli/plugins/nativeapp/version/commands.py +6 -5
  68. snowflake/cli/plugins/nativeapp/version/version_processor.py +2 -0
  69. snowflake/cli/plugins/notebook/commands.py +21 -0
  70. snowflake/cli/plugins/notebook/exceptions.py +6 -0
  71. snowflake/cli/plugins/notebook/manager.py +46 -3
  72. snowflake/cli/plugins/notebook/types.py +2 -0
  73. snowflake/cli/plugins/object/command_aliases.py +80 -0
  74. snowflake/cli/plugins/object/commands.py +10 -6
  75. snowflake/cli/plugins/object/common.py +2 -0
  76. snowflake/cli/plugins/object_stage_deprecated/__init__.py +1 -0
  77. snowflake/cli/plugins/object_stage_deprecated/plugin_spec.py +20 -0
  78. snowflake/cli/plugins/snowpark/commands.py +62 -6
  79. snowflake/cli/plugins/snowpark/common.py +17 -6
  80. snowflake/cli/plugins/spcs/compute_pool/commands.py +22 -1
  81. snowflake/cli/plugins/spcs/compute_pool/manager.py +2 -0
  82. snowflake/cli/plugins/spcs/image_repository/commands.py +25 -1
  83. snowflake/cli/plugins/spcs/image_repository/manager.py +3 -1
  84. snowflake/cli/plugins/spcs/services/commands.py +39 -5
  85. snowflake/cli/plugins/spcs/services/manager.py +2 -0
  86. snowflake/cli/plugins/sql/commands.py +13 -5
  87. snowflake/cli/plugins/sql/manager.py +40 -19
  88. snowflake/cli/plugins/stage/commands.py +29 -3
  89. snowflake/cli/plugins/stage/diff.py +2 -0
  90. snowflake/cli/plugins/streamlit/commands.py +26 -10
  91. snowflake/cli/plugins/streamlit/manager.py +9 -10
  92. {snowflake_cli_labs-2.3.0rc1.dist-info → snowflake_cli_labs-2.4.0rc0.dist-info}/METADATA +4 -2
  93. {snowflake_cli_labs-2.3.0rc1.dist-info → snowflake_cli_labs-2.4.0rc0.dist-info}/RECORD +97 -77
  94. /snowflake/cli/plugins/{object/stage_deprecated → object_stage_deprecated}/commands.py +0 -0
  95. {snowflake_cli_labs-2.3.0rc1.dist-info → snowflake_cli_labs-2.4.0rc0.dist-info}/WHEEL +0 -0
  96. {snowflake_cli_labs-2.3.0rc1.dist-info → snowflake_cli_labs-2.4.0rc0.dist-info}/entry_points.txt +0 -0
  97. {snowflake_cli_labs-2.3.0rc1.dist-info → snowflake_cli_labs-2.4.0rc0.dist-info}/licenses/LICENSE +0 -0
@@ -5,9 +5,10 @@ from abc import ABC, abstractmethod
5
5
  from functools import cached_property
6
6
  from pathlib import Path
7
7
  from textwrap import dedent
8
- from typing import List, Optional
8
+ from typing import List, Optional, TypedDict
9
9
 
10
10
  import jinja2
11
+ from click import ClickException
11
12
  from snowflake.cli.api.console import cli_console as cc
12
13
  from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
13
14
  from snowflake.cli.api.project.definition import (
@@ -24,13 +25,15 @@ from snowflake.cli.api.project.util import (
24
25
  from snowflake.cli.api.sql_execution import SqlExecutionMixin
25
26
  from snowflake.cli.plugins.connection.util import make_snowsight_url
26
27
  from snowflake.cli.plugins.nativeapp.artifacts import (
27
- ArtifactDeploymentMap,
28
28
  ArtifactMapping,
29
+ BundleMap,
29
30
  build_bundle,
30
31
  resolve_without_follow,
31
- source_path_to_deploy_path,
32
32
  translate_artifact,
33
33
  )
34
+ from snowflake.cli.plugins.nativeapp.codegen.compiler import (
35
+ NativeAppCompiler,
36
+ )
34
37
  from snowflake.cli.plugins.nativeapp.constants import (
35
38
  ALLOWED_SPECIAL_COMMENTS,
36
39
  COMMENT_COL,
@@ -47,6 +50,7 @@ from snowflake.cli.plugins.nativeapp.exceptions import (
47
50
  MissingPackageScriptError,
48
51
  UnexpectedOwnerError,
49
52
  )
53
+ from snowflake.cli.plugins.nativeapp.feature_flags import FeatureFlag
50
54
  from snowflake.cli.plugins.nativeapp.utils import verify_exists, verify_no_directories
51
55
  from snowflake.cli.plugins.stage.diff import (
52
56
  DiffResult,
@@ -59,6 +63,8 @@ from snowflake.cli.plugins.stage.diff import (
59
63
  from snowflake.connector import ProgrammingError
60
64
  from snowflake.connector.cursor import DictCursor
61
65
 
66
+ ApplicationOwnedObject = TypedDict("ApplicationOwnedObject", {"name": str, "type": str})
67
+
62
68
 
63
69
  def generic_sql_error_handler(
64
70
  err: ProgrammingError, role: Optional[str] = None, warehouse: Optional[str] = None
@@ -159,6 +165,10 @@ class NativeAppManager(SqlExecutionMixin):
159
165
  def deploy_root(self) -> Path:
160
166
  return Path(self.project_root, self.definition.deploy_root)
161
167
 
168
+ @cached_property
169
+ def generated_root(self) -> Path:
170
+ return Path(self.deploy_root, self.definition.generated_root)
171
+
162
172
  @cached_property
163
173
  def package_scripts(self) -> List[str]:
164
174
  """
@@ -305,11 +315,20 @@ class NativeAppManager(SqlExecutionMixin):
305
315
  return False
306
316
  return True
307
317
 
308
- def build_bundle(self) -> ArtifactDeploymentMap:
318
+ def build_bundle(self) -> BundleMap:
309
319
  """
310
320
  Populates the local deploy root from artifact sources.
311
321
  """
312
- return build_bundle(self.project_root, self.deploy_root, self.artifacts)
322
+ mapped_files = build_bundle(self.project_root, self.deploy_root, self.artifacts)
323
+ if FeatureFlag.ENABLE_SETUP_SCRIPT_GENERATION.is_enabled():
324
+ compiler = NativeAppCompiler(
325
+ project_definition=self._project_definition,
326
+ project_root=self.project_root,
327
+ deploy_root=self.deploy_root,
328
+ generated_root=self.generated_root,
329
+ )
330
+ compiler.compile_artifacts()
331
+ return mapped_files
313
332
 
314
333
  def sync_deploy_root_with_stage(
315
334
  self,
@@ -317,7 +336,7 @@ class NativeAppManager(SqlExecutionMixin):
317
336
  prune: bool,
318
337
  recursive: bool,
319
338
  local_paths_to_sync: List[Path] | None = None,
320
- mapped_files: Optional[ArtifactDeploymentMap] = None,
339
+ bundle_map: Optional[BundleMap] = None,
321
340
  ) -> DiffResult:
322
341
  """
323
342
  Ensures that the files on our remote stage match the artifacts we have in
@@ -329,7 +348,7 @@ class NativeAppManager(SqlExecutionMixin):
329
348
  recursive (bool): Whether to traverse directories recursively.
330
349
  local_paths_to_sync (List[Path], optional): List of local paths to sync. Defaults to None to sync all
331
350
  local paths. Note that providing an empty list here is equivalent to None.
332
- mapped_files: the file mapping computed during the `bundle` step. Required when local_paths_to_sync is
351
+ bundle_map: the artifact mapping computed during the `bundle` step. Required when local_paths_to_sync is
333
352
  provided.
334
353
 
335
354
  Returns:
@@ -359,7 +378,7 @@ class NativeAppManager(SqlExecutionMixin):
359
378
 
360
379
  files_not_removed = []
361
380
  if local_paths_to_sync:
362
- assert mapped_files is not None
381
+ assert bundle_map is not None
363
382
 
364
383
  # Deploying specific files/directories
365
384
  resolved_paths_to_sync = [
@@ -367,13 +386,17 @@ class NativeAppManager(SqlExecutionMixin):
367
386
  ]
368
387
  if not recursive:
369
388
  verify_no_directories(resolved_paths_to_sync)
370
- deploy_paths_to_sync = [
371
- source_path_to_deploy_path(p, mapped_files)
372
- for p in resolved_paths_to_sync
373
- ]
374
- verify_exists(deploy_paths_to_sync)
389
+
390
+ deploy_paths_to_sync = []
391
+ for resolved_path in resolved_paths_to_sync:
392
+ verify_exists(resolved_path)
393
+ deploy_paths = bundle_map.to_deploy_paths(resolved_path)
394
+ if not deploy_paths:
395
+ raise ClickException(f"No artifact found for {resolved_path}")
396
+ deploy_paths_to_sync.extend(deploy_paths)
397
+
375
398
  stage_paths_to_sync = _get_stage_paths_to_sync(
376
- deploy_paths_to_sync, self.deploy_root.resolve()
399
+ deploy_paths_to_sync, resolve_without_follow(self.deploy_root)
377
400
  )
378
401
  diff = preserve_from_diff(diff, stage_paths_to_sync)
379
402
  else:
@@ -429,6 +452,16 @@ class NativeAppManager(SqlExecutionMixin):
429
452
  "application packages", self.package_name, name_col=NAME_COL
430
453
  )
431
454
 
455
+ def get_objects_owned_by_application(self) -> List[ApplicationOwnedObject]:
456
+ """
457
+ Returns all application objects owned by this application.
458
+ """
459
+ with self.use_role(self.app_role):
460
+ results = self._execute_query(
461
+ f"show objects owned by application {self.app_name}"
462
+ ).fetchall()
463
+ return [{"name": row[1], "type": row[2]} for row in results]
464
+
432
465
  def get_snowsight_url(self) -> str:
433
466
  """Returns the URL that can be used to visit this app via Snowsight."""
434
467
  name = unquote_identifier(self.app_name)
@@ -522,7 +555,7 @@ class NativeAppManager(SqlExecutionMixin):
522
555
  prune: bool,
523
556
  recursive: bool,
524
557
  local_paths_to_sync: List[Path] | None = None,
525
- mapped_files: Optional[ArtifactDeploymentMap] = None,
558
+ bundle_map: Optional[BundleMap] = None,
526
559
  ) -> DiffResult:
527
560
  """app deploy process"""
528
561
 
@@ -535,7 +568,7 @@ class NativeAppManager(SqlExecutionMixin):
535
568
 
536
569
  # 3. Upload files from deploy root local folder to the above stage
537
570
  diff = self.sync_deploy_root_with_stage(
538
- self.package_role, prune, recursive, local_paths_to_sync, mapped_files
571
+ self.package_role, prune, recursive, local_paths_to_sync, bundle_map
539
572
  )
540
573
 
541
574
  return diff
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from abc import ABC, abstractmethod
2
4
  from typing import Optional
3
5
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pathlib import Path
2
4
  from textwrap import dedent
3
5
  from typing import Optional
@@ -7,10 +9,16 @@ from click import UsageError
7
9
  from snowflake.cli.api.console import cli_console as cc
8
10
  from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
9
11
  from snowflake.cli.api.project.schemas.native_app.native_app import NativeApp
12
+ from snowflake.cli.api.project.util import (
13
+ identifier_to_show_like_pattern,
14
+ unquote_identifier,
15
+ )
16
+ from snowflake.cli.api.utils.cursor import find_all_rows
10
17
  from snowflake.cli.plugins.nativeapp.constants import (
11
18
  ALLOWED_SPECIAL_COMMENTS,
12
19
  COMMENT_COL,
13
20
  LOOSE_FILES_MAGIC_VERSION,
21
+ PATCH_COL,
14
22
  SPECIAL_COMMENT,
15
23
  VERSION_COL,
16
24
  )
@@ -28,7 +36,7 @@ from snowflake.cli.plugins.nativeapp.policy import PolicyBase
28
36
  from snowflake.cli.plugins.stage.diff import DiffResult
29
37
  from snowflake.cli.plugins.stage.manager import StageManager
30
38
  from snowflake.connector import ProgrammingError
31
- from snowflake.connector.cursor import SnowflakeCursor
39
+ from snowflake.connector.cursor import DictCursor, SnowflakeCursor
32
40
 
33
41
  UPGRADE_RESTRICTION_CODES = {93044, 93055, 93045, 93046}
34
42
 
@@ -134,24 +142,34 @@ class NativeAppRunProcessor(NativeAppManager, NativeAppCommandProcessor):
134
142
 
135
143
  def get_existing_version_info(self, version: str) -> Optional[dict]:
136
144
  """
137
- Get an existing version, if defined, by the same name in an application package.
138
- It executes a 'show versions like ... in application package' query and returns the result as single row, if one exists.
145
+ Get the latest patch on an existing version by name in the application package.
146
+ Executes 'show versions like ... in application package' query and returns
147
+ the latest patch in the version as a single row, if one exists. Otherwise,
148
+ returns None.
139
149
  """
140
150
  with self.use_role(self.package_role):
141
151
  try:
142
- version_obj = self.show_specific_object(
143
- "versions",
144
- version,
145
- name_col=VERSION_COL,
146
- in_clause=f"in application package {self.package_name}",
152
+ query = f"show versions like {identifier_to_show_like_pattern(version)} in application package {self.package_name}"
153
+ cursor = self._execute_query(query, cursor_class=DictCursor)
154
+
155
+ if cursor.rowcount is None:
156
+ raise SnowflakeSQLExecutionError(query)
157
+
158
+ matching_rows = find_all_rows(
159
+ cursor, lambda row: row[VERSION_COL] == unquote_identifier(version)
147
160
  )
161
+
162
+ if not matching_rows:
163
+ return None
164
+
165
+ return max(matching_rows, key=lambda row: row[PATCH_COL])
166
+
148
167
  except ProgrammingError as err:
149
168
  if err.msg.__contains__("does not exist or not authorized"):
150
169
  raise ApplicationPackageDoesNotExistError(self.package_name)
151
170
  else:
152
171
  generic_sql_error_handler(err=err, role=self.package_role)
153
-
154
- return version_obj
172
+ return None
155
173
 
156
174
  def drop_application_before_upgrade(self, policy: PolicyBase, is_interactive: bool):
157
175
  """
@@ -1,6 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  from pathlib import Path
2
4
  from textwrap import dedent
3
- from typing import Dict
5
+ from typing import Dict, Optional
4
6
 
5
7
  import typer
6
8
  from snowflake.cli.api.console import cli_console as cc
@@ -16,11 +18,14 @@ from snowflake.cli.plugins.nativeapp.exceptions import (
16
18
  CouldNotDropApplicationPackageWithVersions,
17
19
  )
18
20
  from snowflake.cli.plugins.nativeapp.manager import (
21
+ ApplicationOwnedObject,
19
22
  NativeAppCommandProcessor,
20
23
  NativeAppManager,
21
24
  ensure_correct_owner,
22
25
  )
23
- from snowflake.cli.plugins.nativeapp.utils import needs_confirmation
26
+ from snowflake.cli.plugins.nativeapp.utils import (
27
+ needs_confirmation,
28
+ )
24
29
  from snowflake.connector.cursor import DictCursor
25
30
 
26
31
 
@@ -28,13 +33,17 @@ class NativeAppTeardownProcessor(NativeAppManager, NativeAppCommandProcessor):
28
33
  def __init__(self, project_definition: Dict, project_root: Path):
29
34
  super().__init__(project_definition, project_root)
30
35
 
31
- def drop_generic_object(self, object_type: str, object_name: str, role: str):
36
+ def drop_generic_object(
37
+ self, object_type: str, object_name: str, role: str, cascade: bool = False
38
+ ):
32
39
  """
33
40
  Drop object using the given role.
34
41
  """
35
42
  with self.use_role(role):
36
43
  cc.step(f"Dropping {object_type} {object_name} now.")
37
44
  drop_query = f"drop {object_type} {object_name}"
45
+ if cascade:
46
+ drop_query += " cascade"
38
47
  try:
39
48
  self._execute_query(drop_query)
40
49
  except:
@@ -42,7 +51,23 @@ class NativeAppTeardownProcessor(NativeAppManager, NativeAppCommandProcessor):
42
51
 
43
52
  cc.message(f"Dropped {object_type} {object_name} successfully.")
44
53
 
45
- def drop_application(self, auto_yes: bool):
54
+ def _application_objects_to_str(
55
+ self, application_objects: ApplicationOwnedObject
56
+ ) -> str:
57
+ """
58
+ Returns a list in an "(Object Type) Object Name" format. Database-level and schema-level object names are fully qualified:
59
+ (COMPUTE_POOL) POOL_NAME
60
+ (DATABASE) DB_NAME
61
+ (SCHEMA) DB_NAME.PUBLIC
62
+ ...
63
+ """
64
+ return "\n".join(
65
+ [f"({obj['type']}) {obj['name']}" for obj in application_objects]
66
+ )
67
+
68
+ def drop_application(
69
+ self, auto_yes: bool, interactive: bool = False, cascade: Optional[bool] = None
70
+ ):
46
71
  """
47
72
  Attempts to drop the application object if all validations and user prompts allow so.
48
73
  """
@@ -89,9 +114,45 @@ class NativeAppTeardownProcessor(NativeAppManager, NativeAppCommandProcessor):
89
114
  cc.message(f"Did not drop application object {self.app_name}.")
90
115
  return # The user desires to keep the app, therefore exit gracefully
91
116
 
92
- # 4. All validations have passed, drop object
117
+ # 4. Check for application objects owned by the application
118
+ application_objects = self.get_objects_owned_by_application()
119
+ if len(application_objects) > 0:
120
+ application_objects_str = self._application_objects_to_str(
121
+ application_objects
122
+ )
123
+ if cascade is True:
124
+ cc.message(
125
+ f"The following objects are owned by application {self.app_name} and will be dropped:\n{application_objects_str}"
126
+ )
127
+ elif cascade is False:
128
+ cc.message(
129
+ f"The following objects are owned by application {self.app_name}:\n{application_objects_str}"
130
+ )
131
+ elif interactive:
132
+ if interactive:
133
+ api_integration = typer.prompt(
134
+ f"The following objects are owned by application {self.app_name}:\n{application_objects_str}\n\nWould you like to drop these objects in addition to the application? [y/n/ABORT]"
135
+ )
136
+ if api_integration in ["y", "yes", "Y", "Yes", "YES"]:
137
+ cascade = True
138
+ elif api_integration in ["n", "no", "N", "No", "NO"]:
139
+ cascade = False
140
+ else:
141
+ raise typer.Abort()
142
+ else:
143
+ cc.message(
144
+ f"The following application objects are owned by application {self.app_name}:\n{application_objects_str}\n\nRe-run teardown again with --cascade or --no-cascade to specify whether these objects should be dropped along with the application."
145
+ )
146
+ raise typer.Abort()
147
+ elif cascade is None:
148
+ cascade = False
149
+
150
+ # 5. All validations have passed, drop object
93
151
  self.drop_generic_object(
94
- object_type="application", object_name=self.app_name, role=self.app_role
152
+ object_type="application",
153
+ object_name=self.app_name,
154
+ role=self.app_role,
155
+ cascade=cascade,
95
156
  )
96
157
  return # The application object was successfully dropped, therefore exit gracefully
97
158
 
@@ -176,10 +237,19 @@ class NativeAppTeardownProcessor(NativeAppManager, NativeAppCommandProcessor):
176
237
  )
177
238
  return # The application package was successfully dropped, therefore exit gracefully
178
239
 
179
- def process(self, force_drop: bool = False, *args, **kwargs):
240
+ def process(
241
+ self,
242
+ interactive: bool,
243
+ force_drop: bool = False,
244
+ cascade: Optional[bool] = None,
245
+ *args,
246
+ **kwargs,
247
+ ):
180
248
 
181
249
  # Drop the application object
182
- self.drop_application(auto_yes=force_drop)
250
+ self.drop_application(
251
+ auto_yes=force_drop, interactive=interactive, cascade=cascade
252
+ )
183
253
 
184
254
  # Drop the application package
185
255
  self.drop_package(auto_yes=force_drop)
@@ -1,4 +1,6 @@
1
- from os import PathLike
1
+ from __future__ import annotations
2
+
3
+ import os
2
4
  from pathlib import Path
3
5
  from sys import stdin, stdout
4
6
  from typing import List, Optional, Union
@@ -43,7 +45,7 @@ def get_first_paragraph_from_markdown_file(file_path: Path) -> Optional[str]:
43
45
  return paragraph_text
44
46
 
45
47
 
46
- def shallow_git_clone(url: Union[str, PathLike], to_path: Union[str, PathLike]):
48
+ def shallow_git_clone(url: Union[str, os.PathLike], to_path: Union[str, os.PathLike]):
47
49
  """
48
50
  Performs a shallow clone of the repository at the provided url to the path specified
49
51
 
@@ -77,7 +79,6 @@ def verify_no_directories(paths_to_sync: List[Path]):
77
79
  )
78
80
 
79
81
 
80
- def verify_exists(paths_to_sync: List[Path]):
81
- for path in paths_to_sync:
82
- if not path.exists():
83
- raise ClickException(f"The following path does not exist: {path}")
82
+ def verify_exists(path: Path):
83
+ if not path.exists():
84
+ raise ClickException(f"The following path does not exist: {path}")
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  from typing import Optional
3
5
 
@@ -16,7 +18,6 @@ from snowflake.cli.plugins.nativeapp.policy import (
16
18
  DenyAlwaysPolicy,
17
19
  )
18
20
  from snowflake.cli.plugins.nativeapp.run_processor import NativeAppRunProcessor
19
- from snowflake.cli.plugins.nativeapp.utils import is_tty_interactive
20
21
  from snowflake.cli.plugins.nativeapp.version.version_processor import (
21
22
  NativeAppVersionCreateProcessor,
22
23
  NativeAppVersionDropProcessor,
@@ -49,7 +50,7 @@ def create(
49
50
  help="When enabled, the Snowflake CLI skips checking if your project has any untracked or stages files in git. Default: unset.",
50
51
  is_flag=True,
51
52
  ),
52
- interactive: Optional[bool] = InteractiveOption,
53
+ interactive: bool = InteractiveOption,
53
54
  force: Optional[bool] = ForceOption,
54
55
  **options,
55
56
  ) -> CommandResult:
@@ -62,7 +63,7 @@ def create(
62
63
  is_interactive = False
63
64
  if force:
64
65
  policy = AllowAlwaysPolicy()
65
- elif interactive or is_tty_interactive():
66
+ elif interactive:
66
67
  is_interactive = True
67
68
  policy = AskAlwaysPolicy()
68
69
  else:
@@ -113,7 +114,7 @@ def drop(
113
114
  None,
114
115
  help="Version defined in an application package that you want to drop. Defaults to the version specified in the `manifest.yml` file.",
115
116
  ),
116
- interactive: Optional[bool] = InteractiveOption,
117
+ interactive: bool = InteractiveOption,
117
118
  force: Optional[bool] = ForceOption,
118
119
  **options,
119
120
  ) -> CommandResult:
@@ -124,7 +125,7 @@ def drop(
124
125
  is_interactive = False
125
126
  if force:
126
127
  policy = AllowAlwaysPolicy()
127
- elif interactive or is_tty_interactive():
128
+ elif interactive:
128
129
  is_interactive = True
129
130
  policy = AskAlwaysPolicy()
130
131
  else:
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pathlib import Path
2
4
  from textwrap import dedent
3
5
  from typing import Dict, List, Optional
@@ -6,6 +6,8 @@ from snowflake.cli.api.commands.snow_typer import SnowTyper
6
6
  from snowflake.cli.api.feature_flags import FeatureFlag
7
7
  from snowflake.cli.api.output.types import MessageResult
8
8
  from snowflake.cli.plugins.notebook.manager import NotebookManager
9
+ from snowflake.cli.plugins.notebook.types import NotebookName, NotebookStagePath
10
+ from typing_extensions import Annotated
9
11
 
10
12
  app = SnowTyper(
11
13
  name="notebook",
@@ -15,6 +17,11 @@ app = SnowTyper(
15
17
  log = logging.getLogger(__name__)
16
18
 
17
19
  NOTEBOOK_IDENTIFIER = identifier_argument(sf_object="notebook", example="MY_NOTEBOOK")
20
+ NotebookFile: NotebookStagePath = typer.Option(
21
+ "--notebook-file",
22
+ "-f",
23
+ help="Stage path with notebook file. For example `@stage/path/to/notebook.ipynb`",
24
+ )
18
25
 
19
26
 
20
27
  @app.command(requires_connection=True)
@@ -49,3 +56,17 @@ def open_cmd(
49
56
  url = NotebookManager().get_url(notebook_name=identifier)
50
57
  typer.launch(url)
51
58
  return MessageResult(message=url)
59
+
60
+
61
+ @app.command(requires_connection=True)
62
+ def create(
63
+ identifier: Annotated[NotebookName, NOTEBOOK_IDENTIFIER],
64
+ notebook_file: Annotated[NotebookStagePath, NotebookFile],
65
+ **options,
66
+ ):
67
+ """Creates notebook from stage."""
68
+ notebook_url = NotebookManager().create(
69
+ notebook_name=identifier,
70
+ notebook_file=notebook_file,
71
+ )
72
+ return MessageResult(message=notebook_url)
@@ -0,0 +1,6 @@
1
+ from click.exceptions import ClickException
2
+
3
+
4
+ class NotebookStagePathError(ClickException):
5
+ def __init__(self, path: str):
6
+ super().__init__(f"Cannot extract notebook file name from {path=}")
@@ -1,14 +1,57 @@
1
+ from pathlib import Path
2
+ from textwrap import dedent
3
+
4
+ from snowflake.cli.api.cli_global_context import cli_context
5
+ from snowflake.cli.api.identifiers import FQN
1
6
  from snowflake.cli.api.sql_execution import SqlExecutionMixin
2
7
  from snowflake.cli.plugins.connection.util import make_snowsight_url
8
+ from snowflake.cli.plugins.notebook.exceptions import NotebookStagePathError
9
+ from snowflake.cli.plugins.notebook.types import NotebookName, NotebookStagePath
3
10
 
4
11
 
5
12
  class NotebookManager(SqlExecutionMixin):
6
- def execute(self, notebook_name: str):
13
+ def execute(self, notebook_name: NotebookName):
7
14
  query = f"EXECUTE NOTEBOOK {notebook_name}()"
8
15
  return self._execute_query(query=query)
9
16
 
10
- def get_url(self, notebook_name: str):
17
+ def get_url(self, notebook_name: NotebookName):
18
+ fqn = FQN.from_string(notebook_name).using_connection(self._conn)
11
19
  return make_snowsight_url(
12
20
  self._conn,
13
- f"/#/notebooks/{self.qualified_name_for_url(notebook_name)}",
21
+ f"/#/notebooks/{fqn.url_identifier}",
22
+ )
23
+
24
+ @staticmethod
25
+ def parse_stage_as_path(notebook_file: NotebookName) -> Path:
26
+ """Parses notebook file path to pathlib.Path."""
27
+ if not notebook_file.endswith(".ipynb"):
28
+ raise NotebookStagePathError(notebook_file)
29
+ stage_path = Path(notebook_file)
30
+ if len(stage_path.parts) < 2:
31
+ raise NotebookStagePathError(notebook_file)
32
+
33
+ return stage_path
34
+
35
+ def create(
36
+ self,
37
+ notebook_name: NotebookName,
38
+ notebook_file: NotebookStagePath,
39
+ ) -> str:
40
+ notebook_fqn = FQN.from_string(notebook_name).using_connection(self._conn)
41
+ stage_path = self.parse_stage_as_path(notebook_file)
42
+
43
+ queries = dedent(
44
+ f"""
45
+ CREATE OR REPLACE NOTEBOOK {notebook_fqn.identifier}
46
+ FROM '{stage_path.parent}'
47
+ QUERY_WAREHOUSE = '{cli_context.connection.warehouse}'
48
+ MAIN_FILE = '{stage_path.name}';
49
+
50
+ ALTER NOTEBOOK {notebook_fqn.identifier} ADD LIVE VERSION FROM LAST;
51
+ """
52
+ )
53
+ self._execute_queries(queries=queries)
54
+
55
+ return make_snowsight_url(
56
+ self._conn, f"/#/notebooks/{notebook_fqn.url_identifier}"
14
57
  )
@@ -0,0 +1,2 @@
1
+ NotebookName = str
2
+ NotebookStagePath = str
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Optional, Tuple
4
+
5
+ import typer
6
+ from click import ClickException
7
+ from snowflake.cli.api.commands.snow_typer import SnowTyper
8
+ from snowflake.cli.api.constants import ObjectType
9
+ from snowflake.cli.plugins.object.commands import (
10
+ ScopeOption,
11
+ describe,
12
+ drop,
13
+ list_,
14
+ scope_option, # noqa: F401
15
+ )
16
+
17
+
18
+ def add_object_command_aliases(
19
+ app: SnowTyper,
20
+ object_type: ObjectType,
21
+ name_argument: typer.Argument,
22
+ like_option: Optional[typer.Option],
23
+ scope_option: Optional[typer.Option],
24
+ ommit_commands: List[str] = [],
25
+ ):
26
+ if "list" not in ommit_commands:
27
+ if not like_option:
28
+ raise ClickException('[like_option] have to be defined for "list" command')
29
+
30
+ if not scope_option:
31
+
32
+ @app.command("list", requires_connection=True)
33
+ def list_cmd(like: str = like_option, **options): # type: ignore
34
+ list_(
35
+ object_type=object_type.value.cli_name,
36
+ like=like,
37
+ scope=ScopeOption.default,
38
+ **options,
39
+ )
40
+
41
+ else:
42
+
43
+ @app.command("list", requires_connection=True)
44
+ def list_cmd(
45
+ like: str = like_option, # type: ignore
46
+ scope: Tuple[str, str] = scope_option, # type: ignore
47
+ **options,
48
+ ):
49
+ list_(
50
+ object_type=object_type.value.cli_name,
51
+ like=like,
52
+ scope=scope,
53
+ **options,
54
+ )
55
+
56
+ list_cmd.__doc__ = f"Lists all available {object_type.value.sf_plural_name}."
57
+
58
+ if "drop" not in ommit_commands:
59
+
60
+ @app.command("drop", requires_connection=True)
61
+ def drop_cmd(name: str = name_argument, **options):
62
+ drop(
63
+ object_type=object_type.value.cli_name,
64
+ object_name=name,
65
+ **options,
66
+ )
67
+
68
+ drop_cmd.__doc__ = f"Drops {object_type.value.sf_name} with given name."
69
+
70
+ if "describe" not in ommit_commands:
71
+
72
+ @app.command("describe", requires_connection=True)
73
+ def describe_cmd(name: str = name_argument, **options):
74
+ describe(
75
+ object_type=object_type.value.cli_name,
76
+ object_name=name,
77
+ **options,
78
+ )
79
+
80
+ describe_cmd.__doc__ = f"Provides description of {object_type.value.sf_name}."