snowflake-cli 2.8.2__py3-none-any.whl → 3.0.2__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 → _app}/__main__.py +1 -1
- snowflake/cli/{app → _app}/cli_app.py +22 -13
- snowflake/cli/{app → _app}/commands_registration/builtin_plugins.py +15 -19
- snowflake/cli/{app → _app}/commands_registration/command_plugins_loader.py +9 -9
- snowflake/cli/{app → _app}/commands_registration/commands_registration_with_callbacks.py +4 -4
- snowflake/cli/{app → _app}/commands_registration/exception_logging.py +2 -2
- snowflake/cli/{app → _app}/commands_registration/typer_registration.py +2 -2
- snowflake/cli/{app → _app}/dev/docs/commands_docs_generator.py +30 -12
- snowflake/cli/{app → _app}/dev/docs/generator.py +3 -3
- snowflake/cli/{app → _app}/dev/docs/project_definition_docs_generator.py +4 -4
- snowflake/cli/{app → _app}/dev/docs/templates/usage.rst.jinja2 +14 -4
- snowflake/cli/{app → _app}/main_typer.py +2 -2
- snowflake/cli/{app → _app}/printing.py +2 -2
- snowflake/cli/_app/secret.py +9 -0
- snowflake/cli/{app → _app}/snow_connector.py +127 -61
- snowflake/cli/{app → _app}/telemetry.py +38 -7
- snowflake/cli/_app/version_check.py +74 -0
- snowflake/cli/{plugins → _plugins}/connection/commands.py +34 -11
- snowflake/cli/_plugins/connection/plugin_spec.py +30 -0
- snowflake/cli/{plugins → _plugins}/connection/util.py +16 -0
- snowflake/cli/{plugins → _plugins}/cortex/commands.py +54 -49
- snowflake/cli/{plugins → _plugins}/cortex/constants.py +1 -1
- snowflake/cli/{plugins → _plugins}/cortex/manager.py +5 -5
- snowflake/cli/{plugins → _plugins}/cortex/plugin_spec.py +1 -1
- snowflake/cli/{plugins → _plugins}/git/commands.py +11 -7
- snowflake/cli/{plugins → _plugins}/git/manager.py +55 -9
- snowflake/cli/{plugins → _plugins}/git/plugin_spec.py +1 -1
- snowflake/cli/_plugins/helpers/commands.py +90 -0
- snowflake/cli/{plugins/notebook → _plugins/helpers}/plugin_spec.py +1 -1
- snowflake/cli/{plugins → _plugins}/init/commands.py +2 -2
- snowflake/cli/{plugins → _plugins}/init/plugin_spec.py +1 -1
- snowflake/cli/{plugins → _plugins}/nativeapp/artifacts.py +24 -9
- snowflake/cli/_plugins/nativeapp/bundle_context.py +31 -0
- snowflake/cli/{plugins → _plugins}/nativeapp/codegen/artifact_processor.py +4 -4
- snowflake/cli/{plugins → _plugins}/nativeapp/codegen/compiler.py +37 -18
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +249 -0
- snowflake/cli/{plugins → _plugins}/nativeapp/codegen/setup/setup_driver.py.source +5 -2
- snowflake/cli/{plugins → _plugins}/nativeapp/codegen/snowpark/extension_function_utils.py +5 -5
- snowflake/cli/{plugins → _plugins}/nativeapp/codegen/snowpark/models.py +1 -1
- snowflake/cli/{plugins → _plugins}/nativeapp/codegen/snowpark/python_processor.py +29 -34
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +114 -0
- snowflake/cli/{plugins → _plugins}/nativeapp/commands.py +252 -132
- snowflake/cli/{plugins → _plugins}/nativeapp/common_flags.py +1 -1
- snowflake/cli/_plugins/nativeapp/entities/application.py +878 -0
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +1392 -0
- snowflake/cli/{plugins → _plugins}/nativeapp/exceptions.py +3 -12
- snowflake/cli/_plugins/nativeapp/manager.py +415 -0
- snowflake/cli/{plugins/connection → _plugins/nativeapp}/plugin_spec.py +1 -1
- snowflake/cli/{plugins → _plugins}/nativeapp/policy.py +3 -0
- snowflake/cli/{plugins → _plugins}/nativeapp/project_model.py +36 -20
- snowflake/cli/_plugins/nativeapp/run_processor.py +184 -0
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +70 -0
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +70 -0
- snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +262 -0
- snowflake/cli/{plugins → _plugins}/nativeapp/version/commands.py +20 -49
- snowflake/cli/_plugins/nativeapp/version/version_processor.py +98 -0
- snowflake/cli/{plugins → _plugins}/notebook/commands.py +3 -2
- snowflake/cli/{plugins → _plugins}/notebook/manager.py +5 -5
- snowflake/cli/{plugins/nativeapp → _plugins/notebook}/plugin_spec.py +1 -1
- snowflake/cli/{plugins → _plugins}/object/command_aliases.py +4 -4
- snowflake/cli/{plugins → _plugins}/object/commands.py +4 -5
- snowflake/cli/{plugins → _plugins}/object/manager.py +36 -15
- snowflake/cli/{plugins → _plugins}/object/plugin_spec.py +1 -1
- snowflake/cli/_plugins/snowpark/commands.py +448 -0
- snowflake/cli/_plugins/snowpark/common.py +268 -0
- snowflake/cli/{plugins → _plugins}/snowpark/models.py +0 -7
- snowflake/cli/{plugins → _plugins}/snowpark/package/anaconda_packages.py +2 -36
- snowflake/cli/{plugins → _plugins}/snowpark/package/commands.py +13 -76
- snowflake/cli/{plugins → _plugins}/snowpark/package/manager.py +2 -2
- snowflake/cli/{plugins → _plugins}/snowpark/package_utils.py +32 -43
- snowflake/cli/_plugins/snowpark/plugin_spec.py +30 -0
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +29 -0
- snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +176 -0
- snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +109 -0
- snowflake/cli/{plugins → _plugins}/snowpark/snowpark_shared.py +0 -36
- snowflake/cli/{plugins → _plugins}/snowpark/zipper.py +16 -8
- snowflake/cli/{plugins → _plugins}/spcs/__init__.py +5 -7
- snowflake/cli/{plugins → _plugins}/spcs/compute_pool/commands.py +8 -8
- snowflake/cli/{plugins → _plugins}/spcs/compute_pool/manager.py +3 -3
- snowflake/cli/{plugins → _plugins}/spcs/image_registry/commands.py +3 -3
- snowflake/cli/{plugins → _plugins}/spcs/image_repository/commands.py +6 -6
- snowflake/cli/{plugins → _plugins}/spcs/image_repository/manager.py +1 -1
- snowflake/cli/{plugins → _plugins}/spcs/plugin_spec.py +1 -1
- snowflake/cli/{plugins → _plugins}/spcs/services/commands.py +44 -11
- snowflake/cli/{plugins → _plugins}/spcs/services/manager.py +43 -5
- snowflake/cli/{plugins → _plugins}/sql/commands.py +20 -17
- snowflake/cli/{plugins → _plugins}/sql/manager.py +1 -1
- snowflake/cli/{plugins → _plugins}/sql/plugin_spec.py +1 -1
- snowflake/cli/{plugins → _plugins}/stage/commands.py +15 -14
- snowflake/cli/{plugins → _plugins}/stage/diff.py +1 -47
- snowflake/cli/{plugins → _plugins}/stage/manager.py +12 -7
- snowflake/cli/{plugins → _plugins}/stage/plugin_spec.py +1 -1
- snowflake/cli/_plugins/stage/utils.py +54 -0
- snowflake/cli/{plugins → _plugins}/streamlit/commands.py +64 -48
- snowflake/cli/{plugins → _plugins}/streamlit/manager.py +67 -69
- snowflake/cli/_plugins/streamlit/plugin_spec.py +30 -0
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +12 -0
- snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +66 -0
- snowflake/cli/_plugins/workspace/action_context.py +18 -0
- snowflake/cli/_plugins/workspace/commands.py +306 -0
- snowflake/cli/_plugins/workspace/manager.py +74 -0
- snowflake/cli/_plugins/workspace/plugin_spec.py +30 -0
- snowflake/cli/api/cli_global_context.py +152 -295
- snowflake/cli/api/commands/common.py +25 -0
- snowflake/cli/api/commands/decorators.py +19 -4
- snowflake/cli/api/commands/experimental_behaviour.py +2 -3
- snowflake/cli/api/commands/flags.py +127 -228
- snowflake/cli/api/commands/overrideable_parameter.py +143 -0
- snowflake/cli/api/commands/snow_typer.py +21 -11
- snowflake/cli/api/commands/utils.py +18 -0
- snowflake/cli/api/config.py +44 -12
- snowflake/cli/api/connections.py +216 -0
- snowflake/cli/api/console/abc.py +8 -3
- snowflake/cli/api/constants.py +11 -0
- snowflake/cli/api/entities/common.py +56 -0
- snowflake/cli/api/entities/utils.py +370 -0
- snowflake/cli/api/errno.py +1 -0
- snowflake/cli/api/exceptions.py +31 -5
- snowflake/cli/api/feature_flags.py +0 -1
- snowflake/cli/api/identifiers.py +28 -5
- snowflake/cli/api/metrics.py +92 -0
- snowflake/cli/api/project/definition.py +48 -6
- snowflake/cli/api/project/definition_conversion.py +400 -0
- snowflake/cli/api/project/definition_manager.py +16 -5
- snowflake/cli/api/project/project_verification.py +3 -3
- snowflake/cli/api/project/schemas/entities/common.py +91 -16
- snowflake/cli/api/project/schemas/entities/entities.py +37 -6
- snowflake/cli/api/project/schemas/project_definition.py +180 -49
- snowflake/cli/api/project/schemas/updatable_model.py +11 -3
- snowflake/cli/api/project/schemas/v1/__init__.py +0 -0
- snowflake/cli/api/project/schemas/{identifier_model.py → v1/identifier_model.py} +3 -1
- snowflake/cli/api/project/schemas/v1/native_app/__init__.py +0 -0
- snowflake/cli/api/project/schemas/{native_app → v1/native_app}/application.py +8 -9
- snowflake/cli/api/project/schemas/{native_app → v1/native_app}/native_app.py +4 -4
- snowflake/cli/api/project/schemas/{native_app → v1/native_app}/package.py +7 -1
- snowflake/cli/api/project/schemas/v1/snowpark/__init__.py +0 -0
- snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/callable.py +2 -2
- snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/snowpark.py +2 -2
- snowflake/cli/api/project/schemas/v1/streamlit/__init__.py +0 -0
- snowflake/cli/api/project/schemas/{streamlit → v1/streamlit}/streamlit.py +2 -1
- snowflake/cli/api/project/util.py +23 -6
- snowflake/cli/api/rendering/jinja.py +14 -8
- snowflake/cli/api/rendering/project_definition_templates.py +5 -1
- snowflake/cli/api/rendering/sql_templates.py +56 -11
- snowflake/cli/api/rest_api.py +11 -5
- snowflake/cli/api/secure_path.py +16 -18
- snowflake/cli/api/secure_utils.py +90 -1
- snowflake/cli/api/sql_execution.py +43 -23
- snowflake/cli/api/utils/definition_rendering.py +45 -13
- {snowflake_cli-2.8.2.dist-info → snowflake_cli-3.0.2.dist-info}/METADATA +18 -18
- snowflake_cli-3.0.2.dist-info/RECORD +242 -0
- snowflake_cli-3.0.2.dist-info/entry_points.txt +2 -0
- snowflake/cli/api/commands/project_initialisation.py +0 -65
- snowflake/cli/api/commands/typer_pre_execute.py +0 -26
- snowflake/cli/api/project/schemas/entities/application_entity.py +0 -44
- snowflake/cli/api/project/schemas/entities/application_package_entity.py +0 -66
- snowflake/cli/app/build_and_push.sh +0 -8
- snowflake/cli/plugins/nativeapp/codegen/setup/native_app_setup_processor.py +0 -172
- snowflake/cli/plugins/nativeapp/init.py +0 -345
- snowflake/cli/plugins/nativeapp/manager.py +0 -823
- snowflake/cli/plugins/nativeapp/run_processor.py +0 -389
- snowflake/cli/plugins/nativeapp/teardown_processor.py +0 -301
- snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +0 -135
- snowflake/cli/plugins/nativeapp/version/version_processor.py +0 -362
- snowflake/cli/plugins/object_stage_deprecated/__init__.py +0 -15
- snowflake/cli/plugins/object_stage_deprecated/commands.py +0 -122
- snowflake/cli/plugins/object_stage_deprecated/plugin_spec.py +0 -32
- snowflake/cli/plugins/snowpark/commands.py +0 -546
- snowflake/cli/plugins/snowpark/common.py +0 -307
- snowflake/cli/plugins/snowpark/manager.py +0 -109
- snowflake/cli/plugins/snowpark/plugin_spec.py +0 -30
- snowflake/cli/plugins/snowpark/snowpark_package_paths.py +0 -65
- snowflake/cli/plugins/spcs/jobs/commands.py +0 -78
- snowflake/cli/plugins/spcs/jobs/manager.py +0 -53
- snowflake/cli/plugins/streamlit/__init__.py +0 -13
- snowflake/cli/plugins/streamlit/plugin_spec.py +0 -30
- snowflake/cli/plugins/workspace/__init__.py +0 -13
- snowflake/cli/plugins/workspace/commands.py +0 -35
- snowflake/cli/plugins/workspace/plugin_spec.py +0 -30
- snowflake/cli/templates/default_snowpark/.gitignore +0 -4
- snowflake/cli/templates/default_snowpark/app/common.py +0 -2
- snowflake/cli/templates/default_snowpark/app/functions.py +0 -15
- snowflake/cli/templates/default_snowpark/app/procedures.py +0 -22
- snowflake/cli/templates/default_snowpark/requirements.txt +0 -1
- snowflake/cli/templates/default_snowpark/snowflake.yml +0 -23
- snowflake/cli/templates/default_streamlit/.gitignore +0 -4
- snowflake/cli/templates/default_streamlit/common/hello.py +0 -2
- snowflake/cli/templates/default_streamlit/environment.yml +0 -6
- snowflake/cli/templates/default_streamlit/pages/my_page.py +0 -3
- snowflake/cli/templates/default_streamlit/snowflake.yml +0 -10
- snowflake/cli/templates/default_streamlit/streamlit_app.py +0 -4
- snowflake_cli-2.8.2.dist-info/RECORD +0 -240
- snowflake_cli-2.8.2.dist-info/entry_points.txt +0 -2
- /snowflake/cli/{app → _app}/__init__.py +0 -0
- /snowflake/cli/{api/project/schemas/native_app → _app/api_impl}/__init__.py +0 -0
- /snowflake/cli/{api/project/schemas/snowpark → _app/api_impl/plugin}/__init__.py +0 -0
- /snowflake/cli/{app → _app}/api_impl/plugin/plugin_config_provider_impl.py +0 -0
- /snowflake/cli/{app → _app}/commands_registration/__init__.py +0 -0
- /snowflake/cli/{app → _app}/commands_registration/threadsafe.py +0 -0
- /snowflake/cli/{app → _app}/constants.py +0 -0
- /snowflake/cli/{api/project/schemas/streamlit → _app/dev}/__init__.py +0 -0
- /snowflake/cli/{app → _app}/dev/commands_structure.py +0 -0
- /snowflake/cli/{app/api_impl → _app/dev/docs}/__init__.py +0 -0
- /snowflake/cli/{app → _app}/dev/docs/project_definition_generate_json_schema.py +0 -0
- /snowflake/cli/{app → _app}/dev/docs/template_utils.py +0 -0
- /snowflake/cli/{app → _app}/dev/docs/templates/definition_description.rst.jinja2 +0 -0
- /snowflake/cli/{app → _app}/dev/docs/templates/overview.rst.jinja2 +0 -0
- /snowflake/cli/{app → _app}/dev/pycharm_remote_debug.py +0 -0
- /snowflake/cli/{app → _app}/loggers.py +0 -0
- /snowflake/cli/{app/api_impl/plugin → _plugins}/__init__.py +0 -0
- /snowflake/cli/{app/dev → _plugins/connection}/__init__.py +0 -0
- /snowflake/cli/{app/dev/docs → _plugins/cortex}/__init__.py +0 -0
- /snowflake/cli/{plugins → _plugins}/cortex/types.py +0 -0
- /snowflake/cli/{plugins → _plugins/git}/__init__.py +0 -0
- /snowflake/cli/{plugins/connection → _plugins/helpers}/__init__.py +0 -0
- /snowflake/cli/{plugins/cortex → _plugins/init}/__init__.py +0 -0
- /snowflake/cli/{plugins/git → _plugins/nativeapp}/__init__.py +0 -0
- /snowflake/cli/{plugins/init → _plugins/nativeapp/codegen}/__init__.py +0 -0
- /snowflake/cli/{plugins → _plugins}/nativeapp/codegen/sandbox.py +0 -0
- /snowflake/cli/{plugins → _plugins}/nativeapp/codegen/snowpark/callback_source.py.jinja +0 -0
- /snowflake/cli/{plugins → _plugins}/nativeapp/constants.py +0 -0
- /snowflake/cli/{templates/default_snowpark/app → _plugins/nativeapp/entities}/__init__.py +0 -0
- /snowflake/cli/{plugins → _plugins}/nativeapp/feature_flags.py +0 -0
- /snowflake/cli/{plugins → _plugins}/nativeapp/utils.py +0 -0
- /snowflake/cli/{plugins/nativeapp → _plugins/nativeapp/version}/__init__.py +0 -0
- /snowflake/cli/{plugins/nativeapp/codegen → _plugins/notebook}/__init__.py +0 -0
- /snowflake/cli/{plugins → _plugins}/notebook/exceptions.py +0 -0
- /snowflake/cli/{plugins → _plugins}/notebook/types.py +0 -0
- /snowflake/cli/{plugins/nativeapp/version → _plugins/object}/__init__.py +0 -0
- /snowflake/cli/{plugins → _plugins}/object/common.py +0 -0
- /snowflake/cli/{plugins/notebook → _plugins/snowpark}/__init__.py +0 -0
- /snowflake/cli/{plugins/object → _plugins/snowpark/package}/__init__.py +0 -0
- /snowflake/cli/{plugins → _plugins}/snowpark/package/utils.py +0 -0
- /snowflake/cli/{plugins → _plugins}/spcs/common.py +0 -0
- /snowflake/cli/{plugins/snowpark → _plugins/spcs/compute_pool}/__init__.py +0 -0
- /snowflake/cli/{plugins/snowpark/package → _plugins/spcs/image_registry}/__init__.py +0 -0
- /snowflake/cli/{plugins → _plugins}/spcs/image_registry/manager.py +0 -0
- /snowflake/cli/{plugins/spcs/compute_pool → _plugins/spcs/image_repository}/__init__.py +0 -0
- /snowflake/cli/{plugins/spcs/image_registry → _plugins/spcs/services}/__init__.py +0 -0
- /snowflake/cli/{plugins/spcs/image_repository → _plugins/sql}/__init__.py +0 -0
- /snowflake/cli/{plugins → _plugins}/sql/snowsql_templating.py +0 -0
- /snowflake/cli/{plugins/spcs/jobs → _plugins/stage}/__init__.py +0 -0
- /snowflake/cli/{plugins → _plugins}/stage/md5.py +0 -0
- /snowflake/cli/{plugins/spcs/services → _plugins/streamlit}/__init__.py +0 -0
- /snowflake/cli/{plugins/sql → _plugins/workspace}/__init__.py +0 -0
- /snowflake/cli/{plugins/stage → api/project/schemas/entities}/__init__.py +0 -0
- /snowflake/cli/api/project/schemas/{native_app → v1/native_app}/path_mapping.py +0 -0
- /snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/argument.py +0 -0
- {snowflake_cli-2.8.2.dist-info → snowflake_cli-3.0.2.dist-info}/WHEEL +0 -0
- {snowflake_cli-2.8.2.dist-info → snowflake_cli-3.0.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -34,6 +34,9 @@ UNQUOTED_IDENTIFIER_REGEX = r"([a-zA-Z_])([a-zA-Z0-9_$]{0,254})"
|
|
|
34
34
|
QUOTED_IDENTIFIER_REGEX = r'"((""|[^"]){0,255})"'
|
|
35
35
|
VALID_IDENTIFIER_REGEX = f"(?:{UNQUOTED_IDENTIFIER_REGEX}|{QUOTED_IDENTIFIER_REGEX})"
|
|
36
36
|
|
|
37
|
+
# An env var that is used to suffix the names of some account-level resources
|
|
38
|
+
TEST_RESOURCE_SUFFIX_VAR = "SNOWFLAKE_CLI_TEST_RESOURCE_SUFFIX"
|
|
39
|
+
|
|
37
40
|
|
|
38
41
|
def encode_uri_component(s: str) -> str:
|
|
39
42
|
"""
|
|
@@ -191,12 +194,6 @@ def extract_schema(qualified_name: str):
|
|
|
191
194
|
return None
|
|
192
195
|
|
|
193
196
|
|
|
194
|
-
def generate_user_env(username: str) -> dict:
|
|
195
|
-
return {
|
|
196
|
-
"USER": username,
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
197
|
def first_set_env(*keys: str):
|
|
201
198
|
for k in keys:
|
|
202
199
|
v = os.getenv(k)
|
|
@@ -259,3 +256,23 @@ def identifier_to_show_like_pattern(identifier: str) -> str:
|
|
|
259
256
|
matching this identifier
|
|
260
257
|
"""
|
|
261
258
|
return f"'{escape_like_pattern(unquote_identifier(identifier))}'"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def append_test_resource_suffix(identifier: str) -> str:
|
|
262
|
+
"""
|
|
263
|
+
Append a suffix that should be added to specified account-level resources.
|
|
264
|
+
|
|
265
|
+
This is an internal concern that is currently only used in tests
|
|
266
|
+
to isolate concurrent runs and to add the test name to resources.
|
|
267
|
+
"""
|
|
268
|
+
suffix = os.environ.get(TEST_RESOURCE_SUFFIX_VAR, "")
|
|
269
|
+
if identifier_to_str(identifier).endswith(identifier_to_str(suffix)):
|
|
270
|
+
# If the suffix has already been added, don't add it again
|
|
271
|
+
return identifier
|
|
272
|
+
if is_valid_quoted_identifier(identifier) or is_valid_quoted_identifier(suffix):
|
|
273
|
+
# If either identifier is already quoted, use concat_identifier
|
|
274
|
+
# to add the suffix inside the quotes
|
|
275
|
+
return concat_identifiers([identifier, suffix])
|
|
276
|
+
# Otherwise just append the string, don't add quotes
|
|
277
|
+
# in case the user doesn't want them
|
|
278
|
+
return f"{identifier}{suffix}"
|
|
@@ -17,7 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from textwrap import dedent
|
|
20
|
-
from typing import Dict, Optional
|
|
20
|
+
from typing import Any, Dict, Optional
|
|
21
21
|
|
|
22
22
|
import jinja2
|
|
23
23
|
from jinja2 import Environment, StrictUndefined, loaders
|
|
@@ -82,8 +82,18 @@ class IgnoreAttrEnvironment(Environment):
|
|
|
82
82
|
return self.undefined(obj=obj, name=argument)
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def get_basic_jinja_env(loader: Optional[loaders.BaseLoader] = None) -> Environment:
|
|
86
|
+
return env_bootstrap(
|
|
87
|
+
IgnoreAttrEnvironment(
|
|
88
|
+
loader=loader or loaders.BaseLoader(),
|
|
89
|
+
keep_trailing_newline=True,
|
|
90
|
+
undefined=StrictUndefined,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
85
95
|
def jinja_render_from_file(
|
|
86
|
-
template_path: Path, data: Dict, output_file_path: Optional[Path] = None
|
|
96
|
+
template_path: Path, data: Dict[str, Any], output_file_path: Optional[Path] = None
|
|
87
97
|
) -> Optional[str]:
|
|
88
98
|
"""
|
|
89
99
|
Renders a jinja template and outputs either the rendered contents as string or writes to a file.
|
|
@@ -96,12 +106,8 @@ def jinja_render_from_file(
|
|
|
96
106
|
Returns:
|
|
97
107
|
None if file path is provided, else returns the rendered string.
|
|
98
108
|
"""
|
|
99
|
-
env =
|
|
100
|
-
|
|
101
|
-
loader=loaders.FileSystemLoader(template_path.parent),
|
|
102
|
-
keep_trailing_newline=True,
|
|
103
|
-
undefined=StrictUndefined,
|
|
104
|
-
)
|
|
109
|
+
env = get_basic_jinja_env(
|
|
110
|
+
loader=loaders.FileSystemLoader(template_path.parent.as_posix())
|
|
105
111
|
)
|
|
106
112
|
loaded_template = env.get_template(template_path.name)
|
|
107
113
|
rendered_result = loaded_template.render(**data)
|
|
@@ -24,7 +24,11 @@ _YML_TEMPLATE_START = "<%"
|
|
|
24
24
|
_YML_TEMPLATE_END = "%>"
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def
|
|
27
|
+
def has_client_side_templates(template_content: str) -> bool:
|
|
28
|
+
return _YML_TEMPLATE_START in template_content
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_client_side_jinja_env() -> Environment:
|
|
28
32
|
_random_block = "___very___unique___block___to___disable___logic___blocks___"
|
|
29
33
|
return env_bootstrap(
|
|
30
34
|
IgnoreAttrEnvironment(
|
|
@@ -17,8 +17,11 @@ from __future__ import annotations
|
|
|
17
17
|
from typing import Dict, Optional
|
|
18
18
|
|
|
19
19
|
from click import ClickException
|
|
20
|
-
from jinja2 import StrictUndefined, loaders
|
|
21
|
-
from snowflake.cli.api.cli_global_context import
|
|
20
|
+
from jinja2 import Environment, StrictUndefined, loaders, meta
|
|
21
|
+
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
22
|
+
from snowflake.cli.api.console.console import cli_console
|
|
23
|
+
from snowflake.cli.api.exceptions import InvalidTemplate
|
|
24
|
+
from snowflake.cli.api.metrics import CLICounterField
|
|
22
25
|
from snowflake.cli.api.rendering.jinja import (
|
|
23
26
|
CONTEXT_KEY,
|
|
24
27
|
FUNCTION_KEY,
|
|
@@ -26,26 +29,62 @@ from snowflake.cli.api.rendering.jinja import (
|
|
|
26
29
|
env_bootstrap,
|
|
27
30
|
)
|
|
28
31
|
|
|
29
|
-
_SQL_TEMPLATE_START = "
|
|
30
|
-
_SQL_TEMPLATE_END = "
|
|
32
|
+
_SQL_TEMPLATE_START = "<%"
|
|
33
|
+
_SQL_TEMPLATE_END = "%>"
|
|
34
|
+
_OLD_SQL_TEMPLATE_START = "&{"
|
|
35
|
+
_OLD_SQL_TEMPLATE_END = "}"
|
|
31
36
|
RESERVED_KEYS = [CONTEXT_KEY, FUNCTION_KEY]
|
|
32
37
|
|
|
33
38
|
|
|
34
|
-
def
|
|
39
|
+
def _get_sql_jinja_env(template_start: str, template_end: str) -> Environment:
|
|
35
40
|
_random_block = "___very___unique___block___to___disable___logic___blocks___"
|
|
36
41
|
return env_bootstrap(
|
|
37
42
|
IgnoreAttrEnvironment(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
variable_end_string=_SQL_TEMPLATE_END,
|
|
43
|
+
variable_start_string=template_start,
|
|
44
|
+
variable_end_string=template_end,
|
|
45
|
+
loader=loaders.BaseLoader(),
|
|
42
46
|
block_start_string=_random_block,
|
|
43
47
|
block_end_string=_random_block,
|
|
48
|
+
keep_trailing_newline=True,
|
|
44
49
|
undefined=StrictUndefined,
|
|
45
50
|
)
|
|
46
51
|
)
|
|
47
52
|
|
|
48
53
|
|
|
54
|
+
def _does_template_have_env_syntax(env: Environment, template_content: str) -> bool:
|
|
55
|
+
template = env.parse(template_content)
|
|
56
|
+
return bool(meta.find_undeclared_variables(template))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def has_sql_templates(template_content: str) -> bool:
|
|
60
|
+
return (
|
|
61
|
+
_OLD_SQL_TEMPLATE_START in template_content
|
|
62
|
+
or _SQL_TEMPLATE_START in template_content
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def choose_sql_jinja_env_based_on_template_syntax(
|
|
67
|
+
template_content: str, reference_name: Optional[str] = None
|
|
68
|
+
) -> Environment:
|
|
69
|
+
old_syntax_env = _get_sql_jinja_env(_OLD_SQL_TEMPLATE_START, _OLD_SQL_TEMPLATE_END)
|
|
70
|
+
new_syntax_env = _get_sql_jinja_env(_SQL_TEMPLATE_START, _SQL_TEMPLATE_END)
|
|
71
|
+
has_old_syntax = _does_template_have_env_syntax(old_syntax_env, template_content)
|
|
72
|
+
has_new_syntax = _does_template_have_env_syntax(new_syntax_env, template_content)
|
|
73
|
+
reference_name_str = f" in {reference_name}" if reference_name else ""
|
|
74
|
+
if has_old_syntax and has_new_syntax:
|
|
75
|
+
raise InvalidTemplate(
|
|
76
|
+
f"The SQL query{reference_name_str} mixes {_OLD_SQL_TEMPLATE_START} ... {_OLD_SQL_TEMPLATE_END} syntax"
|
|
77
|
+
f" and {_SQL_TEMPLATE_START} ... {_SQL_TEMPLATE_END} syntax."
|
|
78
|
+
)
|
|
79
|
+
if has_old_syntax:
|
|
80
|
+
cli_console.warning(
|
|
81
|
+
f"Warning: {_OLD_SQL_TEMPLATE_START} ... {_OLD_SQL_TEMPLATE_END} syntax{reference_name_str} is deprecated."
|
|
82
|
+
f" Use {_SQL_TEMPLATE_START} ... {_SQL_TEMPLATE_END} syntax instead."
|
|
83
|
+
)
|
|
84
|
+
return old_syntax_env
|
|
85
|
+
return new_syntax_env
|
|
86
|
+
|
|
87
|
+
|
|
49
88
|
def snowflake_sql_jinja_render(content: str, data: Dict | None = None) -> str:
|
|
50
89
|
data = data or {}
|
|
51
90
|
|
|
@@ -55,6 +94,12 @@ def snowflake_sql_jinja_render(content: str, data: Dict | None = None) -> str:
|
|
|
55
94
|
f"{reserved_key} in user defined data. The `{reserved_key}` variable is reserved for CLI usage."
|
|
56
95
|
)
|
|
57
96
|
|
|
58
|
-
context_data =
|
|
97
|
+
context_data = get_cli_context().template_context
|
|
59
98
|
context_data.update(data)
|
|
60
|
-
|
|
99
|
+
env = choose_sql_jinja_env_based_on_template_syntax(content)
|
|
100
|
+
|
|
101
|
+
get_cli_context().metrics.set_counter(
|
|
102
|
+
CLICounterField.SQL_TEMPLATES, int(has_sql_templates(content))
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return env.from_string(content).render(context_data)
|
snowflake/cli/api/rest_api.py
CHANGED
|
@@ -21,8 +21,9 @@ from typing import Any, Dict, Optional
|
|
|
21
21
|
from click import ClickException
|
|
22
22
|
from snowflake.cli.api.constants import SF_REST_API_URL_PREFIX
|
|
23
23
|
from snowflake.connector.connection import SnowflakeConnection
|
|
24
|
-
from snowflake.connector.errors import BadRequest
|
|
24
|
+
from snowflake.connector.errors import BadRequest
|
|
25
25
|
from snowflake.connector.network import SnowflakeRestful
|
|
26
|
+
from snowflake.connector.vendored.requests.exceptions import HTTPError
|
|
26
27
|
|
|
27
28
|
log = logging.getLogger(__name__)
|
|
28
29
|
|
|
@@ -47,10 +48,10 @@ class RestApi:
|
|
|
47
48
|
Check whether [get] endpoint exists under given URL.
|
|
48
49
|
"""
|
|
49
50
|
try:
|
|
50
|
-
|
|
51
|
-
return
|
|
52
|
-
except
|
|
53
|
-
if
|
|
51
|
+
self.send_rest_request(url, method="get")
|
|
52
|
+
return True
|
|
53
|
+
except HTTPError as err:
|
|
54
|
+
if err.response.status_code == 404:
|
|
54
55
|
return False
|
|
55
56
|
raise err
|
|
56
57
|
|
|
@@ -60,6 +61,10 @@ class RestApi:
|
|
|
60
61
|
return bool(result)
|
|
61
62
|
except BadRequest:
|
|
62
63
|
return False
|
|
64
|
+
except HTTPError as err:
|
|
65
|
+
if err.response.status_code == 404:
|
|
66
|
+
return False
|
|
67
|
+
raise err
|
|
63
68
|
|
|
64
69
|
def send_rest_request(
|
|
65
70
|
self, url: str, method: str, data: Optional[Dict[str, Any]] = None
|
|
@@ -91,6 +96,7 @@ class RestApi:
|
|
|
91
96
|
token=self.rest.token,
|
|
92
97
|
data=json.dumps(data if data else {}),
|
|
93
98
|
no_retry=True,
|
|
99
|
+
raise_raw_http_failure=True,
|
|
94
100
|
)
|
|
95
101
|
|
|
96
102
|
def _database_exists(self, db_name: str) -> bool:
|
snowflake/cli/api/secure_path.py
CHANGED
|
@@ -24,6 +24,12 @@ from pathlib import Path
|
|
|
24
24
|
from typing import Optional, Union
|
|
25
25
|
|
|
26
26
|
from snowflake.cli.api.exceptions import DirectoryIsNotEmptyError, FileTooLargeError
|
|
27
|
+
from snowflake.cli.api.secure_utils import (
|
|
28
|
+
chmod as secure_chmod,
|
|
29
|
+
)
|
|
30
|
+
from snowflake.cli.api.secure_utils import (
|
|
31
|
+
restrict_file_permissions,
|
|
32
|
+
)
|
|
27
33
|
|
|
28
34
|
log = logging.getLogger(__name__)
|
|
29
35
|
|
|
@@ -47,6 +53,12 @@ class SecurePath:
|
|
|
47
53
|
"""
|
|
48
54
|
return self._path
|
|
49
55
|
|
|
56
|
+
def chmod(self, permissions_mask: int) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Change the file mode and permissions, like os.chmod().
|
|
59
|
+
"""
|
|
60
|
+
secure_chmod(self._path, permissions_mask)
|
|
61
|
+
|
|
50
62
|
@property
|
|
51
63
|
def parent(self):
|
|
52
64
|
"""
|
|
@@ -97,28 +109,11 @@ class SecurePath:
|
|
|
97
109
|
"""A string representing the final path component."""
|
|
98
110
|
return self._path.name
|
|
99
111
|
|
|
100
|
-
def chmod(self, permissions_mask: int) -> None:
|
|
101
|
-
"""
|
|
102
|
-
Change the file mode and permissions, like os.chmod().
|
|
103
|
-
"""
|
|
104
|
-
log.info(
|
|
105
|
-
"Update permissions of file %s to %s", self._path, oct(permissions_mask)
|
|
106
|
-
)
|
|
107
|
-
self._path.chmod(permissions_mask)
|
|
108
|
-
|
|
109
112
|
def restrict_permissions(self) -> None:
|
|
110
113
|
"""
|
|
111
114
|
Restrict file/directory permissions to owner-only.
|
|
112
115
|
"""
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
owner_permissions = (
|
|
116
|
-
# https://docs.python.org/3/library/stat.html
|
|
117
|
-
stat.S_IRUSR # readable by owner
|
|
118
|
-
| stat.S_IWUSR # writeable by owner
|
|
119
|
-
| stat.S_IXUSR # executable by owner
|
|
120
|
-
)
|
|
121
|
-
self.chmod(self._path.stat().st_mode & owner_permissions)
|
|
116
|
+
restrict_file_permissions(self._path)
|
|
122
117
|
|
|
123
118
|
def touch(self, permissions_mask: int = 0o600, exist_ok: bool = True) -> None:
|
|
124
119
|
"""
|
|
@@ -349,6 +344,9 @@ class SecurePath:
|
|
|
349
344
|
):
|
|
350
345
|
raise FileTooLargeError(self._path.resolve(), size_limit_in_mb)
|
|
351
346
|
|
|
347
|
+
def rename(self, new_name: Union[str | Path]):
|
|
348
|
+
self._path.rename(new_name)
|
|
349
|
+
|
|
352
350
|
|
|
353
351
|
def _raise_file_exists_error(path: Path):
|
|
354
352
|
raise FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), path)
|
|
@@ -12,11 +12,64 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
import logging
|
|
15
16
|
import stat
|
|
16
17
|
from pathlib import Path
|
|
18
|
+
from typing import List
|
|
17
19
|
|
|
20
|
+
from snowflake.connector.compat import IS_WINDOWS
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
log = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_windows_whitelisted_users():
|
|
26
|
+
# whitelisted users list obtained in consultation with prodsec: CASEC-9627
|
|
27
|
+
import os
|
|
28
|
+
|
|
29
|
+
return [
|
|
30
|
+
"SYSTEM",
|
|
31
|
+
"Administrators",
|
|
32
|
+
"Network",
|
|
33
|
+
"Domain Admins",
|
|
34
|
+
"Domain Users",
|
|
35
|
+
os.getlogin(),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _run_icacls(file_path: Path) -> str:
|
|
40
|
+
import subprocess
|
|
41
|
+
|
|
42
|
+
return subprocess.check_output(["icacls", str(file_path)], text=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _windows_permissions_are_denied(permission_codes: str) -> bool:
|
|
46
|
+
# according to https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/icacls
|
|
47
|
+
return "(DENY)" in permission_codes or "(N)" in permission_codes
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def windows_get_not_whitelisted_users_with_access(file_path: Path) -> List[str]:
|
|
51
|
+
import re
|
|
52
|
+
|
|
53
|
+
# according to https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/icacls
|
|
54
|
+
icacls_output_regex = (
|
|
55
|
+
rf"({re.escape(str(file_path))})?.*\\(?P<user>.*):(?P<permissions>[(A-Z),]+)"
|
|
56
|
+
)
|
|
57
|
+
whitelisted_users = _get_windows_whitelisted_users()
|
|
58
|
+
|
|
59
|
+
users_with_access = []
|
|
60
|
+
for permission in re.finditer(icacls_output_regex, _run_icacls(file_path)):
|
|
61
|
+
if (permission.group("user") not in whitelisted_users) and (
|
|
62
|
+
not _windows_permissions_are_denied(permission.group("permissions"))
|
|
63
|
+
):
|
|
64
|
+
users_with_access.append(permission.group("user"))
|
|
65
|
+
return list(set(users_with_access))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _windows_file_permissions_are_strict(file_path: Path) -> bool:
|
|
69
|
+
return windows_get_not_whitelisted_users_with_access(file_path) == []
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _unix_file_permissions_are_strict(file_path: Path) -> bool:
|
|
20
73
|
accessible_by_others = (
|
|
21
74
|
# https://docs.python.org/3/library/stat.html
|
|
22
75
|
stat.S_IRGRP # readable by group
|
|
@@ -27,3 +80,39 @@ def file_permissions_are_strict(file_path: Path) -> bool:
|
|
|
27
80
|
| stat.S_IXOTH # executable by others
|
|
28
81
|
)
|
|
29
82
|
return (file_path.stat().st_mode & accessible_by_others) == 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def file_permissions_are_strict(file_path: Path) -> bool:
|
|
86
|
+
if IS_WINDOWS:
|
|
87
|
+
return _windows_file_permissions_are_strict(file_path)
|
|
88
|
+
return _unix_file_permissions_are_strict(file_path)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def chmod(path: Path, permissions_mask: int) -> None:
|
|
92
|
+
log.info("Update permissions of file %s to %s", path, oct(permissions_mask))
|
|
93
|
+
path.chmod(permissions_mask)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _unix_restrict_file_permissions(path: Path) -> None:
|
|
97
|
+
owner_permissions = (
|
|
98
|
+
# https://docs.python.org/3/library/stat.html
|
|
99
|
+
stat.S_IRUSR # readable by owner
|
|
100
|
+
| stat.S_IWUSR # writeable by owner
|
|
101
|
+
| stat.S_IXUSR # executable by owner
|
|
102
|
+
)
|
|
103
|
+
chmod(path, path.stat().st_mode & owner_permissions)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _windows_restrict_file_permissions(path: Path) -> None:
|
|
107
|
+
import subprocess
|
|
108
|
+
|
|
109
|
+
for user in windows_get_not_whitelisted_users_with_access(path):
|
|
110
|
+
log.info("Removing permissions of user %s from file %s", user, path)
|
|
111
|
+
subprocess.run(["icacls", str(path), "/DENY", f"{user}:F"])
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def restrict_file_permissions(file_path: Path) -> None:
|
|
115
|
+
if IS_WINDOWS:
|
|
116
|
+
_windows_restrict_file_permissions(file_path)
|
|
117
|
+
else:
|
|
118
|
+
_unix_restrict_file_permissions(file_path)
|
|
@@ -21,7 +21,7 @@ from io import StringIO
|
|
|
21
21
|
from textwrap import dedent
|
|
22
22
|
from typing import Iterable, Optional, Tuple
|
|
23
23
|
|
|
24
|
-
from snowflake.cli.api.cli_global_context import
|
|
24
|
+
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
25
25
|
from snowflake.cli.api.console import cli_console
|
|
26
26
|
from snowflake.cli.api.constants import ObjectType
|
|
27
27
|
from snowflake.cli.api.exceptions import (
|
|
@@ -35,27 +35,21 @@ from snowflake.cli.api.project.util import (
|
|
|
35
35
|
unquote_identifier,
|
|
36
36
|
)
|
|
37
37
|
from snowflake.cli.api.utils.cursor import find_first_row
|
|
38
|
+
from snowflake.connector import SnowflakeConnection
|
|
38
39
|
from snowflake.connector.cursor import DictCursor, SnowflakeCursor
|
|
39
40
|
from snowflake.connector.errors import ProgrammingError
|
|
40
41
|
|
|
41
42
|
|
|
42
|
-
class
|
|
43
|
-
def __init__(self):
|
|
43
|
+
class SqlExecutor:
|
|
44
|
+
def __init__(self, connection: SnowflakeConnection | None = None):
|
|
44
45
|
self._snowpark_session = None
|
|
46
|
+
self._connection = connection
|
|
45
47
|
|
|
46
48
|
@property
|
|
47
|
-
def _conn(self):
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def snowpark_session(self):
|
|
52
|
-
if not self._snowpark_session:
|
|
53
|
-
from snowflake.snowpark.session import Session
|
|
54
|
-
|
|
55
|
-
self._snowpark_session = Session.builder.configs(
|
|
56
|
-
{"connection": self._conn}
|
|
57
|
-
).create()
|
|
58
|
-
return self._snowpark_session
|
|
49
|
+
def _conn(self) -> SnowflakeConnection:
|
|
50
|
+
if self._connection:
|
|
51
|
+
return self._connection
|
|
52
|
+
return get_cli_context().connection
|
|
59
53
|
|
|
60
54
|
@cached_property
|
|
61
55
|
def _log(self):
|
|
@@ -88,6 +82,12 @@ class SqlExecutionMixin:
|
|
|
88
82
|
def _execute_queries(self, queries: str, **kwargs):
|
|
89
83
|
return list(self._execute_string(dedent(queries), **kwargs))
|
|
90
84
|
|
|
85
|
+
def execute_query(self, query: str, **kwargs):
|
|
86
|
+
return self._execute_query(query, **kwargs)
|
|
87
|
+
|
|
88
|
+
def execute_queries(self, queries: str, **kwargs):
|
|
89
|
+
return self._execute_queries(queries, **kwargs)
|
|
90
|
+
|
|
91
91
|
def use(self, object_type: ObjectType, name: str):
|
|
92
92
|
try:
|
|
93
93
|
self._execute_query(f"use {object_type.value.sf_name} {name}")
|
|
@@ -97,16 +97,16 @@ class SqlExecutionMixin:
|
|
|
97
97
|
f"Could not use {object_type} {name}. Object does not exist, or operation cannot be performed."
|
|
98
98
|
)
|
|
99
99
|
|
|
100
|
+
def current_role(self) -> str:
|
|
101
|
+
return self._execute_query(f"select current_role()").fetchone()[0]
|
|
102
|
+
|
|
100
103
|
@contextmanager
|
|
101
104
|
def use_role(self, new_role: str):
|
|
102
105
|
"""
|
|
103
106
|
Switches to a different role for a while, then switches back.
|
|
104
107
|
This is a no-op if the requested role is already active.
|
|
105
108
|
"""
|
|
106
|
-
|
|
107
|
-
f"select current_role()", cursor_class=DictCursor
|
|
108
|
-
).fetchone()
|
|
109
|
-
prev_role = role_result["CURRENT_ROLE()"]
|
|
109
|
+
prev_role = self.current_role()
|
|
110
110
|
is_different_role = new_role.lower() != prev_role.lower()
|
|
111
111
|
if is_different_role:
|
|
112
112
|
self._log.debug("Assuming different role: %s", new_role)
|
|
@@ -117,6 +117,12 @@ class SqlExecutionMixin:
|
|
|
117
117
|
if is_different_role:
|
|
118
118
|
self._execute_query(f"use role {prev_role}")
|
|
119
119
|
|
|
120
|
+
def session_has_warehouse(self) -> bool:
|
|
121
|
+
result = self._execute_query(
|
|
122
|
+
"select current_warehouse() is not null"
|
|
123
|
+
).fetchone()
|
|
124
|
+
return bool(result[0])
|
|
125
|
+
|
|
120
126
|
@contextmanager
|
|
121
127
|
def use_warehouse(self, new_wh: str):
|
|
122
128
|
"""
|
|
@@ -125,12 +131,10 @@ class SqlExecutionMixin:
|
|
|
125
131
|
If there is no default warehouse in the account, it will throw an error.
|
|
126
132
|
"""
|
|
127
133
|
|
|
128
|
-
wh_result = self._execute_query(
|
|
129
|
-
f"select current_warehouse()", cursor_class=DictCursor
|
|
130
|
-
).fetchone()
|
|
134
|
+
wh_result = self._execute_query(f"select current_warehouse()").fetchone()
|
|
131
135
|
# If user has an assigned default warehouse, prev_wh will contain a value even if the warehouse is suspended.
|
|
132
136
|
try:
|
|
133
|
-
prev_wh = wh_result[
|
|
137
|
+
prev_wh = wh_result[0]
|
|
134
138
|
except:
|
|
135
139
|
prev_wh = None
|
|
136
140
|
|
|
@@ -254,6 +258,22 @@ class SqlExecutionMixin:
|
|
|
254
258
|
return show_obj_row
|
|
255
259
|
|
|
256
260
|
|
|
261
|
+
class SqlExecutionMixin(SqlExecutor):
|
|
262
|
+
def __init__(self, *args, **kwargs):
|
|
263
|
+
super().__init__(*args, **kwargs)
|
|
264
|
+
self._snowpark_session = None
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def snowpark_session(self):
|
|
268
|
+
if not self._snowpark_session:
|
|
269
|
+
from snowflake.snowpark.session import Session
|
|
270
|
+
|
|
271
|
+
self._snowpark_session = Session.builder.configs(
|
|
272
|
+
{"connection": self._conn}
|
|
273
|
+
).create()
|
|
274
|
+
return self._snowpark_session
|
|
275
|
+
|
|
276
|
+
|
|
257
277
|
class VerboseCursor(SnowflakeCursor):
|
|
258
278
|
def execute(self, command: str, *args, **kwargs):
|
|
259
279
|
cli_console.message(command)
|
|
@@ -19,16 +19,18 @@ from typing import Any, Optional
|
|
|
19
19
|
|
|
20
20
|
from jinja2 import Environment, TemplateSyntaxError, nodes
|
|
21
21
|
from packaging.version import Version
|
|
22
|
+
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
22
23
|
from snowflake.cli.api.console import cli_console as cc
|
|
23
24
|
from snowflake.cli.api.exceptions import CycleDetectedError, InvalidTemplate
|
|
25
|
+
from snowflake.cli.api.metrics import CLICounterField
|
|
24
26
|
from snowflake.cli.api.project.schemas.project_definition import (
|
|
25
27
|
ProjectProperties,
|
|
26
28
|
build_project_definition,
|
|
27
29
|
)
|
|
28
30
|
from snowflake.cli.api.project.schemas.updatable_model import context
|
|
29
|
-
from snowflake.cli.api.rendering.jinja import CONTEXT_KEY
|
|
31
|
+
from snowflake.cli.api.rendering.jinja import CONTEXT_KEY, FUNCTION_KEY
|
|
30
32
|
from snowflake.cli.api.rendering.project_definition_templates import (
|
|
31
|
-
|
|
33
|
+
get_client_side_jinja_env,
|
|
32
34
|
)
|
|
33
35
|
from snowflake.cli.api.utils.dict_utils import deep_merge_dicts, traverse
|
|
34
36
|
from snowflake.cli.api.utils.graph import Graph, Node
|
|
@@ -96,7 +98,7 @@ class TemplatedEnvironment:
|
|
|
96
98
|
)
|
|
97
99
|
or current_attr_chain is not None
|
|
98
100
|
):
|
|
99
|
-
raise InvalidTemplate(f"Unexpected
|
|
101
|
+
raise InvalidTemplate(f"Unexpected template syntax in {template_value}")
|
|
100
102
|
|
|
101
103
|
for child_node in ast_node.iter_child_nodes():
|
|
102
104
|
all_referenced_vars.update(
|
|
@@ -266,6 +268,12 @@ def _get_referenced_vars_in_definition(
|
|
|
266
268
|
return referenced_vars
|
|
267
269
|
|
|
268
270
|
|
|
271
|
+
def _has_referenced_vars_in_definition(
|
|
272
|
+
template_env: TemplatedEnvironment, definition: Definition
|
|
273
|
+
) -> bool:
|
|
274
|
+
return len(_get_referenced_vars_in_definition(template_env, definition)) > 0
|
|
275
|
+
|
|
276
|
+
|
|
269
277
|
def _template_version_warning():
|
|
270
278
|
cc.warning(
|
|
271
279
|
"Ignoring template pattern in project definition file. "
|
|
@@ -277,7 +285,9 @@ def _add_defaults_to_definition(original_definition: Definition) -> Definition:
|
|
|
277
285
|
with context({"skip_validation_on_templates": True}):
|
|
278
286
|
# pass a flag to Pydantic to skip validation for templated scalars
|
|
279
287
|
# populate the defaults
|
|
280
|
-
project_definition = build_project_definition(
|
|
288
|
+
project_definition = build_project_definition(
|
|
289
|
+
**copy.deepcopy(original_definition)
|
|
290
|
+
)
|
|
281
291
|
|
|
282
292
|
definition_with_defaults = project_definition.model_dump(
|
|
283
293
|
exclude_none=True, warnings=False, by_alias=True
|
|
@@ -289,6 +299,17 @@ def _add_defaults_to_definition(original_definition: Definition) -> Definition:
|
|
|
289
299
|
return definition_with_defaults
|
|
290
300
|
|
|
291
301
|
|
|
302
|
+
def _update_metrics(template_env: TemplatedEnvironment, definition: Definition):
|
|
303
|
+
metrics = get_cli_context().metrics
|
|
304
|
+
|
|
305
|
+
# render_definition_template is invoked multiple times both by the user
|
|
306
|
+
# and by us so we should make sure we don't overwrite a 1 with a 0 here
|
|
307
|
+
metrics.set_counter_default(CLICounterField.PDF_TEMPLATES, 0)
|
|
308
|
+
|
|
309
|
+
if _has_referenced_vars_in_definition(template_env, definition):
|
|
310
|
+
metrics.set_counter(CLICounterField.PDF_TEMPLATES, 1)
|
|
311
|
+
|
|
312
|
+
|
|
292
313
|
def render_definition_template(
|
|
293
314
|
original_definition: Optional[Definition], context_overrides: Context
|
|
294
315
|
) -> ProjectProperties:
|
|
@@ -318,16 +339,13 @@ def render_definition_template(
|
|
|
318
339
|
if definition is None:
|
|
319
340
|
return ProjectProperties(None, {CONTEXT_KEY: {"env": environment_overrides}})
|
|
320
341
|
|
|
321
|
-
template_env = TemplatedEnvironment(
|
|
342
|
+
template_env = TemplatedEnvironment(get_client_side_jinja_env())
|
|
322
343
|
|
|
323
344
|
if "definition_version" not in definition or Version(
|
|
324
345
|
definition["definition_version"]
|
|
325
346
|
) < Version("1.1"):
|
|
326
347
|
try:
|
|
327
|
-
|
|
328
|
-
template_env, definition
|
|
329
|
-
)
|
|
330
|
-
if referenced_vars:
|
|
348
|
+
if _has_referenced_vars_in_definition(template_env, definition):
|
|
331
349
|
_template_version_warning()
|
|
332
350
|
except Exception:
|
|
333
351
|
# also warn on Exception, as it means the user is incorrectly attempting to use templating
|
|
@@ -338,13 +356,17 @@ def render_definition_template(
|
|
|
338
356
|
project_context[CONTEXT_KEY]["env"] = environment_overrides
|
|
339
357
|
return ProjectProperties(project_definition, project_context)
|
|
340
358
|
|
|
359
|
+
# need to have the metrics added here since we add defaults to the
|
|
360
|
+
# definition that the user might not have added themselves later
|
|
361
|
+
_update_metrics(template_env, definition)
|
|
362
|
+
|
|
341
363
|
definition = _add_defaults_to_definition(definition)
|
|
342
364
|
project_context = {CONTEXT_KEY: definition}
|
|
343
365
|
|
|
344
366
|
_validate_env_section(definition.get("env", {}))
|
|
345
367
|
|
|
346
368
|
# add available templating functions
|
|
347
|
-
project_context[
|
|
369
|
+
project_context[FUNCTION_KEY] = get_templating_functions()
|
|
348
370
|
|
|
349
371
|
referenced_vars = _get_referenced_vars_in_definition(template_env, definition)
|
|
350
372
|
|
|
@@ -353,9 +375,7 @@ def render_definition_template(
|
|
|
353
375
|
)
|
|
354
376
|
|
|
355
377
|
def on_cycle_action(node: Node[TemplateVar]):
|
|
356
|
-
raise CycleDetectedError(
|
|
357
|
-
f"Cycle detected in templating variable {node.data.key}"
|
|
358
|
-
)
|
|
378
|
+
raise CycleDetectedError(f"Cycle detected in template variable {node.data.key}")
|
|
359
379
|
|
|
360
380
|
dependencies_graph.dfs(
|
|
361
381
|
visit_action=lambda node: _render_graph_node(template_env, node),
|
|
@@ -374,10 +394,22 @@ def render_definition_template(
|
|
|
374
394
|
)
|
|
375
395
|
|
|
376
396
|
project_definition = build_project_definition(**definition)
|
|
397
|
+
|
|
398
|
+
# Use the values originally provided by the user as the template context
|
|
399
|
+
# This intentionally doesn't reflect any field changes made by
|
|
400
|
+
# validators, to minimize user surprise when templating values
|
|
377
401
|
project_context[CONTEXT_KEY] = definition
|
|
402
|
+
|
|
378
403
|
# Use `ProjectEnvironment` in project context in order to
|
|
379
404
|
# handle env variables overrides from OS env and from CLI arguments.
|
|
380
405
|
project_context[CONTEXT_KEY]["env"] = ProjectEnvironment(
|
|
381
406
|
default_env=project_context[CONTEXT_KEY].get("env"), override_env=override_env
|
|
382
407
|
)
|
|
383
408
|
return ProjectProperties(project_definition, project_context)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def raw_project_properties(definition: Definition) -> ProjectProperties:
|
|
412
|
+
"""
|
|
413
|
+
Returns the raw project definition data without any templating.
|
|
414
|
+
"""
|
|
415
|
+
return ProjectProperties(build_project_definition(**definition), {})
|