snowflake-cli 3.10.1__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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/auth/__init__.py +13 -0
- snowflake/cli/_app/auth/errors.py +28 -0
- snowflake/cli/_app/auth/oidc_providers.py +393 -0
- snowflake/cli/_app/cli_app.py +0 -1
- snowflake/cli/_app/constants.py +10 -0
- snowflake/cli/_app/printing.py +153 -19
- snowflake/cli/_app/snow_connector.py +35 -0
- snowflake/cli/_plugins/auth/__init__.py +4 -2
- snowflake/cli/_plugins/auth/keypair/commands.py +2 -0
- snowflake/cli/_plugins/auth/oidc/__init__.py +13 -0
- snowflake/cli/_plugins/auth/oidc/commands.py +47 -0
- snowflake/cli/_plugins/auth/oidc/manager.py +66 -0
- snowflake/cli/_plugins/auth/oidc/plugin_spec.py +30 -0
- snowflake/cli/_plugins/connection/commands.py +37 -3
- snowflake/cli/_plugins/dbt/commands.py +37 -8
- snowflake/cli/_plugins/dbt/manager.py +144 -12
- snowflake/cli/_plugins/dcm/commands.py +102 -136
- snowflake/cli/_plugins/dcm/manager.py +136 -89
- snowflake/cli/_plugins/logs/commands.py +7 -0
- snowflake/cli/_plugins/logs/manager.py +21 -1
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +3 -1
- snowflake/cli/_plugins/notebook/notebook_entity.py +2 -0
- snowflake/cli/_plugins/notebook/notebook_entity_model.py +8 -1
- snowflake/cli/_plugins/object/command_aliases.py +16 -1
- snowflake/cli/_plugins/object/commands.py +27 -1
- snowflake/cli/_plugins/object/manager.py +12 -1
- snowflake/cli/_plugins/snowpark/commands.py +8 -1
- snowflake/cli/_plugins/snowpark/common.py +1 -0
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +29 -5
- snowflake/cli/_plugins/snowpark/package_utils.py +44 -3
- snowflake/cli/_plugins/spcs/services/manager.py +5 -4
- snowflake/cli/_plugins/sql/lexer/types.py +1 -0
- snowflake/cli/_plugins/sql/repl.py +100 -26
- snowflake/cli/_plugins/sql/repl_commands.py +607 -0
- snowflake/cli/_plugins/sql/statement_reader.py +44 -20
- snowflake/cli/api/artifacts/bundle_map.py +32 -2
- snowflake/cli/api/artifacts/regex_resolver.py +54 -0
- snowflake/cli/api/artifacts/upload.py +5 -1
- snowflake/cli/api/artifacts/utils.py +12 -1
- snowflake/cli/api/cli_global_context.py +7 -0
- snowflake/cli/api/commands/decorators.py +7 -0
- snowflake/cli/api/commands/flags.py +26 -0
- snowflake/cli/api/config.py +24 -0
- snowflake/cli/api/connections.py +1 -0
- snowflake/cli/api/console/abc.py +13 -2
- snowflake/cli/api/console/console.py +20 -0
- snowflake/cli/api/constants.py +9 -0
- snowflake/cli/api/entities/utils.py +10 -6
- snowflake/cli/api/feature_flags.py +1 -0
- snowflake/cli/api/identifiers.py +18 -1
- snowflake/cli/api/project/schemas/entities/entities.py +0 -6
- snowflake/cli/api/rendering/sql_templates.py +2 -0
- snowflake/cli/api/utils/dict_utils.py +42 -1
- {snowflake_cli-3.10.1.dist-info → snowflake_cli-3.12.0.dist-info}/METADATA +15 -41
- {snowflake_cli-3.10.1.dist-info → snowflake_cli-3.12.0.dist-info}/RECORD +59 -52
- snowflake/cli/_plugins/dcm/dcm_project_entity_model.py +0 -59
- snowflake/cli/_plugins/sql/snowsql_commands.py +0 -331
- {snowflake_cli-3.10.1.dist-info → snowflake_cli-3.12.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.10.1.dist-info → snowflake_cli-3.12.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.10.1.dist-info → snowflake_cli-3.12.0.dist-info}/licenses/LICENSE +0 -0
snowflake/cli/__about__.py
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright (c) 2024 Snowflake Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class OidcProviderError(Exception):
|
|
2
|
+
"""Base exception for OIDC provider related errors."""
|
|
3
|
+
|
|
4
|
+
...
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OidcProviderNotFoundError(OidcProviderError):
|
|
8
|
+
"""Exception raised when requested OIDC provider is not found or unknown."""
|
|
9
|
+
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OidcProviderUnavailableError(OidcProviderError):
|
|
14
|
+
"""Exception raised when OIDC provider is not available in current environment."""
|
|
15
|
+
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OidcProviderAutoDetectionError(OidcProviderError):
|
|
20
|
+
"""Exception raised when auto-detection of OIDC provider fails."""
|
|
21
|
+
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OidcTokenRetrievalError(OidcProviderError):
|
|
26
|
+
"""Exception raised when OIDC token cannot be retrieved."""
|
|
27
|
+
|
|
28
|
+
...
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# Copyright (c) 2024 Snowflake Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import importlib
|
|
16
|
+
import inspect
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
from abc import ABC, abstractmethod
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from typing import Dict, List, Literal, Optional, Type
|
|
22
|
+
|
|
23
|
+
import id as oidc_id
|
|
24
|
+
from snowflake.cli._app.auth.errors import (
|
|
25
|
+
OidcProviderAutoDetectionError,
|
|
26
|
+
OidcProviderNotFoundError,
|
|
27
|
+
OidcProviderUnavailableError,
|
|
28
|
+
OidcTokenRetrievalError,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
ACTIONS_ID_TOKEN_REQUEST_URL_ENV: Literal[
|
|
35
|
+
"ACTIONS_ID_TOKEN_REQUEST_URL"
|
|
36
|
+
] = "ACTIONS_ID_TOKEN_REQUEST_URL"
|
|
37
|
+
GITHUB_ACTIONS_ENV: Literal["GITHUB_ACTIONS"] = "GITHUB_ACTIONS"
|
|
38
|
+
SNOWFLAKE_AUDIENCE_ENV: Literal["SNOWFLAKE_AUDIENCE"] = "SNOWFLAKE_AUDIENCE"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class OidcProviderType(Enum):
|
|
42
|
+
"""Enum for OIDC provider types."""
|
|
43
|
+
|
|
44
|
+
GITHUB = "github"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class OidcProviderTypeWithAuto(Enum):
|
|
48
|
+
"""Extended version of OidcProviderType with AUTO."""
|
|
49
|
+
|
|
50
|
+
AUTO = "auto"
|
|
51
|
+
GITHUB = "github"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class OidcTokenProvider(ABC):
|
|
55
|
+
"""
|
|
56
|
+
Abstract base class for OIDC token providers.
|
|
57
|
+
Each CI environment should implement this interface.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def provider_name(self) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Returns the name of the CI provider (e.g., 'github', 'gitlab', 'azure-devops').
|
|
65
|
+
"""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def is_available(self) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Checks if this provider is available in the current environment.
|
|
73
|
+
Should return True if the provider can detect credentials in the current context.
|
|
74
|
+
"""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def issuer(self) -> str:
|
|
80
|
+
"""
|
|
81
|
+
Returns the OIDC issuer URL for this provider.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
The OIDC issuer URL
|
|
85
|
+
"""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def get_token(self) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Retrieves the OIDC token from the CI environment.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The OIDC token string
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
OidcProviderError: If token cannot be retrieved
|
|
98
|
+
"""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class GitHubOidcProvider(OidcTokenProvider):
|
|
103
|
+
"""
|
|
104
|
+
OIDC token provider for GitHub Actions.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def _is_ci(self):
|
|
109
|
+
logger.debug("Checking if GitHub Actions environment is available")
|
|
110
|
+
|
|
111
|
+
# Check if we're in a GitHub Actions environment
|
|
112
|
+
github_actions_env = os.getenv(GITHUB_ACTIONS_ENV)
|
|
113
|
+
logger.debug(
|
|
114
|
+
"%s environment variable: %s",
|
|
115
|
+
GITHUB_ACTIONS_ENV,
|
|
116
|
+
github_actions_env,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
is_github_actions = github_actions_env == "true"
|
|
120
|
+
logger.debug("Running in GitHub Actions: %s", is_github_actions)
|
|
121
|
+
return is_github_actions
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def audience(self) -> str:
|
|
125
|
+
"""
|
|
126
|
+
Returns the audience URL for GitHub OIDC.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
The audience URL, defaults to 'snowflakecomputing.com' if SNOWFLAKE_AUDIENCE environment variable is not set
|
|
130
|
+
"""
|
|
131
|
+
return os.getenv(SNOWFLAKE_AUDIENCE_ENV, "snowflakecomputing.com")
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def issuer(self) -> str:
|
|
135
|
+
"""
|
|
136
|
+
Returns the GitHub OIDC issuer URL.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The GitHub OIDC issuer URL from ACTIONS_ID_TOKEN_REQUEST_URL environment variable,
|
|
140
|
+
or the default GitHub issuer URL if the environment variable is not set
|
|
141
|
+
"""
|
|
142
|
+
issuer_url = os.getenv(ACTIONS_ID_TOKEN_REQUEST_URL_ENV)
|
|
143
|
+
if not issuer_url and self._is_ci:
|
|
144
|
+
raise OidcTokenRetrievalError(
|
|
145
|
+
"%s environment variable is not set. "
|
|
146
|
+
"This variable is required for Github Actions OIDC authentication"
|
|
147
|
+
% ACTIONS_ID_TOKEN_REQUEST_URL_ENV
|
|
148
|
+
)
|
|
149
|
+
return issuer_url or "https://token.actions.githubusercontent.com"
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def provider_name(self) -> str:
|
|
153
|
+
return OidcProviderType.GITHUB.value
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def is_available(self) -> bool:
|
|
157
|
+
"""
|
|
158
|
+
Checks if GitHub Actions environment is available.
|
|
159
|
+
"""
|
|
160
|
+
return self._is_ci
|
|
161
|
+
|
|
162
|
+
def get_token(self) -> str:
|
|
163
|
+
"""
|
|
164
|
+
Retrieves the OIDC token from GitHub Actions.
|
|
165
|
+
"""
|
|
166
|
+
logger.debug("Retrieving OIDC token from GitHub Actions")
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
logger.debug("Detecting OIDC credentials for token retrieval")
|
|
170
|
+
# Use configurable audience for workload identity
|
|
171
|
+
token = oidc_id.detect_credential(self.audience)
|
|
172
|
+
if not token:
|
|
173
|
+
logger.error("No OIDC credentials detected")
|
|
174
|
+
raise OidcTokenRetrievalError(
|
|
175
|
+
"No OIDC credentials detected. This command should be run in a GitHub Actions environment."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
logger.info("Successfully retrieved OIDC token")
|
|
179
|
+
return token
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error("Failed to detect OIDC credentials: %s", str(e))
|
|
182
|
+
raise OidcTokenRetrievalError(
|
|
183
|
+
"Failed to detect OIDC credentials: %s" % str(e)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class OidcProviderRegistry:
|
|
188
|
+
"""
|
|
189
|
+
Registry for managing OIDC token providers.
|
|
190
|
+
Handles registration, storage, and retrieval of providers.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
def __init__(self) -> None:
|
|
194
|
+
self._providers: Dict[str, Type[OidcTokenProvider]] = {}
|
|
195
|
+
self._auto_discover_providers()
|
|
196
|
+
|
|
197
|
+
def _auto_discover_providers(self) -> None:
|
|
198
|
+
"""
|
|
199
|
+
Auto-discovers all OIDC token providers in the current module.
|
|
200
|
+
"""
|
|
201
|
+
logger.debug("Auto-discovering OIDC token providers")
|
|
202
|
+
current_module = importlib.import_module(__name__)
|
|
203
|
+
|
|
204
|
+
for name, obj in inspect.getmembers(current_module):
|
|
205
|
+
if (
|
|
206
|
+
inspect.isclass(obj)
|
|
207
|
+
and issubclass(obj, OidcTokenProvider)
|
|
208
|
+
and obj != OidcTokenProvider
|
|
209
|
+
):
|
|
210
|
+
provider_instance = obj()
|
|
211
|
+
provider_name = provider_instance.provider_name
|
|
212
|
+
logger.debug("Discovered OIDC provider: %s (%s)", provider_name, name)
|
|
213
|
+
self._providers[provider_name] = obj
|
|
214
|
+
|
|
215
|
+
logger.info(
|
|
216
|
+
"Auto-discovered %d OIDC provider(s): %s",
|
|
217
|
+
len(self._providers),
|
|
218
|
+
list(self._providers.keys()),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def register_provider(self, provider_class: Type[OidcTokenProvider]) -> None:
|
|
222
|
+
"""
|
|
223
|
+
Manually register a provider class.
|
|
224
|
+
"""
|
|
225
|
+
provider_instance = provider_class()
|
|
226
|
+
self._providers[provider_instance.provider_name] = provider_class
|
|
227
|
+
|
|
228
|
+
def get_provider(self, provider_name: str) -> Optional[OidcTokenProvider]:
|
|
229
|
+
"""
|
|
230
|
+
Get a specific provider by name.
|
|
231
|
+
"""
|
|
232
|
+
provider_class = self._providers.get(provider_name)
|
|
233
|
+
if provider_class:
|
|
234
|
+
return provider_class()
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
def get_provider_class(
|
|
238
|
+
self, provider_name: str
|
|
239
|
+
) -> Optional[Type[OidcTokenProvider]]:
|
|
240
|
+
"""
|
|
241
|
+
Get a specific provider class by name.
|
|
242
|
+
"""
|
|
243
|
+
return self._providers.get(provider_name)
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def provider_names(self) -> List[str]:
|
|
247
|
+
"""
|
|
248
|
+
List all registered provider names.
|
|
249
|
+
"""
|
|
250
|
+
return list(self._providers.keys())
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def all_providers(self) -> List[OidcTokenProvider]:
|
|
254
|
+
"""
|
|
255
|
+
Get instances of all registered providers.
|
|
256
|
+
"""
|
|
257
|
+
return [provider_class() for provider_class in self._providers.values()]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# Global registry instance
|
|
261
|
+
_registry = OidcProviderRegistry()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def get_oidc_provider(provider_name: str) -> OidcTokenProvider:
|
|
265
|
+
"""
|
|
266
|
+
Get a specific OIDC provider by name without checking availability.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
provider_name: Name of the provider to get
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
The requested OIDC provider instance
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
OidcProviderNotFoundError: If provider is unknown
|
|
276
|
+
"""
|
|
277
|
+
provider = _registry.get_provider(provider_name)
|
|
278
|
+
|
|
279
|
+
if not provider:
|
|
280
|
+
providers_list = ", ".join(_registry.provider_names)
|
|
281
|
+
raise OidcProviderNotFoundError(
|
|
282
|
+
"Unknown provider '%s'. Available providers: %s"
|
|
283
|
+
% (
|
|
284
|
+
provider_name,
|
|
285
|
+
providers_list,
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return provider
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def get_active_oidc_provider(provider_name: str) -> OidcTokenProvider:
|
|
293
|
+
"""
|
|
294
|
+
Get a specific OIDC provider by name and ensure it's available.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
provider_name: Name of the provider to get
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
The requested OIDC provider instance
|
|
301
|
+
|
|
302
|
+
Raises:
|
|
303
|
+
OidcProviderNotFoundError: If provider is unknown
|
|
304
|
+
OidcProviderUnavailableError: If provider is not available
|
|
305
|
+
"""
|
|
306
|
+
provider = get_oidc_provider(provider_name)
|
|
307
|
+
|
|
308
|
+
if not provider.is_available:
|
|
309
|
+
raise OidcProviderUnavailableError(
|
|
310
|
+
"Provider '%s' is not available in the current environment." % provider_name
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return provider
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def get_oidc_provider_class(provider_name: str) -> Type[OidcTokenProvider]:
|
|
317
|
+
"""
|
|
318
|
+
Get a specific OIDC provider class by name.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
provider_name: Name of the provider to get
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
The requested OIDC provider class
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
OidcProviderNotFoundError: If provider is unknown
|
|
328
|
+
"""
|
|
329
|
+
provider_class = _registry.get_provider_class(provider_name)
|
|
330
|
+
|
|
331
|
+
if not provider_class:
|
|
332
|
+
providers_list = ", ".join(_registry.provider_names)
|
|
333
|
+
raise OidcProviderNotFoundError(
|
|
334
|
+
"Unknown provider '%s'. Available providers: %s"
|
|
335
|
+
% (
|
|
336
|
+
provider_name,
|
|
337
|
+
providers_list,
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return provider_class
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def auto_detect_oidc_provider() -> OidcTokenProvider:
|
|
345
|
+
"""
|
|
346
|
+
Auto-detect a single available OIDC provider in the current environment.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
The single available OIDC provider
|
|
350
|
+
|
|
351
|
+
Raises:
|
|
352
|
+
OidcProviderAutoDetectionError: If no providers are available or multiple providers are available
|
|
353
|
+
"""
|
|
354
|
+
available = [
|
|
355
|
+
provider for provider in _registry.all_providers if provider.is_available
|
|
356
|
+
]
|
|
357
|
+
available_names = [p.provider_name for p in available]
|
|
358
|
+
|
|
359
|
+
all_providers = _registry.provider_names
|
|
360
|
+
match (len(available), all_providers):
|
|
361
|
+
case (1, _):
|
|
362
|
+
# Happy path - single provider found
|
|
363
|
+
logger.info("Found 1 available provider: %s", available_names[0])
|
|
364
|
+
return available[0]
|
|
365
|
+
case (0, providers) if providers:
|
|
366
|
+
# No providers available but some are registered
|
|
367
|
+
providers_list = ", ".join(providers)
|
|
368
|
+
msg = (
|
|
369
|
+
"No OIDC provider detected in current environment. "
|
|
370
|
+
"Available providers: %s. "
|
|
371
|
+
"Use --type <provider> to specify a provider explicitly."
|
|
372
|
+
) % providers_list
|
|
373
|
+
logger.info(msg)
|
|
374
|
+
raise OidcProviderAutoDetectionError(msg)
|
|
375
|
+
case (0, _):
|
|
376
|
+
# No providers available and none are registered
|
|
377
|
+
msg = "No OIDC providers are registered."
|
|
378
|
+
logger.info(msg)
|
|
379
|
+
raise OidcProviderAutoDetectionError(msg)
|
|
380
|
+
case _:
|
|
381
|
+
# Multiple providers available - raise error
|
|
382
|
+
providers_list = ", ".join(available_names)
|
|
383
|
+
msg = (
|
|
384
|
+
"Multiple OIDC providers detected: %s. "
|
|
385
|
+
"Please specify which provider to use with --type <provider>."
|
|
386
|
+
) % providers_list
|
|
387
|
+
logger.info(msg)
|
|
388
|
+
raise OidcProviderAutoDetectionError(msg)
|
|
389
|
+
|
|
390
|
+
# This line should never be reached, but helps mypy understand all paths are covered
|
|
391
|
+
raise OidcProviderAutoDetectionError(
|
|
392
|
+
"Unexpected state in auto_detect_oidc_provider"
|
|
393
|
+
)
|
snowflake/cli/_app/cli_app.py
CHANGED
snowflake/cli/_app/constants.py
CHANGED
|
@@ -21,3 +21,13 @@ PARAM_APPLICATION_NAME: Literal["snowcli"] = "snowcli"
|
|
|
21
21
|
# This is also defined on server side. Changing this parameter would require
|
|
22
22
|
# a change in https://github.com/snowflakedb/snowflake
|
|
23
23
|
INTERNAL_APPLICATION_NAME: Literal["SNOWFLAKE_CLI"] = "SNOWFLAKE_CLI"
|
|
24
|
+
|
|
25
|
+
# Authenticator types
|
|
26
|
+
AUTHENTICATOR_WORKLOAD_IDENTITY: Literal["WORKLOAD_IDENTITY"] = "WORKLOAD_IDENTITY"
|
|
27
|
+
AUTHENTICATOR_SNOWFLAKE_JWT: Literal["SNOWFLAKE_JWT"] = "SNOWFLAKE_JWT"
|
|
28
|
+
AUTHENTICATOR_USERNAME_PASSWORD_MFA: Literal[
|
|
29
|
+
"username_password_mfa"
|
|
30
|
+
] = "username_password_mfa"
|
|
31
|
+
AUTHENTICATOR_OAUTH_AUTHORIZATION_CODE: Literal[
|
|
32
|
+
"OAUTH_AUTHORIZATION_CODE"
|
|
33
|
+
] = "OAUTH_AUTHORIZATION_CODE"
|
snowflake/cli/_app/printing.py
CHANGED
|
@@ -22,7 +22,7 @@ from decimal import Decimal
|
|
|
22
22
|
from json import JSONEncoder
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from textwrap import indent
|
|
25
|
-
from typing import TextIO
|
|
25
|
+
from typing import Any, Dict, TextIO
|
|
26
26
|
|
|
27
27
|
from rich import box, get_console
|
|
28
28
|
from rich import print as rich_print
|
|
@@ -61,13 +61,114 @@ class CustomJSONEncoder(JSONEncoder):
|
|
|
61
61
|
return list(o.result)
|
|
62
62
|
if isinstance(o, (date, datetime, time)):
|
|
63
63
|
return o.isoformat()
|
|
64
|
-
if isinstance(o,
|
|
64
|
+
if isinstance(o, Path):
|
|
65
|
+
return o.as_posix()
|
|
66
|
+
if isinstance(o, Decimal):
|
|
65
67
|
return str(o)
|
|
66
68
|
if isinstance(o, bytearray):
|
|
67
69
|
return o.hex()
|
|
68
70
|
return super().default(o)
|
|
69
71
|
|
|
70
72
|
|
|
73
|
+
class StreamingJSONEncoder(JSONEncoder):
|
|
74
|
+
"""Streaming JSON encoder that doesn't materialize generators into lists"""
|
|
75
|
+
|
|
76
|
+
def default(self, o):
|
|
77
|
+
if isinstance(o, str):
|
|
78
|
+
return sanitize_for_terminal(o)
|
|
79
|
+
if isinstance(o, (ObjectResult, MessageResult)):
|
|
80
|
+
return o.result
|
|
81
|
+
if isinstance(o, (CollectionResult, MultipleResults)):
|
|
82
|
+
raise TypeError(
|
|
83
|
+
f"CollectionResult should be handled by streaming functions, not encoder"
|
|
84
|
+
)
|
|
85
|
+
if isinstance(o, (date, datetime, time)):
|
|
86
|
+
return o.isoformat()
|
|
87
|
+
if isinstance(o, Path):
|
|
88
|
+
return o.as_posix()
|
|
89
|
+
if isinstance(o, Decimal):
|
|
90
|
+
return str(o)
|
|
91
|
+
if isinstance(o, bytearray):
|
|
92
|
+
return o.hex()
|
|
93
|
+
return super().default(o)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _print_json_item_with_array_indentation(item: Any, indent: int):
|
|
97
|
+
"""Print a JSON item with proper indentation for array context"""
|
|
98
|
+
if indent:
|
|
99
|
+
indented_output = json.dumps(item, cls=StreamingJSONEncoder, indent=indent)
|
|
100
|
+
indented_lines = indented_output.split("\n")
|
|
101
|
+
for i, line in enumerate(indented_lines):
|
|
102
|
+
if i == 0:
|
|
103
|
+
print(" " * indent + line, end="")
|
|
104
|
+
else:
|
|
105
|
+
print("\n" + " " * indent + line, end="")
|
|
106
|
+
else:
|
|
107
|
+
json.dump(item, sys.stdout, cls=StreamingJSONEncoder, separators=(",", ":"))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _stream_collection_as_json(result: CollectionResult, indent: int = 4):
|
|
111
|
+
"""Stream a CollectionResult as a JSON array without loading all data into memory"""
|
|
112
|
+
items = iter(result.result)
|
|
113
|
+
try:
|
|
114
|
+
first_item = next(items)
|
|
115
|
+
except StopIteration:
|
|
116
|
+
print("[]", end="")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
print("[")
|
|
120
|
+
|
|
121
|
+
_print_json_item_with_array_indentation(first_item, indent)
|
|
122
|
+
|
|
123
|
+
for item in items:
|
|
124
|
+
print(",")
|
|
125
|
+
_print_json_item_with_array_indentation(item, indent)
|
|
126
|
+
|
|
127
|
+
print("\n]", end="")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _stream_collection_as_csv(result: CollectionResult):
|
|
131
|
+
"""Stream a CollectionResult as CSV without loading all data into memory"""
|
|
132
|
+
items = iter(result.result)
|
|
133
|
+
try:
|
|
134
|
+
first_item = next(items)
|
|
135
|
+
except StopIteration:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
fieldnames = list(first_item.keys())
|
|
139
|
+
if not isinstance(first_item, dict):
|
|
140
|
+
raise TypeError("CSV output requires dictionary items")
|
|
141
|
+
|
|
142
|
+
writer = csv.DictWriter(sys.stdout, fieldnames=fieldnames, lineterminator="\n")
|
|
143
|
+
writer.writeheader()
|
|
144
|
+
_write_csv_row(writer, first_item)
|
|
145
|
+
|
|
146
|
+
for item in items:
|
|
147
|
+
_write_csv_row(writer, item)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _write_csv_row(writer: csv.DictWriter, row_data: Dict[str, Any]):
|
|
151
|
+
"""Write a single CSV row, handling special data types"""
|
|
152
|
+
processed_row = {}
|
|
153
|
+
for key, value in row_data.items():
|
|
154
|
+
if isinstance(value, str):
|
|
155
|
+
processed_row[key] = sanitize_for_terminal(value)
|
|
156
|
+
elif isinstance(value, (date, datetime, time)):
|
|
157
|
+
processed_row[key] = value.isoformat()
|
|
158
|
+
elif isinstance(value, Path):
|
|
159
|
+
processed_row[key] = value.as_posix()
|
|
160
|
+
elif isinstance(value, Decimal):
|
|
161
|
+
processed_row[key] = str(value)
|
|
162
|
+
elif isinstance(value, bytearray):
|
|
163
|
+
processed_row[key] = value.hex()
|
|
164
|
+
elif value is None:
|
|
165
|
+
processed_row[key] = ""
|
|
166
|
+
else:
|
|
167
|
+
processed_row[key] = str(value)
|
|
168
|
+
|
|
169
|
+
writer.writerow(processed_row)
|
|
170
|
+
|
|
171
|
+
|
|
71
172
|
def _get_format_type() -> OutputFormat:
|
|
72
173
|
output_format = get_cli_context().output_format
|
|
73
174
|
if output_format:
|
|
@@ -110,12 +211,13 @@ def is_structured_format(output_format):
|
|
|
110
211
|
def print_structured(
|
|
111
212
|
result: CommandResult, output_format: OutputFormat = OutputFormat.JSON
|
|
112
213
|
):
|
|
113
|
-
"""Handles outputs like json,
|
|
214
|
+
"""Handles outputs like json, csv and other structured and parsable formats with streaming."""
|
|
114
215
|
printed_end_line = False
|
|
216
|
+
|
|
115
217
|
if isinstance(result, MultipleResults):
|
|
116
218
|
if output_format == OutputFormat.CSV:
|
|
117
219
|
for command_result in result.result:
|
|
118
|
-
|
|
220
|
+
_print_csv_result_streaming(command_result)
|
|
119
221
|
print(flush=True)
|
|
120
222
|
printed_end_line = True
|
|
121
223
|
else:
|
|
@@ -125,35 +227,67 @@ def print_structured(
|
|
|
125
227
|
# instead of joining all the values into a JSON array or CSV entry set
|
|
126
228
|
for r in result.result:
|
|
127
229
|
if output_format == OutputFormat.CSV:
|
|
128
|
-
|
|
230
|
+
_print_csv_result_streaming(r)
|
|
129
231
|
else:
|
|
130
|
-
json.dump(r, sys.stdout, cls=
|
|
232
|
+
json.dump(r, sys.stdout, cls=StreamingJSONEncoder)
|
|
131
233
|
print(flush=True)
|
|
132
234
|
printed_end_line = True
|
|
133
235
|
else:
|
|
134
236
|
if output_format == OutputFormat.CSV:
|
|
135
|
-
|
|
237
|
+
_print_csv_result_streaming(result)
|
|
136
238
|
printed_end_line = True
|
|
137
239
|
else:
|
|
138
|
-
|
|
240
|
+
_print_json_result_streaming(result)
|
|
241
|
+
|
|
139
242
|
# Adds empty line at the end
|
|
140
243
|
if not printed_end_line:
|
|
141
244
|
print(flush=True)
|
|
142
245
|
|
|
143
246
|
|
|
144
|
-
def
|
|
145
|
-
|
|
247
|
+
def _print_json_result_streaming(result: CommandResult):
|
|
248
|
+
"""Print a single CommandResult as JSON with streaming support"""
|
|
249
|
+
if isinstance(result, CollectionResult):
|
|
250
|
+
_stream_collection_as_json(result, indent=4)
|
|
251
|
+
elif isinstance(result, (ObjectResult, MessageResult)):
|
|
252
|
+
json.dump(result, sys.stdout, cls=StreamingJSONEncoder, indent=4)
|
|
253
|
+
else:
|
|
254
|
+
json.dump(result, sys.stdout, cls=StreamingJSONEncoder, indent=4)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _print_object_result_as_csv(result: ObjectResult):
|
|
258
|
+
"""Print an ObjectResult as a single-row CSV.
|
|
259
|
+
|
|
260
|
+
Converts the object's key-value pairs into a CSV with headers
|
|
261
|
+
from the keys and a single data row from the values.
|
|
262
|
+
"""
|
|
263
|
+
data = result.result
|
|
146
264
|
if isinstance(data, dict):
|
|
147
|
-
writer = csv.DictWriter(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
elif isinstance(data, list):
|
|
151
|
-
if not data:
|
|
152
|
-
return
|
|
153
|
-
writer = csv.DictWriter(sys.stdout, [*data[0]], lineterminator="\n")
|
|
265
|
+
writer = csv.DictWriter(
|
|
266
|
+
sys.stdout, fieldnames=list(data.keys()), lineterminator="\n"
|
|
267
|
+
)
|
|
154
268
|
writer.writeheader()
|
|
155
|
-
|
|
156
|
-
|
|
269
|
+
_write_csv_row(writer, data)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _print_message_result_as_csv(result: MessageResult):
|
|
273
|
+
"""Print a MessageResult as CSV with a single 'message' column.
|
|
274
|
+
|
|
275
|
+
Creates a simple CSV structure with one column named 'message'
|
|
276
|
+
containing the sanitized message text.
|
|
277
|
+
"""
|
|
278
|
+
writer = csv.DictWriter(sys.stdout, fieldnames=["message"], lineterminator="\n")
|
|
279
|
+
writer.writeheader()
|
|
280
|
+
writer.writerow({"message": sanitize_for_terminal(result.message)})
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _print_csv_result_streaming(result: CommandResult):
|
|
284
|
+
"""Print a single CommandResult as CSV with streaming support"""
|
|
285
|
+
if isinstance(result, CollectionResult):
|
|
286
|
+
_stream_collection_as_csv(result)
|
|
287
|
+
elif isinstance(result, ObjectResult):
|
|
288
|
+
_print_object_result_as_csv(result)
|
|
289
|
+
elif isinstance(result, MessageResult):
|
|
290
|
+
_print_message_result_as_csv(result)
|
|
157
291
|
|
|
158
292
|
|
|
159
293
|
def _stream_json(result):
|