snowflake-cli 3.11.0__py3-none-any.whl → 3.12.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 (38) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/cli_app.py +0 -1
  3. snowflake/cli/_app/printing.py +153 -19
  4. snowflake/cli/_plugins/dbt/commands.py +37 -8
  5. snowflake/cli/_plugins/dbt/manager.py +144 -10
  6. snowflake/cli/_plugins/dcm/commands.py +65 -90
  7. snowflake/cli/_plugins/dcm/manager.py +137 -50
  8. snowflake/cli/_plugins/logs/commands.py +7 -0
  9. snowflake/cli/_plugins/logs/manager.py +21 -1
  10. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +3 -1
  11. snowflake/cli/_plugins/snowpark/common.py +1 -0
  12. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +29 -5
  13. snowflake/cli/_plugins/snowpark/package_utils.py +44 -3
  14. snowflake/cli/_plugins/spcs/services/manager.py +5 -4
  15. snowflake/cli/_plugins/sql/lexer/types.py +1 -0
  16. snowflake/cli/_plugins/sql/repl.py +100 -26
  17. snowflake/cli/_plugins/sql/repl_commands.py +607 -0
  18. snowflake/cli/_plugins/sql/statement_reader.py +44 -20
  19. snowflake/cli/api/artifacts/bundle_map.py +32 -2
  20. snowflake/cli/api/artifacts/regex_resolver.py +54 -0
  21. snowflake/cli/api/artifacts/upload.py +5 -1
  22. snowflake/cli/api/artifacts/utils.py +12 -1
  23. snowflake/cli/api/cli_global_context.py +7 -0
  24. snowflake/cli/api/console/abc.py +13 -2
  25. snowflake/cli/api/console/console.py +20 -0
  26. snowflake/cli/api/constants.py +9 -0
  27. snowflake/cli/api/entities/utils.py +10 -6
  28. snowflake/cli/api/feature_flags.py +1 -0
  29. snowflake/cli/api/identifiers.py +18 -1
  30. snowflake/cli/api/project/schemas/entities/entities.py +0 -6
  31. snowflake/cli/api/rendering/sql_templates.py +2 -0
  32. {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.12.0.dist-info}/METADATA +5 -5
  33. {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.12.0.dist-info}/RECORD +36 -36
  34. snowflake/cli/_plugins/dcm/dcm_project_entity_model.py +0 -59
  35. snowflake/cli/_plugins/sql/snowsql_commands.py +0 -331
  36. {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.12.0.dist-info}/WHEEL +0 -0
  37. {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.12.0.dist-info}/entry_points.txt +0 -0
  38. {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -255,14 +255,55 @@ def split_downloaded_dependencies(
255
255
  anaconda_packages: AnacondaPackages,
256
256
  skip_version_check: bool,
257
257
  ) -> SplitDownloadedDependenciesResult:
258
- packages_metadata: Dict[str, WheelMetadata] = {
259
- meta.name: meta
258
+ # Build metadata for all downloaded wheels
259
+ all_wheels_metadata = [
260
+ meta
260
261
  for meta in (
261
262
  WheelMetadata.from_wheel(wheel_path)
262
263
  for wheel_path in downloads_dir.glob("*.whl")
263
264
  )
264
265
  if meta is not None
265
- }
266
+ ]
267
+
268
+ # Detect and handle duplicate packages
269
+ packages_metadata: Dict[str, WheelMetadata] = {}
270
+ duplicate_packages = set()
271
+
272
+ for meta in all_wheels_metadata:
273
+ if meta.name in packages_metadata:
274
+ duplicate_packages.add(meta.name)
275
+ log.warning(
276
+ "Multiple versions of package '%s' found in dependencies. "
277
+ "Using: %s, Ignoring: %s",
278
+ meta.name,
279
+ packages_metadata[meta.name].wheel_path.name,
280
+ meta.wheel_path.name,
281
+ )
282
+ else:
283
+ packages_metadata[meta.name] = meta
284
+
285
+ if duplicate_packages:
286
+ log.warning(
287
+ "Found duplicate packages: %s. This may cause deployment issues. "
288
+ "Consider pinning package versions in requirements.txt to avoid conflicts.",
289
+ ", ".join(sorted(duplicate_packages)),
290
+ )
291
+
292
+ # Remove duplicate wheel files to prevent them from being extracted
293
+ for meta in all_wheels_metadata:
294
+ if (
295
+ meta.name in duplicate_packages
296
+ and meta not in packages_metadata.values()
297
+ ):
298
+ try:
299
+ meta.wheel_path.unlink()
300
+ log.debug("Removed duplicate wheel file: %s", meta.wheel_path.name)
301
+ except Exception as e:
302
+ log.warning(
303
+ "Failed to remove duplicate wheel file %s: %s",
304
+ meta.wheel_path.name,
305
+ e,
306
+ )
266
307
  available_in_snowflake_dependencies: Dict = {}
267
308
  unavailable_dependencies: Dict = {}
268
309
 
@@ -313,11 +313,12 @@ class ServiceManager(SqlExecutionMixin):
313
313
 
314
314
  if new_log_records:
315
315
  dedup_log_records = new_logs_only(prev_log_records, new_log_records)
316
- for log in dedup_log_records:
317
- yield filter_log_timestamp(log, include_timestamps)
316
+ if dedup_log_records:
317
+ for log in dedup_log_records:
318
+ yield filter_log_timestamp(log, include_timestamps)
318
319
 
319
- prev_timestamp = dedup_log_records[-1].split(" ", 1)[0]
320
- prev_log_records = dedup_log_records
320
+ prev_timestamp = dedup_log_records[-1].split(" ", 1)[0]
321
+ prev_log_records = dedup_log_records
321
322
 
322
323
  time.sleep(interval_seconds)
323
324
 
@@ -11,6 +11,7 @@ TYPES = (
11
11
  "DATETIME",
12
12
  "DEC",
13
13
  "DECIMAL",
14
+ "DECFLOAT",
14
15
  "DOUBLE",
15
16
  "FLOAT",
16
17
  "INT",
@@ -1,3 +1,4 @@
1
+ from contextlib import contextmanager
1
2
  from logging import getLogger
2
3
  from typing import Iterable
3
4
 
@@ -10,6 +11,7 @@ from prompt_toolkit.lexers import PygmentsLexer
10
11
  from snowflake.cli._app.printing import print_result
11
12
  from snowflake.cli._plugins.sql.lexer import CliLexer, cli_completer
12
13
  from snowflake.cli._plugins.sql.manager import SqlManager
14
+ from snowflake.cli._plugins.sql.repl_commands import detect_command
13
15
  from snowflake.cli.api.cli_global_context import get_cli_context_manager
14
16
  from snowflake.cli.api.console import cli_console
15
17
  from snowflake.cli.api.output.types import MultipleResults, QueryResult
@@ -28,6 +30,21 @@ EXIT_KEYWORDS = ("exit", "quit")
28
30
  log.debug("setting history file to: %s", HISTORY_FILE.as_posix())
29
31
 
30
32
 
33
+ @contextmanager
34
+ def repl_context(repl_instance):
35
+ """Context manager for REPL execution that handles CLI context registration."""
36
+ context_manager = get_cli_context_manager()
37
+ context_manager.is_repl = True
38
+ context_manager.repl_instance = repl_instance
39
+
40
+ try:
41
+ yield
42
+ finally:
43
+ # Clean up REPL context
44
+ context_manager.is_repl = False
45
+ context_manager.repl_instance = None
46
+
47
+
31
48
  class Repl:
32
49
  """Basic REPL implementation for the Snowflake CLI."""
33
50
 
@@ -45,7 +62,6 @@ class Repl:
45
62
  `retain_comments` how to handle comments in queries
46
63
  """
47
64
  super().__init__()
48
- setattr(get_cli_context_manager(), "is_repl", True)
49
65
  self._data = data or {}
50
66
  self._retain_comments = retain_comments
51
67
  self._template_syntax_config = template_syntax_config
@@ -56,6 +72,7 @@ class Repl:
56
72
  self._yes_no_keybindings = self._setup_yn_key_bindings()
57
73
  self._sql_manager = sql_manager
58
74
  self.session = PromptSession(history=self._history)
75
+ self._next_input: str | None = None
59
76
 
60
77
  def _setup_key_bindings(self) -> KeyBindings:
61
78
  """Key bindings for repl. Helps detecting ; at end of buffer."""
@@ -65,22 +82,52 @@ class Repl:
65
82
  def not_searching():
66
83
  return not is_searching()
67
84
 
85
+ @kb.add(Keys.BracketedPaste)
86
+ def _(event):
87
+ """Handle bracketed paste - normalize line endings and strip trailing whitespace."""
88
+ pasted_data = event.data
89
+ # Normalize line endings: \r\n -> \n, \r -> \n
90
+ normalized_data = pasted_data.replace("\r\n", "\n").replace("\r", "\n")
91
+ # Strip trailing whitespace
92
+ cleaned_data = normalized_data.rstrip()
93
+ buffer = event.app.current_buffer
94
+ buffer.insert_text(cleaned_data)
95
+ log.debug(
96
+ "handled paste operation, normalized line endings and stripped trailing whitespace"
97
+ )
98
+
68
99
  @kb.add(Keys.Enter, filter=not_searching)
69
100
  def _(event):
70
- """Handle Enter key press."""
101
+ """Handle Enter key press with intelligent execution logic.
102
+
103
+ Execution priority:
104
+ 1. Exit keywords (exit, quit) - execute immediately
105
+ 2. REPL commands (starting with !) - execute immediately
106
+ 3. SQL with trailing semicolon - execute immediately
107
+ 4. All other input - add new line for multi-line editing
108
+ """
71
109
  buffer = event.app.current_buffer
72
- stripped_buffer = buffer.text.strip()
110
+ buffer_text = buffer.text
111
+ stripped_text = buffer_text.strip()
73
112
 
74
- if stripped_buffer:
113
+ if stripped_text:
75
114
  log.debug("evaluating repl input")
76
115
  cursor_position = buffer.cursor_position
77
- ends_with_semicolon = buffer.text.endswith(";")
116
+ ends_with_semicolon = stripped_text.endswith(";")
117
+ is_command = detect_command(stripped_text) is not None
78
118
 
79
- if stripped_buffer.lower() in EXIT_KEYWORDS:
80
- log.debug("exit keyword detected %r", stripped_buffer)
119
+ meaningful_content_end = len(buffer_text.rstrip())
120
+ cursor_at_meaningful_end = cursor_position >= meaningful_content_end
121
+
122
+ if stripped_text.lower() in EXIT_KEYWORDS:
123
+ log.debug("exit keyword detected %r", stripped_text)
124
+ buffer.validate_and_handle()
125
+
126
+ elif is_command:
127
+ log.debug("command detected, submitting input")
81
128
  buffer.validate_and_handle()
82
129
 
83
- elif ends_with_semicolon and cursor_position >= len(stripped_buffer):
130
+ elif ends_with_semicolon and cursor_at_meaningful_end:
84
131
  log.debug("semicolon detected, submitting input")
85
132
  buffer.validate_and_handle()
86
133
 
@@ -118,16 +165,27 @@ class Repl:
118
165
 
119
166
  return kb
120
167
 
121
- def repl_propmpt(self, msg: str = " > ") -> str:
122
- """Regular repl prompt."""
123
- return self.session.prompt(
124
- msg,
125
- lexer=self._lexer,
126
- completer=self._completer,
127
- multiline=True,
128
- wrap_lines=True,
129
- key_bindings=self._repl_key_bindings,
130
- )
168
+ def repl_prompt(self, msg: str = " > ") -> str:
169
+ """Regular repl prompt with support for pre-filled input.
170
+
171
+ Checks for queued input from commands like !edit and uses it as
172
+ default text in the prompt. The queued input is cleared after use.
173
+ """
174
+ default_text = self._next_input
175
+
176
+ try:
177
+ return self.session.prompt(
178
+ msg,
179
+ lexer=self._lexer,
180
+ completer=self._completer,
181
+ multiline=True,
182
+ wrap_lines=True,
183
+ key_bindings=self._repl_key_bindings,
184
+ default=default_text or "",
185
+ )
186
+ finally:
187
+ if self._next_input == default_text:
188
+ self._next_input = None
131
189
 
132
190
  def yn_prompt(self, msg: str) -> str:
133
191
  """Yes/No prompt."""
@@ -142,7 +200,7 @@ class Repl:
142
200
 
143
201
  @property
144
202
  def _welcome_banner(self) -> str:
145
- return f"Welcome to Snowflake-CLI REPL\nType 'exit' or 'quit' to leave"
203
+ return "Welcome to Snowflake-CLI REPL\nType 'exit' or 'quit' to leave"
146
204
 
147
205
  def _initialize_connection(self):
148
206
  """Early connection for possible fast fail."""
@@ -163,12 +221,13 @@ class Repl:
163
221
  return cursors
164
222
 
165
223
  def run(self):
166
- try:
167
- cli_console.panel(self._welcome_banner)
168
- self._initialize_connection()
169
- self._repl_loop()
170
- except (KeyboardInterrupt, EOFError):
171
- cli_console.message("\n[bold orange_red1]Leaving REPL, bye ...")
224
+ with repl_context(self):
225
+ try:
226
+ cli_console.panel(self._welcome_banner)
227
+ self._initialize_connection()
228
+ self._repl_loop()
229
+ except (KeyboardInterrupt, EOFError):
230
+ cli_console.message("\n[bold orange_red1]Leaving REPL, bye ...")
172
231
 
173
232
  def _repl_loop(self):
174
233
  """Main REPL loop. Handles input and query execution.
@@ -178,7 +237,7 @@ class Repl:
178
237
  """
179
238
  while True:
180
239
  try:
181
- user_input = self.repl_propmpt().strip()
240
+ user_input = self.repl_prompt().strip()
182
241
 
183
242
  if not user_input:
184
243
  continue
@@ -210,6 +269,21 @@ class Repl:
210
269
  except Exception as e:
211
270
  cli_console.warning(f"\nError occurred: {e}")
212
271
 
272
+ def set_next_input(self, text: str) -> None:
273
+ """Set the text that will be used as the next REPL input."""
274
+ self._next_input = text
275
+ log.debug("Next input has been set")
276
+
277
+ @property
278
+ def next_input(self) -> str | None:
279
+ """Get the next input text that will be used in the prompt."""
280
+ return self._next_input
281
+
282
+ @property
283
+ def history(self) -> FileHistory:
284
+ """Get the FileHistory instance used by the REPL."""
285
+ return self._history
286
+
213
287
  def ask_yn(self, question: str) -> bool:
214
288
  """Asks user a Yes/No question."""
215
289
  try: