snowflake-cli-labs 2.3.1__py3-none-any.whl → 2.4.0rc1__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 (96) 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/manager.py +49 -16
  62. snowflake/cli/plugins/nativeapp/policy.py +2 -0
  63. snowflake/cli/plugins/nativeapp/run_processor.py +2 -0
  64. snowflake/cli/plugins/nativeapp/teardown_processor.py +80 -8
  65. snowflake/cli/plugins/nativeapp/utils.py +7 -6
  66. snowflake/cli/plugins/nativeapp/version/commands.py +6 -5
  67. snowflake/cli/plugins/nativeapp/version/version_processor.py +2 -0
  68. snowflake/cli/plugins/notebook/commands.py +21 -0
  69. snowflake/cli/plugins/notebook/exceptions.py +6 -0
  70. snowflake/cli/plugins/notebook/manager.py +46 -3
  71. snowflake/cli/plugins/notebook/types.py +2 -0
  72. snowflake/cli/plugins/object/command_aliases.py +80 -0
  73. snowflake/cli/plugins/object/commands.py +10 -6
  74. snowflake/cli/plugins/object/common.py +2 -0
  75. snowflake/cli/plugins/object_stage_deprecated/__init__.py +1 -0
  76. snowflake/cli/plugins/object_stage_deprecated/plugin_spec.py +20 -0
  77. snowflake/cli/plugins/snowpark/commands.py +62 -6
  78. snowflake/cli/plugins/snowpark/common.py +17 -6
  79. snowflake/cli/plugins/spcs/compute_pool/commands.py +22 -1
  80. snowflake/cli/plugins/spcs/compute_pool/manager.py +2 -0
  81. snowflake/cli/plugins/spcs/image_repository/commands.py +25 -1
  82. snowflake/cli/plugins/spcs/image_repository/manager.py +3 -1
  83. snowflake/cli/plugins/spcs/services/commands.py +39 -5
  84. snowflake/cli/plugins/spcs/services/manager.py +2 -0
  85. snowflake/cli/plugins/sql/commands.py +13 -5
  86. snowflake/cli/plugins/sql/manager.py +40 -19
  87. snowflake/cli/plugins/stage/commands.py +29 -3
  88. snowflake/cli/plugins/stage/diff.py +2 -0
  89. snowflake/cli/plugins/streamlit/commands.py +26 -10
  90. snowflake/cli/plugins/streamlit/manager.py +9 -10
  91. {snowflake_cli_labs-2.3.1.dist-info → snowflake_cli_labs-2.4.0rc1.dist-info}/METADATA +4 -2
  92. {snowflake_cli_labs-2.3.1.dist-info → snowflake_cli_labs-2.4.0rc1.dist-info}/RECORD +96 -76
  93. /snowflake/cli/plugins/{object/stage_deprecated → object_stage_deprecated}/commands.py +0 -0
  94. {snowflake_cli_labs-2.3.1.dist-info → snowflake_cli_labs-2.4.0rc1.dist-info}/WHEEL +0 -0
  95. {snowflake_cli_labs-2.3.1.dist-info → snowflake_cli_labs-2.4.0rc1.dist-info}/entry_points.txt +0 -0
  96. {snowflake_cli_labs-2.3.1.dist-info → snowflake_cli_labs-2.4.0rc1.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
@@ -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,47 @@ 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
+ user_response = 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
+ show_default=False,
136
+ default="ABORT",
137
+ )
138
+ if user_response in ["y", "yes", "Y", "Yes", "YES"]:
139
+ cascade = True
140
+ elif user_response in ["n", "no", "N", "No", "NO"]:
141
+ cascade = False
142
+ else:
143
+ raise typer.Abort()
144
+ else:
145
+ cc.message(
146
+ 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."
147
+ )
148
+ raise typer.Abort()
149
+ elif cascade is None:
150
+ cascade = False
151
+
152
+ # 5. All validations have passed, drop object
93
153
  self.drop_generic_object(
94
- object_type="application", object_name=self.app_name, role=self.app_role
154
+ object_type="application",
155
+ object_name=self.app_name,
156
+ role=self.app_role,
157
+ cascade=cascade,
95
158
  )
96
159
  return # The application object was successfully dropped, therefore exit gracefully
97
160
 
@@ -176,10 +239,19 @@ class NativeAppTeardownProcessor(NativeAppManager, NativeAppCommandProcessor):
176
239
  )
177
240
  return # The application package was successfully dropped, therefore exit gracefully
178
241
 
179
- def process(self, force_drop: bool = False, *args, **kwargs):
242
+ def process(
243
+ self,
244
+ interactive: bool,
245
+ force_drop: bool = False,
246
+ cascade: Optional[bool] = None,
247
+ *args,
248
+ **kwargs,
249
+ ):
180
250
 
181
251
  # Drop the application object
182
- self.drop_application(auto_yes=force_drop)
252
+ self.drop_application(
253
+ auto_yes=force_drop, interactive=interactive, cascade=cascade
254
+ )
183
255
 
184
256
  # Drop the application package
185
257
  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}."
@@ -10,13 +10,11 @@ from snowflake.cli.api.constants import SUPPORTED_OBJECTS, VALID_SCOPES
10
10
  from snowflake.cli.api.output.types import QueryResult
11
11
  from snowflake.cli.api.project.util import is_valid_identifier
12
12
  from snowflake.cli.plugins.object.manager import ObjectManager
13
- from snowflake.cli.plugins.object.stage_deprecated.commands import app as stage_app
14
13
 
15
14
  app = SnowTyper(
16
15
  name="object",
17
16
  help="Manages Snowflake objects like warehouses and stages",
18
17
  )
19
- app.add_typer(stage_app)
20
18
 
21
19
 
22
20
  NameArgument = typer.Argument(help="Name of the object")
@@ -40,10 +38,16 @@ def _scope_validate(object_type: str, scope: Tuple[str, str]):
40
38
  raise ClickException("compute-pool scope is only supported for listing service")
41
39
 
42
40
 
43
- ScopeOption = typer.Option(
44
- (None, None),
45
- "--in",
46
- help="Specifies the scope of this command using '--in <scope> <name>' (e.g. list tables --in database my_db). Some object types have specialized scopes (e.g. list service --in compute-pool my_pool)",
41
+ def scope_option(help_example: str):
42
+ return typer.Option(
43
+ (None, None),
44
+ "--in",
45
+ help=f"Specifies the scope of this command using '--in <scope> <name>', for example {help_example}.",
46
+ )
47
+
48
+
49
+ ScopeOption = scope_option(
50
+ help_example="`list table --in database my_db`. Some object types have specialized scopes (e.g. list service --in compute-pool my_pool)"
47
51
  )
48
52
 
49
53
  SUPPORTED_TYPES_MSG = "\n\nSupported types: " + ", ".join(SUPPORTED_OBJECTS)
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from dataclasses import dataclass
2
4
  from typing import Optional
3
5
 
@@ -0,0 +1 @@
1
+ # TODO 3.0: remove this file
@@ -0,0 +1,20 @@
1
+ # TODO 3.0: remove this file
2
+
3
+ from snowflake.cli.api.plugins.command import (
4
+ CommandPath,
5
+ CommandSpec,
6
+ CommandType,
7
+ plugin_hook_impl,
8
+ )
9
+ from snowflake.cli.plugins.object_stage_deprecated.commands import (
10
+ app as stage_deprecated_app,
11
+ )
12
+
13
+
14
+ @plugin_hook_impl
15
+ def command_spec():
16
+ return CommandSpec(
17
+ parent_command_path=CommandPath(["object"]),
18
+ command_type=CommandType.COMMAND_GROUP,
19
+ typer_instance=stage_deprecated_app,
20
+ )