snowflake-cli 3.7.1__py3-none-any.whl → 3.8.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 (56) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/snow_connector.py +14 -0
  3. snowflake/cli/_app/telemetry.py +11 -0
  4. snowflake/cli/_plugins/connection/commands.py +4 -2
  5. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +1 -1
  6. snowflake/cli/_plugins/nativeapp/entities/application_package.py +20 -7
  7. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +5 -3
  8. snowflake/cli/_plugins/project/commands.py +16 -6
  9. snowflake/cli/_plugins/snowpark/common.py +31 -0
  10. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +3 -0
  11. snowflake/cli/_plugins/snowpark/snowpark_entity.py +21 -1
  12. snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +23 -1
  13. snowflake/cli/_plugins/spcs/common.py +7 -0
  14. snowflake/cli/_plugins/spcs/image_repository/commands.py +7 -2
  15. snowflake/cli/_plugins/spcs/image_repository/manager.py +6 -2
  16. snowflake/cli/_plugins/spcs/services/commands.py +2 -2
  17. snowflake/cli/_plugins/spcs/services/manager.py +36 -1
  18. snowflake/cli/_plugins/sql/commands.py +57 -6
  19. snowflake/cli/_plugins/sql/lexer/__init__.py +7 -0
  20. snowflake/cli/_plugins/sql/lexer/completer.py +12 -0
  21. snowflake/cli/_plugins/sql/lexer/functions.py +421 -0
  22. snowflake/cli/_plugins/sql/lexer/keywords.py +529 -0
  23. snowflake/cli/_plugins/sql/lexer/lexer.py +56 -0
  24. snowflake/cli/_plugins/sql/lexer/types.py +37 -0
  25. snowflake/cli/_plugins/sql/manager.py +43 -9
  26. snowflake/cli/_plugins/sql/repl.py +221 -0
  27. snowflake/cli/_plugins/sql/snowsql_commands.py +331 -0
  28. snowflake/cli/_plugins/sql/statement_reader.py +296 -0
  29. snowflake/cli/_plugins/streamlit/commands.py +30 -15
  30. snowflake/cli/_plugins/streamlit/manager.py +0 -183
  31. snowflake/cli/_plugins/streamlit/streamlit_entity.py +163 -23
  32. snowflake/cli/api/artifacts/upload.py +5 -0
  33. snowflake/cli/api/artifacts/utils.py +0 -2
  34. snowflake/cli/api/cli_global_context.py +7 -3
  35. snowflake/cli/api/commands/decorators.py +70 -0
  36. snowflake/cli/api/commands/flags.py +95 -3
  37. snowflake/cli/api/config.py +10 -0
  38. snowflake/cli/api/connections.py +10 -0
  39. snowflake/cli/api/console/abc.py +8 -2
  40. snowflake/cli/api/console/console.py +16 -0
  41. snowflake/cli/api/console/enum.py +1 -1
  42. snowflake/cli/api/entities/common.py +99 -10
  43. snowflake/cli/api/entities/utils.py +1 -0
  44. snowflake/cli/api/feature_flags.py +6 -0
  45. snowflake/cli/api/project/project_paths.py +5 -0
  46. snowflake/cli/api/rendering/sql_templates.py +2 -1
  47. snowflake/cli/api/sql_execution.py +16 -4
  48. snowflake/cli/api/utils/path_utils.py +15 -0
  49. snowflake/cli/api/utils/python_api_utils.py +12 -0
  50. {snowflake_cli-3.7.1.dist-info → snowflake_cli-3.8.0.dist-info}/METADATA +12 -8
  51. {snowflake_cli-3.7.1.dist-info → snowflake_cli-3.8.0.dist-info}/RECORD +54 -46
  52. snowflake/cli/_plugins/nativeapp/feature_flags.py +0 -28
  53. snowflake/cli/_plugins/sql/source_reader.py +0 -230
  54. {snowflake_cli-3.7.1.dist-info → snowflake_cli-3.8.0.dist-info}/WHEEL +0 -0
  55. {snowflake_cli-3.7.1.dist-info → snowflake_cli-3.8.0.dist-info}/entry_points.txt +0 -0
  56. {snowflake_cli-3.7.1.dist-info → snowflake_cli-3.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,221 @@
1
+ from logging import getLogger
2
+ from typing import Iterable
3
+
4
+ from prompt_toolkit import PromptSession
5
+ from prompt_toolkit.filters import Condition, is_done, is_searching
6
+ from prompt_toolkit.history import FileHistory
7
+ from prompt_toolkit.key_binding.key_bindings import KeyBindings
8
+ from prompt_toolkit.keys import Keys
9
+ from prompt_toolkit.lexers import PygmentsLexer
10
+ from snowflake.cli._app.printing import print_result
11
+ from snowflake.cli._plugins.sql.lexer import CliLexer, cli_completer
12
+ from snowflake.cli._plugins.sql.manager import SqlManager
13
+ from snowflake.cli.api.cli_global_context import get_cli_context_manager
14
+ from snowflake.cli.api.console import cli_console
15
+ from snowflake.cli.api.output.types import MultipleResults, QueryResult
16
+ from snowflake.cli.api.secure_path import SecurePath
17
+ from snowflake.connector.config_manager import CONFIG_MANAGER
18
+ from snowflake.connector.cursor import SnowflakeCursor
19
+
20
+ log = getLogger(__name__)
21
+
22
+ HISTORY_FILE = SecurePath(
23
+ CONFIG_MANAGER.file_path.parent / "repl_history"
24
+ ).path.expanduser()
25
+ EXIT_KEYWORDS = ("exit", "quit")
26
+
27
+ log.debug("setting history file to: %s", HISTORY_FILE.as_posix())
28
+
29
+
30
+ class Repl:
31
+ """Basic REPL implementation for the Snowflake CLI."""
32
+
33
+ def __init__(
34
+ self,
35
+ sql_manager: SqlManager,
36
+ data: dict | None = None,
37
+ retain_comments: bool = False,
38
+ ):
39
+ """Requires a `SqlManager` instance to execute queries.
40
+
41
+ 'pass through' variables for SqlManager.execute method:
42
+ `data` should contain the variables used for template processing,
43
+ `retain_comments` how to handle comments in queries
44
+ """
45
+ super().__init__()
46
+ setattr(get_cli_context_manager(), "is_repl", True)
47
+ self._data = data or {}
48
+ self._retain_comments = retain_comments
49
+ self._history = FileHistory(HISTORY_FILE)
50
+ self._lexer = PygmentsLexer(CliLexer)
51
+ self._completer = cli_completer
52
+ self._repl_key_bindings = self._setup_key_bindings()
53
+ self._yes_no_keybindings = self._setup_yn_key_bindings()
54
+ self._sql_manager = sql_manager
55
+ self.session = PromptSession(history=self._history)
56
+
57
+ def _setup_key_bindings(self) -> KeyBindings:
58
+ """Key bindings for repl. Helps detecting ; at end of buffer."""
59
+ kb = KeyBindings()
60
+
61
+ @Condition
62
+ def not_searching():
63
+ return not is_searching()
64
+
65
+ @kb.add(Keys.Enter, filter=not_searching)
66
+ def _(event):
67
+ """Handle Enter key press."""
68
+ buffer = event.app.current_buffer
69
+ stripped_buffer = buffer.text.strip()
70
+
71
+ if stripped_buffer:
72
+ log.debug("evaluating repl input")
73
+ cursor_position = buffer.cursor_position
74
+ ends_with_semicolon = buffer.text.endswith(";")
75
+
76
+ if stripped_buffer.lower() in EXIT_KEYWORDS:
77
+ log.debug("exit keyword detected %r", stripped_buffer)
78
+ buffer.validate_and_handle()
79
+
80
+ elif ends_with_semicolon and cursor_position >= len(stripped_buffer):
81
+ log.debug("semicolon detected, submitting input")
82
+ buffer.validate_and_handle()
83
+
84
+ else:
85
+ log.debug("adding new line")
86
+ buffer.insert_text("\n")
87
+ else:
88
+ log.debug("empty input")
89
+
90
+ @kb.add(Keys.ControlJ)
91
+ def _(event):
92
+ """Control+J (and alias for Control + Enter) always inserts a new line."""
93
+ event.app.current_buffer.insert_text("\n")
94
+
95
+ return kb
96
+
97
+ def _setup_yn_key_bindings(self) -> KeyBindings:
98
+ """Key bindings for easy handling yes/no prompt."""
99
+ kb = KeyBindings()
100
+
101
+ @kb.add(Keys.Enter, filter=~is_done)
102
+ @kb.add("c-d", filter=~is_done)
103
+ @kb.add("y", filter=~is_done)
104
+ def _(event):
105
+ event.app.exit(result="y")
106
+
107
+ @kb.add("n", filter=~is_done)
108
+ @kb.add("c-c", filter=~is_done)
109
+ def _(event):
110
+ event.app.exit(result="n")
111
+
112
+ @kb.add(Keys.Any)
113
+ def _(event):
114
+ pass
115
+
116
+ return kb
117
+
118
+ def repl_propmpt(self, msg: str = " > ") -> str:
119
+ """Regular repl prompt."""
120
+ return self.session.prompt(
121
+ msg,
122
+ lexer=self._lexer,
123
+ completer=self._completer,
124
+ multiline=True,
125
+ wrap_lines=True,
126
+ key_bindings=self._repl_key_bindings,
127
+ )
128
+
129
+ def yn_prompt(self, msg: str) -> str:
130
+ """Yes/No prompt."""
131
+ return self.session.prompt(
132
+ msg,
133
+ lexer=None,
134
+ completer=None,
135
+ multiline=False,
136
+ wrap_lines=False,
137
+ key_bindings=self._yes_no_keybindings,
138
+ )
139
+
140
+ @property
141
+ def _welcome_banner(self) -> str:
142
+ return f"Welcome to Snowflake-CLI REPL\nType 'exit' or 'quit' to leave"
143
+
144
+ def _initialize_connection(self):
145
+ """Early connection for possible fast fail."""
146
+ cursor = self._execute("select current_version();")
147
+ res = next(iter(cursor))
148
+ log.debug("REPL: Snowflake version: %s", res.fetchall()[0][0])
149
+
150
+ def _execute(self, user_input: str) -> Iterable[SnowflakeCursor]:
151
+ """Executes a query and returns a list of cursors."""
152
+ _, cursors = self._sql_manager.execute(
153
+ query=user_input,
154
+ files=None,
155
+ std_in=False,
156
+ data=self._data,
157
+ retain_comments=self._retain_comments,
158
+ )
159
+ return cursors
160
+
161
+ def run(self):
162
+ try:
163
+ cli_console.panel(self._welcome_banner)
164
+ self._initialize_connection()
165
+ self._repl_loop()
166
+ except (KeyboardInterrupt, EOFError):
167
+ cli_console.message("\n[bold orange_red1]Leaving REPL, bye ...")
168
+
169
+ def _repl_loop(self):
170
+ """Main REPL loop. Handles input and query execution.
171
+
172
+ Sets up prompt session with history and key bindings.
173
+ Honors Ctrl-C and Ctrl-D in REPL loop.
174
+ """
175
+ while True:
176
+ try:
177
+ user_input = self.repl_propmpt().strip()
178
+
179
+ if not user_input:
180
+ continue
181
+
182
+ if user_input.lower() in EXIT_KEYWORDS:
183
+ raise EOFError
184
+
185
+ try:
186
+ log.debug("executing query")
187
+ cursors = self._execute(user_input)
188
+ print_result(MultipleResults(QueryResult(c) for c in cursors))
189
+
190
+ except Exception as e:
191
+ log.debug("error occurred: %s", e)
192
+ cli_console.warning(f"\nError occurred: {e}")
193
+
194
+ except KeyboardInterrupt: # a.k.a Ctrl-C
195
+ log.debug("user interrupted with Ctrl-C")
196
+ continue
197
+
198
+ except EOFError: # a.k.a Ctrl-D
199
+ log.debug("user interrupted with Ctrl-D")
200
+ should_exit = self.ask_yn("Do you want to leave?")
201
+ log.debug("user answered: %r", should_exit)
202
+ if should_exit:
203
+ raise EOFError
204
+ continue
205
+
206
+ except Exception as e:
207
+ cli_console.warning(f"\nError occurred: {e}")
208
+
209
+ def ask_yn(self, question: str) -> bool:
210
+ """Asks user a Yes/No question."""
211
+ try:
212
+ while True:
213
+ log.debug("asking user: %s", question)
214
+ answer = self.yn_prompt(f"{question} (y/n): ")
215
+ log.debug("user answered: %s", answer)
216
+
217
+ return answer == "y"
218
+
219
+ except KeyboardInterrupt:
220
+ log.debug("user interrupted with Ctrl-C returning to REPL")
221
+ return False
@@ -0,0 +1,331 @@
1
+ import enum
2
+ import re
3
+ import time
4
+ from dataclasses import dataclass
5
+ from datetime import datetime
6
+ from typing import Any, Dict, Generator, Iterable, List, Tuple
7
+ from urllib.parse import urlencode
8
+
9
+ from snowflake.cli._app.printing import print_result
10
+ from snowflake.cli.api.output.types import CollectionResult, QueryResult
11
+ from snowflake.connector import SnowflakeConnection
12
+
13
+ VALID_UUID_RE = re.compile(
14
+ r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
15
+ )
16
+
17
+
18
+ class CommandType(enum.Enum):
19
+ QUERIES = "queries"
20
+ UNKNOWN = "unknown"
21
+ URL = "url"
22
+
23
+
24
+ class SnowSQLCommand:
25
+ def execute(self, connection: SnowflakeConnection):
26
+ """Executes command and prints the result."""
27
+ raise NotImplementedError
28
+
29
+ @classmethod
30
+ def from_args(cls, args, kwargs) -> "CompileCommandResult":
31
+ """Validates arguments and creates command ready for execution."""
32
+ raise NotImplementedError
33
+
34
+
35
+ def _print_result_to_stdout(headers: Iterable[str], rows: Iterable[Iterable[Any]]):
36
+ formatted_rows: Generator[Dict[str, Any], None, None] = (
37
+ {key: value for key, value in zip(headers, row)} for row in rows
38
+ )
39
+ print_result(CollectionResult(formatted_rows))
40
+
41
+
42
+ @dataclass
43
+ class CompileCommandResult:
44
+ command: SnowSQLCommand | None = None
45
+ error_message: str | None = None
46
+
47
+
48
+ @dataclass
49
+ class QueriesCommand(SnowSQLCommand):
50
+ help_mode: bool = False
51
+ from_current_session: bool = False
52
+ amount: int = 25
53
+ user: str | None = None
54
+ warehouse: str | None = None
55
+ start_timestamp_ms: int | None = None
56
+ end_timestamp_ms: int | None = None
57
+ duration: str | None = None
58
+ stmt_type: str | None = None
59
+ status: str | None = None
60
+
61
+ def execute(self, connection: SnowflakeConnection) -> None:
62
+ if self.help_mode:
63
+ self._execute_help()
64
+ else:
65
+ self._execute_queries(connection)
66
+
67
+ def _execute_help(self):
68
+ headers = ["FILTER", "ARGUMENT", "DEFAULT"]
69
+ filters = [
70
+ ["amount", "integer", "25"],
71
+ ["status", "string", "any"],
72
+ ["warehouse", "string", "any"],
73
+ ["user", "string", "any"],
74
+ [
75
+ "start_date",
76
+ "datetime in ISO format (for example YYYY-MM-DDTHH:mm:ss.sss)",
77
+ "any",
78
+ ],
79
+ [
80
+ "end_date",
81
+ "datetime in ISO format (for example YYYY-MM-DDTHH:mm:ss.sss)",
82
+ "any",
83
+ ],
84
+ ["start", "timestamp in milliseconds (integer)", "any"],
85
+ ["end", "timestamp in milliseconds (integer)", "any"],
86
+ ["type", "string", "any"],
87
+ ["duration", "time in milliseconds", "any"],
88
+ ["session", "No arguments", "any"],
89
+ ]
90
+ _print_result_to_stdout(headers, filters)
91
+
92
+ def _execute_queries(self, connection: SnowflakeConnection) -> None:
93
+ url_parameters = {
94
+ "_dc": f"{time.time()}",
95
+ "includeDDL": "false",
96
+ "max": self.amount,
97
+ }
98
+ if self.user:
99
+ url_parameters["user"] = self.user
100
+ if self.warehouse:
101
+ url_parameters["wh"] = self.warehouse
102
+ if self.start_timestamp_ms:
103
+ url_parameters["start"] = self.start_timestamp_ms
104
+ if self.end_timestamp_ms:
105
+ url_parameters["end"] = self.end_timestamp_ms
106
+ if self.duration:
107
+ url_parameters["min_duration"] = self.duration
108
+ if self.from_current_session:
109
+ url_parameters["session_id"] = connection.session_id
110
+ if self.status:
111
+ url_parameters["subset"] = self.status
112
+ if self.stmt_type:
113
+ url_parameters["stmt_type"] = self.stmt_type
114
+
115
+ url = "/monitoring/queries?" + urlencode(url_parameters)
116
+ ret = connection.rest.request(url=url, method="get", client="rest")
117
+ if ret.get("data") and ret["data"].get("queries"):
118
+ _result: Generator[Tuple[str, str, str, str], None, None] = (
119
+ (
120
+ query["id"],
121
+ query["sqlText"],
122
+ query["state"],
123
+ query["totalDuration"],
124
+ )
125
+ for query in ret["data"]["queries"]
126
+ )
127
+ _print_result_to_stdout(
128
+ ["QUERY ID", "SQL TEXT", "STATUS", "DURATION_MS"], _result
129
+ )
130
+
131
+ @classmethod
132
+ def from_args(cls, args: List[str], kwargs: Dict[str, Any]) -> CompileCommandResult:
133
+ if "help" in args:
134
+ return CompileCommandResult(command=cls(help_mode=True))
135
+
136
+ # "session" is set by default if no other arguments are provided
137
+ from_current_session = "session" in args or not kwargs
138
+ amount = kwargs.pop("amount", "25")
139
+ if not amount.isdigit():
140
+ return CompileCommandResult(
141
+ error_message=f"Invalid argument passed to 'amount' filter: {amount}"
142
+ )
143
+ user = kwargs.pop("user", None)
144
+ warehouse = kwargs.pop("warehouse", None)
145
+ duration = kwargs.pop("duration", None)
146
+
147
+ start_timestamp_ms = kwargs.pop("start", None)
148
+ if start_timestamp_ms:
149
+ try:
150
+ start_timestamp_ms = int(start_timestamp_ms)
151
+ except ValueError:
152
+ return CompileCommandResult(
153
+ error_message=f"Invalid argument passed to 'start' filter: {start_timestamp_ms}"
154
+ )
155
+ end_timestamp_ms = kwargs.pop("end", None)
156
+ if end_timestamp_ms:
157
+ try:
158
+ end_timestamp_ms = int(end_timestamp_ms)
159
+ except ValueError:
160
+ return CompileCommandResult(
161
+ error_message=f"Invalid argument passed to 'end' filter: {end_timestamp_ms}"
162
+ )
163
+
164
+ start_date = kwargs.pop("start_date", None)
165
+ if start_date:
166
+ if start_timestamp_ms:
167
+ return CompileCommandResult(
168
+ error_message=f"'start_date' filter cannot be used with 'start' filter"
169
+ )
170
+ try:
171
+ seconds = datetime.fromisoformat(start_date).timestamp()
172
+ start_timestamp_ms = int(seconds * 1000) # convert to milliseconds
173
+ except ValueError:
174
+ return CompileCommandResult(
175
+ error_message=f"Invalid date format passed to 'start_date' filter: {start_date}"
176
+ )
177
+ end_date = kwargs.pop("end_date", None)
178
+ if end_date:
179
+ if end_timestamp_ms:
180
+ return CompileCommandResult(
181
+ error_message=f"'end_date' filter cannot be used with 'end' filter"
182
+ )
183
+ try:
184
+ seconds = datetime.fromisoformat(end_date).timestamp()
185
+ end_timestamp_ms = int(seconds * 1000) # convert to milliseconds
186
+ except ValueError:
187
+ return CompileCommandResult(
188
+ error_message=f"Invalid date format passed to 'end_date' filter: {end_date}"
189
+ )
190
+
191
+ stmt_type = kwargs.pop("type", None)
192
+ if stmt_type:
193
+ stmt_type = stmt_type.upper()
194
+ if stmt_type not in [
195
+ "ANY",
196
+ "SELECT",
197
+ "INSERT",
198
+ "UPDATE",
199
+ "DELETE",
200
+ "MERGE",
201
+ "MULTI_TABLE_INSERT",
202
+ "COPY",
203
+ "COMMIT",
204
+ "ROLLBACK",
205
+ "BEGIN_TRANSACTION",
206
+ "SHOW",
207
+ "GRANT",
208
+ "CREATE",
209
+ "ALTER",
210
+ ]:
211
+ return CompileCommandResult(
212
+ error_message=f"Invalid argument passed to 'type' filter: {stmt_type}"
213
+ )
214
+
215
+ status = kwargs.pop("status", None)
216
+ if status:
217
+ status = status.upper()
218
+ if status not in [
219
+ "RUNNING",
220
+ "SUCCEEDED",
221
+ "FAILED",
222
+ "BLOCKED",
223
+ "QUEUED",
224
+ "ABORTED",
225
+ ]:
226
+ return CompileCommandResult(
227
+ error_message=f"Invalid argument passed to 'status' filter: {status}"
228
+ )
229
+
230
+ for arg in args:
231
+ if arg.lower() not in ["session", "help"]:
232
+ return CompileCommandResult(
233
+ error_message=f"Invalid argument passed to 'queries' command: {arg}"
234
+ )
235
+ if kwargs:
236
+ key, value = kwargs.popitem()
237
+ return CompileCommandResult(
238
+ error_message=f"Invalid argument passed to 'queries' command: {key}={value}"
239
+ )
240
+
241
+ return CompileCommandResult(
242
+ command=cls(
243
+ help_mode=False,
244
+ from_current_session=from_current_session,
245
+ amount=int(amount),
246
+ user=user,
247
+ warehouse=warehouse,
248
+ start_timestamp_ms=start_timestamp_ms,
249
+ end_timestamp_ms=end_timestamp_ms,
250
+ duration=duration,
251
+ stmt_type=stmt_type,
252
+ status=status,
253
+ )
254
+ )
255
+
256
+
257
+ def _validate_only_arg_is_query_id(
258
+ command_name: str, args: List[str], kwargs: Dict[str, Any]
259
+ ) -> str | None:
260
+ if kwargs:
261
+ key, value = kwargs.popitem()
262
+ return f"Invalid argument passed to '{command_name}' command: {key}={value}"
263
+ if len(args) != 1:
264
+ amount = "Too many" if args else "No"
265
+ return f"{amount} arguments passed to '{command_name}' command. Usage: `!{command_name} <query id>`"
266
+
267
+ qid = args[0]
268
+ if not VALID_UUID_RE.match(qid):
269
+ return f"Invalid query ID passed to '{command_name}' command: {qid}"
270
+
271
+ return None
272
+
273
+
274
+ @dataclass
275
+ class ResultCommand(SnowSQLCommand):
276
+ query_id: str
277
+
278
+ def execute(self, connection: SnowflakeConnection):
279
+ cursor = connection.cursor()
280
+ cursor.query_result(self.query_id)
281
+ print_result(QueryResult(cursor=cursor))
282
+
283
+ @classmethod
284
+ def from_args(cls, args, kwargs) -> CompileCommandResult:
285
+ error_msg = _validate_only_arg_is_query_id("result", args, kwargs)
286
+ if error_msg:
287
+ return CompileCommandResult(error_message=error_msg)
288
+ return CompileCommandResult(command=cls(args[0]))
289
+
290
+
291
+ @dataclass
292
+ class AbortCommand(SnowSQLCommand):
293
+ query_id: str
294
+
295
+ def execute(self, connection: SnowflakeConnection):
296
+ cursor = connection.cursor()
297
+ cursor.execute(f"SELECT SYSTEM$CANCEL_QUERY('{self.query_id}')")
298
+ print_result(QueryResult(cursor=cursor))
299
+
300
+ @classmethod
301
+ def from_args(cls, args, kwargs) -> CompileCommandResult:
302
+ error_msg = _validate_only_arg_is_query_id("abort", args, kwargs)
303
+ if error_msg:
304
+ return CompileCommandResult(error_message=error_msg)
305
+ return CompileCommandResult(command=cls(args[0]))
306
+
307
+
308
+ def compile_snowsql_command(command: str, cmd_args: List[str]):
309
+ """Parses command into SQL query"""
310
+ args = []
311
+ kwargs = {}
312
+ for cmd_arg in cmd_args:
313
+ if "=" not in cmd_arg:
314
+ args.append(cmd_arg)
315
+ else:
316
+ key, val = cmd_arg.split("=", maxsplit=1)
317
+ if key in kwargs:
318
+ return CompileCommandResult(
319
+ error_message=f"duplicated argument '{key}' for command '{command}'",
320
+ )
321
+ kwargs[key] = val
322
+
323
+ match command.lower():
324
+ case "!queries":
325
+ return QueriesCommand.from_args(args, kwargs)
326
+ case "!result":
327
+ return ResultCommand.from_args(args, kwargs)
328
+ case "!abort":
329
+ return AbortCommand.from_args(args, kwargs)
330
+ case _:
331
+ return CompileCommandResult(error_message=f"Unknown command '{command}'")