snowflake-cli 3.7.2__py3-none-any.whl → 3.8.1__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/snow_connector.py +14 -0
- snowflake/cli/_app/telemetry.py +11 -0
- snowflake/cli/_app/version_check.py +4 -3
- snowflake/cli/_plugins/connection/commands.py +4 -2
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +1 -1
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +20 -7
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +5 -3
- snowflake/cli/_plugins/project/commands.py +16 -6
- snowflake/cli/_plugins/snowpark/common.py +31 -0
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +3 -0
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +21 -1
- snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +23 -1
- snowflake/cli/_plugins/spcs/common.py +7 -0
- snowflake/cli/_plugins/spcs/image_repository/commands.py +7 -2
- snowflake/cli/_plugins/spcs/image_repository/manager.py +6 -2
- snowflake/cli/_plugins/spcs/services/commands.py +2 -2
- snowflake/cli/_plugins/spcs/services/manager.py +36 -1
- snowflake/cli/_plugins/sql/commands.py +57 -6
- snowflake/cli/_plugins/sql/lexer/__init__.py +7 -0
- snowflake/cli/_plugins/sql/lexer/completer.py +12 -0
- snowflake/cli/_plugins/sql/lexer/functions.py +421 -0
- snowflake/cli/_plugins/sql/lexer/keywords.py +529 -0
- snowflake/cli/_plugins/sql/lexer/lexer.py +56 -0
- snowflake/cli/_plugins/sql/lexer/types.py +37 -0
- snowflake/cli/_plugins/sql/manager.py +43 -9
- snowflake/cli/_plugins/sql/repl.py +221 -0
- snowflake/cli/_plugins/sql/snowsql_commands.py +331 -0
- snowflake/cli/_plugins/sql/statement_reader.py +296 -0
- snowflake/cli/_plugins/streamlit/commands.py +30 -15
- snowflake/cli/_plugins/streamlit/manager.py +0 -183
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +163 -23
- snowflake/cli/api/artifacts/upload.py +5 -0
- snowflake/cli/api/artifacts/utils.py +0 -2
- snowflake/cli/api/cli_global_context.py +7 -3
- snowflake/cli/api/commands/decorators.py +70 -0
- snowflake/cli/api/commands/flags.py +95 -3
- snowflake/cli/api/config.py +10 -0
- snowflake/cli/api/connections.py +10 -0
- snowflake/cli/api/console/abc.py +8 -2
- snowflake/cli/api/console/console.py +16 -0
- snowflake/cli/api/console/enum.py +1 -1
- snowflake/cli/api/entities/common.py +99 -10
- snowflake/cli/api/entities/utils.py +1 -0
- snowflake/cli/api/feature_flags.py +6 -0
- snowflake/cli/api/project/project_paths.py +5 -0
- snowflake/cli/api/rendering/sql_templates.py +2 -1
- snowflake/cli/api/sql_execution.py +16 -4
- snowflake/cli/api/utils/path_utils.py +15 -0
- snowflake/cli/api/utils/python_api_utils.py +12 -0
- {snowflake_cli-3.7.2.dist-info → snowflake_cli-3.8.1.dist-info}/METADATA +10 -6
- {snowflake_cli-3.7.2.dist-info → snowflake_cli-3.8.1.dist-info}/RECORD +55 -47
- snowflake/cli/_plugins/nativeapp/feature_flags.py +0 -28
- snowflake/cli/_plugins/sql/source_reader.py +0 -230
- {snowflake_cli-3.7.2.dist-info → snowflake_cli-3.8.1.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.7.2.dist-info → snowflake_cli-3.8.1.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.7.2.dist-info → snowflake_cli-3.8.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,19 +1,27 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
from typing import Optional
|
|
3
4
|
|
|
4
5
|
from click import ClickException
|
|
5
6
|
from snowflake.cli._plugins.connection.util import make_snowsight_url
|
|
6
7
|
from snowflake.cli._plugins.nativeapp.artifacts import build_bundle
|
|
7
|
-
from snowflake.cli._plugins.
|
|
8
|
+
from snowflake.cli._plugins.stage.manager import StageManager
|
|
8
9
|
from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
|
|
9
10
|
StreamlitEntityModel,
|
|
10
11
|
)
|
|
11
12
|
from snowflake.cli._plugins.workspace.context import ActionContext
|
|
13
|
+
from snowflake.cli.api.artifacts.bundle_map import BundleMap
|
|
12
14
|
from snowflake.cli.api.entities.common import EntityBase
|
|
15
|
+
from snowflake.cli.api.entities.utils import EntityActions, sync_deploy_root_with_stage
|
|
16
|
+
from snowflake.cli.api.feature_flags import FeatureFlag as GlobalFeatureFlag
|
|
17
|
+
from snowflake.cli.api.identifiers import FQN
|
|
13
18
|
from snowflake.cli.api.project.project_paths import bundle_root
|
|
14
|
-
from snowflake.cli.api.project.schemas.entities.common import PathMapping
|
|
19
|
+
from snowflake.cli.api.project.schemas.entities.common import Identifier, PathMapping
|
|
20
|
+
from snowflake.connector import ProgrammingError
|
|
15
21
|
from snowflake.connector.cursor import SnowflakeCursor
|
|
16
22
|
|
|
23
|
+
log = logging.getLogger(__name__)
|
|
24
|
+
|
|
17
25
|
|
|
18
26
|
class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
19
27
|
"""
|
|
@@ -21,8 +29,6 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
21
29
|
"""
|
|
22
30
|
|
|
23
31
|
def __init__(self, *args, **kwargs):
|
|
24
|
-
if not FeatureFlag.ENABLE_NATIVE_APP_CHILDREN.is_enabled():
|
|
25
|
-
raise NotImplementedError("Streamlit entity is not implemented yet")
|
|
26
32
|
super().__init__(*args, **kwargs)
|
|
27
33
|
|
|
28
34
|
@property
|
|
@@ -37,10 +43,10 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
37
43
|
return self.bundle()
|
|
38
44
|
|
|
39
45
|
def action_deploy(self, action_ctx: ActionContext, *args, **kwargs):
|
|
40
|
-
|
|
41
|
-
# To copy artifacts to destination on stage.
|
|
46
|
+
return self.deploy(action_ctx, *args, **kwargs)
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
def action_describe(self, action_ctx: ActionContext, *args, **kwargs):
|
|
49
|
+
return self.describe()
|
|
44
50
|
|
|
45
51
|
def action_drop(self, action_ctx: ActionContext, *args, **kwargs):
|
|
46
52
|
return self._execute_query(self.get_drop_sql())
|
|
@@ -58,10 +64,10 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
58
64
|
self._conn, f"/#/streamlit-apps/{name.url_identifier}"
|
|
59
65
|
)
|
|
60
66
|
|
|
61
|
-
def bundle(self, output_dir: Optional[Path] = None):
|
|
62
|
-
build_bundle(
|
|
67
|
+
def bundle(self, output_dir: Optional[Path] = None) -> BundleMap:
|
|
68
|
+
return build_bundle(
|
|
63
69
|
self.root,
|
|
64
|
-
output_dir or bundle_root(self.root, "streamlit"),
|
|
70
|
+
output_dir or bundle_root(self.root, "streamlit") / self.entity_id,
|
|
65
71
|
[
|
|
66
72
|
PathMapping(
|
|
67
73
|
src=artifact.src, dest=artifact.dest, processors=artifact.processors
|
|
@@ -70,14 +76,89 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
70
76
|
],
|
|
71
77
|
)
|
|
72
78
|
|
|
73
|
-
def deploy(
|
|
74
|
-
|
|
79
|
+
def deploy(
|
|
80
|
+
self,
|
|
81
|
+
action_context: ActionContext,
|
|
82
|
+
_open: bool,
|
|
83
|
+
replace: bool,
|
|
84
|
+
prune: bool = False,
|
|
85
|
+
bundle_map: Optional[BundleMap] = None,
|
|
86
|
+
experimental: Optional[bool] = False,
|
|
87
|
+
*args,
|
|
88
|
+
**kwargs,
|
|
89
|
+
):
|
|
90
|
+
if (
|
|
91
|
+
bundle_map is None
|
|
92
|
+
): # TODO: maybe we could hold bundle map as a cached property?
|
|
93
|
+
bundle_map = self.bundle()
|
|
94
|
+
|
|
95
|
+
console = self._workspace_ctx.console
|
|
96
|
+
console.step(f"Checking if object exists")
|
|
97
|
+
if self._object_exists() and not replace:
|
|
98
|
+
raise ClickException(
|
|
99
|
+
f"Streamlit {self.model.fqn.sql_identifier} already exists. Use 'replace' option to overwrite."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
console.step(f"Creating stage {self.model.stage} if not exists")
|
|
103
|
+
stage = self._create_stage_if_not_exists()
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
experimental
|
|
107
|
+
or GlobalFeatureFlag.ENABLE_STREAMLIT_VERSIONED_STAGE.is_enabled()
|
|
108
|
+
or GlobalFeatureFlag.ENABLE_STREAMLIT_EMBEDDED_STAGE.is_enabled()
|
|
109
|
+
):
|
|
110
|
+
self._deploy_experimental(bundle_map=bundle_map, replace=replace)
|
|
111
|
+
else:
|
|
112
|
+
console.step(f"Uploading artifacts to stage {self.model.stage}")
|
|
113
|
+
|
|
114
|
+
# We use a static method from StageManager here, but maybe this logic could be implemented elswhere, as we implement entities?
|
|
115
|
+
name = (
|
|
116
|
+
self.model.identifier.name
|
|
117
|
+
if isinstance(self.model.identifier, Identifier)
|
|
118
|
+
else self.model.identifier
|
|
119
|
+
)
|
|
120
|
+
stage_root = StageManager.get_standard_stage_prefix(
|
|
121
|
+
f"{FQN.from_string(self.model.stage).using_connection(self._conn)}/{name}"
|
|
122
|
+
)
|
|
123
|
+
if prune:
|
|
124
|
+
sync_deploy_root_with_stage(
|
|
125
|
+
console=self._workspace_ctx.console,
|
|
126
|
+
deploy_root=bundle_map.deploy_root(),
|
|
127
|
+
bundle_map=bundle_map,
|
|
128
|
+
prune=prune,
|
|
129
|
+
recursive=True,
|
|
130
|
+
stage_path=StageManager().stage_path_parts_from_str(stage_root),
|
|
131
|
+
print_diff=True,
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
self._upload_files_to_stage(stage, bundle_map, None)
|
|
135
|
+
|
|
136
|
+
console.step(f"Creating Streamlit object {self.model.fqn.sql_identifier}")
|
|
137
|
+
|
|
138
|
+
self._execute_query(
|
|
139
|
+
self.get_deploy_sql(replace=replace, from_stage_name=stage_root)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return self.perform(EntityActions.GET_URL, action_context, *args, **kwargs)
|
|
143
|
+
|
|
144
|
+
def describe(self) -> SnowflakeCursor:
|
|
145
|
+
return self._execute_query(self.get_describe_sql())
|
|
75
146
|
|
|
76
147
|
def action_share(
|
|
77
148
|
self, action_ctx: ActionContext, to_role: str, *args, **kwargs
|
|
78
149
|
) -> SnowflakeCursor:
|
|
79
150
|
return self._execute_query(self.get_share_sql(to_role))
|
|
80
151
|
|
|
152
|
+
def get_add_live_version_sql(
|
|
153
|
+
self, schema: Optional[str] = None, database: Optional[str] = None
|
|
154
|
+
):
|
|
155
|
+
return f"ALTER STREAMLIT {self._get_identifier(schema,database)} ADD LIVE VERSION FROM LAST;"
|
|
156
|
+
|
|
157
|
+
def get_checkout_sql(
|
|
158
|
+
self, schema: Optional[str] = None, database: Optional[str] = None
|
|
159
|
+
):
|
|
160
|
+
return f"ALTER STREAMLIT {self._get_identifier(schema,database)} CHECKOUT;"
|
|
161
|
+
|
|
81
162
|
def get_deploy_sql(
|
|
82
163
|
self,
|
|
83
164
|
if_not_exists: bool = False,
|
|
@@ -85,21 +166,19 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
85
166
|
from_stage_name: Optional[str] = None,
|
|
86
167
|
artifacts_dir: Optional[Path] = None,
|
|
87
168
|
schema: Optional[str] = None,
|
|
169
|
+
database: Optional[str] = None,
|
|
88
170
|
*args,
|
|
89
171
|
**kwargs,
|
|
90
|
-
):
|
|
91
|
-
if replace and if_not_exists:
|
|
92
|
-
raise ClickException("Cannot specify both replace and if_not_exists")
|
|
172
|
+
) -> str:
|
|
93
173
|
|
|
94
174
|
if replace:
|
|
95
|
-
query = "CREATE OR REPLACE "
|
|
175
|
+
query = "CREATE OR REPLACE STREAMLIT"
|
|
96
176
|
elif if_not_exists:
|
|
97
|
-
query = "CREATE IF NOT EXISTS
|
|
177
|
+
query = "CREATE STREAMLIT IF NOT EXISTS"
|
|
98
178
|
else:
|
|
99
|
-
query = "CREATE "
|
|
179
|
+
query = "CREATE STREAMLIT"
|
|
100
180
|
|
|
101
|
-
|
|
102
|
-
query += f"STREAMLIT {self._entity_model.fqn.set_schema(schema_to_use).sql_identifier}"
|
|
181
|
+
query += f" {self._get_identifier(schema, database)}"
|
|
103
182
|
|
|
104
183
|
if from_stage_name:
|
|
105
184
|
query += f"\nROOT_LOCATION = '{from_stage_name}'"
|
|
@@ -112,7 +191,12 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
112
191
|
query += "\n" + self.model.get_imports_sql()
|
|
113
192
|
|
|
114
193
|
if self.model.query_warehouse:
|
|
115
|
-
query += f"\nQUERY_WAREHOUSE =
|
|
194
|
+
query += f"\nQUERY_WAREHOUSE = {self.model.query_warehouse}"
|
|
195
|
+
else:
|
|
196
|
+
self._workspace_ctx.console.warning(
|
|
197
|
+
"[Deprecation] In next major version we will remove default query_warehouse='streamlit'."
|
|
198
|
+
)
|
|
199
|
+
query += f"\nQUERY_WAREHOUSE = 'streamlit'"
|
|
116
200
|
|
|
117
201
|
if self.model.title:
|
|
118
202
|
query += f"\nTITLE = '{self.model.title}'"
|
|
@@ -128,11 +212,14 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
128
212
|
|
|
129
213
|
return query + ";"
|
|
130
214
|
|
|
215
|
+
def get_describe_sql(self) -> str:
|
|
216
|
+
return f"DESCRIBE STREAMLIT {self._get_identifier()};"
|
|
217
|
+
|
|
131
218
|
def get_share_sql(self, to_role: str) -> str:
|
|
132
|
-
return f"GRANT USAGE ON STREAMLIT {self.
|
|
219
|
+
return f"GRANT USAGE ON STREAMLIT {self._get_identifier()} TO ROLE {to_role};"
|
|
133
220
|
|
|
134
221
|
def get_execute_sql(self):
|
|
135
|
-
return f"EXECUTE STREAMLIT {self.
|
|
222
|
+
return f"EXECUTE STREAMLIT {self._get_identifier()}();"
|
|
136
223
|
|
|
137
224
|
def get_usage_grant_sql(self, app_role: str, schema: Optional[str] = None) -> str:
|
|
138
225
|
entity_id = self.entity_id
|
|
@@ -140,3 +227,56 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
140
227
|
return (
|
|
141
228
|
f"GRANT USAGE ON STREAMLIT {streamlit_name} TO APPLICATION ROLE {app_role};"
|
|
142
229
|
)
|
|
230
|
+
|
|
231
|
+
def _object_exists(self) -> bool:
|
|
232
|
+
try:
|
|
233
|
+
self.describe()
|
|
234
|
+
return True
|
|
235
|
+
except ProgrammingError:
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
def _deploy_experimental(
|
|
239
|
+
self, bundle_map: BundleMap, replace: bool = False, prune: bool = False
|
|
240
|
+
):
|
|
241
|
+
self._execute_query(
|
|
242
|
+
self.get_deploy_sql(
|
|
243
|
+
if_not_exists=True,
|
|
244
|
+
replace=replace,
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
try:
|
|
248
|
+
if GlobalFeatureFlag.ENABLE_STREAMLIT_VERSIONED_STAGE.is_enabled():
|
|
249
|
+
self._execute_query(self.get_add_live_version_sql())
|
|
250
|
+
elif not GlobalFeatureFlag.ENABLE_STREAMLIT_NO_CHECKOUTS.is_enabled():
|
|
251
|
+
self._execute_query(self.get_checkout_sql())
|
|
252
|
+
except ProgrammingError as e:
|
|
253
|
+
if "Checkout already exists" in str(
|
|
254
|
+
e
|
|
255
|
+
) or "There is already a live version" in str(e):
|
|
256
|
+
log.info("Checkout already exists, continuing")
|
|
257
|
+
else:
|
|
258
|
+
raise
|
|
259
|
+
|
|
260
|
+
embeded_stage_name = (
|
|
261
|
+
f"snow://streamlit/{self.model.fqn.using_connection(self._conn).identifier}"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if GlobalFeatureFlag.ENABLE_STREAMLIT_VERSIONED_STAGE.is_enabled():
|
|
265
|
+
stage_root = f"{embeded_stage_name}/versions/live"
|
|
266
|
+
else:
|
|
267
|
+
stage_root = f"{embeded_stage_name}/default_checkout"
|
|
268
|
+
|
|
269
|
+
stage_resource = self._create_stage_if_not_exists(embeded_stage_name)
|
|
270
|
+
|
|
271
|
+
if prune:
|
|
272
|
+
sync_deploy_root_with_stage(
|
|
273
|
+
console=self._workspace_ctx.console,
|
|
274
|
+
deploy_root=bundle_map.deploy_root(),
|
|
275
|
+
bundle_map=bundle_map,
|
|
276
|
+
prune=prune,
|
|
277
|
+
recursive=True,
|
|
278
|
+
stage_path=StageManager().stage_path_parts_from_str(stage_root),
|
|
279
|
+
print_diff=True,
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
self._upload_files_to_stage(stage_resource, bundle_map)
|
|
@@ -6,6 +6,7 @@ from snowflake.cli.api.console import cli_console
|
|
|
6
6
|
from snowflake.cli.api.entities.utils import sync_deploy_root_with_stage
|
|
7
7
|
from snowflake.cli.api.project.project_paths import ProjectPaths
|
|
8
8
|
from snowflake.cli.api.project.schemas.entities.common import PathMapping
|
|
9
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def sync_artifacts_with_stage(
|
|
@@ -17,6 +18,9 @@ def sync_artifacts_with_stage(
|
|
|
17
18
|
if artifacts is None:
|
|
18
19
|
artifacts = []
|
|
19
20
|
|
|
21
|
+
project_paths.remove_up_bundle_root()
|
|
22
|
+
SecurePath(project_paths.bundle_root).mkdir(parents=True, exist_ok=True)
|
|
23
|
+
|
|
20
24
|
bundle_map = bundle_artifacts(project_paths, artifacts)
|
|
21
25
|
stage_path_parts = StageManager().stage_path_parts_from_str(stage_root)
|
|
22
26
|
# We treat the bundle root as deploy root
|
|
@@ -29,6 +33,7 @@ def sync_artifacts_with_stage(
|
|
|
29
33
|
stage_path=stage_path_parts,
|
|
30
34
|
print_diff=True,
|
|
31
35
|
)
|
|
36
|
+
project_paths.clean_up_output()
|
|
32
37
|
|
|
33
38
|
|
|
34
39
|
def put_files(
|
|
@@ -68,8 +68,6 @@ def bundle_artifacts(project_paths: ProjectPaths, artifacts: Artifacts) -> Bundl
|
|
|
68
68
|
for artifact in artifacts:
|
|
69
69
|
bundle_map.add(artifact)
|
|
70
70
|
|
|
71
|
-
project_paths.remove_up_bundle_root()
|
|
72
|
-
SecurePath(project_paths.bundle_root).mkdir(parents=True, exist_ok=True)
|
|
73
71
|
for absolute_src, absolute_dest in bundle_map.all_mappings(
|
|
74
72
|
absolute=True, expand_directories=True
|
|
75
73
|
):
|
|
@@ -31,6 +31,7 @@ from snowflake.connector import SnowflakeConnection
|
|
|
31
31
|
if TYPE_CHECKING:
|
|
32
32
|
from snowflake.cli.api.project.definition_manager import DefinitionManager
|
|
33
33
|
from snowflake.cli.api.project.schemas.project_definition import ProjectDefinition
|
|
34
|
+
from snowflake.core import Root
|
|
34
35
|
|
|
35
36
|
_CONNECTION_CACHE = OpenConnectionCache()
|
|
36
37
|
|
|
@@ -47,6 +48,7 @@ class _CliGlobalContextManager:
|
|
|
47
48
|
verbose: bool = False
|
|
48
49
|
experimental: bool = False
|
|
49
50
|
enable_tracebacks: bool = True
|
|
51
|
+
is_repl: bool = False
|
|
50
52
|
|
|
51
53
|
metrics: CLIMetrics = field(default_factory=CLIMetrics)
|
|
52
54
|
|
|
@@ -200,9 +202,7 @@ class _CliGlobalContextAccess:
|
|
|
200
202
|
@property
|
|
201
203
|
def snow_api_root(
|
|
202
204
|
self,
|
|
203
|
-
) -> Optional[
|
|
204
|
-
object
|
|
205
|
-
]: # Should be Optional[Root], but we need local import for performance reasons
|
|
205
|
+
) -> Optional[Root]:
|
|
206
206
|
from snowflake.core import Root
|
|
207
207
|
|
|
208
208
|
if self.connection:
|
|
@@ -214,6 +214,10 @@ class _CliGlobalContextAccess:
|
|
|
214
214
|
def enhanced_exit_codes(self) -> bool:
|
|
215
215
|
return self._manager.enhanced_exit_codes
|
|
216
216
|
|
|
217
|
+
@property
|
|
218
|
+
def is_repl(self) -> bool:
|
|
219
|
+
return self._manager.is_repl
|
|
220
|
+
|
|
217
221
|
|
|
218
222
|
_CLI_CONTEXT_MANAGER: ContextVar[_CliGlobalContextManager | None] = ContextVar(
|
|
219
223
|
"cli_context", default=None
|
|
@@ -23,6 +23,7 @@ from snowflake.cli.api.cli_global_context import get_cli_context
|
|
|
23
23
|
from snowflake.cli.api.commands.flags import (
|
|
24
24
|
AccountOption,
|
|
25
25
|
AuthenticatorOption,
|
|
26
|
+
ClientStoreTemporaryCredentialOption,
|
|
26
27
|
ConnectionOption,
|
|
27
28
|
DatabaseOption,
|
|
28
29
|
DebugOption,
|
|
@@ -33,6 +34,15 @@ from snowflake.cli.api.commands.flags import (
|
|
|
33
34
|
HostOption,
|
|
34
35
|
MasterTokenOption,
|
|
35
36
|
MfaPasscodeOption,
|
|
37
|
+
OauthAuthorizationUrlOption,
|
|
38
|
+
OauthClientIdOption,
|
|
39
|
+
OauthClientSecretOption,
|
|
40
|
+
OauthDisablePkceOption,
|
|
41
|
+
OauthEnableRefreshTokensOption,
|
|
42
|
+
OauthEnableSingleUseRefreshTokensOption,
|
|
43
|
+
OauthRedirectUriOption,
|
|
44
|
+
OauthScopeOption,
|
|
45
|
+
OauthTokenRequestUrlOption,
|
|
36
46
|
OutputFormatOption,
|
|
37
47
|
PasswordOption,
|
|
38
48
|
PortOption,
|
|
@@ -329,6 +339,66 @@ GLOBAL_CONNECTION_OPTIONS = [
|
|
|
329
339
|
annotation=Optional[str],
|
|
330
340
|
default=DiagAllowlistPathOption,
|
|
331
341
|
),
|
|
342
|
+
inspect.Parameter(
|
|
343
|
+
"oauth_client_id",
|
|
344
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
345
|
+
annotation=Optional[str],
|
|
346
|
+
default=OauthClientIdOption,
|
|
347
|
+
),
|
|
348
|
+
inspect.Parameter(
|
|
349
|
+
"oauth_client_secret",
|
|
350
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
351
|
+
annotation=Optional[str],
|
|
352
|
+
default=OauthClientSecretOption,
|
|
353
|
+
),
|
|
354
|
+
inspect.Parameter(
|
|
355
|
+
"oauth_authorization_url",
|
|
356
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
357
|
+
annotation=Optional[str],
|
|
358
|
+
default=OauthAuthorizationUrlOption,
|
|
359
|
+
),
|
|
360
|
+
inspect.Parameter(
|
|
361
|
+
"oauth_token_request_url",
|
|
362
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
363
|
+
annotation=Optional[str],
|
|
364
|
+
default=OauthTokenRequestUrlOption,
|
|
365
|
+
),
|
|
366
|
+
inspect.Parameter(
|
|
367
|
+
"oauth_redirect_uri",
|
|
368
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
369
|
+
annotation=Optional[str],
|
|
370
|
+
default=OauthRedirectUriOption,
|
|
371
|
+
),
|
|
372
|
+
inspect.Parameter(
|
|
373
|
+
"oauth_scope",
|
|
374
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
375
|
+
annotation=Optional[str],
|
|
376
|
+
default=OauthScopeOption,
|
|
377
|
+
),
|
|
378
|
+
inspect.Parameter(
|
|
379
|
+
"oauth_disable_pkce",
|
|
380
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
381
|
+
annotation=Optional[bool],
|
|
382
|
+
default=OauthDisablePkceOption,
|
|
383
|
+
),
|
|
384
|
+
inspect.Parameter(
|
|
385
|
+
"oauth_enable_refresh_tokens",
|
|
386
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
387
|
+
annotation=Optional[bool],
|
|
388
|
+
default=OauthEnableRefreshTokensOption,
|
|
389
|
+
),
|
|
390
|
+
inspect.Parameter(
|
|
391
|
+
"oauth_enable_single_use_refresh_tokens",
|
|
392
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
393
|
+
annotation=Optional[bool],
|
|
394
|
+
default=OauthEnableSingleUseRefreshTokensOption,
|
|
395
|
+
),
|
|
396
|
+
inspect.Parameter(
|
|
397
|
+
"client_store_temporary_credential",
|
|
398
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
399
|
+
annotation=Optional[bool],
|
|
400
|
+
default=ClientStoreTemporaryCredentialOption,
|
|
401
|
+
),
|
|
332
402
|
]
|
|
333
403
|
|
|
334
404
|
GLOBAL_OPTIONS = [
|
|
@@ -284,6 +284,99 @@ EnableDiagOption = typer.Option(
|
|
|
284
284
|
rich_help_panel=_CONNECTION_SECTION,
|
|
285
285
|
)
|
|
286
286
|
|
|
287
|
+
OauthClientIdOption = typer.Option(
|
|
288
|
+
None,
|
|
289
|
+
"--oauth-client-id",
|
|
290
|
+
help="Value of client id provided by the Identity Provider for Snowflake integration.",
|
|
291
|
+
callback=_connection_callback("oauth_client_id"),
|
|
292
|
+
show_default=False,
|
|
293
|
+
rich_help_panel=_CONNECTION_SECTION,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
OauthClientSecretOption = typer.Option(
|
|
297
|
+
None,
|
|
298
|
+
help="Value of the client secret provided by the Identity Provider for Snowflake integration.",
|
|
299
|
+
callback=_connection_callback("oauth_client_secret"),
|
|
300
|
+
show_default=False,
|
|
301
|
+
rich_help_panel=_CONNECTION_SECTION,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
OauthAuthorizationUrlOption = typer.Option(
|
|
305
|
+
None,
|
|
306
|
+
"--oauth-authorization-url",
|
|
307
|
+
help="Identity Provider endpoint supplying the authorization code to the driver.",
|
|
308
|
+
callback=_connection_callback("oauth_authorization_url"),
|
|
309
|
+
show_default=False,
|
|
310
|
+
rich_help_panel=_CONNECTION_SECTION,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
OauthTokenRequestUrlOption = typer.Option(
|
|
314
|
+
None,
|
|
315
|
+
"--oauth-token-request-url",
|
|
316
|
+
help="Identity Provider endpoint supplying the access tokens to the driver.",
|
|
317
|
+
callback=_connection_callback("oauth_token_request_url"),
|
|
318
|
+
show_default=False,
|
|
319
|
+
rich_help_panel=_CONNECTION_SECTION,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
OauthRedirectUriOption = typer.Option(
|
|
323
|
+
None,
|
|
324
|
+
"--oauth-redirect-uri",
|
|
325
|
+
help="URI to use for authorization code redirection.",
|
|
326
|
+
callback=_connection_callback("oauth_redirect_uri"),
|
|
327
|
+
show_default=False,
|
|
328
|
+
rich_help_panel=_CONNECTION_SECTION,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
OauthScopeOption = typer.Option(
|
|
332
|
+
None,
|
|
333
|
+
"--oauth-scope",
|
|
334
|
+
help="Scope requested in the Identity Provider authorization request.",
|
|
335
|
+
callback=_connection_callback("oauth_scope"),
|
|
336
|
+
show_default=False,
|
|
337
|
+
rich_help_panel=_CONNECTION_SECTION,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
OauthDisablePkceOption = typer.Option(
|
|
341
|
+
None,
|
|
342
|
+
"--oauth-disable-pkce",
|
|
343
|
+
help="Disables Proof Key for Code Exchange (PKCE). Default: `False`.",
|
|
344
|
+
callback=_connection_callback("oauth_disable_pkce"),
|
|
345
|
+
show_default=False,
|
|
346
|
+
is_flag=True,
|
|
347
|
+
rich_help_panel=_CONNECTION_SECTION,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
OauthEnableRefreshTokensOption = typer.Option(
|
|
351
|
+
None,
|
|
352
|
+
"--oauth-enable-refresh-tokens",
|
|
353
|
+
help="Enables a silent re-authentication when the actual access token becomes outdated. Default: `False`.",
|
|
354
|
+
callback=_connection_callback("oauth_enable_refresh_tokens"),
|
|
355
|
+
show_default=False,
|
|
356
|
+
is_flag=True,
|
|
357
|
+
rich_help_panel=_CONNECTION_SECTION,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
OauthEnableSingleUseRefreshTokensOption = typer.Option(
|
|
361
|
+
None,
|
|
362
|
+
"--oauth-enable-single-use-refresh-tokens",
|
|
363
|
+
help="Whether to opt-in to single-use refresh token semantics. Default: `False`.",
|
|
364
|
+
callback=_connection_callback("oauth_enable_single_use_refresh_tokens"),
|
|
365
|
+
show_default=False,
|
|
366
|
+
is_flag=True,
|
|
367
|
+
rich_help_panel=_CONNECTION_SECTION,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
ClientStoreTemporaryCredentialOption = typer.Option(
|
|
371
|
+
None,
|
|
372
|
+
"--client-store-temporary-credential",
|
|
373
|
+
help="Store the temporary credential.",
|
|
374
|
+
callback=_connection_callback("client_store_temporary_credential"),
|
|
375
|
+
is_flag=True,
|
|
376
|
+
show_default=False,
|
|
377
|
+
rich_help_panel=_CONNECTION_SECTION,
|
|
378
|
+
)
|
|
379
|
+
|
|
287
380
|
# Set default via callback to avoid including tempdir path in generated docs (snow --docs).
|
|
288
381
|
# Use constant instead of None, as None is removed from telemetry data.
|
|
289
382
|
_DIAG_LOG_DEFAULT_VALUE = "<system_temporary_directory>"
|
|
@@ -382,7 +475,6 @@ EnhancedExitCodesOption = typer.Option(
|
|
|
382
475
|
envvar="SNOWFLAKE_ENHANCED_EXIT_CODES",
|
|
383
476
|
)
|
|
384
477
|
|
|
385
|
-
|
|
386
478
|
# If IfExistsOption, IfNotExistsOption, or ReplaceOption are used with names other than those in CREATE_MODE_OPTION_NAMES,
|
|
387
479
|
# you must also override mutually_exclusive if you want to retain the validation that at most one of these flags is
|
|
388
480
|
# passed.
|
|
@@ -425,9 +517,9 @@ NoInteractiveOption = typer.Option(False, "--no-interactive", help="Disable prom
|
|
|
425
517
|
|
|
426
518
|
PruneOption = OverrideableOption(
|
|
427
519
|
False,
|
|
428
|
-
"--prune",
|
|
520
|
+
"--prune/--no-prune",
|
|
429
521
|
help=f"Delete files that exist in the stage, but not in the local filesystem.",
|
|
430
|
-
show_default=
|
|
522
|
+
show_default=True,
|
|
431
523
|
)
|
|
432
524
|
|
|
433
525
|
|
snowflake/cli/api/config.py
CHANGED
|
@@ -83,6 +83,16 @@ class ConnectionConfig:
|
|
|
83
83
|
authenticator: Optional[str] = None
|
|
84
84
|
private_key_file: Optional[str] = None
|
|
85
85
|
token_file_path: Optional[str] = None
|
|
86
|
+
oauth_client_id: Optional[str] = None
|
|
87
|
+
oauth_client_secret: Optional[str] = None
|
|
88
|
+
oauth_authorization_url: Optional[str] = None
|
|
89
|
+
oauth_token_request_url: Optional[str] = None
|
|
90
|
+
oauth_redirect_uri: Optional[str] = None
|
|
91
|
+
oauth_scope: Optional[str] = None
|
|
92
|
+
oatuh_enable_pkce: Optional[bool] = None
|
|
93
|
+
oauth_enable_refresh_tokens: Optional[bool] = None
|
|
94
|
+
oauth_enable_single_use_refresh_tokens: Optional[bool] = None
|
|
95
|
+
client_store_temporary_credential: Optional[bool] = None
|
|
86
96
|
|
|
87
97
|
_other_settings: dict = field(default_factory=lambda: {})
|
|
88
98
|
|
snowflake/cli/api/connections.py
CHANGED
|
@@ -55,6 +55,16 @@ class ConnectionContext:
|
|
|
55
55
|
session_token: Optional[str] = None
|
|
56
56
|
master_token: Optional[str] = None
|
|
57
57
|
token_file_path: Optional[Path] = None
|
|
58
|
+
oauth_client_id: Optional[str] = None
|
|
59
|
+
oauth_client_secret: Optional[str] = None
|
|
60
|
+
oauth_authorization_url: Optional[str] = None
|
|
61
|
+
oauth_token_request_url: Optional[str] = None
|
|
62
|
+
oauth_redirect_uri: Optional[str] = None
|
|
63
|
+
oauth_scope: Optional[str] = None
|
|
64
|
+
oauth_disable_pkce: Optional[bool] = None
|
|
65
|
+
oauth_enable_refresh_tokens: Optional[bool] = None
|
|
66
|
+
oauth_enable_single_use_refresh_tokens: Optional[bool] = None
|
|
67
|
+
client_store_temporary_credential: Optional[bool] = None
|
|
58
68
|
|
|
59
69
|
VALIDATED_FIELD_NAMES = ["schema"]
|
|
60
70
|
|
snowflake/cli/api/console/abc.py
CHANGED
|
@@ -19,7 +19,7 @@ from contextlib import contextmanager
|
|
|
19
19
|
from typing import Callable, Iterator, Optional
|
|
20
20
|
|
|
21
21
|
from rich import print as rich_print
|
|
22
|
-
from rich.
|
|
22
|
+
from rich.jupyter import JupyterMixin
|
|
23
23
|
from snowflake.cli.api.cli_global_context import (
|
|
24
24
|
_CliGlobalContextAccess,
|
|
25
25
|
get_cli_context,
|
|
@@ -57,7 +57,7 @@ class AbstractConsole(ABC):
|
|
|
57
57
|
"""Indicated whether output should be grouped."""
|
|
58
58
|
return self._in_phase
|
|
59
59
|
|
|
60
|
-
def _print(self, text:
|
|
60
|
+
def _print(self, text: JupyterMixin):
|
|
61
61
|
if self.is_silent:
|
|
62
62
|
return
|
|
63
63
|
rich_print(text)
|
|
@@ -92,3 +92,9 @@ class AbstractConsole(ABC):
|
|
|
92
92
|
"""Displays message in a style that makes it visually stand out from other output.
|
|
93
93
|
|
|
94
94
|
Intended for displaying messages related to important messages."""
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def panel(self, message: str):
|
|
98
|
+
"""Displays message in a panel that makes it visually stand out from other output.
|
|
99
|
+
|
|
100
|
+
Intended for displaying visually separated messages."""
|
|
@@ -17,14 +17,23 @@ from __future__ import annotations
|
|
|
17
17
|
from contextlib import contextmanager
|
|
18
18
|
from typing import Optional
|
|
19
19
|
|
|
20
|
+
from rich import get_console
|
|
21
|
+
from rich.panel import Panel
|
|
20
22
|
from rich.style import Style
|
|
21
23
|
from rich.text import Text
|
|
22
24
|
from snowflake.cli.api.console.abc import AbstractConsole
|
|
23
25
|
from snowflake.cli.api.console.enum import Output
|
|
24
26
|
|
|
27
|
+
# ensure we do not break URLs that wrap lines
|
|
28
|
+
get_console().soft_wrap = True
|
|
29
|
+
|
|
30
|
+
# Disable markup to avoid escaping errors
|
|
31
|
+
get_console()._markup = False # noqa: SLF001
|
|
32
|
+
|
|
25
33
|
PHASE_STYLE: Style = Style(bold=True)
|
|
26
34
|
STEP_STYLE: Style = Style(italic=True)
|
|
27
35
|
INFO_STYLE: Style = Style()
|
|
36
|
+
PANEL_STYLE: Style = Style()
|
|
28
37
|
IMPORTANT_STYLE: Style = Style(bold=True, italic=True)
|
|
29
38
|
INDENTATION_LEVEL: int = 2
|
|
30
39
|
|
|
@@ -47,6 +56,7 @@ class CliConsole(AbstractConsole):
|
|
|
47
56
|
Output.STEP: STEP_STYLE,
|
|
48
57
|
Output.INFO: None,
|
|
49
58
|
Output.IMPORTANT: IMPORTANT_STYLE,
|
|
59
|
+
Output.PANEL: PANEL_STYLE,
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
def _format_message(self, message: str, output: Output) -> Text:
|
|
@@ -110,6 +120,12 @@ class CliConsole(AbstractConsole):
|
|
|
110
120
|
text = self._format_message(message, Output.IMPORTANT)
|
|
111
121
|
self._print(text)
|
|
112
122
|
|
|
123
|
+
def panel(self, message: str):
|
|
124
|
+
"""Displays a message in a panel."""
|
|
125
|
+
style = self._styles.get(Output.PANEL, Style())
|
|
126
|
+
panel = Panel(message, style=style)
|
|
127
|
+
self._print(panel)
|
|
128
|
+
|
|
113
129
|
|
|
114
130
|
def get_cli_console() -> AbstractConsole:
|
|
115
131
|
console = CliConsole()
|