snowflake-cli-labs 3.0.0rc2__py3-none-any.whl → 3.0.0rc4__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 (88) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/commands_registration/builtin_plugins.py +2 -0
  3. snowflake/cli/_app/secret.py +9 -0
  4. snowflake/cli/_app/snow_connector.py +39 -27
  5. snowflake/cli/_app/telemetry.py +28 -0
  6. snowflake/cli/_plugins/connection/commands.py +9 -4
  7. snowflake/cli/_plugins/git/manager.py +53 -7
  8. snowflake/cli/_plugins/helpers/commands.py +61 -0
  9. snowflake/cli/{api/project/schemas/snowpark/__init__.py → _plugins/helpers/plugin_spec.py} +17 -0
  10. snowflake/cli/_plugins/nativeapp/artifacts.py +10 -9
  11. snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
  12. snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
  13. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +6 -1
  14. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +1 -1
  15. snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +1 -1
  16. snowflake/cli/_plugins/nativeapp/codegen/snowpark/models.py +1 -1
  17. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +5 -1
  18. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +4 -1
  19. snowflake/cli/_plugins/nativeapp/commands.py +87 -96
  20. snowflake/cli/_plugins/nativeapp/entities/__init__.py +0 -0
  21. snowflake/cli/{api/entities/application_entity.py → _plugins/nativeapp/entities/application.py} +264 -83
  22. snowflake/cli/_plugins/nativeapp/entities/application_package.py +1392 -0
  23. snowflake/cli/_plugins/nativeapp/exceptions.py +0 -9
  24. snowflake/cli/_plugins/nativeapp/manager.py +69 -185
  25. snowflake/cli/_plugins/nativeapp/policy.py +3 -0
  26. snowflake/cli/_plugins/nativeapp/project_model.py +2 -2
  27. snowflake/cli/_plugins/nativeapp/run_processor.py +17 -20
  28. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -4
  29. snowflake/cli/_plugins/nativeapp/teardown_processor.py +4 -6
  30. snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +122 -88
  31. snowflake/cli/_plugins/nativeapp/version/commands.py +7 -39
  32. snowflake/cli/_plugins/nativeapp/version/version_processor.py +46 -312
  33. snowflake/cli/_plugins/object/manager.py +36 -15
  34. snowflake/cli/_plugins/snowpark/commands.py +4 -4
  35. snowflake/cli/_plugins/snowpark/common.py +4 -4
  36. snowflake/cli/{api/entities → _plugins/snowpark}/snowpark_entity.py +2 -2
  37. snowflake/cli/{api/project/schemas/entities/snowpark_entity.py → _plugins/snowpark/snowpark_entity_model.py} +3 -6
  38. snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +1 -1
  39. snowflake/cli/_plugins/stage/manager.py +9 -4
  40. snowflake/cli/_plugins/streamlit/commands.py +15 -3
  41. snowflake/cli/_plugins/streamlit/manager.py +12 -4
  42. snowflake/cli/{api/entities → _plugins/streamlit}/streamlit_entity.py +2 -2
  43. snowflake/cli/{api/project/schemas/entities → _plugins/streamlit}/streamlit_entity_model.py +5 -12
  44. snowflake/cli/_plugins/workspace/commands.py +116 -36
  45. snowflake/cli/_plugins/workspace/plugin_spec.py +1 -1
  46. snowflake/cli/api/cli_global_context.py +7 -0
  47. snowflake/cli/api/commands/decorators.py +14 -0
  48. snowflake/cli/api/commands/flags.py +18 -0
  49. snowflake/cli/api/commands/snow_typer.py +1 -1
  50. snowflake/cli/api/config.py +25 -6
  51. snowflake/cli/api/connections.py +3 -1
  52. snowflake/cli/api/entities/common.py +4 -0
  53. snowflake/cli/api/entities/utils.py +3 -14
  54. snowflake/cli/api/errno.py +1 -0
  55. snowflake/cli/api/identifiers.py +4 -3
  56. snowflake/cli/api/metrics.py +92 -0
  57. snowflake/cli/api/project/definition_conversion.py +61 -18
  58. snowflake/cli/api/project/schemas/entities/common.py +17 -4
  59. snowflake/cli/api/project/schemas/entities/entities.py +11 -10
  60. snowflake/cli/api/project/schemas/project_definition.py +5 -7
  61. snowflake/cli/api/project/schemas/v1/__init__.py +0 -0
  62. snowflake/cli/api/project/schemas/{identifier_model.py → v1/identifier_model.py} +0 -7
  63. snowflake/cli/api/project/schemas/v1/native_app/__init__.py +0 -0
  64. snowflake/cli/api/project/schemas/{native_app → v1/native_app}/native_app.py +4 -4
  65. snowflake/cli/api/project/schemas/v1/snowpark/__init__.py +0 -0
  66. snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/callable.py +2 -2
  67. snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/snowpark.py +2 -2
  68. snowflake/cli/api/project/schemas/v1/streamlit/__init__.py +0 -0
  69. snowflake/cli/api/project/schemas/{streamlit → v1/streamlit}/streamlit.py +2 -1
  70. snowflake/cli/api/rendering/sql_templates.py +6 -0
  71. snowflake/cli/api/rest_api.py +11 -5
  72. snowflake/cli/api/sql_execution.py +6 -15
  73. snowflake/cli/api/utils/definition_rendering.py +24 -4
  74. {snowflake_cli_labs-3.0.0rc2.dist-info → snowflake_cli_labs-3.0.0rc4.dist-info}/METADATA +9 -7
  75. {snowflake_cli_labs-3.0.0rc2.dist-info → snowflake_cli_labs-3.0.0rc4.dist-info}/RECORD +83 -79
  76. snowflake/cli/_plugins/nativeapp/init.py +0 -345
  77. snowflake/cli/api/entities/application_package_entity.py +0 -658
  78. snowflake/cli/api/project/schemas/entities/application_entity_model.py +0 -56
  79. snowflake/cli/api/project/schemas/entities/application_package_entity_model.py +0 -94
  80. snowflake/cli/api/project/schemas/streamlit/__init__.py +0 -13
  81. /snowflake/cli/{api/project/schemas/native_app → _plugins/helpers}/__init__.py +0 -0
  82. /snowflake/cli/api/project/schemas/{native_app → v1/native_app}/application.py +0 -0
  83. /snowflake/cli/api/project/schemas/{native_app → v1/native_app}/package.py +0 -0
  84. /snowflake/cli/api/project/schemas/{native_app → v1/native_app}/path_mapping.py +0 -0
  85. /snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/argument.py +0 -0
  86. {snowflake_cli_labs-3.0.0rc2.dist-info → snowflake_cli_labs-3.0.0rc4.dist-info}/WHEEL +0 -0
  87. {snowflake_cli_labs-3.0.0rc2.dist-info → snowflake_cli_labs-3.0.0rc4.dist-info}/entry_points.txt +0 -0
  88. {snowflake_cli_labs-3.0.0rc2.dist-info → snowflake_cli_labs-3.0.0rc4.dist-info}/licenses/LICENSE +0 -0
@@ -26,15 +26,15 @@ from snowflake.cli._plugins.connection.util import (
26
26
  )
27
27
  from snowflake.cli._plugins.object.manager import ObjectManager
28
28
  from snowflake.cli._plugins.stage.manager import StageManager
29
+ from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
30
+ StreamlitEntityModel,
31
+ )
29
32
  from snowflake.cli.api.commands.experimental_behaviour import (
30
33
  experimental_behaviour_enabled,
31
34
  )
32
35
  from snowflake.cli.api.console import cli_console
33
36
  from snowflake.cli.api.feature_flags import FeatureFlag
34
37
  from snowflake.cli.api.identifiers import FQN
35
- from snowflake.cli.api.project.schemas.entities.streamlit_entity_model import (
36
- StreamlitEntityModel,
37
- )
38
38
  from snowflake.cli.api.sql_execution import SqlExecutionMixin
39
39
  from snowflake.connector.cursor import SnowflakeCursor
40
40
  from snowflake.connector.errors import ProgrammingError
@@ -43,6 +43,10 @@ log = logging.getLogger(__name__)
43
43
 
44
44
 
45
45
  class StreamlitManager(SqlExecutionMixin):
46
+ def execute(self, app_name: FQN):
47
+ query = f"EXECUTE STREAMLIT {app_name.sql_identifier}()"
48
+ return self._execute_query(query=query)
49
+
46
50
  def share(self, streamlit_name: FQN, to_role: str) -> SnowflakeCursor:
47
51
  return self._execute_query(
48
52
  f"grant usage on streamlit {streamlit_name.sql_identifier} to role {to_role}"
@@ -98,12 +102,16 @@ class StreamlitManager(SqlExecutionMixin):
98
102
  query.append(f"ROOT_LOCATION = '{from_stage_name}'")
99
103
 
100
104
  query.append(f"MAIN_FILE = '{streamlit.main_file}'")
101
-
105
+ if streamlit.imports:
106
+ query.append(streamlit.get_imports_sql())
102
107
  if streamlit.query_warehouse:
103
108
  query.append(f"QUERY_WAREHOUSE = {streamlit.query_warehouse}")
104
109
  if streamlit.title:
105
110
  query.append(f"TITLE = '{streamlit.title}'")
106
111
 
112
+ if streamlit.comment:
113
+ query.append(f"COMMENT = '{streamlit.comment}'")
114
+
107
115
  if streamlit.external_access_integrations:
108
116
  query.append(streamlit.get_external_access_integrations_sql())
109
117
 
@@ -1,7 +1,7 @@
1
- from snowflake.cli.api.entities.common import EntityBase
2
- from snowflake.cli.api.project.schemas.entities.streamlit_entity_model import (
1
+ from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
3
2
  StreamlitEntityModel,
4
3
  )
4
+ from snowflake.cli.api.entities.common import EntityBase
5
5
 
6
6
 
7
7
  class StreamlitEntity(EntityBase[StreamlitEntityModel]):
@@ -20,17 +20,19 @@ from pydantic import Field, model_validator
20
20
  from snowflake.cli.api.project.schemas.entities.common import (
21
21
  EntityModelBase,
22
22
  ExternalAccessBaseModel,
23
+ ImportsBaseModel,
23
24
  )
24
25
  from snowflake.cli.api.project.schemas.updatable_model import (
25
26
  DiscriminatorField,
26
27
  )
27
28
 
28
29
 
29
- class StreamlitEntityModel(EntityModelBase, ExternalAccessBaseModel):
30
+ class StreamlitEntityModel(EntityModelBase, ExternalAccessBaseModel, ImportsBaseModel):
30
31
  type: Literal["streamlit"] = DiscriminatorField() # noqa: A003
31
32
  title: Optional[str] = Field(
32
33
  title="Human-readable title for the Streamlit dashboard", default=None
33
34
  )
35
+ comment: Optional[str] = Field(title="Comment for the Streamlit app", default=None)
34
36
  query_warehouse: str = Field(
35
37
  title="Snowflake warehouse to host the app", default=None
36
38
  )
@@ -48,23 +50,14 @@ class StreamlitEntityModel(EntityModelBase, ExternalAccessBaseModel):
48
50
  default=None,
49
51
  )
50
52
 
51
- @model_validator(mode="after")
52
- def main_file_must_be_in_artifacts(self):
53
- if not self.artifacts:
54
- return self
55
-
56
- if Path(self.main_file) not in self.artifacts:
57
- raise ValueError(
58
- f"Specified main file {self.main_file} is not included in artifacts."
59
- )
60
- return self
61
-
62
53
  @model_validator(mode="after")
63
54
  def artifacts_must_exists(self):
64
55
  if not self.artifacts:
65
56
  return self
66
57
 
67
58
  for artifact in self.artifacts:
59
+ if "*" in artifact.name:
60
+ continue
68
61
  if not artifact.exists():
69
62
  raise ValueError(
70
63
  f"Specified artifact {artifact} does not exist locally."
@@ -20,7 +20,7 @@ from textwrap import dedent
20
20
  from typing import List, Optional
21
21
 
22
22
  import typer
23
- import yaml
23
+ from click import MissingParameter
24
24
  from snowflake.cli._plugins.nativeapp.artifacts import BundleMap
25
25
  from snowflake.cli._plugins.nativeapp.common_flags import (
26
26
  ForceOption,
@@ -30,50 +30,19 @@ from snowflake.cli._plugins.nativeapp.common_flags import (
30
30
  from snowflake.cli._plugins.workspace.manager import WorkspaceManager
31
31
  from snowflake.cli.api.cli_global_context import get_cli_context
32
32
  from snowflake.cli.api.commands.decorators import with_project_definition
33
- from snowflake.cli.api.commands.snow_typer import SnowTyper
33
+ from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
34
34
  from snowflake.cli.api.entities.common import EntityActions
35
35
  from snowflake.cli.api.exceptions import IncompatibleParametersError
36
- from snowflake.cli.api.output.types import MessageResult
37
- from snowflake.cli.api.project.definition_conversion import (
38
- convert_project_definition_to_v2,
39
- )
40
- from snowflake.cli.api.project.definition_manager import DefinitionManager
41
- from snowflake.cli.api.secure_path import SecurePath
36
+ from snowflake.cli.api.output.types import MessageResult, QueryResult
42
37
 
43
- ws = SnowTyper(
38
+ ws = SnowTyperFactory(
44
39
  name="ws",
45
40
  help="Deploy and interact with snowflake.yml-based entities.",
41
+ is_hidden=lambda: True,
46
42
  )
47
43
  log = logging.getLogger(__name__)
48
44
 
49
45
 
50
- @ws.command()
51
- def migrate(
52
- accept_templates: bool = typer.Option(
53
- False, "-t", "--accept-templates", help="Allows the migration of templates."
54
- ),
55
- **options,
56
- ):
57
- """Migrates the Snowpark, Streamlit, and Native App project definition files from V1 to V2."""
58
- manager = DefinitionManager()
59
- pd = manager.unrendered_project_definition
60
-
61
- if pd.meets_version_requirement("2"):
62
- return MessageResult("Project definition is already at version 2.")
63
-
64
- pd_v2 = convert_project_definition_to_v2(manager.project_root, pd, accept_templates)
65
-
66
- SecurePath("snowflake.yml").rename("snowflake_V1.yml")
67
- with open("snowflake.yml", "w") as file:
68
- yaml.dump(
69
- pd_v2.model_dump(
70
- exclude_unset=True, exclude_none=True, mode="json", by_alias=True
71
- ),
72
- file,
73
- )
74
- return MessageResult("Project definition migrated to version 2.")
75
-
76
-
77
46
  @ws.command(requires_connection=True, hidden=True)
78
47
  @with_project_definition()
79
48
  def bundle(
@@ -211,6 +180,8 @@ def validate(
211
180
  entity_id: str = typer.Option(
212
181
  help=f"""The ID of the entity you want to validate.""",
213
182
  ),
183
+ interactive: bool = InteractiveOption,
184
+ force: Optional[bool] = ForceOption,
214
185
  **options,
215
186
  ):
216
187
  """Validates the specified entity."""
@@ -223,4 +194,113 @@ def validate(
223
194
  ws.perform_action(
224
195
  entity_id,
225
196
  EntityActions.VALIDATE,
197
+ interactive=interactive,
198
+ force=force,
199
+ )
200
+
201
+
202
+ version = SnowTyperFactory(
203
+ name="version",
204
+ help="Manages versions for project entities.",
205
+ )
206
+ ws.add_typer(version)
207
+
208
+
209
+ @version.command(name="list", requires_connection=True, hidden=True)
210
+ @with_project_definition()
211
+ def version_list(
212
+ entity_id: str = typer.Option(
213
+ help="The ID of the entity you want to list versions for.",
214
+ ),
215
+ **options,
216
+ ):
217
+ """Lists the versions of the specified entity."""
218
+ cli_context = get_cli_context()
219
+ ws = WorkspaceManager(
220
+ project_definition=cli_context.project_definition,
221
+ project_root=cli_context.project_root,
222
+ )
223
+ cursor = ws.perform_action(
224
+ entity_id,
225
+ EntityActions.VERSION_LIST,
226
+ )
227
+ return QueryResult(cursor)
228
+
229
+
230
+ @version.command(name="create", requires_connection=True, hidden=True)
231
+ @with_project_definition()
232
+ def version_create(
233
+ entity_id: str = typer.Option(
234
+ help="The ID of the entity you want to create a version for.",
235
+ ),
236
+ version: Optional[str] = typer.Argument(
237
+ None,
238
+ help=f"""Version to define in your application package. If the version already exists, an auto-incremented patch is added to the version instead. Defaults to the version specified in the `manifest.yml` file.""",
239
+ ),
240
+ patch: Optional[int] = typer.Option(
241
+ None,
242
+ "--patch",
243
+ help=f"""The patch number you want to create for an existing version.
244
+ 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.""",
245
+ ),
246
+ skip_git_check: Optional[bool] = typer.Option(
247
+ False,
248
+ "--skip-git-check",
249
+ help="When enabled, the Snowflake CLI skips checking if your project has any untracked or stages files in git. Default: unset.",
250
+ is_flag=True,
251
+ ),
252
+ interactive: bool = InteractiveOption,
253
+ force: Optional[bool] = ForceOption,
254
+ **options,
255
+ ):
256
+ """Creates a new version for the specified entity."""
257
+ if version is None and patch is not None:
258
+ raise MissingParameter("Cannot provide a patch without version!")
259
+
260
+ cli_context = get_cli_context()
261
+ ws = WorkspaceManager(
262
+ project_definition=cli_context.project_definition,
263
+ project_root=cli_context.project_root,
264
+ )
265
+ ws.perform_action(
266
+ entity_id,
267
+ EntityActions.VERSION_CREATE,
268
+ version=version,
269
+ patch=patch,
270
+ skip_git_check=skip_git_check,
271
+ interactive=interactive,
272
+ force=force,
273
+ )
274
+
275
+
276
+ @version.command(name="drop", requires_connection=True, hidden=True)
277
+ @with_project_definition()
278
+ def version_drop(
279
+ entity_id: str = typer.Option(
280
+ help="The ID of the entity you want to create a version for.",
281
+ ),
282
+ version: Optional[str] = typer.Argument(
283
+ None,
284
+ help=f"""Version to define in your application package. If the version already exists, an auto-incremented patch is added to the version instead. Defaults to the version specified in the `manifest.yml` file.""",
285
+ ),
286
+ interactive: bool = InteractiveOption,
287
+ force: Optional[bool] = ForceOption,
288
+ **options,
289
+ ):
290
+ """
291
+ Drops a version defined for your entity. Versions can either be passed in as an argument to the command or read from the `manifest.yml` file.
292
+ Dropping patches is not allowed.
293
+ """
294
+
295
+ cli_context = get_cli_context()
296
+ ws = WorkspaceManager(
297
+ project_definition=cli_context.project_definition,
298
+ project_root=cli_context.project_root,
299
+ )
300
+ ws.perform_action(
301
+ entity_id,
302
+ EntityActions.VERSION_CREATE,
303
+ version=version,
304
+ interactive=interactive,
305
+ force=force,
226
306
  )
@@ -26,5 +26,5 @@ def command_spec():
26
26
  return CommandSpec(
27
27
  parent_command_path=SNOWCLI_ROOT_COMMAND_PATH,
28
28
  command_type=CommandType.COMMAND_GROUP,
29
- typer_instance=commands.ws,
29
+ typer_instance=commands.ws.create_instance(),
30
30
  )
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Iterator
22
22
 
23
23
  from snowflake.cli.api.connections import ConnectionContext, OpenConnectionCache
24
24
  from snowflake.cli.api.exceptions import MissingConfiguration
25
+ from snowflake.cli.api.metrics import CLIMetrics
25
26
  from snowflake.cli.api.output.formats import OutputFormat
26
27
  from snowflake.cli.api.rendering.jinja import CONTEXT_KEY
27
28
  from snowflake.connector import SnowflakeConnection
@@ -46,6 +47,8 @@ class _CliGlobalContextManager:
46
47
  experimental: bool = False
47
48
  enable_tracebacks: bool = True
48
49
 
50
+ metrics: CLIMetrics = field(default_factory=CLIMetrics)
51
+
49
52
  project_path_arg: str | None = None
50
53
  project_is_optional: bool = True
51
54
  project_env_overrides_args: dict[str, str] = field(default_factory=dict)
@@ -152,6 +155,10 @@ class _CliGlobalContextAccess:
152
155
  def enable_tracebacks(self) -> bool:
153
156
  return self._manager.enable_tracebacks
154
157
 
158
+ @property
159
+ def metrics(self):
160
+ return self._manager.metrics
161
+
155
162
  @property
156
163
  def output_format(self) -> OutputFormat:
157
164
  return self._manager.output_format
@@ -29,10 +29,12 @@ from snowflake.cli.api.commands.flags import (
29
29
  DiagAllowlistPathOption,
30
30
  DiagLogPathOption,
31
31
  EnableDiagOption,
32
+ HostOption,
32
33
  MasterTokenOption,
33
34
  MfaPasscodeOption,
34
35
  OutputFormatOption,
35
36
  PasswordOption,
37
+ PortOption,
36
38
  PrivateKeyPathOption,
37
39
  RoleOption,
38
40
  SchemaOption,
@@ -210,6 +212,18 @@ GLOBAL_CONNECTION_OPTIONS = [
210
212
  annotation=Optional[str],
211
213
  default=ConnectionOption,
212
214
  ),
215
+ inspect.Parameter(
216
+ "host",
217
+ inspect.Parameter.KEYWORD_ONLY,
218
+ annotation=Optional[str],
219
+ default=HostOption,
220
+ ),
221
+ inspect.Parameter(
222
+ "port",
223
+ inspect.Parameter.KEYWORD_ONLY,
224
+ annotation=Optional[int],
225
+ default=PortOption,
226
+ ),
213
227
  inspect.Parameter(
214
228
  "account",
215
229
  inspect.Parameter.KEYWORD_ONLY,
@@ -100,6 +100,24 @@ TemporaryConnectionOption = typer.Option(
100
100
  rich_help_panel=_CONNECTION_SECTION,
101
101
  )
102
102
 
103
+ HostOption = typer.Option(
104
+ None,
105
+ "--host",
106
+ help="Host address for the connection. Overrides the value specified for the connection.",
107
+ callback=_connection_callback("host"),
108
+ show_default=False,
109
+ rich_help_panel=_CONNECTION_SECTION,
110
+ )
111
+
112
+ PortOption = typer.Option(
113
+ None,
114
+ "--port",
115
+ help="Port for the connection. Overrides the value specified for the connection.",
116
+ callback=_connection_callback("port"),
117
+ show_default=False,
118
+ rich_help_panel=_CONNECTION_SECTION,
119
+ )
120
+
103
121
  AccountOption = typer.Option(
104
122
  None,
105
123
  "--account",
@@ -105,7 +105,7 @@ class SnowTyper(typer.Typer):
105
105
  result = command_callable(*args, **kw)
106
106
  self.process_result(result)
107
107
  execution.complete(ExecutionStatus.SUCCESS)
108
- except Exception as err:
108
+ except BaseException as err:
109
109
  execution.complete(ExecutionStatus.FAILURE)
110
110
  exception = self.exception_handler(err, execution)
111
111
  raise exception
@@ -130,12 +130,21 @@ def config_init(config_file: Optional[Path]):
130
130
  create_initial_loggers()
131
131
 
132
132
 
133
- def add_connection(name: str, connection_config: ConnectionConfig):
134
- set_config_value(
135
- section=CONNECTIONS_SECTION,
136
- key=name,
137
- value=connection_config.to_dict_of_all_non_empty_values(),
138
- )
133
+ def add_connection_to_proper_file(name: str, connection_config: ConnectionConfig):
134
+ if CONNECTIONS_FILE.exists():
135
+ existing_connections = _read_connections_toml()
136
+ existing_connections.update(
137
+ {name: connection_config.to_dict_of_all_non_empty_values()}
138
+ )
139
+ _update_connections_toml(existing_connections)
140
+ return CONNECTIONS_FILE
141
+ else:
142
+ set_config_value(
143
+ section=CONNECTIONS_SECTION,
144
+ key=name,
145
+ value=connection_config.to_dict_of_all_non_empty_values(),
146
+ )
147
+ return CONFIG_MANAGER.file_path
139
148
 
140
149
 
141
150
  _DEFAULT_LOGS_CONFIG = {
@@ -359,3 +368,13 @@ def get_feature_flags_section() -> Dict[str, bool | Literal["UNKNOWN"]]:
359
368
  return "UNKNOWN"
360
369
 
361
370
  return {k: _bool_or_unknown(v) for k, v in flags.items()}
371
+
372
+
373
+ def _read_connections_toml() -> dict:
374
+ with open(CONNECTIONS_FILE, "r") as f:
375
+ return tomlkit.loads(f.read()).unwrap()
376
+
377
+
378
+ def _update_connections_toml(connections: dict):
379
+ with open(CONNECTIONS_FILE, "w") as f:
380
+ f.write(tomlkit.dumps(connections))
@@ -36,6 +36,8 @@ schema_pattern = re.compile(r".+\..+")
36
36
  class ConnectionContext:
37
37
  # FIXME: can reduce duplication using config.ConnectionConfig
38
38
  connection_name: Optional[str] = None
39
+ host: Optional[str] = None
40
+ port: Optional[int] = None
39
41
  account: Optional[str] = None
40
42
  database: Optional[str] = None
41
43
  role: Optional[str] = None
@@ -71,7 +73,7 @@ class ConnectionContext:
71
73
  Raises KeyError if a non-property is specified as a key.
72
74
  """
73
75
  field_map = {field.name: field for field in fields(self)}
74
- for (key, value) in updates.items():
76
+ for key, value in updates.items():
75
77
  # ensure key represents a property
76
78
  if key not in field_map:
77
79
  raise KeyError(f"{key} is not a field of {self.__class__.__name__}")
@@ -11,6 +11,10 @@ class EntityActions(str, Enum):
11
11
  DROP = "action_drop"
12
12
  VALIDATE = "action_validate"
13
13
 
14
+ VERSION_LIST = "action_version_list"
15
+ VERSION_CREATE = "action_version_create"
16
+ VERSION_DROP = "action_version_drop"
17
+
14
18
 
15
19
  T = TypeVar("T")
16
20
 
@@ -9,11 +9,9 @@ from snowflake.cli._plugins.nativeapp.artifacts import (
9
9
  BundleMap,
10
10
  resolve_without_follow,
11
11
  )
12
- from snowflake.cli._plugins.nativeapp.constants import OWNER_COL
13
12
  from snowflake.cli._plugins.nativeapp.exceptions import (
14
13
  InvalidTemplateInFileError,
15
14
  MissingScriptError,
16
- UnexpectedOwnerError,
17
15
  )
18
16
  from snowflake.cli._plugins.nativeapp.utils import verify_exists, verify_no_directories
19
17
  from snowflake.cli._plugins.stage.diff import (
@@ -33,8 +31,8 @@ from snowflake.cli.api.errno import (
33
31
  NO_WAREHOUSE_SELECTED_IN_SESSION,
34
32
  )
35
33
  from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
34
+ from snowflake.cli.api.metrics import CLICounterField
36
35
  from snowflake.cli.api.project.schemas.entities.common import PostDeployHook
37
- from snowflake.cli.api.project.util import unquote_identifier
38
36
  from snowflake.cli.api.rendering.sql_templates import (
39
37
  choose_sql_jinja_env_based_on_template_syntax,
40
38
  )
@@ -80,17 +78,6 @@ def generic_sql_error_handler(
80
78
  raise err
81
79
 
82
80
 
83
- def ensure_correct_owner(row: dict, role: str, obj_name: str) -> None:
84
- """
85
- Check if an object has the right owner role
86
- """
87
- actual_owner = row[
88
- OWNER_COL
89
- ].upper() # Because unquote_identifier() always returns uppercase str
90
- if actual_owner != unquote_identifier(role):
91
- raise UnexpectedOwnerError(obj_name, role, actual_owner)
92
-
93
-
94
81
  def _get_stage_paths_to_sync(
95
82
  local_paths_to_sync: List[Path], deploy_root: Path
96
83
  ) -> List[StagePath]:
@@ -263,6 +250,8 @@ def execute_post_deploy_hooks(
263
250
  if not post_deploy_hooks:
264
251
  return
265
252
 
253
+ get_cli_context().metrics.set_counter(CLICounterField.POST_DEPLOY_SCRIPTS, 1)
254
+
266
255
  with console.phase(f"Executing {deployed_object_type} post-deploy actions"):
267
256
  sql_scripts_paths = []
268
257
  for hook in post_deploy_hooks:
@@ -17,6 +17,7 @@ NO_WAREHOUSE_SELECTED_IN_SESSION = 606
17
17
 
18
18
  DOES_NOT_EXIST_OR_NOT_AUTHORIZED = 2003
19
19
  DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED = 2043
20
+ INSUFFICIENT_PRIVILEGES = 3001
20
21
 
21
22
  # Native Apps
22
23
  CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION = 93044
@@ -18,8 +18,7 @@ import re
18
18
 
19
19
  from click import ClickException
20
20
  from snowflake.cli.api.exceptions import FQNInconsistencyError, FQNNameError
21
- from snowflake.cli.api.project.schemas.identifier_model import (
22
- Identifier,
21
+ from snowflake.cli.api.project.schemas.v1.identifier_model import (
23
22
  ObjectIdentifierBaseModel,
24
23
  )
25
24
  from snowflake.cli.api.project.util import VALID_IDENTIFIER_REGEX, identifier_for_url
@@ -142,8 +141,10 @@ class FQN:
142
141
  return fqn.set_database(model.database).set_schema(model.schema_name)
143
142
 
144
143
  @classmethod
145
- def from_identifier_model_v2(cls, model: Identifier) -> "FQN":
144
+ def from_identifier_model_v2(cls, model) -> "FQN":
146
145
  """Create an instance from object model."""
146
+ from snowflake.cli.api.project.schemas.entities.common import Identifier
147
+
147
148
  if not isinstance(model, Identifier):
148
149
  raise ClickException(f"Expected {type(Identifier).__name__}, got {model}.")
149
150
 
@@ -0,0 +1,92 @@
1
+ # Copyright (c) 2024 Snowflake Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from typing import Dict, Optional
16
+
17
+
18
+ class _TypePrefix:
19
+ FEATURES = "features"
20
+
21
+
22
+ class _DomainPrefix:
23
+ GLOBAL = "global"
24
+ APP = "app"
25
+ SQL = "sql"
26
+
27
+
28
+ class CLICounterField:
29
+ """
30
+ for each counter field we're adopting a convention of
31
+ <type>.<domain>.<name>
32
+ for example, if we're tracking a global feature, then the field name would be
33
+ features.global.feature_name
34
+
35
+ The metrics API is implemented to be generic, but we are adopting a convention
36
+ for feature tracking with the following model for a given command execution:
37
+ * counter not present -> feature is not available
38
+ * counter == 0 -> feature is available, but not used
39
+ * counter == 1 -> feature is used
40
+ this makes it easy to compute percentages for feature dashboards in Snowsight
41
+ """
42
+
43
+ TEMPLATES_PROCESSOR = (
44
+ f"{_TypePrefix.FEATURES}.{_DomainPrefix.GLOBAL}.templates_processor"
45
+ )
46
+ SQL_TEMPLATES = f"{_TypePrefix.FEATURES}.{_DomainPrefix.SQL}.sql_templates"
47
+ PDF_TEMPLATES = f"{_TypePrefix.FEATURES}.{_DomainPrefix.GLOBAL}.pdf_templates"
48
+ SNOWPARK_PROCESSOR = (
49
+ f"{_TypePrefix.FEATURES}.{_DomainPrefix.APP}.snowpark_processor"
50
+ )
51
+ POST_DEPLOY_SCRIPTS = (
52
+ f"{_TypePrefix.FEATURES}.{_DomainPrefix.APP}.post_deploy_scripts"
53
+ )
54
+ PACKAGE_SCRIPTS = f"{_TypePrefix.FEATURES}.{_DomainPrefix.APP}.package_scripts"
55
+
56
+
57
+ class CLIMetrics:
58
+ """
59
+ Class to track various metrics across the execution of a command
60
+ """
61
+
62
+ def __init__(self):
63
+ self._counters: Dict[str, int] = {}
64
+
65
+ def __eq__(self, other):
66
+ if isinstance(other, CLIMetrics):
67
+ return self._counters == other._counters
68
+ return False
69
+
70
+ def get_counter(self, name: str) -> Optional[int]:
71
+ return self._counters.get(name)
72
+
73
+ def set_counter(self, name: str, value: int) -> None:
74
+ self._counters[name] = value
75
+
76
+ def set_counter_default(self, name: str, value: int) -> None:
77
+ """
78
+ sets the counter if it does not already exist
79
+ """
80
+ if name not in self._counters:
81
+ self.set_counter(name, value)
82
+
83
+ def increment_counter(self, name: str, value: int = 1) -> None:
84
+ if name not in self._counters:
85
+ self.set_counter(name, value)
86
+ else:
87
+ self._counters[name] += value
88
+
89
+ @property
90
+ def counters(self) -> Dict[str, int]:
91
+ # return a copy of the original dict to avoid mutating the original
92
+ return self._counters.copy()