snowflake-cli 3.0.2__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 (84) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/cli_app.py +3 -0
  3. snowflake/cli/_app/dev/docs/templates/overview.rst.jinja2 +1 -1
  4. snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +2 -2
  5. snowflake/cli/_app/telemetry.py +69 -4
  6. snowflake/cli/_plugins/connection/commands.py +152 -99
  7. snowflake/cli/_plugins/connection/util.py +54 -9
  8. snowflake/cli/_plugins/cortex/manager.py +1 -1
  9. snowflake/cli/_plugins/git/commands.py +6 -3
  10. snowflake/cli/_plugins/git/manager.py +9 -4
  11. snowflake/cli/_plugins/nativeapp/artifacts.py +77 -13
  12. snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
  13. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +7 -0
  14. snowflake/cli/_plugins/nativeapp/codegen/sandbox.py +10 -10
  15. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
  16. snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +1 -1
  17. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +8 -8
  18. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
  19. snowflake/cli/_plugins/nativeapp/commands.py +144 -188
  20. snowflake/cli/_plugins/nativeapp/constants.py +1 -0
  21. snowflake/cli/_plugins/nativeapp/entities/application.py +564 -351
  22. snowflake/cli/_plugins/nativeapp/entities/application_package.py +583 -929
  23. snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
  24. snowflake/cli/_plugins/nativeapp/exceptions.py +12 -0
  25. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
  26. snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
  27. snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
  28. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
  29. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
  30. snowflake/cli/_plugins/nativeapp/v2_conversions/{v2_to_v1_decorator.py → compat.py} +88 -117
  31. snowflake/cli/_plugins/nativeapp/version/commands.py +36 -32
  32. snowflake/cli/_plugins/notebook/manager.py +2 -2
  33. snowflake/cli/_plugins/object/commands.py +10 -1
  34. snowflake/cli/_plugins/object/manager.py +13 -5
  35. snowflake/cli/_plugins/snowpark/common.py +63 -21
  36. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +3 -3
  37. snowflake/cli/_plugins/spcs/common.py +29 -0
  38. snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
  39. snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
  40. snowflake/cli/_plugins/spcs/image_repository/commands.py +4 -37
  41. snowflake/cli/_plugins/spcs/image_repository/manager.py +4 -1
  42. snowflake/cli/_plugins/spcs/services/commands.py +100 -17
  43. snowflake/cli/_plugins/spcs/services/manager.py +108 -16
  44. snowflake/cli/_plugins/sql/commands.py +9 -1
  45. snowflake/cli/_plugins/sql/manager.py +9 -4
  46. snowflake/cli/_plugins/stage/commands.py +28 -19
  47. snowflake/cli/_plugins/stage/diff.py +17 -17
  48. snowflake/cli/_plugins/stage/manager.py +304 -84
  49. snowflake/cli/_plugins/stage/md5.py +1 -1
  50. snowflake/cli/_plugins/streamlit/manager.py +5 -5
  51. snowflake/cli/_plugins/workspace/commands.py +27 -4
  52. snowflake/cli/_plugins/workspace/context.py +38 -0
  53. snowflake/cli/_plugins/workspace/manager.py +23 -13
  54. snowflake/cli/api/cli_global_context.py +4 -3
  55. snowflake/cli/api/commands/flags.py +23 -7
  56. snowflake/cli/api/config.py +30 -9
  57. snowflake/cli/api/connections.py +12 -1
  58. snowflake/cli/api/console/console.py +4 -19
  59. snowflake/cli/api/entities/common.py +4 -2
  60. snowflake/cli/api/entities/utils.py +36 -69
  61. snowflake/cli/api/errno.py +2 -0
  62. snowflake/cli/api/exceptions.py +41 -0
  63. snowflake/cli/api/identifiers.py +8 -0
  64. snowflake/cli/api/metrics.py +223 -7
  65. snowflake/cli/api/output/types.py +1 -1
  66. snowflake/cli/api/project/definition_conversion.py +293 -77
  67. snowflake/cli/api/project/schemas/entities/common.py +11 -0
  68. snowflake/cli/api/project/schemas/project_definition.py +30 -25
  69. snowflake/cli/api/rest_api.py +26 -4
  70. snowflake/cli/api/secure_utils.py +1 -1
  71. snowflake/cli/api/sql_execution.py +40 -29
  72. snowflake/cli/api/stage_path.py +244 -0
  73. snowflake/cli/api/utils/definition_rendering.py +3 -5
  74. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +14 -15
  75. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +78 -77
  76. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
  77. snowflake/cli/_plugins/nativeapp/manager.py +0 -415
  78. snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
  79. snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
  80. snowflake/cli/_plugins/nativeapp/teardown_processor.py +0 -70
  81. snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -98
  82. snowflake/cli/_plugins/workspace/action_context.py +0 -18
  83. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
  84. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -188,3 +188,44 @@ class IncompatibleParametersError(UsageError):
188
188
  super().__init__(
189
189
  f"Parameters {comma_separated_options} and {options_with_quotes[-1]} are incompatible and cannot be used simultaneously."
190
190
  )
191
+
192
+
193
+ class UnmetParametersError(UsageError):
194
+ def __init__(self, options: list[str]):
195
+ options_with_quotes = [f"'{option}'" for option in options]
196
+ comma_separated_options = ", ".join(options_with_quotes[:-1])
197
+ super().__init__(
198
+ f"Parameters {comma_separated_options} and {options_with_quotes[-1]} must be used simultaneously."
199
+ )
200
+
201
+
202
+ class NoWarehouseSelectedInSessionError(ClickException):
203
+ def __init__(self, msg: str):
204
+ super().__init__(
205
+ "Received the following error message while executing SQL statement:\n"
206
+ f"'{msg}'\n"
207
+ "Please provide a warehouse for the active session role in your project definition file, config.toml file, or via command line."
208
+ )
209
+
210
+
211
+ class DoesNotExistOrUnauthorizedError(ClickException):
212
+ def __init__(self, msg: str):
213
+ super().__init__(
214
+ "Received the following error message while executing SQL statement:\n"
215
+ f"'{msg}'\n"
216
+ "Please check the name of the resource you are trying to query or the permissions of the role you are using to run the query."
217
+ )
218
+
219
+
220
+ class CouldNotUseObjectError(ClickException):
221
+ def __init__(self, object_type: ObjectType, name: str):
222
+ super().__init__(
223
+ f"Could not use {object_type} {name}. Object does not exist, or operation cannot be performed."
224
+ )
225
+
226
+
227
+ class ShowSpecificObjectMultipleRowsError(RuntimeError):
228
+ def __init__(self, show_obj_query: str):
229
+ super().__init__(
230
+ f"Received multiple rows from result of SQL statement: {show_obj_query}. Usage of 'show_specific_object' may not be properly scoped."
231
+ )
@@ -15,6 +15,7 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import re
18
+ from pathlib import Path
18
19
 
19
20
  from click import ClickException
20
21
  from snowflake.cli.api.exceptions import FQNInconsistencyError, FQNNameError
@@ -121,8 +122,15 @@ class FQN:
121
122
  name = stage
122
123
  if stage.startswith("@"):
123
124
  name = stage[1:]
125
+ if stage.startswith("~"):
126
+ return cls(name="~", database=None, schema=None)
124
127
  return cls.from_string(name)
125
128
 
129
+ @classmethod
130
+ def from_stage_path(cls, stage_path: str) -> "FQN":
131
+ stage = Path(stage_path).parts[0]
132
+ return cls.from_stage(stage)
133
+
126
134
  @classmethod
127
135
  def from_identifier_model_v1(cls, model: ObjectIdentifierBaseModel) -> "FQN":
128
136
  """Create an instance from object model."""
@@ -11,8 +11,25 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
+ from __future__ import annotations
14
15
 
15
- from typing import Dict, Optional
16
+ import time
17
+ import uuid
18
+ from contextlib import contextmanager
19
+ from dataclasses import dataclass, field, replace
20
+ from heapq import nsmallest
21
+ from typing import ClassVar, Dict, Iterator, List, Optional, Set
22
+
23
+
24
+ class CLIMetricsInvalidUsageError(RuntimeError):
25
+ """
26
+ Indicative of bug in the code where a call to CLIMetrics was made erroneously
27
+
28
+ We do not want metrics errors to break the execution of commands,
29
+ so only raise this error in the event that an invariant was broken during setup
30
+ """
31
+
32
+ pass
16
33
 
17
34
 
18
35
  class _TypePrefix:
@@ -52,20 +69,135 @@ class CLICounterField:
52
69
  f"{_TypePrefix.FEATURES}.{_DomainPrefix.APP}.post_deploy_scripts"
53
70
  )
54
71
  PACKAGE_SCRIPTS = f"{_TypePrefix.FEATURES}.{_DomainPrefix.APP}.package_scripts"
72
+ EVENT_SHARING = f"{_TypePrefix.FEATURES}.{_DomainPrefix.APP}.event_sharing"
73
+ EVENT_SHARING_WARNING = (
74
+ f"{_TypePrefix.FEATURES}.{_DomainPrefix.APP}.event_sharing_warning"
75
+ )
76
+ EVENT_SHARING_ERROR = (
77
+ f"{_TypePrefix.FEATURES}.{_DomainPrefix.APP}.event_sharing_error"
78
+ )
79
+
80
+
81
+ @dataclass
82
+ class CLIMetricsSpan:
83
+ """
84
+ class for holding metrics span data and encapsulating related operations
85
+ """
86
+
87
+ # keys for dict representation
88
+ ID_KEY: ClassVar[str] = "id"
89
+ NAME_KEY: ClassVar[str] = "name"
90
+ PARENT_KEY: ClassVar[str] = "parent"
91
+ PARENT_ID_KEY: ClassVar[str] = "parent_id"
92
+ START_TIME_KEY: ClassVar[str] = "start_time"
93
+ EXECUTION_TIME_KEY: ClassVar[str] = "execution_time"
94
+ ERROR_KEY: ClassVar[str] = "error"
95
+ # total number of spans started under this span, inclusive of itself and its children's children (recursively)
96
+ SPAN_COUNT_IN_SUBTREE_KEY: ClassVar[str] = "span_count_in_subtree"
97
+ # the number of spans in the path between the current span and the topmost parent span, inclusive of both
98
+ SPAN_DEPTH_KEY: ClassVar[str] = "span_depth"
99
+ # denotes whether direct children were trimmed from telemetry payload
100
+ TRIMMED_KEY: ClassVar[str] = "trimmed"
101
+
102
+ # constructor vars
103
+ name: str
104
+ start_time: float # relative to when the command first started executing
105
+ parent: Optional[CLIMetricsSpan] = None
106
+
107
+ # vars for reporting
108
+ span_id: str = field(init=False, default_factory=lambda: uuid.uuid4().hex)
109
+ execution_time: Optional[float] = field(init=False, default=None)
110
+ error: Optional[BaseException] = field(init=False, default=None)
111
+ span_depth: int = field(init=False, default=1)
112
+ span_count_in_subtree: int = field(init=False, default=1)
113
+
114
+ # vars for postprocessing
115
+ # spans started directly under this one
116
+ children: Set[CLIMetricsSpan] = field(init=False, default_factory=set)
117
+
118
+ # private state
119
+ # start time of the step from the monotonic clock in order to calculate execution time
120
+ _monotonic_start: float = field(
121
+ init=False, default_factory=lambda: time.monotonic()
122
+ )
123
+
124
+ def __hash__(self) -> int:
125
+ return hash(self.span_id)
126
+
127
+ def __post_init__(self):
128
+ if not self.name:
129
+ raise CLIMetricsInvalidUsageError("span name must not be empty")
130
+
131
+ if self.parent:
132
+ self.parent.add_child(self)
133
+ self.span_depth = self.parent.span_depth + 1
134
+
135
+ def increment_subtree_node_count(self) -> None:
136
+ self.span_count_in_subtree += 1
137
+
138
+ if self.parent:
139
+ self.parent.increment_subtree_node_count()
140
+
141
+ def add_child(self, child: CLIMetricsSpan) -> None:
142
+ self.children.add(child)
143
+ self.increment_subtree_node_count()
144
+
145
+ def finish(self, error: Optional[BaseException] = None) -> None:
146
+ """
147
+ Sets the execution time and (optionally) error raised for the span
148
+
149
+ If already called, this method is a no-op
150
+ """
151
+ if self.execution_time is not None:
152
+ return
153
+
154
+ if error:
155
+ self.error = error
55
156
 
157
+ self.execution_time = time.monotonic() - self._monotonic_start
56
158
 
159
+ def to_dict(self) -> Dict:
160
+ """
161
+ Custom dict conversion function to be used for reporting telemetry
162
+ """
163
+
164
+ return {
165
+ self.ID_KEY: self.span_id,
166
+ self.NAME_KEY: self.name,
167
+ self.START_TIME_KEY: self.start_time,
168
+ self.PARENT_KEY: self.parent.name if self.parent is not None else None,
169
+ self.PARENT_ID_KEY: (
170
+ self.parent.span_id if self.parent is not None else None
171
+ ),
172
+ self.EXECUTION_TIME_KEY: self.execution_time,
173
+ self.ERROR_KEY: type(self.error).__name__ if self.error else None,
174
+ self.SPAN_COUNT_IN_SUBTREE_KEY: self.span_count_in_subtree,
175
+ self.SPAN_DEPTH_KEY: self.span_depth,
176
+ }
177
+
178
+
179
+ @dataclass
57
180
  class CLIMetrics:
58
181
  """
59
182
  Class to track various metrics across the execution of a command
60
183
  """
61
184
 
62
- def __init__(self):
63
- self._counters: Dict[str, int] = {}
185
+ # limits for reporting purposes
186
+ SPAN_DEPTH_LIMIT: ClassVar[int] = 5
187
+ SPAN_TOTAL_LIMIT: ClassVar[int] = 100
188
+
189
+ _counters: Dict[str, int] = field(init=False, default_factory=dict)
190
+ # stack of in progress spans as command is executing
191
+ _in_progress_spans: List[CLIMetricsSpan] = field(init=False, default_factory=list)
192
+ # list of finished steps for telemetry to process
193
+ _completed_spans: List[CLIMetricsSpan] = field(init=False, default_factory=list)
194
+ # monotonic clock time of when this class was initialized to approximate when the command first started executing
195
+ _monotonic_start: float = field(
196
+ init=False, default_factory=lambda: time.monotonic(), compare=False
197
+ )
64
198
 
65
- def __eq__(self, other):
66
- if isinstance(other, CLIMetrics):
67
- return self._counters == other._counters
68
- return False
199
+ def clone(self) -> CLIMetrics:
200
+ return replace(self)
69
201
 
70
202
  def get_counter(self, name: str) -> Optional[int]:
71
203
  return self._counters.get(name)
@@ -86,7 +218,91 @@ class CLIMetrics:
86
218
  else:
87
219
  self._counters[name] += value
88
220
 
221
+ @property
222
+ def current_span(self) -> Optional[CLIMetricsSpan]:
223
+ return self._in_progress_spans[-1] if len(self._in_progress_spans) > 0 else None
224
+
225
+ @contextmanager
226
+ def start_span(self, name: str) -> Iterator[CLIMetricsSpan]:
227
+ """
228
+ Starts a new span that tracks various metrics throughout its execution
229
+
230
+ Assumes that parent spans contain the entirety of their child spans
231
+ If not provided, parent spans are automatically populated as the most recently executed spans
232
+
233
+ Spans are not emitted in telemetry if depth/total limits are exceeded
234
+
235
+ :raises CliMetricsInvalidUsageError: if the step name is empty
236
+ """
237
+ new_span = CLIMetricsSpan(
238
+ name=name,
239
+ start_time=time.monotonic() - self._monotonic_start,
240
+ parent=self.current_span,
241
+ )
242
+
243
+ self._in_progress_spans.append(new_span)
244
+
245
+ try:
246
+ yield new_span
247
+ except BaseException as err:
248
+ new_span.finish(error=err)
249
+ raise
250
+ else:
251
+ new_span.finish()
252
+ finally:
253
+ self._completed_spans.append(new_span)
254
+ self._in_progress_spans.remove(new_span)
255
+
89
256
  @property
90
257
  def counters(self) -> Dict[str, int]:
91
258
  # return a copy of the original dict to avoid mutating the original
92
259
  return self._counters.copy()
260
+
261
+ @property
262
+ def num_spans_past_depth_limit(self) -> int:
263
+ return len(
264
+ [
265
+ span
266
+ for span in self._completed_spans
267
+ if span.span_depth > self.SPAN_DEPTH_LIMIT
268
+ ]
269
+ )
270
+
271
+ @property
272
+ def num_spans_past_total_limit(self) -> int:
273
+ return max(0, len(self._completed_spans) - self.SPAN_TOTAL_LIMIT)
274
+
275
+ @property
276
+ def completed_spans(self) -> List[Dict]:
277
+ """
278
+ Returns the completed spans tracked throughout a command, sorted by start time, for reporting telemetry
279
+
280
+ Ensures that the spans we send are within the configured limits and marks
281
+ certain spans as trimmed if their children would bypass the limits we set
282
+ """
283
+ # take spans breadth-first within the depth and total limits
284
+ # since we care more about the big picture than granularity
285
+ spans_to_report = set(
286
+ nsmallest(
287
+ n=self.SPAN_TOTAL_LIMIT,
288
+ iterable=(
289
+ span
290
+ for span in self._completed_spans
291
+ if span.span_depth <= self.SPAN_DEPTH_LIMIT
292
+ ),
293
+ key=lambda span: (span.span_depth, span.start_time),
294
+ )
295
+ )
296
+
297
+ # sort by start time to make reading the payload easier
298
+ sorted_spans_to_report = sorted(
299
+ spans_to_report, key=lambda span: span.start_time
300
+ )
301
+
302
+ return [
303
+ {
304
+ **span.to_dict(),
305
+ CLIMetricsSpan.TRIMMED_KEY: not span.children <= spans_to_report,
306
+ }
307
+ for span in sorted_spans_to_report
308
+ ]
@@ -37,7 +37,7 @@ class ObjectResult(CommandResult):
37
37
 
38
38
 
39
39
  class CollectionResult(CommandResult):
40
- def __init__(self, elements: t.Iterable[t.Dict]):
40
+ def __init__(self, elements: t.Iterable[t.Dict] | t.Generator[t.Dict, None, None]):
41
41
  self._elements = elements
42
42
 
43
43
  @property