snowflake-cli-labs 3.0.0rc0__py3-none-any.whl → 3.0.0rc2__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 (66) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/cli_app.py +10 -1
  3. snowflake/cli/_app/snow_connector.py +91 -37
  4. snowflake/cli/_app/telemetry.py +8 -4
  5. snowflake/cli/_app/version_check.py +74 -0
  6. snowflake/cli/_plugins/connection/commands.py +3 -2
  7. snowflake/cli/_plugins/git/commands.py +55 -14
  8. snowflake/cli/_plugins/git/manager.py +14 -6
  9. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +18 -2
  10. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +123 -42
  11. snowflake/cli/_plugins/nativeapp/codegen/setup/setup_driver.py.source +5 -2
  12. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +6 -11
  13. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +111 -0
  14. snowflake/cli/_plugins/nativeapp/exceptions.py +3 -3
  15. snowflake/cli/_plugins/nativeapp/manager.py +74 -144
  16. snowflake/cli/_plugins/nativeapp/project_model.py +2 -9
  17. snowflake/cli/_plugins/nativeapp/run_processor.py +56 -260
  18. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +74 -0
  19. snowflake/cli/_plugins/nativeapp/teardown_processor.py +17 -246
  20. snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +91 -17
  21. snowflake/cli/_plugins/snowpark/commands.py +5 -65
  22. snowflake/cli/_plugins/snowpark/common.py +17 -1
  23. snowflake/cli/_plugins/snowpark/models.py +2 -1
  24. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -35
  25. snowflake/cli/_plugins/sql/commands.py +1 -2
  26. snowflake/cli/_plugins/stage/commands.py +2 -2
  27. snowflake/cli/_plugins/stage/manager.py +46 -15
  28. snowflake/cli/_plugins/streamlit/commands.py +4 -63
  29. snowflake/cli/_plugins/streamlit/manager.py +13 -0
  30. snowflake/cli/_plugins/workspace/action_context.py +7 -0
  31. snowflake/cli/_plugins/workspace/commands.py +145 -32
  32. snowflake/cli/_plugins/workspace/manager.py +21 -4
  33. snowflake/cli/api/cli_global_context.py +136 -313
  34. snowflake/cli/api/commands/decorators.py +1 -1
  35. snowflake/cli/api/commands/flags.py +106 -102
  36. snowflake/cli/api/commands/snow_typer.py +15 -6
  37. snowflake/cli/api/config.py +18 -5
  38. snowflake/cli/api/connections.py +214 -0
  39. snowflake/cli/api/console/abc.py +4 -2
  40. snowflake/cli/api/constants.py +11 -0
  41. snowflake/cli/api/entities/application_entity.py +687 -2
  42. snowflake/cli/api/entities/application_package_entity.py +407 -9
  43. snowflake/cli/api/entities/common.py +7 -2
  44. snowflake/cli/api/entities/utils.py +80 -20
  45. snowflake/cli/api/exceptions.py +12 -2
  46. snowflake/cli/api/feature_flags.py +0 -2
  47. snowflake/cli/api/identifiers.py +3 -0
  48. snowflake/cli/api/project/definition.py +35 -1
  49. snowflake/cli/api/project/definition_conversion.py +352 -0
  50. snowflake/cli/api/project/schemas/entities/application_package_entity_model.py +17 -0
  51. snowflake/cli/api/project/schemas/entities/common.py +0 -12
  52. snowflake/cli/api/project/schemas/identifier_model.py +2 -2
  53. snowflake/cli/api/project/schemas/project_definition.py +102 -43
  54. snowflake/cli/api/rendering/jinja.py +2 -16
  55. snowflake/cli/api/rendering/project_definition_templates.py +5 -1
  56. snowflake/cli/api/rendering/sql_templates.py +14 -4
  57. snowflake/cli/api/secure_path.py +13 -18
  58. snowflake/cli/api/secure_utils.py +90 -1
  59. snowflake/cli/api/sql_execution.py +13 -0
  60. snowflake/cli/api/utils/definition_rendering.py +7 -7
  61. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/METADATA +9 -9
  62. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/RECORD +65 -61
  63. snowflake/cli/api/commands/typer_pre_execute.py +0 -26
  64. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/WHEEL +0 -0
  65. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/entry_points.txt +0 -0
  66. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/licenses/LICENSE +0 -0
@@ -14,337 +14,126 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- import re
18
- import warnings
17
+ from contextlib import contextmanager
18
+ from contextvars import ContextVar
19
+ from dataclasses import dataclass, field, replace
19
20
  from pathlib import Path
20
- from typing import TYPE_CHECKING, Callable, Optional
21
+ from typing import TYPE_CHECKING, Iterator
21
22
 
22
- from snowflake.cli.api.exceptions import InvalidSchemaError
23
+ from snowflake.cli.api.connections import ConnectionContext, OpenConnectionCache
24
+ from snowflake.cli.api.exceptions import MissingConfiguration
23
25
  from snowflake.cli.api.output.formats import OutputFormat
26
+ from snowflake.cli.api.rendering.jinja import CONTEXT_KEY
24
27
  from snowflake.connector import SnowflakeConnection
25
- from snowflake.connector.compat import IS_WINDOWS
26
28
 
27
29
  if TYPE_CHECKING:
30
+ from snowflake.cli.api.project.definition_manager import DefinitionManager
28
31
  from snowflake.cli.api.project.schemas.project_definition import ProjectDefinition
29
32
 
30
- schema_pattern = re.compile(r".+\..+")
31
-
32
-
33
- class _ConnectionContext:
34
- def __init__(self):
35
- self._cached_connection: Optional[SnowflakeConnection] = None
36
-
37
- self._connection_name: Optional[str] = None
38
- self._account: Optional[str] = None
39
- self._database: Optional[str] = None
40
- self._role: Optional[str] = None
41
- self._schema: Optional[str] = None
42
- self._user: Optional[str] = None
43
- self._password: Optional[str] = None
44
- self._authenticator: Optional[str] = None
45
- self._private_key_path: Optional[str] = None
46
- self._warehouse: Optional[str] = None
47
- self._mfa_passcode: Optional[str] = None
48
- self._enable_diag: Optional[bool] = False
49
- self._diag_log_path: Optional[Path] = None
50
- self._diag_allowlist_path: Optional[Path] = None
51
- self._temporary_connection: bool = False
52
- self._session_token: Optional[str] = None
53
- self._master_token: Optional[str] = None
54
- self._token_file_path: Optional[Path] = None
55
-
56
- def __setattr__(self, key, value):
57
- """
58
- We invalidate connection cache every time connection attributes change.
59
- """
60
- super().__setattr__(key, value)
61
- if key != "_cached_connection":
62
- self._cached_connection = None
63
-
64
- @property
65
- def connection_name(self) -> Optional[str]:
66
- return self._connection_name
67
-
68
- def set_connection_name(self, value: Optional[str]):
69
- self._connection_name = value
70
-
71
- @property
72
- def account(self) -> Optional[str]:
73
- return self._account
74
-
75
- def set_account(self, value: Optional[str]):
76
- self._account = value
77
-
78
- @property
79
- def database(self) -> Optional[str]:
80
- return self._database
81
-
82
- def set_database(self, value: Optional[str]):
83
- self._database = value
84
-
85
- @property
86
- def role(self) -> Optional[str]:
87
- return self._role
88
-
89
- def set_role(self, value: Optional[str]):
90
- self._role = value
91
-
92
- @property
93
- def schema(self) -> Optional[str]:
94
- return self._schema
95
-
96
- def set_schema(self, value: Optional[str]):
97
- if (
98
- value
99
- and not (value.startswith('"') and value.endswith('"'))
100
- # if schema is fully qualified name (db.schema)
101
- and schema_pattern.match(value)
102
- ):
103
- raise InvalidSchemaError(value)
104
- self._schema = value
105
-
106
- @property
107
- def user(self) -> Optional[str]:
108
- return self._user
109
-
110
- def set_user(self, value: Optional[str]):
111
- self._user = value
112
-
113
- @property
114
- def password(self) -> Optional[str]:
115
- return self._password
116
-
117
- def set_password(self, value: Optional[str]):
118
- self._password = value
119
-
120
- @property
121
- def authenticator(self) -> Optional[str]:
122
- return self._authenticator
123
-
124
- def set_authenticator(self, value: Optional[str]):
125
- self._authenticator = value
126
-
127
- @property
128
- def private_key_path(self) -> Optional[str]:
129
- return self._private_key_path
130
-
131
- def set_private_key_path(self, value: Optional[str]):
132
- self._private_key_path = value
133
-
134
- @property
135
- def warehouse(self) -> Optional[str]:
136
- return self._warehouse
137
-
138
- def set_warehouse(self, value: Optional[str]):
139
- self._warehouse = value
140
-
141
- @property
142
- def mfa_passcode(self) -> Optional[str]:
143
- return self._mfa_passcode
144
-
145
- def set_mfa_passcode(self, value: Optional[str]):
146
- self._mfa_passcode = value
147
-
148
- @property
149
- def enable_diag(self) -> Optional[bool]:
150
- return self._enable_diag
151
-
152
- def set_enable_diag(self, value: Optional[bool]):
153
- self._enable_diag = value
154
-
155
- @property
156
- def diag_log_path(self) -> Optional[Path]:
157
- return self._diag_log_path
158
-
159
- def set_diag_log_path(self, value: Optional[Path]):
160
- self._diag_log_path = value
161
-
162
- @property
163
- def diag_allowlist_path(self) -> Optional[Path]:
164
- return self._diag_allowlist_path
165
-
166
- def set_diag_allowlist_path(self, value: Optional[Path]):
167
- self._diag_allowlist_path = value
168
-
169
- @property
170
- def temporary_connection(self) -> bool:
171
- return self._temporary_connection
172
-
173
- def set_temporary_connection(self, value: bool):
174
- self._temporary_connection = value
175
-
176
- @property
177
- def session_token(self) -> Optional[str]:
178
- return self._session_token
179
-
180
- def set_session_token(self, value: Optional[str]):
181
- self._session_token = value
182
-
183
- @property
184
- def master_token(self) -> Optional[str]:
185
- return self._master_token
186
-
187
- def set_master_token(self, value: Optional[str]):
188
- self._master_token = value
189
-
190
- @property
191
- def token_file_path(self) -> Optional[Path]:
192
- return self._token_file_path
193
-
194
- def set_token_file_path(self, value: Optional[Path]):
195
- self._token_file_path = value
196
-
197
- @property
198
- def connection(self) -> SnowflakeConnection:
199
- if not self._cached_connection:
200
- self._cached_connection = self._build_connection()
201
- return self._cached_connection
202
-
203
- def _collect_not_empty_connection_attributes(self):
204
- return {
205
- "account": self.account,
206
- "user": self.user,
207
- "password": self.password,
208
- "authenticator": self.authenticator,
209
- "private_key_path": self.private_key_path,
210
- "database": self.database,
211
- "schema": self.schema,
212
- "role": self.role,
213
- "warehouse": self.warehouse,
214
- "session_token": self.session_token,
215
- "master_token": self.master_token,
216
- "token_file_path": self.token_file_path,
217
- }
218
-
219
- def _build_connection(self):
220
- from snowflake.cli._app.snow_connector import connect_to_snowflake
221
-
222
- # Ignore warnings about bad owner or permissions on Windows
223
- # Telemetry omit our warning filter from config.py
224
- if IS_WINDOWS:
225
- warnings.filterwarnings(
226
- action="ignore",
227
- message="Bad owner or permissions.*",
228
- module="snowflake.connector.config_manager",
229
- )
230
-
231
- return connect_to_snowflake(
232
- temporary_connection=self.temporary_connection,
233
- mfa_passcode=self._mfa_passcode,
234
- enable_diag=self._enable_diag,
235
- diag_log_path=self._diag_log_path,
236
- diag_allowlist_path=self._diag_allowlist_path,
237
- connection_name=self.connection_name,
238
- **self._collect_not_empty_connection_attributes(),
239
- )
33
+ _CONNECTION_CACHE = OpenConnectionCache()
240
34
 
241
35
 
36
+ @dataclass
242
37
  class _CliGlobalContextManager:
243
- def __init__(self):
244
- self._connection_context = _ConnectionContext()
245
- self._enable_tracebacks = True
246
- self._output_format = OutputFormat.TABLE
247
- self._verbose = False
248
- self._experimental = False
249
- self._project_definition = None
250
- self._project_root = None
251
- self._project_path_arg = None
252
- self._project_env_overrides_args = {}
253
- self._typer_pre_execute_commands = []
254
- self._template_context = None
255
- self._silent: bool = False
38
+ connection_context: ConnectionContext = field(default_factory=ConnectionContext)
39
+ connection_cache: OpenConnectionCache = (
40
+ _CONNECTION_CACHE # by default, use global cache
41
+ )
42
+
43
+ output_format: OutputFormat = OutputFormat.TABLE
44
+ silent: bool = False
45
+ verbose: bool = False
46
+ experimental: bool = False
47
+ enable_tracebacks: bool = True
48
+
49
+ project_path_arg: str | None = None
50
+ project_is_optional: bool = True
51
+ project_env_overrides_args: dict[str, str] = field(default_factory=dict)
52
+
53
+ # FIXME: this property only exists to help implement
54
+ # nativeapp_definition_v2_to_v1. Consider changing the way
55
+ # this calculation is provided to commands in order to remove
56
+ # this logic (then make project_definition a non-cloned @property)
57
+ override_project_definition: ProjectDefinition | None = None
58
+
59
+ _definition_manager: DefinitionManager | None = None
60
+
61
+ # which properties invalidate our current DefinitionManager?
62
+ DEFINITION_MANAGER_DEPENDENCIES = [
63
+ "project_path_arg",
64
+ "project_is_optional",
65
+ "project_env_overrides_args",
66
+ ]
256
67
 
257
68
  def reset(self):
258
69
  self.__init__()
259
70
 
260
- @property
261
- def enable_tracebacks(self) -> bool:
262
- return self._enable_tracebacks
263
-
264
- def set_enable_tracebacks(self, value: bool):
265
- self._enable_tracebacks = value
266
-
267
- @property
268
- def output_format(self) -> OutputFormat:
269
- return self._output_format
270
-
271
- def set_output_format(self, value: OutputFormat):
272
- self._output_format = value
273
-
274
- @property
275
- def verbose(self) -> bool:
276
- return self._verbose
277
-
278
- def set_verbose(self, value: bool):
279
- self._verbose = value
280
-
281
- @property
282
- def experimental(self) -> bool:
283
- return self._experimental
284
-
285
- def set_experimental(self, value: bool):
286
- self._experimental = value
287
-
288
- @property
289
- def project_definition(self) -> Optional[ProjectDefinition]:
290
- return self._project_definition
291
-
292
- def set_project_definition(self, value: ProjectDefinition):
293
- self._project_definition = value
71
+ def clone(self) -> _CliGlobalContextManager:
72
+ return replace(
73
+ self,
74
+ connection_context=self.connection_context.clone(),
75
+ project_env_overrides_args=self.project_env_overrides_args.copy(),
76
+ )
294
77
 
295
- @property
296
- def project_root(self):
297
- return self._project_root
78
+ def __setattr__(self, prop, val):
79
+ if prop in self.DEFINITION_MANAGER_DEPENDENCIES:
80
+ self._clear_definition_manager()
298
81
 
299
- def set_project_root(self, project_root: Path):
300
- self._project_root = project_root
82
+ super().__setattr__(prop, val)
301
83
 
302
84
  @property
303
- def project_path_arg(self) -> Optional[str]:
304
- return self._project_path_arg
85
+ def project_definition(self) -> ProjectDefinition | None:
86
+ if self.override_project_definition:
87
+ return self.override_project_definition
305
88
 
306
- def set_project_path_arg(self, project_path_arg: str):
307
- self._project_path_arg = project_path_arg
89
+ return self._definition_manager_or_raise().project_definition
308
90
 
309
91
  @property
310
- def project_env_overrides_args(self) -> dict[str, str]:
311
- return self._project_env_overrides_args
312
-
313
- def set_project_env_overrides_args(
314
- self, project_env_overrides_args: dict[str, str]
315
- ):
316
- self._project_env_overrides_args = project_env_overrides_args
92
+ def project_root(self) -> Path:
93
+ return Path(self._definition_manager_or_raise().project_root)
317
94
 
318
95
  @property
319
96
  def template_context(self) -> dict:
320
- return self._template_context
321
-
322
- def set_template_context(self, template_context: dict):
323
- self._template_context = template_context
97
+ return self._definition_manager_or_raise().template_context
324
98
 
325
99
  @property
326
- def typer_pre_execute_commands(self) -> list[Callable[[], None]]:
327
- return self._typer_pre_execute_commands
328
-
329
- def add_typer_pre_execute_commands(
330
- self, typer_pre_execute_command: Callable[[], None]
331
- ):
332
- self._typer_pre_execute_commands.append(typer_pre_execute_command)
100
+ def connection(self) -> SnowflakeConnection:
101
+ """
102
+ Returns a connection for our configured context from the configured cache.
103
+ By default, this is the global _CONNECTION_CACHE. If a matching connection
104
+ does not already exist, creates a new connection and caches it.
105
+ """
106
+ self.connection_context.validate_and_complete()
107
+ return self.connection_cache[self.connection_context]
333
108
 
334
- @property
335
- def connection_context(self) -> _ConnectionContext:
336
- return self._connection_context
109
+ def _definition_manager_or_raise(self) -> DefinitionManager:
110
+ """
111
+ (Re-)parses project definition based on project args (project_path_arg and
112
+ project_env_overrides_args). If we cannot provide a project definition
113
+ (i.e. no snowflake.yml) and require one, raises MissingConfiguration.
114
+ """
115
+ from snowflake.cli.api.project.definition_manager import DefinitionManager
337
116
 
338
- @property
339
- def connection(self) -> SnowflakeConnection:
340
- return self.connection_context.connection
117
+ # don't need to re-parse definition if we already have one
118
+ if not self._definition_manager:
119
+ dm = DefinitionManager(
120
+ self.project_path_arg,
121
+ {CONTEXT_KEY: {"env": self.project_env_overrides_args}},
122
+ )
123
+ if not dm.has_definition_file and not self.project_is_optional:
124
+ raise MissingConfiguration(
125
+ "Cannot find project definition (snowflake.yml). Please provide a path to the project or run this command in a valid project directory."
126
+ )
127
+ self._definition_manager = dm
341
128
 
342
- @property
343
- def silent(self) -> bool:
344
- return self._silent
129
+ return self._definition_manager
345
130
 
346
- def set_silent(self, value: bool):
347
- self._silent = value
131
+ def _clear_definition_manager(self):
132
+ """
133
+ Force re-calculation of definition_manager and its dependent attributes
134
+ (template_context, project_definition, and project_root).
135
+ """
136
+ self._definition_manager = None
348
137
 
349
138
 
350
139
  class _CliGlobalContextAccess:
@@ -356,7 +145,7 @@ class _CliGlobalContextAccess:
356
145
  return self._manager.connection
357
146
 
358
147
  @property
359
- def connection_context(self) -> _ConnectionContext:
148
+ def connection_context(self) -> ConnectionContext:
360
149
  return self._manager.connection_context
361
150
 
362
151
  @property
@@ -380,8 +169,8 @@ class _CliGlobalContextAccess:
380
169
  return self._manager.project_definition
381
170
 
382
171
  @property
383
- def project_root(self) -> Path:
384
- return Path(self._manager.project_root)
172
+ def project_root(self) -> Path | None:
173
+ return self._manager.project_root
385
174
 
386
175
  @property
387
176
  def template_context(self) -> dict:
@@ -399,19 +188,53 @@ class _CliGlobalContextAccess:
399
188
  return self._manager.output_format == OutputFormat.JSON
400
189
 
401
190
 
402
- _CLI_CONTEXT_MANAGER: _CliGlobalContextManager | None = None
403
- _CLI_CONTEXT: _CliGlobalContextAccess | None = None
191
+ _CLI_CONTEXT_MANAGER: ContextVar[_CliGlobalContextManager | None] = ContextVar(
192
+ "cli_context", default=None
193
+ )
404
194
 
405
195
 
406
196
  def get_cli_context_manager() -> _CliGlobalContextManager:
407
- global _CLI_CONTEXT_MANAGER
408
- if _CLI_CONTEXT_MANAGER is None:
409
- _CLI_CONTEXT_MANAGER = _CliGlobalContextManager()
410
- return _CLI_CONTEXT_MANAGER
197
+ mgr = _CLI_CONTEXT_MANAGER.get()
198
+ if not mgr:
199
+ mgr = _CliGlobalContextManager()
200
+ _CLI_CONTEXT_MANAGER.set(mgr)
201
+ return mgr
411
202
 
412
203
 
413
204
  def get_cli_context() -> _CliGlobalContextAccess:
414
- global _CLI_CONTEXT
415
- if _CLI_CONTEXT is None:
416
- _CLI_CONTEXT = _CliGlobalContextAccess(get_cli_context_manager())
417
- return _CLI_CONTEXT
205
+ return _CliGlobalContextAccess(get_cli_context_manager())
206
+
207
+
208
+ @contextmanager
209
+ def fork_cli_context(
210
+ connection_overrides: dict | None = None,
211
+ project_env_overrides: dict[str, str] | None = None,
212
+ project_is_optional: bool | None = None,
213
+ project_path: str | None = None,
214
+ ) -> Iterator[_CliGlobalContextAccess]:
215
+ """
216
+ Forks the global CLI context, making changes that are only visible
217
+ (e.g. via get_cli_context()) while inside this context manager.
218
+
219
+ Please note that environment variable changes are only visible through
220
+ the project definition; os.getenv / os.environ / get_env_value are not
221
+ affected by these new values.
222
+ """
223
+ old_manager = get_cli_context_manager()
224
+ new_manager = old_manager.clone()
225
+ token = _CLI_CONTEXT_MANAGER.set(new_manager)
226
+
227
+ if connection_overrides:
228
+ new_manager.connection_context.update(**connection_overrides)
229
+
230
+ if project_env_overrides:
231
+ new_manager.project_env_overrides_args.update(project_env_overrides)
232
+
233
+ if project_is_optional is not None:
234
+ new_manager.project_is_optional = project_is_optional
235
+
236
+ if project_path:
237
+ new_manager.project_path_arg = project_path
238
+
239
+ yield _CliGlobalContextAccess(new_manager)
240
+ _CLI_CONTEXT_MANAGER.reset(token)
@@ -235,7 +235,7 @@ GLOBAL_CONNECTION_OPTIONS = [
235
235
  default=AuthenticatorOption,
236
236
  ),
237
237
  inspect.Parameter(
238
- "private_key_path",
238
+ "private_key_file",
239
239
  inspect.Parameter.KEYWORD_ONLY,
240
240
  annotation=Optional[str],
241
241
  default=PrivateKeyPathOption,