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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/cli_app.py +3 -0
- snowflake/cli/_app/dev/docs/templates/overview.rst.jinja2 +1 -1
- snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +2 -2
- snowflake/cli/_app/telemetry.py +69 -4
- snowflake/cli/_plugins/connection/commands.py +152 -99
- snowflake/cli/_plugins/connection/util.py +54 -9
- snowflake/cli/_plugins/cortex/manager.py +1 -1
- snowflake/cli/_plugins/git/commands.py +6 -3
- snowflake/cli/_plugins/git/manager.py +9 -4
- snowflake/cli/_plugins/nativeapp/artifacts.py +77 -13
- snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +7 -0
- snowflake/cli/_plugins/nativeapp/codegen/sandbox.py +10 -10
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +8 -8
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
- snowflake/cli/_plugins/nativeapp/commands.py +144 -188
- snowflake/cli/_plugins/nativeapp/constants.py +1 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +564 -351
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +583 -929
- snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
- snowflake/cli/_plugins/nativeapp/exceptions.py +12 -0
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
- snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
- snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
- snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
- snowflake/cli/_plugins/nativeapp/v2_conversions/{v2_to_v1_decorator.py → compat.py} +88 -117
- snowflake/cli/_plugins/nativeapp/version/commands.py +36 -32
- snowflake/cli/_plugins/notebook/manager.py +2 -2
- snowflake/cli/_plugins/object/commands.py +10 -1
- snowflake/cli/_plugins/object/manager.py +13 -5
- snowflake/cli/_plugins/snowpark/common.py +63 -21
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +3 -3
- snowflake/cli/_plugins/spcs/common.py +29 -0
- snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
- snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
- snowflake/cli/_plugins/spcs/image_repository/commands.py +4 -37
- snowflake/cli/_plugins/spcs/image_repository/manager.py +4 -1
- snowflake/cli/_plugins/spcs/services/commands.py +100 -17
- snowflake/cli/_plugins/spcs/services/manager.py +108 -16
- snowflake/cli/_plugins/sql/commands.py +9 -1
- snowflake/cli/_plugins/sql/manager.py +9 -4
- snowflake/cli/_plugins/stage/commands.py +28 -19
- snowflake/cli/_plugins/stage/diff.py +17 -17
- snowflake/cli/_plugins/stage/manager.py +304 -84
- snowflake/cli/_plugins/stage/md5.py +1 -1
- snowflake/cli/_plugins/streamlit/manager.py +5 -5
- snowflake/cli/_plugins/workspace/commands.py +27 -4
- snowflake/cli/_plugins/workspace/context.py +38 -0
- snowflake/cli/_plugins/workspace/manager.py +23 -13
- snowflake/cli/api/cli_global_context.py +4 -3
- snowflake/cli/api/commands/flags.py +23 -7
- snowflake/cli/api/config.py +30 -9
- snowflake/cli/api/connections.py +12 -1
- snowflake/cli/api/console/console.py +4 -19
- snowflake/cli/api/entities/common.py +4 -2
- snowflake/cli/api/entities/utils.py +36 -69
- snowflake/cli/api/errno.py +2 -0
- snowflake/cli/api/exceptions.py +41 -0
- snowflake/cli/api/identifiers.py +8 -0
- snowflake/cli/api/metrics.py +223 -7
- snowflake/cli/api/output/types.py +1 -1
- snowflake/cli/api/project/definition_conversion.py +293 -77
- snowflake/cli/api/project/schemas/entities/common.py +11 -0
- snowflake/cli/api/project/schemas/project_definition.py +30 -25
- snowflake/cli/api/rest_api.py +26 -4
- snowflake/cli/api/secure_utils.py +1 -1
- snowflake/cli/api/sql_execution.py +40 -29
- snowflake/cli/api/stage_path.py +244 -0
- snowflake/cli/api/utils/definition_rendering.py +3 -5
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +14 -15
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +78 -77
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
- snowflake/cli/_plugins/nativeapp/manager.py +0 -415
- snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
- snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +0 -70
- snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -98
- snowflake/cli/_plugins/workspace/action_context.py +0 -18
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/licenses/LICENSE +0 -0
snowflake/cli/api/exceptions.py
CHANGED
|
@@ -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
|
+
)
|
snowflake/cli/api/identifiers.py
CHANGED
|
@@ -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."""
|
snowflake/cli/api/metrics.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
|
66
|
-
|
|
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
|