snowflake-cli-labs 2.3.1__py3-none-any.whl → 2.4.0rc1__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/api/__init__.py +2 -0
- snowflake/cli/api/cli_global_context.py +8 -1
- snowflake/cli/api/commands/decorators.py +2 -2
- snowflake/cli/api/commands/flags.py +49 -4
- snowflake/cli/api/commands/snow_typer.py +2 -0
- snowflake/cli/api/console/abc.py +2 -0
- snowflake/cli/api/console/console.py +6 -5
- snowflake/cli/api/constants.py +5 -0
- snowflake/cli/api/exceptions.py +12 -0
- snowflake/cli/api/identifiers.py +123 -0
- snowflake/cli/api/plugins/command/__init__.py +2 -0
- snowflake/cli/api/plugins/plugin_config.py +2 -0
- snowflake/cli/api/project/definition.py +2 -0
- snowflake/cli/api/project/errors.py +3 -3
- snowflake/cli/api/project/schemas/identifier_model.py +35 -0
- snowflake/cli/api/project/schemas/native_app/native_app.py +4 -0
- snowflake/cli/api/project/schemas/native_app/path_mapping.py +21 -3
- snowflake/cli/api/project/schemas/project_definition.py +58 -6
- snowflake/cli/api/project/schemas/snowpark/argument.py +2 -0
- snowflake/cli/api/project/schemas/snowpark/callable.py +8 -17
- snowflake/cli/api/project/schemas/streamlit/streamlit.py +2 -2
- snowflake/cli/api/project/schemas/updatable_model.py +2 -0
- snowflake/cli/api/project/util.py +2 -0
- snowflake/cli/api/secure_path.py +2 -0
- snowflake/cli/api/sql_execution.py +14 -54
- snowflake/cli/api/utils/cursor.py +2 -0
- snowflake/cli/api/utils/models.py +23 -0
- snowflake/cli/api/utils/naming_utils.py +0 -27
- snowflake/cli/api/utils/rendering.py +178 -23
- snowflake/cli/app/api_impl/plugin/plugin_config_provider_impl.py +2 -0
- snowflake/cli/app/cli_app.py +4 -1
- snowflake/cli/app/commands_registration/builtin_plugins.py +8 -0
- snowflake/cli/app/commands_registration/command_plugins_loader.py +2 -0
- snowflake/cli/app/commands_registration/commands_registration_with_callbacks.py +2 -0
- snowflake/cli/app/commands_registration/typer_registration.py +2 -0
- snowflake/cli/app/dev/pycharm_remote_debug.py +2 -0
- snowflake/cli/app/loggers.py +2 -0
- snowflake/cli/app/main_typer.py +1 -1
- snowflake/cli/app/printing.py +3 -1
- snowflake/cli/app/snow_connector.py +2 -2
- snowflake/cli/plugins/connection/commands.py +5 -14
- snowflake/cli/plugins/connection/util.py +1 -1
- snowflake/cli/plugins/cortex/__init__.py +0 -0
- snowflake/cli/plugins/cortex/commands.py +312 -0
- snowflake/cli/plugins/cortex/constants.py +3 -0
- snowflake/cli/plugins/cortex/manager.py +175 -0
- snowflake/cli/plugins/cortex/plugin_spec.py +16 -0
- snowflake/cli/plugins/cortex/types.py +8 -0
- snowflake/cli/plugins/git/commands.py +15 -0
- snowflake/cli/plugins/nativeapp/artifacts.py +368 -123
- snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +45 -0
- snowflake/cli/plugins/nativeapp/codegen/compiler.py +104 -0
- snowflake/cli/plugins/nativeapp/codegen/sandbox.py +2 -0
- snowflake/cli/plugins/nativeapp/codegen/snowpark/callback_source.py.jinja +181 -0
- snowflake/cli/plugins/nativeapp/codegen/snowpark/extension_function_utils.py +196 -0
- snowflake/cli/plugins/nativeapp/codegen/snowpark/models.py +47 -0
- snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +489 -0
- snowflake/cli/plugins/nativeapp/commands.py +11 -4
- snowflake/cli/plugins/nativeapp/common_flags.py +12 -5
- snowflake/cli/plugins/nativeapp/manager.py +49 -16
- snowflake/cli/plugins/nativeapp/policy.py +2 -0
- snowflake/cli/plugins/nativeapp/run_processor.py +2 -0
- snowflake/cli/plugins/nativeapp/teardown_processor.py +80 -8
- snowflake/cli/plugins/nativeapp/utils.py +7 -6
- snowflake/cli/plugins/nativeapp/version/commands.py +6 -5
- snowflake/cli/plugins/nativeapp/version/version_processor.py +2 -0
- snowflake/cli/plugins/notebook/commands.py +21 -0
- snowflake/cli/plugins/notebook/exceptions.py +6 -0
- snowflake/cli/plugins/notebook/manager.py +46 -3
- snowflake/cli/plugins/notebook/types.py +2 -0
- snowflake/cli/plugins/object/command_aliases.py +80 -0
- snowflake/cli/plugins/object/commands.py +10 -6
- snowflake/cli/plugins/object/common.py +2 -0
- snowflake/cli/plugins/object_stage_deprecated/__init__.py +1 -0
- snowflake/cli/plugins/object_stage_deprecated/plugin_spec.py +20 -0
- snowflake/cli/plugins/snowpark/commands.py +62 -6
- snowflake/cli/plugins/snowpark/common.py +17 -6
- snowflake/cli/plugins/spcs/compute_pool/commands.py +22 -1
- snowflake/cli/plugins/spcs/compute_pool/manager.py +2 -0
- snowflake/cli/plugins/spcs/image_repository/commands.py +25 -1
- snowflake/cli/plugins/spcs/image_repository/manager.py +3 -1
- snowflake/cli/plugins/spcs/services/commands.py +39 -5
- snowflake/cli/plugins/spcs/services/manager.py +2 -0
- snowflake/cli/plugins/sql/commands.py +13 -5
- snowflake/cli/plugins/sql/manager.py +40 -19
- snowflake/cli/plugins/stage/commands.py +29 -3
- snowflake/cli/plugins/stage/diff.py +2 -0
- snowflake/cli/plugins/streamlit/commands.py +26 -10
- snowflake/cli/plugins/streamlit/manager.py +9 -10
- {snowflake_cli_labs-2.3.1.dist-info → snowflake_cli_labs-2.4.0rc1.dist-info}/METADATA +4 -2
- {snowflake_cli_labs-2.3.1.dist-info → snowflake_cli_labs-2.4.0rc1.dist-info}/RECORD +96 -76
- /snowflake/cli/plugins/{object/stage_deprecated → object_stage_deprecated}/commands.py +0 -0
- {snowflake_cli_labs-2.3.1.dist-info → snowflake_cli_labs-2.4.0rc1.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-2.3.1.dist-info → snowflake_cli_labs-2.4.0rc1.dist-info}/entry_points.txt +0 -0
- {snowflake_cli_labs-2.3.1.dist-info → snowflake_cli_labs-2.4.0rc1.dist-info}/licenses/LICENSE +0 -0
|
@@ -14,12 +14,12 @@ from snowflake.cli.api.exceptions import (
|
|
|
14
14
|
SchemaNotProvidedError,
|
|
15
15
|
SnowflakeSQLExecutionError,
|
|
16
16
|
)
|
|
17
|
+
from snowflake.cli.api.identifiers import FQN
|
|
17
18
|
from snowflake.cli.api.project.util import (
|
|
18
19
|
identifier_to_show_like_pattern,
|
|
19
20
|
unquote_identifier,
|
|
20
21
|
)
|
|
21
22
|
from snowflake.cli.api.utils.cursor import find_first_row
|
|
22
|
-
from snowflake.cli.api.utils.naming_utils import from_qualified_name
|
|
23
23
|
from snowflake.connector.cursor import DictCursor, SnowflakeCursor
|
|
24
24
|
from snowflake.connector.errors import ProgrammingError
|
|
25
25
|
|
|
@@ -128,56 +128,22 @@ class SqlExecutionMixin:
|
|
|
128
128
|
"""
|
|
129
129
|
Checks if a database and schema are provided, either through the connection context or a qualified name.
|
|
130
130
|
"""
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
else:
|
|
134
|
-
schema, database = None, None
|
|
135
|
-
schema = schema or self._conn.schema
|
|
136
|
-
database = database or self._conn.database
|
|
137
|
-
if not database:
|
|
131
|
+
fqn = FQN.from_string(name).using_connection(self._conn)
|
|
132
|
+
if not fqn.database:
|
|
138
133
|
raise DatabaseNotProvidedError()
|
|
139
|
-
if not schema:
|
|
134
|
+
if not fqn.schema:
|
|
140
135
|
raise SchemaNotProvidedError()
|
|
141
136
|
|
|
142
|
-
def to_fully_qualified_name(
|
|
143
|
-
self, name: str, database: Optional[str] = None, schema: Optional[str] = None
|
|
144
|
-
):
|
|
145
|
-
current_parts = name.split(".")
|
|
146
|
-
if len(current_parts) == 3:
|
|
147
|
-
# already fully qualified name
|
|
148
|
-
return name.upper()
|
|
149
|
-
|
|
150
|
-
if not database:
|
|
151
|
-
if not self._conn.database:
|
|
152
|
-
raise DatabaseNotProvidedError()
|
|
153
|
-
database = self._conn.database
|
|
154
|
-
|
|
155
|
-
if len(current_parts) == 2:
|
|
156
|
-
# we assume name is in form of `schema.object`
|
|
157
|
-
return f"{database}.{name}".upper()
|
|
158
|
-
|
|
159
|
-
schema = schema or self._conn.schema or "public"
|
|
160
|
-
database = database or self._conn.database
|
|
161
|
-
return f"{database}.{schema}.{name}".upper()
|
|
162
|
-
|
|
163
|
-
@staticmethod
|
|
164
|
-
def get_name_from_fully_qualified_name(name):
|
|
165
|
-
"""
|
|
166
|
-
Returns name of the object from the fully-qualified name.
|
|
167
|
-
Assumes that [name] is in format [[database.]schema.]name
|
|
168
|
-
"""
|
|
169
|
-
return from_qualified_name(name)[0]
|
|
170
|
-
|
|
171
137
|
@staticmethod
|
|
172
|
-
def _qualified_name_to_in_clause(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
in_clause = f"in schema {database}.{schema}"
|
|
176
|
-
elif schema:
|
|
177
|
-
in_clause = f"in schema {schema}"
|
|
138
|
+
def _qualified_name_to_in_clause(identifier: FQN) -> Tuple[str, Optional[str]]:
|
|
139
|
+
if identifier.database:
|
|
140
|
+
schema = identifier.schema or "PUBLIC"
|
|
141
|
+
in_clause = f"in schema {identifier.database}.{schema}"
|
|
142
|
+
elif identifier.schema:
|
|
143
|
+
in_clause = f"in schema {identifier.schema}"
|
|
178
144
|
else:
|
|
179
145
|
in_clause = None
|
|
180
|
-
return
|
|
146
|
+
return identifier.name, in_clause
|
|
181
147
|
|
|
182
148
|
class InClauseWithQualifiedNameError(ValueError):
|
|
183
149
|
def __init__(self):
|
|
@@ -202,7 +168,9 @@ class SqlExecutionMixin:
|
|
|
202
168
|
the connection or a qualified name, before executing the query.
|
|
203
169
|
"""
|
|
204
170
|
|
|
205
|
-
unqualified_name, name_in_clause = self._qualified_name_to_in_clause(
|
|
171
|
+
unqualified_name, name_in_clause = self._qualified_name_to_in_clause(
|
|
172
|
+
FQN.from_string(name)
|
|
173
|
+
)
|
|
206
174
|
if in_clause and name_in_clause:
|
|
207
175
|
raise self.InClauseWithQualifiedNameError()
|
|
208
176
|
elif name_in_clause:
|
|
@@ -230,11 +198,3 @@ class SqlExecutionMixin:
|
|
|
230
198
|
lambda row: row[name_col] == unquote_identifier(unqualified_name),
|
|
231
199
|
)
|
|
232
200
|
return show_obj_row
|
|
233
|
-
|
|
234
|
-
def qualified_name_for_url(
|
|
235
|
-
self, object_name: str, database: str | None = None, schema: str | None = None
|
|
236
|
-
):
|
|
237
|
-
fqn = self.to_fully_qualified_name(
|
|
238
|
-
object_name, database=database, schema=schema
|
|
239
|
-
)
|
|
240
|
-
return ".".join(unquote_identifier(part) for part in fqn.split("."))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EnvironWithDefinedDictFallback(Dict):
|
|
8
|
+
def __getattr__(self, item):
|
|
9
|
+
try:
|
|
10
|
+
return self[item]
|
|
11
|
+
except KeyError as e:
|
|
12
|
+
raise AttributeError(e)
|
|
13
|
+
|
|
14
|
+
def __getitem__(self, item):
|
|
15
|
+
if item in os.environ:
|
|
16
|
+
return os.environ[item]
|
|
17
|
+
return super().__getitem__(item)
|
|
18
|
+
|
|
19
|
+
def __contains__(self, item):
|
|
20
|
+
return item in os.environ or super().__contains__(item)
|
|
21
|
+
|
|
22
|
+
def update_from_dict(self, update_values: Dict[str, Any]):
|
|
23
|
+
return super().update(update_values)
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from typing import Optional, Tuple
|
|
3
|
-
|
|
4
|
-
from snowflake.cli.api.project.util import (
|
|
5
|
-
VALID_IDENTIFIER_REGEX,
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def from_qualified_name(name: str) -> Tuple[str, Optional[str], Optional[str]]:
|
|
10
|
-
"""
|
|
11
|
-
Takes in an object name in the form [[database.]schema.]name. Returns a tuple (name, [schema], [database])
|
|
12
|
-
"""
|
|
13
|
-
# TODO: Use regex to match object name to a valid identifier or valid identifier (args). Second case is for sprocs and UDFs
|
|
14
|
-
qualifier_pattern = rf"(?:(?P<first_qualifier>{VALID_IDENTIFIER_REGEX})\.)?(?:(?P<second_qualifier>{VALID_IDENTIFIER_REGEX})\.)?(?P<name>.*)"
|
|
15
|
-
result = re.fullmatch(qualifier_pattern, name)
|
|
16
|
-
|
|
17
|
-
if result is None:
|
|
18
|
-
raise ValueError(f"'{name}' is not a valid qualified name")
|
|
19
|
-
|
|
20
|
-
unqualified_name = result.group("name")
|
|
21
|
-
if result.group("second_qualifier") is not None:
|
|
22
|
-
database = result.group("first_qualifier")
|
|
23
|
-
schema = result.group("second_qualifier")
|
|
24
|
-
else:
|
|
25
|
-
database = None
|
|
26
|
-
schema = result.group("first_qualifier")
|
|
27
|
-
return unqualified_name, schema, database
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import re
|
|
4
|
+
from collections import defaultdict, deque
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from textwrap import dedent
|
|
6
|
-
from typing import Dict, Optional
|
|
7
|
+
from typing import Dict, List, Optional, Set, cast
|
|
7
8
|
|
|
8
9
|
import jinja2
|
|
9
|
-
from
|
|
10
|
+
from click import ClickException
|
|
11
|
+
from jinja2 import Environment, StrictUndefined, UndefinedError, loaders
|
|
12
|
+
from snowflake.cli.api.cli_global_context import cli_context
|
|
13
|
+
from snowflake.cli.api.project.schemas.project_definition import (
|
|
14
|
+
ProjectDefinition,
|
|
15
|
+
)
|
|
10
16
|
from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
|
|
17
|
+
from snowflake.cli.api.utils.models import EnvironWithDefinedDictFallback
|
|
18
|
+
|
|
19
|
+
_CONTEXT_KEY = "ctx"
|
|
20
|
+
_YML_TEMPLATE_START = "<%"
|
|
21
|
+
_YML_TEMPLATE_END = "%>"
|
|
11
22
|
|
|
12
23
|
|
|
13
24
|
def read_file_content(file_name: str):
|
|
@@ -45,6 +56,21 @@ def _env_bootstrap(env: Environment) -> Environment:
|
|
|
45
56
|
|
|
46
57
|
|
|
47
58
|
def get_snowflake_cli_jinja_env():
|
|
59
|
+
_random_block = "___very___unique___block___to___disable___logic___blocks___"
|
|
60
|
+
return _env_bootstrap(
|
|
61
|
+
Environment(
|
|
62
|
+
loader=loaders.BaseLoader(),
|
|
63
|
+
keep_trailing_newline=True,
|
|
64
|
+
variable_start_string=_YML_TEMPLATE_START,
|
|
65
|
+
variable_end_string=_YML_TEMPLATE_END,
|
|
66
|
+
block_start_string=_random_block,
|
|
67
|
+
block_end_string=_random_block,
|
|
68
|
+
undefined=StrictUndefined,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_sql_cli_jinja_env():
|
|
48
74
|
_random_block = "___very___unique___block___to___disable___logic___blocks___"
|
|
49
75
|
return _env_bootstrap(
|
|
50
76
|
Environment(
|
|
@@ -61,9 +87,9 @@ def get_snowflake_cli_jinja_env():
|
|
|
61
87
|
|
|
62
88
|
def jinja_render_from_file(
|
|
63
89
|
template_path: Path, data: Dict, output_file_path: Optional[Path] = None
|
|
64
|
-
):
|
|
90
|
+
) -> Optional[str]:
|
|
65
91
|
"""
|
|
66
|
-
|
|
92
|
+
Renders a jinja template and outputs either the rendered contents as string or writes to a file.
|
|
67
93
|
|
|
68
94
|
Args:
|
|
69
95
|
template_path (Path): Path to the template
|
|
@@ -71,7 +97,7 @@ def jinja_render_from_file(
|
|
|
71
97
|
output_file_path (Optional[Path]): If provided then rendered template will be written to this file
|
|
72
98
|
|
|
73
99
|
Returns:
|
|
74
|
-
None
|
|
100
|
+
None if file path is provided, else returns the rendered string.
|
|
75
101
|
"""
|
|
76
102
|
env = _env_bootstrap(
|
|
77
103
|
Environment(
|
|
@@ -84,28 +110,157 @@ def jinja_render_from_file(
|
|
|
84
110
|
rendered_result = loaded_template.render(**data)
|
|
85
111
|
if output_file_path:
|
|
86
112
|
SecurePath(output_file_path).write_text(rendered_result)
|
|
113
|
+
return None
|
|
87
114
|
else:
|
|
88
|
-
|
|
115
|
+
return rendered_result
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _add_project_context(project_definition: ProjectDefinition) -> Dict:
|
|
119
|
+
"""
|
|
120
|
+
Updates the external data with variables from snowflake.yml definition file.
|
|
121
|
+
"""
|
|
122
|
+
context_data = _resolve_variables_in_project(project_definition)
|
|
123
|
+
return context_data
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _remove_ctx_env_prefix(text: str) -> str:
|
|
127
|
+
prefix = "ctx.env."
|
|
128
|
+
if text.startswith(prefix):
|
|
129
|
+
return text[len(prefix) :]
|
|
130
|
+
return text
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def string_includes_template(text: str) -> bool:
|
|
134
|
+
return bool(re.search(rf"{_YML_TEMPLATE_START}.+{_YML_TEMPLATE_END}", text))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _resolve_variables_in_project(project_definition: ProjectDefinition):
|
|
138
|
+
# If there's project definition file then resolve variables from it
|
|
139
|
+
if not project_definition or not project_definition.meets_version_requirement(
|
|
140
|
+
"1.1"
|
|
141
|
+
):
|
|
142
|
+
return {_CONTEXT_KEY: {"env": EnvironWithDefinedDictFallback({})}}
|
|
143
|
+
|
|
144
|
+
variables_data: EnvironWithDefinedDictFallback = cast(
|
|
145
|
+
EnvironWithDefinedDictFallback, project_definition.env
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
env_with_unresolved_keys: List[str] = _get_variables_with_dependencies(
|
|
149
|
+
variables_data
|
|
150
|
+
)
|
|
151
|
+
other_env = set(variables_data) - set(env_with_unresolved_keys)
|
|
152
|
+
env = get_snowflake_cli_jinja_env()
|
|
153
|
+
context_data = {_CONTEXT_KEY: project_definition}
|
|
154
|
+
|
|
155
|
+
# Resolve env section dependencies
|
|
156
|
+
while env_with_unresolved_keys:
|
|
157
|
+
key = env_with_unresolved_keys.pop()
|
|
158
|
+
value = variables_data[key]
|
|
159
|
+
if not isinstance(value, str):
|
|
160
|
+
continue
|
|
161
|
+
try: # try to evaluate the template given current state of know variables
|
|
162
|
+
variables_data[key] = env.from_string(value).render(context_data)
|
|
163
|
+
if string_includes_template(variables_data[key]):
|
|
164
|
+
env_with_unresolved_keys.append(key)
|
|
165
|
+
except UndefinedError:
|
|
166
|
+
env_with_unresolved_keys.append(key)
|
|
167
|
+
|
|
168
|
+
# Resolve templates in variables without references, for example
|
|
169
|
+
for key in other_env:
|
|
170
|
+
variable_value = variables_data[key]
|
|
171
|
+
if not isinstance(variable_value, str):
|
|
172
|
+
continue
|
|
173
|
+
variables_data[key] = env.from_string(variable_value).render(context_data)
|
|
174
|
+
|
|
175
|
+
return context_data
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _check_for_cycles(nodes: defaultdict):
|
|
179
|
+
nodes = nodes.copy()
|
|
180
|
+
for key in list(nodes):
|
|
181
|
+
q = deque([key])
|
|
182
|
+
visited: List[str] = []
|
|
183
|
+
while q:
|
|
184
|
+
curr = q.popleft()
|
|
185
|
+
if curr in visited:
|
|
186
|
+
raise ClickException(
|
|
187
|
+
"Cycle detected between variables: {}".format(" -> ".join(visited))
|
|
188
|
+
)
|
|
189
|
+
# Only nodes that have references can cause cycles
|
|
190
|
+
if curr in nodes:
|
|
191
|
+
visited.append(curr)
|
|
192
|
+
q.extendleft(nodes[curr])
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _get_variables_with_dependencies(variables_data: EnvironWithDefinedDictFallback):
|
|
196
|
+
"""
|
|
197
|
+
Checks consistency of provided dictionary by
|
|
198
|
+
1. checking reference cycles
|
|
199
|
+
2. checking for missing variables
|
|
200
|
+
"""
|
|
201
|
+
# Variables that are not specified in env section
|
|
202
|
+
missing_variables: Set[str] = set()
|
|
203
|
+
# Variables that require other variables
|
|
204
|
+
variables_with_dependencies = defaultdict(list)
|
|
89
205
|
|
|
206
|
+
for key, value in variables_data.items():
|
|
207
|
+
# Templates are reserved only to string variables
|
|
208
|
+
if not isinstance(value, str):
|
|
209
|
+
continue
|
|
90
210
|
|
|
91
|
-
|
|
92
|
-
def __init__(self, data_dict):
|
|
93
|
-
self._data_dict = data_dict
|
|
211
|
+
required_variables = _search_for_required_variables(value)
|
|
94
212
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
raise AttributeError(f"No attribute {item}")
|
|
98
|
-
return self._data_dict[item]
|
|
213
|
+
if required_variables:
|
|
214
|
+
variables_with_dependencies[key] = required_variables
|
|
99
215
|
|
|
216
|
+
for variable in required_variables:
|
|
217
|
+
if variable not in variables_data:
|
|
218
|
+
missing_variables.add(variable)
|
|
100
219
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
220
|
+
# If there are unknown env variables then we raise an error
|
|
221
|
+
if missing_variables:
|
|
222
|
+
raise ClickException(
|
|
223
|
+
"The following variables are used in environment definition but are not defined: {}".format(
|
|
224
|
+
", ".join(missing_variables)
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Look for cycles between variables
|
|
229
|
+
_check_for_cycles(variables_with_dependencies)
|
|
230
|
+
|
|
231
|
+
# Sort by number of dependencies
|
|
232
|
+
return sorted(
|
|
233
|
+
list(variables_with_dependencies.keys()),
|
|
234
|
+
key=lambda k: len(variables_with_dependencies[k]),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _search_for_required_variables(variable_value: str):
|
|
239
|
+
"""
|
|
240
|
+
Look for pattern in variable value. Returns a list of env variables required
|
|
241
|
+
to expand this template.`
|
|
242
|
+
"""
|
|
243
|
+
ctx_env_prefix = f"{_CONTEXT_KEY}.env."
|
|
244
|
+
found_variables = re.findall(
|
|
245
|
+
rf"({_YML_TEMPLATE_START}([\.\w ]+){_YML_TEMPLATE_END})+", variable_value
|
|
246
|
+
)
|
|
247
|
+
required_variables = []
|
|
248
|
+
for _, variable in found_variables:
|
|
249
|
+
var: str = variable.strip()
|
|
250
|
+
if var.startswith(ctx_env_prefix):
|
|
251
|
+
required_variables.append(var[len(ctx_env_prefix) :])
|
|
252
|
+
return required_variables
|
|
107
253
|
|
|
108
254
|
|
|
109
|
-
def
|
|
110
|
-
data =
|
|
111
|
-
|
|
255
|
+
def snowflake_sql_jinja_render(content: str, data: Dict | None = None) -> str:
|
|
256
|
+
data = data or {}
|
|
257
|
+
if _CONTEXT_KEY in data:
|
|
258
|
+
raise ClickException(
|
|
259
|
+
f"{_CONTEXT_KEY} in user defined data. The `{_CONTEXT_KEY}` variable is reserved for CLI usage."
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
context_data = _add_project_context(
|
|
263
|
+
project_definition=cli_context.project_definition
|
|
264
|
+
)
|
|
265
|
+
context_data.update(data)
|
|
266
|
+
return get_sql_cli_jinja_env().from_string(content).render(**context_data)
|
snowflake/cli/app/cli_app.py
CHANGED
|
@@ -132,8 +132,9 @@ def _info_callback(value: bool):
|
|
|
132
132
|
def app_factory() -> SnowCliMainTyper:
|
|
133
133
|
app = SnowCliMainTyper()
|
|
134
134
|
|
|
135
|
-
@app.callback()
|
|
135
|
+
@app.callback(invoke_without_command=True)
|
|
136
136
|
def default(
|
|
137
|
+
ctx: typer.Context,
|
|
137
138
|
version: bool = typer.Option(
|
|
138
139
|
None,
|
|
139
140
|
"--version",
|
|
@@ -217,6 +218,8 @@ def app_factory() -> SnowCliMainTyper:
|
|
|
217
218
|
"""
|
|
218
219
|
Snowflake CLI tool for developers.
|
|
219
220
|
"""
|
|
221
|
+
if not ctx.invoked_subcommand:
|
|
222
|
+
typer.echo(ctx.get_help())
|
|
220
223
|
setup_pycharm_remote_debugger_if_provided(
|
|
221
224
|
pycharm_debug_library_path=pycharm_debug_library_path,
|
|
222
225
|
pycharm_debug_server_host=pycharm_debug_server_host,
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
from snowflake.cli.plugins.connection import plugin_spec as connection_plugin_spec
|
|
2
|
+
from snowflake.cli.plugins.cortex import plugin_spec as cortex_plugin_spec
|
|
2
3
|
from snowflake.cli.plugins.git import plugin_spec as git_plugin_spec
|
|
3
4
|
from snowflake.cli.plugins.nativeapp import plugin_spec as nativeapp_plugin_spec
|
|
4
5
|
from snowflake.cli.plugins.notebook import plugin_spec as notebook_plugin_spec
|
|
5
6
|
from snowflake.cli.plugins.object import plugin_spec as object_plugin_spec
|
|
7
|
+
|
|
8
|
+
# TODO 3.0: remove this import
|
|
9
|
+
from snowflake.cli.plugins.object_stage_deprecated import (
|
|
10
|
+
plugin_spec as object_stage_deprecated_plugin_spec,
|
|
11
|
+
)
|
|
6
12
|
from snowflake.cli.plugins.snowpark import plugin_spec as snowpark_plugin_spec
|
|
7
13
|
from snowflake.cli.plugins.spcs import plugin_spec as spcs_plugin_spec
|
|
8
14
|
from snowflake.cli.plugins.sql import plugin_spec as sql_plugin_spec
|
|
@@ -23,6 +29,8 @@ def get_builtin_plugin_name_to_plugin_spec():
|
|
|
23
29
|
"streamlit": streamlit_plugin_spec,
|
|
24
30
|
"git": git_plugin_spec,
|
|
25
31
|
"notebook": notebook_plugin_spec,
|
|
32
|
+
"object-stage-deprecated": object_stage_deprecated_plugin_spec,
|
|
33
|
+
"cortex": cortex_plugin_spec,
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
return plugin_specs
|
snowflake/cli/app/loggers.py
CHANGED
snowflake/cli/app/main_typer.py
CHANGED
snowflake/cli/app/printing.py
CHANGED
|
@@ -84,7 +84,9 @@ def print_structured(result: CommandResult):
|
|
|
84
84
|
if isinstance(result, MultipleResults):
|
|
85
85
|
_stream_json(result)
|
|
86
86
|
else:
|
|
87
|
-
|
|
87
|
+
json.dump(result, sys.stdout, cls=CustomJSONEncoder, indent=4)
|
|
88
|
+
# Adds empty line at the end
|
|
89
|
+
print()
|
|
88
90
|
|
|
89
91
|
|
|
90
92
|
def _stream_json(result):
|
|
@@ -157,8 +157,8 @@ def _update_connection_application_name(connection_parameters: Dict):
|
|
|
157
157
|
"""Update version and name of app handling connection."""
|
|
158
158
|
connection_application_params = {
|
|
159
159
|
"application_name": PARAM_APPLICATION_NAME,
|
|
160
|
-
"
|
|
161
|
-
"
|
|
160
|
+
"internal_application_name": PARAM_INTERNAL_APPLICATION_NAME,
|
|
161
|
+
"internal_application_version": VERSION,
|
|
162
162
|
}
|
|
163
163
|
connection_parameters.update(connection_application_params)
|
|
164
164
|
|
|
@@ -9,11 +9,6 @@ from click.types import StringParamType
|
|
|
9
9
|
from snowflake.cli.api.cli_global_context import cli_context
|
|
10
10
|
from snowflake.cli.api.commands.flags import (
|
|
11
11
|
PLAIN_PASSWORD_MSG,
|
|
12
|
-
ConnectionOption,
|
|
13
|
-
DiagAllowlistPathOption,
|
|
14
|
-
DiagLogPathOption,
|
|
15
|
-
EnableDiagOption,
|
|
16
|
-
MfaPasscodeOption,
|
|
17
12
|
)
|
|
18
13
|
from snowflake.cli.api.commands.snow_typer import SnowTyper
|
|
19
14
|
from snowflake.cli.api.config import (
|
|
@@ -237,13 +232,8 @@ def add(
|
|
|
237
232
|
)
|
|
238
233
|
|
|
239
234
|
|
|
240
|
-
@app.command(requires_connection=
|
|
235
|
+
@app.command(requires_connection=True)
|
|
241
236
|
def test(
|
|
242
|
-
connection: str = ConnectionOption,
|
|
243
|
-
mfa_passcode: str = MfaPasscodeOption,
|
|
244
|
-
enable_diag: bool = EnableDiagOption,
|
|
245
|
-
diag_log_path: str = DiagLogPathOption,
|
|
246
|
-
diag_allowlist_path: str = DiagAllowlistPathOption,
|
|
247
237
|
**options,
|
|
248
238
|
) -> CommandResult:
|
|
249
239
|
"""
|
|
@@ -272,8 +262,9 @@ def test(
|
|
|
272
262
|
except ProgrammingError as err:
|
|
273
263
|
raise ClickException(str(err))
|
|
274
264
|
|
|
265
|
+
conn_ctx = cli_context.connection_context
|
|
275
266
|
result = {
|
|
276
|
-
"Connection name":
|
|
267
|
+
"Connection name": conn_ctx.connection_name,
|
|
277
268
|
"Status": "OK",
|
|
278
269
|
"Host": conn.host,
|
|
279
270
|
"Account": conn.account,
|
|
@@ -283,10 +274,10 @@ def test(
|
|
|
283
274
|
"Warehouse": f'{conn.warehouse or "not set"}',
|
|
284
275
|
}
|
|
285
276
|
|
|
286
|
-
if enable_diag:
|
|
277
|
+
if conn_ctx.enable_diag:
|
|
287
278
|
result[
|
|
288
279
|
"Diag Report Location"
|
|
289
|
-
] = f"{diag_log_path}/SnowflakeConnectionTestReport.txt"
|
|
280
|
+
] = f"{conn_ctx.diag_log_path}/SnowflakeConnectionTestReport.txt"
|
|
290
281
|
|
|
291
282
|
return ObjectResult(result)
|
|
292
283
|
|
|
@@ -37,7 +37,7 @@ def is_regionless_redirect(conn: SnowflakeConnection) -> bool:
|
|
|
37
37
|
return cursor.fetchone()["REGIONLESS"].lower() == "true"
|
|
38
38
|
except:
|
|
39
39
|
# by default, assume that
|
|
40
|
-
log.
|
|
40
|
+
log.warning("Cannot determine regionless redirect; assuming True.")
|
|
41
41
|
return True
|
|
42
42
|
|
|
43
43
|
|
|
File without changes
|