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
@@ -16,7 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  import inspect
18
18
  from functools import wraps
19
- from typing import Any, Dict, Optional, Type, TypeVar, Union
19
+ from typing import Optional, Type, TypeVar
20
20
 
21
21
  import typer
22
22
  from click import ClickException
@@ -29,35 +29,44 @@ from snowflake.cli.api.cli_global_context import (
29
29
  get_cli_context_manager,
30
30
  )
31
31
  from snowflake.cli.api.commands.decorators import _options_decorator_factory
32
+ from snowflake.cli.api.project.definition_conversion import (
33
+ convert_project_definition_to_v2,
34
+ )
32
35
  from snowflake.cli.api.project.schemas.entities.common import EntityModelBase
33
36
  from snowflake.cli.api.project.schemas.project_definition import (
34
- DefinitionV11,
35
37
  DefinitionV20,
38
+ ProjectDefinition,
39
+ ProjectDefinitionV1,
36
40
  )
37
- from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import PathMapping
38
- from snowflake.cli.api.utils.definition_rendering import render_definition_template
39
-
40
-
41
- def _convert_v2_artifact_to_v1_dict(
42
- v2_artifact: Union[PathMapping, str]
43
- ) -> Union[Dict, str]:
44
- if isinstance(v2_artifact, PathMapping):
45
- return {
46
- "src": v2_artifact.src,
47
- "dest": v2_artifact.dest,
48
- "processors": v2_artifact.processors,
49
- }
50
- return v2_artifact
51
41
 
52
-
53
- def _pdf_v2_to_v1(
42
+ APP_AND_PACKAGE_OPTIONS = [
43
+ inspect.Parameter(
44
+ "package_entity_id",
45
+ inspect.Parameter.KEYWORD_ONLY,
46
+ annotation=Optional[str],
47
+ default=typer.Option(
48
+ default="",
49
+ help="The ID of the package entity on which to operate when definition_version is 2 or higher.",
50
+ ),
51
+ ),
52
+ inspect.Parameter(
53
+ "app_entity_id",
54
+ inspect.Parameter.KEYWORD_ONLY,
55
+ annotation=Optional[str],
56
+ default=typer.Option(
57
+ default="",
58
+ help="The ID of the application entity on which to operate when definition_version is 2 or higher.",
59
+ ),
60
+ ),
61
+ ]
62
+
63
+
64
+ def _find_app_and_package_entities(
54
65
  v2_definition: DefinitionV20,
55
- package_entity_id: str = "",
56
- app_entity_id: str = "",
57
- app_required: bool = False,
58
- ) -> DefinitionV11:
59
- pdfv1: Dict[str, Any] = {"definition_version": "1.1", "native_app": {}}
60
-
66
+ package_entity_id: str,
67
+ app_entity_id: str,
68
+ app_required: bool,
69
+ ):
61
70
  # Determine the application entity to convert, there can be zero or one
62
71
  app_definition = find_entity(
63
72
  v2_definition,
@@ -66,7 +75,6 @@ def _pdf_v2_to_v1(
66
75
  disambiguation_option="--app-entity-id",
67
76
  required=app_required,
68
77
  )
69
-
70
78
  # Infer or verify the package if we have an app entity to convert
71
79
  if app_definition:
72
80
  target_package = app_definition.from_.target
@@ -87,7 +95,6 @@ def _pdf_v2_to_v1(
87
95
  ):
88
96
  # If the user didn't target a specific package entity, use the one the app entity targets
89
97
  package_entity_id = target_package
90
-
91
98
  # Determine the package entity to convert, there must be one
92
99
  app_package_definition = find_entity(
93
100
  v2_definition,
@@ -97,63 +104,7 @@ def _pdf_v2_to_v1(
97
104
  required=True,
98
105
  )
99
106
  assert app_package_definition is not None # satisfy mypy
100
-
101
- # NativeApp
102
- if app_definition and app_definition.fqn.identifier:
103
- pdfv1["native_app"]["name"] = app_definition.fqn.identifier
104
- else:
105
- pdfv1["native_app"]["name"] = app_package_definition.fqn.identifier.split(
106
- "_pkg_"
107
- )[0]
108
- pdfv1["native_app"]["artifacts"] = [
109
- _convert_v2_artifact_to_v1_dict(a) for a in app_package_definition.artifacts
110
- ]
111
- pdfv1["native_app"]["source_stage"] = app_package_definition.stage
112
- pdfv1["native_app"]["bundle_root"] = app_package_definition.bundle_root
113
- pdfv1["native_app"]["generated_root"] = app_package_definition.generated_root
114
- pdfv1["native_app"]["deploy_root"] = app_package_definition.deploy_root
115
- pdfv1["native_app"]["scratch_stage"] = app_package_definition.scratch_stage
116
-
117
- # Package
118
- pdfv1["native_app"]["package"] = {}
119
- pdfv1["native_app"]["package"]["name"] = app_package_definition.fqn.identifier
120
- if app_package_definition.distribution:
121
- pdfv1["native_app"]["package"][
122
- "distribution"
123
- ] = app_package_definition.distribution
124
- if app_package_definition.meta and app_package_definition.meta.post_deploy:
125
- pdfv1["native_app"]["package"][
126
- "post_deploy"
127
- ] = app_package_definition.meta.post_deploy
128
- if app_package_definition.meta:
129
- if app_package_definition.meta.role:
130
- pdfv1["native_app"]["package"]["role"] = app_package_definition.meta.role
131
- if app_package_definition.meta.warehouse:
132
- pdfv1["native_app"]["package"][
133
- "warehouse"
134
- ] = app_package_definition.meta.warehouse
135
-
136
- # Application
137
- if app_definition:
138
- pdfv1["native_app"]["application"] = {}
139
- pdfv1["native_app"]["application"]["name"] = app_definition.fqn.identifier
140
- if app_definition.debug:
141
- pdfv1["native_app"]["application"]["debug"] = app_definition.debug
142
- if app_definition.meta:
143
- if app_definition.meta.role:
144
- pdfv1["native_app"]["application"]["role"] = app_definition.meta.role
145
- if app_definition.meta.warehouse:
146
- pdfv1["native_app"]["application"][
147
- "warehouse"
148
- ] = app_definition.meta.warehouse
149
- if app_definition.meta.post_deploy:
150
- pdfv1["native_app"]["application"][
151
- "post_deploy"
152
- ] = app_definition.meta.post_deploy
153
-
154
- result = render_definition_template(pdfv1, {})
155
- # Override the definition object in global context
156
- return result.project_definition
107
+ return app_definition, app_package_definition
157
108
 
158
109
 
159
110
  T = TypeVar("T", bound=EntityModelBase)
@@ -209,54 +160,74 @@ def find_entity(
209
160
  return entity
210
161
 
211
162
 
212
- def nativeapp_definition_v2_to_v1(*, app_required: bool = False):
163
+ def force_project_definition_v2(
164
+ *, single_app_and_package: bool = True, app_required: bool = False
165
+ ):
213
166
  """
214
- A command decorator that attempts to automatically convert a native app project from
215
- definition v2 to v1.1. Assumes with_project_definition() has already been called.
216
- The definition object in CliGlobalContext will be replaced with the converted object.
217
- Exactly one application package entity type is expected, and up to one application
218
- entity type is expected.
167
+ A command decorator that forces the project definition to be converted to v2.
168
+
169
+ If a v1 definition is found, it is converted to v2 in-memory and the global context
170
+ is updated with the new definition object.
171
+
172
+ If a v2 definition is already found, it is used as-is, optionally limiting the number
173
+ of application and package entities to one each (true by default).
174
+
175
+ Assumes with_project_definition() has already been called.
219
176
  """
220
177
 
221
178
  def decorator(func):
222
179
  @wraps(func)
223
180
  def wrapper(*args, **kwargs):
224
- original_pdf: Optional[DefinitionV20] = get_cli_context().project_definition
181
+ cli_context = get_cli_context()
182
+ original_pdf: Optional[ProjectDefinition] = cli_context.project_definition
225
183
  if not original_pdf:
226
184
  raise ValueError(
227
- "Project definition could not be found. The nativeapp_definition_v2_to_v1 command decorator assumes with_project_definition() was called before it."
185
+ "Project definition could not be found. "
186
+ "The single_app_and_package() command decorator assumes "
187
+ "that with_project_definition() was called before it."
228
188
  )
229
- if original_pdf.definition_version == "2":
189
+ if isinstance(original_pdf, ProjectDefinitionV1):
190
+ pdfv2 = convert_project_definition_to_v2(
191
+ cli_context.project_root,
192
+ original_pdf,
193
+ accept_templates=False, # Templates should all be rendered by now
194
+ template_context=None, # Force inclusion of all fields
195
+ in_memory=True, # Convert the definition knowing it will be used immediately
196
+ )
197
+ for entity_id, entity in pdfv2.entities.items():
198
+ # Backfill kwargs for the command to use,
199
+ # there can only be one entity of each type
200
+ is_package = isinstance(entity, ApplicationPackageEntityModel)
201
+ key = "package_entity_id" if is_package else "app_entity_id"
202
+ kwargs[key] = entity_id
203
+
204
+ cm = get_cli_context_manager()
205
+
206
+ # Override the project definition so that the command operates on the new entities
207
+ cm.override_project_definition = pdfv2
208
+ elif single_app_and_package:
230
209
  package_entity_id = kwargs.get("package_entity_id", "")
231
210
  app_entity_id = kwargs.get("app_entity_id", "")
232
- pdfv1 = _pdf_v2_to_v1(
211
+ app_definition, app_package_definition = _find_app_and_package_entities(
233
212
  original_pdf, package_entity_id, app_entity_id, app_required
234
213
  )
235
- get_cli_context_manager().override_project_definition = pdfv1
214
+ entities_to_keep = {app_package_definition.entity_id}
215
+ kwargs["package_entity_id"] = app_package_definition.entity_id
216
+ if app_definition:
217
+ entities_to_keep.add(app_definition.entity_id)
218
+ kwargs["app_entity_id"] = app_definition.entity_id
219
+ for entity_id in list(original_pdf.entities):
220
+ if entity_id not in entities_to_keep:
221
+ # This happens after templates are rendered,
222
+ # so we can safely remove the entity
223
+ del original_pdf.entities[entity_id]
236
224
  return func(*args, **kwargs)
237
225
 
238
- return _options_decorator_factory(
239
- wrapper,
240
- additional_options=[
241
- inspect.Parameter(
242
- "package_entity_id",
243
- inspect.Parameter.KEYWORD_ONLY,
244
- annotation=Optional[str],
245
- default=typer.Option(
246
- default="",
247
- help="The ID of the package entity on which to operate when definition_version is 2 or higher.",
248
- ),
249
- ),
250
- inspect.Parameter(
251
- "app_entity_id",
252
- inspect.Parameter.KEYWORD_ONLY,
253
- annotation=Optional[str],
254
- default=typer.Option(
255
- default="",
256
- help="The ID of the application entity on which to operate when definition_version is 2 or higher.",
257
- ),
258
- ),
259
- ],
260
- )
226
+ if single_app_and_package:
227
+ # Add the --app-entity-id and --package-entity-id options to the command
228
+ return _options_decorator_factory(
229
+ wrapper, additional_options=APP_AND_PACKAGE_OPTIONS
230
+ )
231
+ return wrapper
261
232
 
262
233
  return decorator
@@ -18,23 +18,18 @@ import logging
18
18
  from typing import Optional
19
19
 
20
20
  import typer
21
- from click import MissingParameter
22
21
  from snowflake.cli._plugins.nativeapp.common_flags import ForceOption, InteractiveOption
23
- from snowflake.cli._plugins.nativeapp.run_processor import NativeAppRunProcessor
24
- from snowflake.cli._plugins.nativeapp.v2_conversions.v2_to_v1_decorator import (
25
- nativeapp_definition_v2_to_v1,
26
- )
27
- from snowflake.cli._plugins.nativeapp.version.version_processor import (
28
- NativeAppVersionCreateProcessor,
29
- NativeAppVersionDropProcessor,
22
+ from snowflake.cli._plugins.nativeapp.v2_conversions.compat import (
23
+ force_project_definition_v2,
30
24
  )
25
+ from snowflake.cli._plugins.workspace.manager import WorkspaceManager
31
26
  from snowflake.cli.api.cli_global_context import get_cli_context
32
27
  from snowflake.cli.api.commands.decorators import (
33
28
  with_project_definition,
34
29
  )
35
30
  from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
31
+ from snowflake.cli.api.entities.common import EntityActions
36
32
  from snowflake.cli.api.output.types import CommandResult, MessageResult, QueryResult
37
- from snowflake.cli.api.project.project_verification import assert_project_type
38
33
 
39
34
  app = SnowTyperFactory(
40
35
  name="version",
@@ -46,7 +41,7 @@ log = logging.getLogger(__name__)
46
41
 
47
42
  @app.command(requires_connection=True)
48
43
  @with_project_definition()
49
- @nativeapp_definition_v2_to_v1()
44
+ @force_project_definition_v2()
50
45
  def create(
51
46
  version: Optional[str] = typer.Argument(
52
47
  None,
@@ -58,6 +53,11 @@ def create(
58
53
  help=f"""The patch number you want to create for an existing version.
59
54
  Defaults to undefined if it is not set, which means the Snowflake CLI either uses the patch specified in the `manifest.yml` file or automatically generates a new patch number.""",
60
55
  ),
56
+ label: Optional[str] = typer.Option(
57
+ None,
58
+ "--label",
59
+ help="A label for the version that is displayed to consumers. If unset, the version label specified in `manifest.yml` file is used.",
60
+ ),
61
61
  skip_git_check: Optional[bool] = typer.Option(
62
62
  False,
63
63
  "--skip-git-check",
@@ -72,19 +72,18 @@ def create(
72
72
  Adds a new patch to the provided version defined in your application package. If the version does not exist, creates a version with patch 0.
73
73
  """
74
74
 
75
- assert_project_type("native_app")
76
-
77
- if version is None and patch is not None:
78
- raise MissingParameter("Cannot provide a patch without version!")
79
-
80
75
  cli_context = get_cli_context()
81
- processor = NativeAppVersionCreateProcessor(
82
- project_definition=cli_context.project_definition.native_app,
76
+ ws = WorkspaceManager(
77
+ project_definition=cli_context.project_definition,
83
78
  project_root=cli_context.project_root,
84
79
  )
85
- processor.process(
80
+ package_id = options["package_entity_id"]
81
+ ws.perform_action(
82
+ package_id,
83
+ EntityActions.VERSION_CREATE,
86
84
  version=version,
87
85
  patch=patch,
86
+ label=label,
88
87
  force=force,
89
88
  interactive=interactive,
90
89
  skip_git_check=skip_git_check,
@@ -94,28 +93,29 @@ def create(
94
93
 
95
94
  @app.command("list", requires_connection=True)
96
95
  @with_project_definition()
97
- @nativeapp_definition_v2_to_v1()
96
+ @force_project_definition_v2()
98
97
  def version_list(
99
98
  **options,
100
99
  ) -> CommandResult:
101
100
  """
102
101
  Lists all versions defined in an application package.
103
102
  """
104
-
105
- assert_project_type("native_app")
106
-
107
103
  cli_context = get_cli_context()
108
- processor = NativeAppRunProcessor(
109
- project_definition=cli_context.project_definition.native_app,
104
+ ws = WorkspaceManager(
105
+ project_definition=cli_context.project_definition,
110
106
  project_root=cli_context.project_root,
111
107
  )
112
- cursor = processor.get_all_existing_versions()
108
+ package_id = options["package_entity_id"]
109
+ cursor = ws.perform_action(
110
+ package_id,
111
+ EntityActions.VERSION_LIST,
112
+ )
113
113
  return QueryResult(cursor)
114
114
 
115
115
 
116
116
  @app.command(requires_connection=True)
117
117
  @with_project_definition()
118
- @nativeapp_definition_v2_to_v1()
118
+ @force_project_definition_v2()
119
119
  def drop(
120
120
  version: Optional[str] = typer.Argument(
121
121
  None,
@@ -129,13 +129,17 @@ def drop(
129
129
  Drops a version defined in your application package. Versions can either be passed in as an argument to the command or read from the `manifest.yml` file.
130
130
  Dropping patches is not allowed.
131
131
  """
132
-
133
- assert_project_type("native_app")
134
-
135
132
  cli_context = get_cli_context()
136
- processor = NativeAppVersionDropProcessor(
137
- project_definition=cli_context.project_definition.native_app,
133
+ ws = WorkspaceManager(
134
+ project_definition=cli_context.project_definition,
138
135
  project_root=cli_context.project_root,
139
136
  )
140
- processor.process(version, force, interactive)
137
+ package_id = options["package_entity_id"]
138
+ ws.perform_action(
139
+ package_id,
140
+ EntityActions.VERSION_DROP,
141
+ version=version,
142
+ interactive=interactive,
143
+ force=force,
144
+ )
141
145
  return MessageResult(f"Version drop is now complete.")
@@ -26,7 +26,7 @@ from snowflake.cli.api.sql_execution import SqlExecutionMixin
26
26
  class NotebookManager(SqlExecutionMixin):
27
27
  def execute(self, notebook_name: FQN):
28
28
  query = f"EXECUTE NOTEBOOK {notebook_name.sql_identifier}()"
29
- return self._execute_query(query=query)
29
+ return self.execute_query(query=query)
30
30
 
31
31
  def get_url(self, notebook_name: FQN):
32
32
  fqn = notebook_name.using_connection(self._conn)
@@ -64,7 +64,7 @@ class NotebookManager(SqlExecutionMixin):
64
64
  ALTER NOTEBOOK {notebook_fqn.identifier} ADD LIVE VERSION FROM LAST;
65
65
  """
66
66
  )
67
- self._execute_queries(queries=queries)
67
+ self.execute_queries(queries=queries)
68
68
 
69
69
  return make_snowsight_url(
70
70
  self._conn, f"/#/notebooks/{notebook_fqn.url_identifier}"
@@ -21,6 +21,8 @@ from click import ClickException
21
21
  from snowflake.cli._plugins.object.manager import ObjectManager
22
22
  from snowflake.cli.api.commands.flags import (
23
23
  IdentifierType,
24
+ IfNotExistsOption,
25
+ ReplaceOption,
24
26
  like_option,
25
27
  )
26
28
  from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
@@ -145,6 +147,8 @@ def create(
145
147
  object_type: str = ObjectArgument,
146
148
  object_attributes: Optional[List[str]] = ObjectAttributesArgument,
147
149
  object_json: str = ObjectDefinitionJsonOption,
150
+ if_not_exists: bool = IfNotExistsOption(),
151
+ replace: bool = ReplaceOption(),
148
152
  **options,
149
153
  ):
150
154
  """
@@ -176,5 +180,10 @@ def create(
176
180
  "Provide either list of object attributes, or object definition in JSON format"
177
181
  )
178
182
 
179
- result = ObjectManager().create(object_type=object_type, object_data=object_data)
183
+ result = ObjectManager().create(
184
+ object_type=object_type,
185
+ object_data=object_data,
186
+ if_not_exists=if_not_exists,
187
+ replace=replace,
188
+ )
180
189
  return MessageResult(result)
@@ -52,11 +52,11 @@ class ObjectManager(SqlExecutionMixin):
52
52
  query += f" like '{like}'"
53
53
  if scope[0] is not None:
54
54
  query += f" in {scope[0].replace('-', ' ')} {scope[1]}"
55
- return self._execute_query(query, **kwargs)
55
+ return self.execute_query(query, **kwargs)
56
56
 
57
57
  def drop(self, *, object_type: str, fqn: FQN) -> SnowflakeCursor:
58
58
  object_name = _get_object_names(object_type).sf_name
59
- return self._execute_query(f"drop {object_name} {fqn.sql_identifier}")
59
+ return self.execute_query(f"drop {object_name} {fqn.sql_identifier}")
60
60
 
61
61
  def describe(self, *, object_type: str, fqn: FQN):
62
62
  # Image repository is the only supported object that does not have a DESCRIBE command.
@@ -65,7 +65,7 @@ class ObjectManager(SqlExecutionMixin):
65
65
  f"Describe is currently not supported for object of type image-repository"
66
66
  )
67
67
  object_name = _get_object_names(object_type).sf_name
68
- return self._execute_query(f"describe {object_name} {fqn.sql_identifier}")
68
+ return self.execute_query(f"describe {object_name} {fqn.sql_identifier}")
69
69
 
70
70
  def object_exists(self, *, object_type: str, fqn: FQN):
71
71
  try:
@@ -74,9 +74,17 @@ class ObjectManager(SqlExecutionMixin):
74
74
  except ProgrammingError:
75
75
  return False
76
76
 
77
- def create(self, object_type: str, object_data: Dict[str, Any]) -> str:
77
+ def create(
78
+ self,
79
+ object_type: str,
80
+ object_data: Dict[str, Any],
81
+ replace: bool = False,
82
+ if_not_exists: bool = False,
83
+ ) -> str:
78
84
  rest = RestApi(self._conn)
79
- url = rest.determine_url_for_create_query(object_type=object_type)
85
+ url = rest.determine_url_for_create_query(
86
+ object_type=object_type, replace=replace, if_not_exists=if_not_exists
87
+ )
80
88
  try:
81
89
  response = rest.send_rest_request(url=url, method="post", data=object_data)
82
90
  except Exception as err:
@@ -58,9 +58,9 @@ class SnowparkObjectManager(SqlExecutionMixin):
58
58
  self, execution_identifier: str, object_type: SnowparkObject
59
59
  ) -> SnowflakeCursor:
60
60
  if object_type == SnowparkObject.FUNCTION:
61
- return self._execute_query(f"select {execution_identifier}")
61
+ return self.execute_query(f"select {execution_identifier}")
62
62
  if object_type == SnowparkObject.PROCEDURE:
63
- return self._execute_query(f"call {execution_identifier}")
63
+ return self.execute_query(f"call {execution_identifier}")
64
64
  raise UsageError(f"Unknown object type: {object_type}.")
65
65
 
66
66
  def create_or_replace(
@@ -95,7 +95,7 @@ class SnowparkObjectManager(SqlExecutionMixin):
95
95
  if isinstance(entity, ProcedureEntityModel) and entity.execute_as_caller:
96
96
  query.append("execute as caller")
97
97
 
98
- return self._execute_query("\n".join(query))
98
+ return self.execute_query("\n".join(query))
99
99
 
100
100
  def deploy_entity(
101
101
  self,
@@ -158,10 +158,8 @@ def _check_if_replace_is_required(
158
158
  )
159
159
  return True
160
160
 
161
- if (
162
- resource_json["handler"].lower() != entity.handler.lower()
163
- or _sql_to_python_return_type_mapper(resource_json["returns"]).lower()
164
- != entity.returns.lower()
161
+ if resource_json["handler"].lower() != entity.handler.lower() or not same_type(
162
+ resource_json["returns"], entity.returns
165
163
  ):
166
164
  log.info(
167
165
  "Return type or handler types do not match. Replacing the %s.", object_type
@@ -216,24 +214,68 @@ def _snowflake_dependencies_differ(
216
214
  return _standardize(old_dependencies) != _standardize(new_dependencies)
217
215
 
218
216
 
219
- def _sql_to_python_return_type_mapper(resource_return_type: str) -> str:
220
- """
221
- Some of the Python data types get converted to SQL types, when function/procedure is created.
222
- So, to properly compare types, we use mapping based on:
223
- https://docs.snowflake.com/en/developer-guide/udf-stored-procedure-data-type-mapping#sql-python-data-type-mappings
217
+ def same_type(sf_type: str, local_type: str) -> bool:
218
+ sf_type, local_type = sf_type.upper(), local_type.upper()
224
219
 
225
- Mind you, this only applies to cases, in which Snowflake accepts Python type as return.
226
- Ie. if function returns list, it has to be declared as 'array' during creation,
227
- therefore any conversion is not necessary
228
- """
220
+ # 1. Types are equal out of the box
221
+ if sf_type == local_type:
222
+ return True
223
+
224
+ # 2. Local type is alias for Snowflake type
225
+ local_type = user_to_sql_type_mapper(local_type).upper()
226
+ if sf_type == local_type:
227
+ return True
228
+
229
+ # 3. Local type is a subset of Snowflake type, e.g. VARCHAR(N) == VARCHAR
230
+ # We solved for local VARCHAR(N) in point 1 & 2 as those are explicit types
231
+ if sf_type.startswith(local_type):
232
+ return True
233
+
234
+ # 4. Snowflake types is subset of local type
235
+ if local_type.startswith(sf_type):
236
+ return True
237
+
238
+ return False
239
+
240
+
241
+ def user_to_sql_type_mapper(user_provided_type: str) -> str:
229
242
  mapping = {
230
- "number(38,0)": "int",
231
- "timestamp_ntz(9)": "datetime",
232
- "timestamp_tz(9)": "datetime",
233
- "varchar(16777216)": "string",
243
+ ("VARCHAR", "(16777216)"): ("CHAR", "TEXT", "STRING"),
244
+ ("BINARY", "(8388608)"): ("BINARY", "VARBINARY"),
245
+ ("NUMBER", "(38,0)"): (
246
+ "NUMBER",
247
+ "DECIMAL",
248
+ "INT",
249
+ "INTEGER",
250
+ "BIGINT",
251
+ "SMALLINT",
252
+ "TINYINT",
253
+ "BYTEINT",
254
+ ),
255
+ ("FLOAT", ""): (
256
+ "FLOAT",
257
+ "DOUBLE",
258
+ "DOUBLE PRECISION",
259
+ "REAL",
260
+ "FLOAT",
261
+ "FLOAT4",
262
+ "FLOAT8",
263
+ ),
264
+ ("TIMESTAMP_NTZ", ""): ("TIMESTAMP_NTZ", "TIMESTAMPNTZ", "DATETIME"),
265
+ ("TIMESTAMP_LTZ", ""): ("TIMESTAMP_LTZ", "TIMESTAMPLTZ"),
266
+ ("TIMESTAMP_TZ", ""): ("TIMESTAMP_TZ", "TIMESTAMPTZ"),
234
267
  }
235
268
 
236
- return mapping.get(resource_return_type.lower(), resource_return_type.lower())
269
+ user_provided_type = user_provided_type.upper()
270
+ for (cast_type, default), matching_types in mapping.items():
271
+ for type_ in matching_types:
272
+ if user_provided_type == type_:
273
+ # TEXT -> VARCHAR(16777216)
274
+ return cast_type + default
275
+ if user_provided_type.startswith(type_):
276
+ # TEXT(30) -> VARCHAR(30)
277
+ return user_provided_type.replace(type_, cast_type + default)
278
+ return user_provided_type
237
279
 
238
280
 
239
281
  def _compare_imports(
@@ -169,7 +169,7 @@ class AnacondaPackagesManager(SqlExecutionMixin):
169
169
 
170
170
  def find_packages_available_in_snowflake_anaconda(self) -> AnacondaPackages:
171
171
  """
172
- Finds python packages available in Snowflake to use in functions and stored procedures.
172
+ Finds Python packages available in Snowflake to use in functions and stored procedures.
173
173
  It tries to get the list of packages using SQL query
174
174
  but if the try fails then the fallback is to parse JSON containing info about Snowflake's Anaconda channel.
175
175
  """
@@ -177,8 +177,8 @@ class AnacondaPackagesManager(SqlExecutionMixin):
177
177
  return AnacondaPackages(packages)
178
178
 
179
179
  def _query_snowflake_for_available_packages(self) -> dict[str, AvailablePackage]:
180
- cursor = self._execute_query(
181
- "select package_name, version from information_schema.packages where language = 'python'",
180
+ cursor = self.execute_query(
181
+ "select package_name, version from snowflake.information_schema.packages where language = 'python'",
182
182
  cursor_class=DictCursor,
183
183
  )
184
184
  if cursor.rowcount is None or cursor.rowcount == 0:
@@ -95,5 +95,34 @@ def handle_object_already_exists(
95
95
  raise error
96
96
 
97
97
 
98
+ def filter_log_timestamp(log: str, include_timestamps: bool) -> str:
99
+ if include_timestamps:
100
+ return log
101
+ else:
102
+ return log.split(" ", 1)[1] if " " in log else log
103
+
104
+
105
+ def new_logs_only(prev_log_records: list[str], new_log_records: list[str]) -> list[str]:
106
+ # Sort the log records, we get time-ordered logs
107
+ # due to ISO 8601 timestamp format in the log content
108
+ # eg: 2024-10-22T01:12:29.873896187Z Count: 1
109
+ new_log_records_sorted = sorted(new_log_records)
110
+
111
+ # Get the first new log record to establish the overlap point
112
+ first_new_log_record = new_log_records_sorted[0]
113
+
114
+ # Traverse previous logs in reverse and remove duplicates from new logs
115
+ for prev_log in reversed(prev_log_records):
116
+ # Stop if the previous log is earlier than the first new log
117
+ if prev_log < first_new_log_record:
118
+ break
119
+
120
+ # Remove matching previous logs from the new logs list
121
+ if prev_log in new_log_records_sorted:
122
+ new_log_records_sorted.remove(prev_log)
123
+
124
+ return new_log_records_sorted
125
+
126
+
98
127
  class NoPropertiesProvidedError(ClickException):
99
128
  pass