snowflake-cli 3.1.0__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 (60) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +1 -1
  3. snowflake/cli/_plugins/connection/commands.py +124 -109
  4. snowflake/cli/_plugins/connection/util.py +54 -9
  5. snowflake/cli/_plugins/cortex/manager.py +1 -1
  6. snowflake/cli/_plugins/git/manager.py +4 -4
  7. snowflake/cli/_plugins/nativeapp/artifacts.py +64 -10
  8. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
  9. snowflake/cli/_plugins/nativeapp/commands.py +10 -3
  10. snowflake/cli/_plugins/nativeapp/constants.py +1 -0
  11. snowflake/cli/_plugins/nativeapp/entities/application.py +501 -440
  12. snowflake/cli/_plugins/nativeapp/entities/application_package.py +563 -885
  13. snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
  14. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
  15. snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
  16. snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
  17. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
  18. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
  19. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +1 -89
  20. snowflake/cli/_plugins/nativeapp/version/commands.py +6 -3
  21. snowflake/cli/_plugins/notebook/manager.py +2 -2
  22. snowflake/cli/_plugins/object/commands.py +10 -1
  23. snowflake/cli/_plugins/object/manager.py +13 -5
  24. snowflake/cli/_plugins/snowpark/common.py +3 -3
  25. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -1
  26. snowflake/cli/_plugins/spcs/common.py +29 -0
  27. snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
  28. snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
  29. snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
  30. snowflake/cli/_plugins/spcs/services/commands.py +64 -13
  31. snowflake/cli/_plugins/spcs/services/manager.py +75 -15
  32. snowflake/cli/_plugins/sql/commands.py +9 -1
  33. snowflake/cli/_plugins/sql/manager.py +9 -4
  34. snowflake/cli/_plugins/stage/commands.py +20 -16
  35. snowflake/cli/_plugins/stage/diff.py +1 -1
  36. snowflake/cli/_plugins/stage/manager.py +140 -11
  37. snowflake/cli/_plugins/streamlit/manager.py +5 -5
  38. snowflake/cli/_plugins/workspace/commands.py +6 -3
  39. snowflake/cli/api/cli_global_context.py +1 -0
  40. snowflake/cli/api/config.py +23 -5
  41. snowflake/cli/api/console/console.py +4 -19
  42. snowflake/cli/api/entities/utils.py +19 -32
  43. snowflake/cli/api/errno.py +2 -0
  44. snowflake/cli/api/exceptions.py +9 -0
  45. snowflake/cli/api/metrics.py +223 -7
  46. snowflake/cli/api/output/types.py +1 -1
  47. snowflake/cli/api/project/definition_conversion.py +179 -62
  48. snowflake/cli/api/rest_api.py +26 -4
  49. snowflake/cli/api/secure_utils.py +1 -1
  50. snowflake/cli/api/sql_execution.py +35 -22
  51. snowflake/cli/api/stage_path.py +5 -2
  52. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +7 -8
  53. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +56 -55
  54. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
  55. snowflake/cli/_plugins/nativeapp/manager.py +0 -392
  56. snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
  57. snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
  58. snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -56
  59. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
  60. {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -14,4 +14,4 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- VERSION = "3.1.0"
17
+ VERSION = "3.2.0"
@@ -48,7 +48,7 @@ Options
48
48
  {%- if param.type.name != "choice" %}{{ ' {' }}{% else %} {% endif %}{{ param.make_metavar() }}{% if param.type.name != "choice" %}{{ '}' }}
49
49
  {%- endif %}
50
50
  {%- endif %}`
51
- {% if param.help %}{{ " " + param.help | replace("\n", " ") }}{% if param.help[-1] != '.' %}.{% endif %}{% if param.default is not none %} Default: {{ param.default }}.{% endif %}{% else %} TBD{% endif %}
51
+ {% if param.help %}{{ " " + param.help | replace("\n", " ") }}{% if param.help[-1] != '.' %}.{% endif %}{% if param.default is not none and param.default != "" %} Default: {{ param.default }}.{% endif %}{% else %} TBD{% endif %}
52
52
  {% endfor -%}
53
53
  {% else %}
54
54
 
@@ -16,20 +16,38 @@ from __future__ import annotations
16
16
 
17
17
  import logging
18
18
  import os.path
19
+ from pathlib import Path
20
+ from typing import Optional
19
21
 
20
22
  import typer
21
- from click import ClickException, Context, Parameter, UsageError # type: ignore
23
+ from click import ( # type: ignore
24
+ ClickException,
25
+ Context,
26
+ Parameter,
27
+ UsageError,
28
+ )
22
29
  from click.core import ParameterSource # type: ignore
23
- from click.types import StringParamType
24
30
  from snowflake import connector
25
31
  from snowflake.cli._plugins.connection.util import (
26
- strip_and_check_if_exists,
27
32
  strip_if_value_present,
28
33
  )
29
34
  from snowflake.cli._plugins.object.manager import ObjectManager
30
35
  from snowflake.cli.api.cli_global_context import get_cli_context
31
36
  from snowflake.cli.api.commands.flags import (
32
37
  PLAIN_PASSWORD_MSG,
38
+ AccountOption,
39
+ AuthenticatorOption,
40
+ DatabaseOption,
41
+ HostOption,
42
+ NoInteractiveOption,
43
+ PasswordOption,
44
+ PortOption,
45
+ PrivateKeyPathOption,
46
+ RoleOption,
47
+ SchemaOption,
48
+ TokenFilePathOption,
49
+ UserOption,
50
+ WarehouseOption,
33
51
  )
34
52
  from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
35
53
  from snowflake.cli.api.config import (
@@ -64,11 +82,6 @@ class EmptyInput:
64
82
  return "optional"
65
83
 
66
84
 
67
- class OptionalPrompt(StringParamType):
68
- def convert(self, value, param, ctx):
69
- return None if isinstance(value, EmptyInput) else value
70
-
71
-
72
85
  def _mask_password(connection_params: dict):
73
86
  if "password" in connection_params:
74
87
  connection_params["password"] = "****"
@@ -101,7 +114,7 @@ def list_connections(**options) -> CommandResult:
101
114
 
102
115
 
103
116
  def require_integer(field_name: str):
104
- def callback(value: str):
117
+ def callback(ctx: Context, param: Parameter, value: str):
105
118
  if value is None:
106
119
  return None
107
120
  if value.strip().isdigit():
@@ -124,130 +137,91 @@ def add(
124
137
  None,
125
138
  "--connection-name",
126
139
  "-n",
127
- prompt="Name for this connection",
128
140
  help="Name of the new connection.",
129
141
  show_default=False,
130
- callback=strip_if_value_present,
131
142
  ),
132
143
  account: str = typer.Option(
133
144
  None,
134
- "--account",
135
145
  "-a",
136
- "--accountname",
137
- prompt="Snowflake account name",
146
+ *AccountOption.param_decls,
138
147
  help="Account name to use when authenticating with Snowflake.",
139
148
  show_default=False,
140
- callback=strip_if_value_present,
141
149
  ),
142
150
  user: str = typer.Option(
143
151
  None,
144
- "--user",
145
152
  "-u",
146
- "--username",
147
- prompt="Snowflake username",
153
+ *UserOption.param_decls,
148
154
  show_default=False,
149
155
  help="Username to connect to Snowflake.",
150
- callback=strip_if_value_present,
151
156
  ),
152
- password: str = typer.Option(
153
- EmptyInput(),
154
- "--password",
157
+ password: Optional[str] = typer.Option(
158
+ None,
155
159
  "-p",
156
- click_type=OptionalPrompt(),
160
+ *PasswordOption.param_decls,
157
161
  callback=_password_callback,
158
- prompt="Snowflake password",
159
162
  help="Snowflake password.",
160
163
  hide_input=True,
161
164
  ),
162
- role: str = typer.Option(
163
- EmptyInput(),
164
- "--role",
165
+ role: Optional[str] = typer.Option(
166
+ None,
165
167
  "-r",
166
- click_type=OptionalPrompt(),
167
- prompt="Role for the connection",
168
+ *RoleOption.param_decls,
168
169
  help="Role to use on Snowflake.",
169
- callback=strip_if_value_present,
170
170
  ),
171
- warehouse: str = typer.Option(
172
- EmptyInput(),
173
- "--warehouse",
171
+ warehouse: Optional[str] = typer.Option(
172
+ None,
174
173
  "-w",
175
- click_type=OptionalPrompt(),
176
- prompt="Warehouse for the connection",
174
+ *WarehouseOption.param_decls,
177
175
  help="Warehouse to use on Snowflake.",
178
- callback=strip_if_value_present,
179
176
  ),
180
- database: str = typer.Option(
181
- EmptyInput(),
182
- "--database",
177
+ database: Optional[str] = typer.Option(
178
+ None,
183
179
  "-d",
184
- click_type=OptionalPrompt(),
185
- prompt="Database for the connection",
180
+ *DatabaseOption.param_decls,
186
181
  help="Database to use on Snowflake.",
187
- callback=strip_if_value_present,
188
182
  ),
189
- schema: str = typer.Option(
190
- EmptyInput(),
191
- "--schema",
183
+ schema: Optional[str] = typer.Option(
184
+ None,
192
185
  "-s",
193
- click_type=OptionalPrompt(),
194
- prompt="Schema for the connection",
186
+ *SchemaOption.param_decls,
195
187
  help="Schema to use on Snowflake.",
196
- callback=strip_if_value_present,
197
188
  ),
198
- host: str = typer.Option(
199
- EmptyInput(),
200
- "--host",
189
+ host: Optional[str] = typer.Option(
190
+ None,
201
191
  "-h",
202
- click_type=OptionalPrompt(),
203
- prompt="Connection host",
192
+ *HostOption.param_decls,
204
193
  help="Host name the connection attempts to connect to Snowflake.",
205
- callback=strip_if_value_present,
206
194
  ),
207
- port: int = typer.Option(
208
- EmptyInput(),
209
- "--port",
195
+ port: Optional[int] = typer.Option(
196
+ None,
210
197
  "-P",
211
- click_type=OptionalPrompt(),
212
- prompt="Connection port",
198
+ *PortOption.param_decls,
213
199
  help="Port to communicate with on the host.",
214
- callback=require_integer(field_name="port"),
215
200
  ),
216
- region: str = typer.Option(
217
- EmptyInput(),
201
+ region: Optional[str] = typer.Option(
202
+ None,
218
203
  "--region",
219
204
  "-R",
220
- click_type=OptionalPrompt(),
221
- prompt="Snowflake region",
222
205
  help="Region name if not the default Snowflake deployment.",
223
- callback=strip_if_value_present,
224
206
  ),
225
- authenticator: str = typer.Option(
226
- EmptyInput(),
227
- "--authenticator",
207
+ authenticator: Optional[str] = typer.Option(
208
+ None,
228
209
  "-A",
229
- click_type=OptionalPrompt(),
230
- prompt="Authentication method",
210
+ *AuthenticatorOption.param_decls,
231
211
  help="Chosen authenticator, if other than password-based",
232
212
  ),
233
- private_key_file: str = typer.Option(
234
- EmptyInput(),
213
+ private_key_file: Optional[str] = typer.Option(
214
+ None,
235
215
  "--private-key",
236
- "--private-key-path",
237
216
  "-k",
238
- click_type=OptionalPrompt(),
239
- prompt="Path to private key file",
217
+ *PrivateKeyPathOption.param_decls,
240
218
  help="Path to file containing private key",
241
- callback=strip_and_check_if_exists,
242
219
  ),
243
- token_file_path: str = typer.Option(
244
- EmptyInput(),
245
- "--token-file-path",
220
+ token_file_path: Optional[str] = typer.Option(
221
+ None,
246
222
  "-t",
247
- click_type=OptionalPrompt(),
248
- prompt="Path to token file",
223
+ *TokenFilePathOption.param_decls,
249
224
  help="Path to file with an OAuth token that should be used when connecting to Snowflake",
250
- callback=strip_and_check_if_exists,
251
225
  ),
252
226
  set_as_default: bool = typer.Option(
253
227
  False,
@@ -255,29 +229,62 @@ def add(
255
229
  is_flag=True,
256
230
  help="If provided the connection will be configured as default connection.",
257
231
  ),
232
+ no_interactive: bool = NoInteractiveOption,
258
233
  **options,
259
234
  ) -> CommandResult:
260
235
  """Adds a connection to configuration file."""
236
+ connection_options = {
237
+ "connection_name": connection_name,
238
+ "account": account,
239
+ "user": user,
240
+ "password": password,
241
+ "role": role,
242
+ "warehouse": warehouse,
243
+ "database": database,
244
+ "schema": schema,
245
+ "host": host,
246
+ "port": port,
247
+ "region": region,
248
+ "authenticator": authenticator,
249
+ "private_key_file": private_key_file,
250
+ "token_file_path": token_file_path,
251
+ }
252
+
253
+ if not no_interactive:
254
+ for option in connection_options:
255
+ if connection_options[option] is None:
256
+ connection_options[option] = typer.prompt(
257
+ f"Enter {option.replace('_', ' ')}",
258
+ default="",
259
+ value_proc=lambda x: None if not x else x,
260
+ hide_input=option == "password",
261
+ show_default=False,
262
+ )
263
+ if isinstance(connection_options[option], str):
264
+ connection_options[option] = strip_if_value_present(
265
+ connection_options[option]
266
+ )
267
+
268
+ if (value := connection_options["port"]) is not None:
269
+ connection_options["port"] = int(value)
270
+
271
+ if (path := connection_options["private_key_file"]) is not None:
272
+ if not Path(str(path)).exists():
273
+ raise UsageError(f"Path {path} does not exist.")
274
+
275
+ if (path := connection_options["token_file_path"]) is not None:
276
+ if not Path(str(path)).exists():
277
+ raise UsageError(f"Path {path} does not exist.")
278
+
279
+ connection_name = str(connection_options["connection_name"])
280
+ del connection_options["connection_name"]
281
+
261
282
  if connection_exists(connection_name):
262
- raise ClickException(f"Connection {connection_name} already exists")
283
+ raise UsageError(f"Connection {connection_name} already exists")
263
284
 
264
285
  connections_file = add_connection_to_proper_file(
265
286
  connection_name,
266
- ConnectionConfig(
267
- account=account,
268
- user=user,
269
- password=password,
270
- host=host,
271
- region=region,
272
- port=port,
273
- database=database,
274
- schema=schema,
275
- warehouse=warehouse,
276
- role=role,
277
- authenticator=authenticator,
278
- private_key_file=private_key_file,
279
- token_file_path=token_file_path,
280
- ),
287
+ ConnectionConfig(**connection_options),
281
288
  )
282
289
  if set_as_default:
283
290
  set_config_value(
@@ -358,7 +365,7 @@ def set_default(
358
365
  def generate_jwt(
359
366
  **options,
360
367
  ) -> CommandResult:
361
- """Generate and display a JWT token."""
368
+ """Generate a JWT token, which will be printed out and displayed.."""
362
369
  connection_details = get_cli_context().connection_context.update_from_config()
363
370
 
364
371
  msq_template = (
@@ -370,22 +377,30 @@ def generate_jwt(
370
377
  raise UsageError(msq_template.format("Account"))
371
378
  if not connection_details.private_key_file:
372
379
  raise UsageError(msq_template.format("Private key file"))
380
+
373
381
  passphrase = os.getenv("PRIVATE_KEY_PASSPHRASE", None)
374
- if not passphrase:
375
- passphrase = typer.prompt(
376
- "Enter private key file password (Press enter if none)",
377
- hide_input=True,
378
- type=str,
379
- default="",
380
- )
381
382
 
382
- try:
383
- token = connector.auth.get_token_from_private_key(
383
+ def _decrypt(passphrase: str | None):
384
+ return connector.auth.get_token_from_private_key(
384
385
  user=connection_details.user,
385
386
  account=connection_details.account,
386
387
  privatekey_path=connection_details.private_key_file,
387
388
  key_password=passphrase,
388
389
  )
390
+
391
+ try:
392
+ if passphrase is None:
393
+ try:
394
+ token = _decrypt(passphrase=None)
395
+ return MessageResult(token)
396
+ except TypeError:
397
+ passphrase = typer.prompt(
398
+ "Enter private key file password (press enter for empty)",
399
+ hide_input=True,
400
+ type=str,
401
+ default="",
402
+ )
403
+ token = _decrypt(passphrase=passphrase)
389
404
  return MessageResult(token)
390
- except ValueError as err:
405
+ except (ValueError, TypeError) as err:
391
406
  raise ClickException(str(err))
@@ -17,7 +17,10 @@ from __future__ import annotations
17
17
  import json
18
18
  import logging
19
19
  import os
20
- from typing import Optional
20
+ from enum import Enum
21
+ from functools import lru_cache
22
+ from textwrap import dedent
23
+ from typing import Any, Dict, Optional
21
24
 
22
25
  from click.exceptions import ClickException
23
26
  from snowflake.connector import SnowflakeConnection
@@ -25,12 +28,6 @@ from snowflake.connector.cursor import DictCursor
25
28
 
26
29
  log = logging.getLogger(__name__)
27
30
 
28
- REGIONLESS_QUERY = """
29
- select value['value'] as REGIONLESS from table(flatten(
30
- input => parse_json(SYSTEM$BOOTSTRAP_DATA_REQUEST()),
31
- path => 'clientParamsInfo'
32
- )) where value['name'] = 'UI_SNOWSIGHT_ENABLE_REGIONLESS_REDIRECT';
33
- """
34
31
 
35
32
  ALLOWLIST_QUERY = "SELECT SYSTEM$ALLOWLIST()"
36
33
  SNOWFLAKE_DEPLOYMENT = "SNOWFLAKE_DEPLOYMENT"
@@ -54,6 +51,50 @@ class MissingConnectionRegionError(ClickException):
54
51
  )
55
52
 
56
53
 
54
+ class UIParameter(Enum):
55
+ NA_ENABLE_REGIONLESS_REDIRECT = "UI_SNOWSIGHT_ENABLE_REGIONLESS_REDIRECT"
56
+ NA_EVENT_SHARING_V2 = "ENABLE_EVENT_SHARING_V2_IN_THE_SAME_ACCOUNT"
57
+ NA_ENFORCE_MANDATORY_FILTERS = (
58
+ "ENFORCE_MANDATORY_FILTERS_FOR_SAME_ACCOUNT_INSTALLATION"
59
+ )
60
+
61
+
62
+ def get_ui_parameter(
63
+ conn: SnowflakeConnection, parameter: UIParameter, default: Any
64
+ ) -> str:
65
+ """
66
+ Returns the value of a single UI parameter.
67
+ If the parameter is not found, the default value is returned.
68
+ """
69
+
70
+ ui_parameters = get_ui_parameters(conn)
71
+ return ui_parameters.get(parameter, default)
72
+
73
+
74
+ @lru_cache()
75
+ def get_ui_parameters(conn: SnowflakeConnection) -> Dict[UIParameter, Any]:
76
+ """
77
+ Returns the UI parameters from the SYSTEM$BOOTSTRAP_DATA_REQUEST function
78
+ """
79
+
80
+ parameters_to_fetch = sorted([param.value for param in UIParameter])
81
+
82
+ query = dedent(
83
+ f"""
84
+ select value['value']::string as PARAM_VALUE, value['name']::string as PARAM_NAME from table(flatten(
85
+ input => parse_json(SYSTEM$BOOTSTRAP_DATA_REQUEST()),
86
+ path => 'clientParamsInfo'
87
+ )) where value['name'] in ('{"', '".join(parameters_to_fetch)}');
88
+ """
89
+ )
90
+
91
+ *_, cursor = conn.execute_string(query, cursor_class=DictCursor)
92
+
93
+ return {
94
+ UIParameter(row["PARAM_NAME"]): row["PARAM_VALUE"] for row in cursor.fetchall()
95
+ }
96
+
97
+
57
98
  def is_regionless_redirect(conn: SnowflakeConnection) -> bool:
58
99
  """
59
100
  Determines if the deployment this connection refers to uses
@@ -62,8 +103,12 @@ def is_regionless_redirect(conn: SnowflakeConnection) -> bool:
62
103
  assume it's regionless, as this is true for most production deployments.
63
104
  """
64
105
  try:
65
- *_, cursor = conn.execute_string(REGIONLESS_QUERY, cursor_class=DictCursor)
66
- return cursor.fetchone()["REGIONLESS"].lower() == "true"
106
+ return (
107
+ get_ui_parameter(
108
+ conn, UIParameter.NA_ENABLE_REGIONLESS_REDIRECT, "true"
109
+ ).lower()
110
+ == "true"
111
+ )
67
112
  except:
68
113
  log.warning(
69
114
  "Cannot determine regionless redirect; assuming True.", exc_info=True
@@ -180,7 +180,7 @@ class CortexManager(SqlExecutionMixin):
180
180
 
181
181
  def _query_cortex_result_str(self, query: str) -> str:
182
182
  try:
183
- cursor = self._execute_query(query, cursor_class=DictCursor)
183
+ cursor = self.execute_query(query, cursor_class=DictCursor)
184
184
  if cursor.rowcount is None:
185
185
  raise SnowflakeSQLExecutionError(query)
186
186
  return str(cursor.fetchone()["CORTEX_RESULT"])
@@ -84,13 +84,13 @@ class GitManager(StageManager):
84
84
  return StagePath.from_git_str(stage_path)
85
85
 
86
86
  def show_branches(self, repo_name: str, like: str) -> SnowflakeCursor:
87
- return self._execute_query(f"show git branches like '{like}' in {repo_name}")
87
+ return self.execute_query(f"show git branches like '{like}' in {repo_name}")
88
88
 
89
89
  def show_tags(self, repo_name: str, like: str) -> SnowflakeCursor:
90
- return self._execute_query(f"show git tags like '{like}' in {repo_name}")
90
+ return self.execute_query(f"show git tags like '{like}' in {repo_name}")
91
91
 
92
92
  def fetch(self, fqn: FQN) -> SnowflakeCursor:
93
- return self._execute_query(f"alter git repository {fqn} fetch")
93
+ return self.execute_query(f"alter git repository {fqn} fetch")
94
94
 
95
95
  def create(
96
96
  self, repo_name: FQN, api_integration: str, url: str, secret: str
@@ -104,7 +104,7 @@ class GitManager(StageManager):
104
104
  )
105
105
  if secret is not None:
106
106
  query += f"git_credentials = {secret}\n"
107
- return self._execute_query(query)
107
+ return self.execute_query(query)
108
108
 
109
109
  @staticmethod
110
110
  def get_stage_from_path(path: str):
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  import itertools
18
18
  import os
19
+ from collections import namedtuple
19
20
  from pathlib import Path
20
21
  from textwrap import dedent
21
22
  from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union
@@ -164,7 +165,7 @@ class _ArtifactPathMap:
164
165
  if src_is_dir:
165
166
  # mark all subdirectories of this source as directories so that we can
166
167
  # detect accidental clobbering
167
- for (root, _, files) in os.walk(absolute_src, followlinks=True):
168
+ for root, _, files in os.walk(absolute_src, followlinks=True):
168
169
  canonical_subdir = Path(root).relative_to(absolute_src)
169
170
  canonical_dest_subdir = dest / canonical_subdir
170
171
  self._update_dest_is_dir(canonical_dest_subdir, is_dir=True)
@@ -383,7 +384,7 @@ class BundleMap:
383
384
  if absolute_src.is_dir() and expand_directories:
384
385
  # both src and dest are directories, and expanding directories was requested. Traverse src, and map each
385
386
  # file to the dest directory
386
- for (root, subdirs, files) in os.walk(absolute_src, followlinks=True):
387
+ for root, subdirs, files in os.walk(absolute_src, followlinks=True):
387
388
  relative_root = Path(root).relative_to(absolute_src)
388
389
  for name in itertools.chain(subdirs, files):
389
390
  src_file_for_output = src_for_output / relative_root / name
@@ -674,16 +675,27 @@ def build_bundle(
674
675
  if resolved_root.exists():
675
676
  delete(resolved_root)
676
677
 
677
- bundle_map = BundleMap(project_root=project_root, deploy_root=deploy_root)
678
- for artifact in artifacts:
679
- bundle_map.add(artifact)
680
-
678
+ bundle_map = bundle_artifacts(project_root, deploy_root, artifacts)
681
679
  if bundle_map.is_empty():
682
680
  raise ArtifactError(
683
681
  "No artifacts mapping found in project definition, nothing to do."
684
682
  )
685
683
 
686
- for (absolute_src, absolute_dest) in bundle_map.all_mappings(
684
+ return bundle_map
685
+
686
+
687
+ def bundle_artifacts(
688
+ project_root: Path, deploy_root: Path, artifacts: list[PathMapping]
689
+ ):
690
+ """
691
+ Internal implementation of build_bundle that assumes
692
+ that validation is being done by the caller.
693
+ """
694
+ bundle_map = BundleMap(project_root=project_root, deploy_root=deploy_root)
695
+ for artifact in artifacts:
696
+ bundle_map.add(artifact)
697
+
698
+ for absolute_src, absolute_dest in bundle_map.all_mappings(
687
699
  absolute=True, expand_directories=False
688
700
  ):
689
701
  symlink_or_copy(absolute_src, absolute_dest, deploy_root=deploy_root)
@@ -716,7 +728,7 @@ def find_and_read_manifest_file(deploy_root: Path) -> Dict[str, Any]:
716
728
  "r", read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB
717
729
  ) as file:
718
730
  manifest_content = safe_load(file.read())
719
- return manifest_content
731
+ return manifest_content or {}
720
732
 
721
733
 
722
734
  def find_setup_script_file(deploy_root: Path) -> Path:
@@ -743,19 +755,24 @@ def find_setup_script_file(deploy_root: Path) -> Path:
743
755
  )
744
756
 
745
757
 
758
+ VersionInfo = namedtuple("VersionInfo", ["version_name", "patch_number", "label"])
759
+
760
+
746
761
  def find_version_info_in_manifest_file(
747
762
  deploy_root: Path,
748
- ) -> Tuple[Optional[str], Optional[int]]:
763
+ ) -> VersionInfo:
749
764
  """
750
765
  Find version and patch, if available, in the manifest.yml file.
751
766
  """
752
767
  name_field = "name"
753
768
  patch_field = "patch"
769
+ label_field = "label"
754
770
 
755
771
  manifest_content = find_and_read_manifest_file(deploy_root=deploy_root)
756
772
 
757
773
  version_name: Optional[str] = None
758
774
  patch_number: Optional[int] = None
775
+ label: Optional[str] = None
759
776
 
760
777
  version_info = manifest_content.get("version", None)
761
778
  if version_info:
@@ -763,5 +780,42 @@ def find_version_info_in_manifest_file(
763
780
  version_name = to_identifier(str(version_info[name_field]))
764
781
  if patch_field in version_info:
765
782
  patch_number = int(version_info[patch_field])
783
+ if label_field in version_info:
784
+ label = str(version_info[label_field])
785
+
786
+ return VersionInfo(version_name, patch_number, label)
787
+
788
+
789
+ def find_events_definitions_in_manifest_file(
790
+ deploy_root: Path,
791
+ ) -> List[Dict[str, str]]:
792
+ """
793
+ Find events definitions, if available, in the manifest.yml file.
794
+ Events definitions can be found under this section in the manifest.yml file:
795
+
796
+ configuration:
797
+ telemetry_event_definitions:
798
+ - type: ERRORS_AND_WARNINGS
799
+ sharing: MANDATORY
800
+ - type: DEBUG_LOGS
801
+ sharing: OPTIONAL
802
+ """
803
+ manifest_content = find_and_read_manifest_file(deploy_root=deploy_root)
804
+
805
+ configuration_section = manifest_content.get("configuration", None)
806
+ events_definitions = []
807
+ if configuration_section and isinstance(configuration_section, dict):
808
+ telemetry_section = configuration_section.get("telemetry_event_definitions", [])
809
+ if isinstance(telemetry_section, list):
810
+ for event in telemetry_section:
811
+ if isinstance(event, dict):
812
+ event_type = event.get("type", "")
813
+ events_definitions.append(
814
+ {
815
+ "name": f"SNOWFLAKE${event_type}",
816
+ "type": event_type,
817
+ "sharing": event.get("sharing", ""),
818
+ }
819
+ )
766
820
 
767
- return version_name, patch_number
821
+ return events_definitions
@@ -15,7 +15,7 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  from pathlib import Path
18
- from typing import Optional
18
+ from typing import Any, Optional
19
19
 
20
20
  import jinja2
21
21
  from snowflake.cli._plugins.nativeapp.artifacts import BundleMap
@@ -49,7 +49,9 @@ class TemplatesProcessor(ArtifactProcessor):
49
49
  Processor class to perform template expansion on all relevant artifacts (specified in the project definition file).
50
50
  """
51
51
 
52
- def expand_templates_in_file(self, src: Path, dest: Path) -> None:
52
+ def expand_templates_in_file(
53
+ self, src: Path, dest: Path, template_context: dict[str, Any] | None = None
54
+ ) -> None:
53
55
  """
54
56
  Expand templates in the file.
55
57
  """
@@ -74,7 +76,7 @@ class TemplatesProcessor(ArtifactProcessor):
74
76
  else get_client_side_jinja_env()
75
77
  )
76
78
  expanded_template = jinja_env.from_string(file.contents).render(
77
- get_cli_context().template_context
79
+ template_context or get_cli_context().template_context
78
80
  )
79
81
 
80
82
  # For now, we are printing the source file path in the error message