snowflake-cli 3.0.1__py3-none-any.whl → 3.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/cli_app.py +3 -0
- snowflake/cli/_app/dev/docs/templates/overview.rst.jinja2 +1 -1
- snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +2 -2
- snowflake/cli/_app/telemetry.py +69 -4
- snowflake/cli/_plugins/connection/commands.py +40 -2
- snowflake/cli/_plugins/git/commands.py +6 -3
- snowflake/cli/_plugins/git/manager.py +5 -0
- snowflake/cli/_plugins/nativeapp/artifacts.py +13 -3
- snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +7 -0
- snowflake/cli/_plugins/nativeapp/codegen/sandbox.py +10 -10
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +8 -8
- snowflake/cli/_plugins/nativeapp/commands.py +135 -186
- snowflake/cli/_plugins/nativeapp/entities/application.py +176 -24
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +112 -136
- snowflake/cli/_plugins/nativeapp/exceptions.py +12 -0
- snowflake/cli/_plugins/nativeapp/manager.py +3 -26
- snowflake/cli/_plugins/nativeapp/v2_conversions/{v2_to_v1_decorator.py → compat.py} +131 -72
- snowflake/cli/_plugins/nativeapp/version/commands.py +30 -29
- snowflake/cli/_plugins/nativeapp/version/version_processor.py +1 -43
- snowflake/cli/_plugins/snowpark/commands.py +0 -2
- snowflake/cli/_plugins/snowpark/common.py +60 -18
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +2 -2
- snowflake/cli/_plugins/snowpark/package/commands.py +0 -2
- snowflake/cli/_plugins/snowpark/package_utils.py +27 -38
- snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +5 -2
- snowflake/cli/_plugins/spcs/image_repository/commands.py +4 -37
- snowflake/cli/_plugins/spcs/image_repository/manager.py +4 -1
- snowflake/cli/_plugins/spcs/services/commands.py +36 -4
- snowflake/cli/_plugins/spcs/services/manager.py +36 -4
- snowflake/cli/_plugins/stage/commands.py +8 -3
- snowflake/cli/_plugins/stage/diff.py +16 -16
- snowflake/cli/_plugins/stage/manager.py +164 -73
- snowflake/cli/_plugins/stage/md5.py +1 -1
- snowflake/cli/_plugins/workspace/commands.py +21 -1
- snowflake/cli/_plugins/workspace/context.py +38 -0
- snowflake/cli/_plugins/workspace/manager.py +23 -13
- snowflake/cli/api/cli_global_context.py +3 -3
- snowflake/cli/api/commands/flags.py +23 -7
- snowflake/cli/api/config.py +7 -4
- snowflake/cli/api/connections.py +12 -1
- snowflake/cli/api/entities/common.py +4 -2
- snowflake/cli/api/entities/utils.py +17 -37
- snowflake/cli/api/exceptions.py +32 -0
- snowflake/cli/api/identifiers.py +8 -0
- snowflake/cli/api/project/definition_conversion.py +139 -40
- snowflake/cli/api/project/schemas/entities/common.py +11 -0
- snowflake/cli/api/project/schemas/project_definition.py +30 -25
- snowflake/cli/api/sql_execution.py +5 -7
- snowflake/cli/api/stage_path.py +241 -0
- snowflake/cli/api/utils/definition_rendering.py +3 -5
- {snowflake_cli-3.0.1.dist-info → snowflake_cli-3.1.0.dist-info}/METADATA +11 -11
- {snowflake_cli-3.0.1.dist-info → snowflake_cli-3.1.0.dist-info}/RECORD +59 -59
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +0 -70
- snowflake/cli/_plugins/workspace/action_context.py +0 -18
- {snowflake_cli-3.0.1.dist-info → snowflake_cli-3.1.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.0.1.dist-info → snowflake_cli-3.1.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.0.1.dist-info → snowflake_cli-3.1.0.dist-info}/licenses/LICENSE +0 -0
snowflake/cli/__about__.py
CHANGED
snowflake/cli/_app/cli_app.py
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
|
+
import os
|
|
18
19
|
import platform
|
|
19
20
|
import sys
|
|
20
21
|
from dataclasses import dataclass
|
|
@@ -141,6 +142,7 @@ def _info_callback(value: bool):
|
|
|
141
142
|
{"key": "python_version", "value": sys.version},
|
|
142
143
|
{"key": "system_info", "value": platform.platform()},
|
|
143
144
|
{"key": "feature_flags", "value": get_feature_flags_section()},
|
|
145
|
+
{"key": "SNOWFLAKE_HOME", "value": os.getenv("SNOWFLAKE_HOME")},
|
|
144
146
|
],
|
|
145
147
|
)
|
|
146
148
|
print_result(result, output_format=OutputFormat.JSON)
|
|
@@ -155,6 +157,7 @@ def app_factory() -> SnowCliMainTyper:
|
|
|
155
157
|
invoke_without_command=True,
|
|
156
158
|
epilog=new_version_msg,
|
|
157
159
|
result_callback=show_new_version_banner_callback(new_version_msg),
|
|
160
|
+
help=f"Snowflake CLI tool for developers [v{__about__.VERSION}]",
|
|
158
161
|
)
|
|
159
162
|
def default(
|
|
160
163
|
ctx: typer.Context,
|
|
@@ -5,5 +5,5 @@ Global options
|
|
|
5
5
|
===============================================================================
|
|
6
6
|
{% for param in options if not param.hidden %}
|
|
7
7
|
:samp:`{% for p in param.opts %}{{ p }}{{ ", " if not loop.last }}{% endfor %}{% if not param.is_flag %} {{ '{' }}{{ param.name }}{{ '}' }}{% endif %}`
|
|
8
|
-
{% if param.help %}{{ " " + param.help
|
|
8
|
+
{% if param.help %}{{ " " + param.help}}{% if param.help[-1] != '.' %}.{% endif %}{% else %} TBD{% endif %}
|
|
9
9
|
{% endfor %}
|
|
@@ -26,7 +26,7 @@ Arguments
|
|
|
26
26
|
{{ param.make_metavar().replace("[", "").replace("]", "").lower() }}
|
|
27
27
|
{%- endif -%}
|
|
28
28
|
{{ '}' }}`
|
|
29
|
-
{% if param.help %}{{ " " + param.help | replace("
|
|
29
|
+
{% if param.help %}{{ " " + param.help | replace("\n", " ") }}{% if param.help[-1] != '.' %}.{% endif %}{% if param.default %} Default: {{ param.default }}.{% endif %}{% else %} TBD{% endif %}
|
|
30
30
|
{% endfor %}
|
|
31
31
|
{% else %}
|
|
32
32
|
|
|
@@ -48,7 +48,7 @@ Options
|
|
|
48
48
|
{%- if param.type.name != "choice" %}{{ ' {' }}{% else %} {% endif %}{{ param.make_metavar() }}{% if param.type.name != "choice" %}{{ '}' }}
|
|
49
49
|
{%- endif %}
|
|
50
50
|
{%- endif %}`
|
|
51
|
-
{% if param.help %}{{ " " + param.help | replace("
|
|
51
|
+
{% if param.help %}{{ " " + param.help | replace("\n", " ") }}{% if param.help[-1] != '.' %}.{% endif %}{% if param.default is not none %} Default: {{ param.default }}.{% endif %}{% else %} TBD{% endif %}
|
|
52
52
|
{% endfor -%}
|
|
53
53
|
{% else %}
|
|
54
54
|
|
snowflake/cli/_app/telemetry.py
CHANGED
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import os
|
|
17
18
|
import platform
|
|
18
19
|
import sys
|
|
19
20
|
from enum import Enum, unique
|
|
20
21
|
from typing import Any, Dict, Union
|
|
21
22
|
|
|
22
23
|
import click
|
|
24
|
+
import typer
|
|
23
25
|
from snowflake.cli.__about__ import VERSION
|
|
24
26
|
from snowflake.cli._app.constants import PARAM_APPLICATION_NAME
|
|
25
27
|
from snowflake.cli.api.cli_global_context import (
|
|
@@ -30,6 +32,7 @@ from snowflake.cli.api.commands.execution_metadata import ExecutionMetadata
|
|
|
30
32
|
from snowflake.cli.api.config import get_feature_flags_section
|
|
31
33
|
from snowflake.cli.api.output.formats import OutputFormat
|
|
32
34
|
from snowflake.cli.api.utils.error_handling import ignore_exceptions
|
|
35
|
+
from snowflake.connector import ProgrammingError
|
|
33
36
|
from snowflake.connector.telemetry import (
|
|
34
37
|
TelemetryData,
|
|
35
38
|
TelemetryField,
|
|
@@ -59,6 +62,7 @@ class CLITelemetryField(Enum):
|
|
|
59
62
|
COMMAND_RESULT_STATUS = "command_result_status"
|
|
60
63
|
COMMAND_OUTPUT_TYPE = "command_output_type"
|
|
61
64
|
COMMAND_EXECUTION_TIME = "command_execution_time"
|
|
65
|
+
COMMAND_CI_ENVIRONMENT = "command_ci_environment"
|
|
62
66
|
# Configuration
|
|
63
67
|
CONFIG_FEATURE_FLAGS = "config_feature_flags"
|
|
64
68
|
# Metrics
|
|
@@ -67,6 +71,9 @@ class CLITelemetryField(Enum):
|
|
|
67
71
|
EVENT = "event"
|
|
68
72
|
ERROR_MSG = "error_msg"
|
|
69
73
|
ERROR_TYPE = "error_type"
|
|
74
|
+
ERROR_CODE = "error_code"
|
|
75
|
+
ERROR_CAUSE = "error_cause"
|
|
76
|
+
SQL_STATE = "sql_state"
|
|
70
77
|
IS_CLI_EXCEPTION = "is_cli_exception"
|
|
71
78
|
# Project context
|
|
72
79
|
PROJECT_DEFINITION_VERSION = "project_definition_version"
|
|
@@ -81,6 +88,43 @@ class TelemetryEvent(Enum):
|
|
|
81
88
|
TelemetryDict = Dict[Union[CLITelemetryField, TelemetryField], Any]
|
|
82
89
|
|
|
83
90
|
|
|
91
|
+
def _is_cli_exception(exception: Exception) -> bool:
|
|
92
|
+
return isinstance(
|
|
93
|
+
exception,
|
|
94
|
+
(
|
|
95
|
+
click.ClickException,
|
|
96
|
+
typer.Exit,
|
|
97
|
+
typer.Abort,
|
|
98
|
+
BrokenPipeError,
|
|
99
|
+
KeyboardInterrupt,
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _get_additional_exception_information(exception: Exception) -> TelemetryDict:
|
|
105
|
+
"""
|
|
106
|
+
Attach the errno and sqlstate if the exception or the
|
|
107
|
+
cause of the exception is a ProgrammingError
|
|
108
|
+
"""
|
|
109
|
+
additional_info = {}
|
|
110
|
+
|
|
111
|
+
if isinstance(exception, ProgrammingError):
|
|
112
|
+
additional_info[CLITelemetryField.ERROR_CODE] = exception.errno
|
|
113
|
+
additional_info[CLITelemetryField.SQL_STATE] = exception.sqlstate
|
|
114
|
+
|
|
115
|
+
if exception.__cause__:
|
|
116
|
+
cause = exception.__cause__
|
|
117
|
+
additional_info[CLITelemetryField.ERROR_CAUSE] = type(cause).__name__
|
|
118
|
+
|
|
119
|
+
if isinstance(cause, ProgrammingError):
|
|
120
|
+
if not additional_info.get(CLITelemetryField.ERROR_CODE):
|
|
121
|
+
additional_info[CLITelemetryField.ERROR_CODE] = cause.errno
|
|
122
|
+
if not additional_info.get(CLITelemetryField.SQL_STATE):
|
|
123
|
+
additional_info[CLITelemetryField.SQL_STATE] = cause.sqlstate
|
|
124
|
+
|
|
125
|
+
return additional_info
|
|
126
|
+
|
|
127
|
+
|
|
84
128
|
def _get_command_metrics() -> TelemetryDict:
|
|
85
129
|
cli_context = get_cli_context()
|
|
86
130
|
|
|
@@ -110,9 +154,14 @@ def _find_command_info() -> TelemetryDict:
|
|
|
110
154
|
|
|
111
155
|
|
|
112
156
|
def _get_definition_version() -> str | None:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
157
|
+
try:
|
|
158
|
+
cli_context = get_cli_context()
|
|
159
|
+
if cli_context.project_definition:
|
|
160
|
+
return cli_context.project_definition.definition_version
|
|
161
|
+
except Exception:
|
|
162
|
+
# Don't let an invalid project definition file break telemetry
|
|
163
|
+
# (especially for commands that don't normally load it)
|
|
164
|
+
pass
|
|
116
165
|
return None
|
|
117
166
|
|
|
118
167
|
|
|
@@ -122,6 +171,20 @@ def _get_installation_source() -> CLIInstallationSource:
|
|
|
122
171
|
return CLIInstallationSource.PYPI
|
|
123
172
|
|
|
124
173
|
|
|
174
|
+
def _get_ci_environment_type() -> str:
|
|
175
|
+
if "GITHUB_ACTIONS" in os.environ:
|
|
176
|
+
return "GITHUB_ACTIONS"
|
|
177
|
+
if "GITLAB_CI" in os.environ:
|
|
178
|
+
return "GITLAB_CI"
|
|
179
|
+
if "CIRCLECI" in os.environ:
|
|
180
|
+
return "CIRCLECI"
|
|
181
|
+
if "JENKINS_URL" in os.environ or "HUDSON_URL" in os.environ:
|
|
182
|
+
return "JENKINS"
|
|
183
|
+
if "TF_BUILD" in os.environ:
|
|
184
|
+
return "AZURE_DEVOPS"
|
|
185
|
+
return "UNKNOWN"
|
|
186
|
+
|
|
187
|
+
|
|
125
188
|
def command_info() -> str:
|
|
126
189
|
info = _find_command_info()
|
|
127
190
|
command = ".".join(info[CLITelemetryField.COMMAND])
|
|
@@ -148,6 +211,7 @@ class CLITelemetryClient:
|
|
|
148
211
|
CLITelemetryField.VERSION_CLI: VERSION,
|
|
149
212
|
CLITelemetryField.VERSION_OS: platform.platform(),
|
|
150
213
|
CLITelemetryField.VERSION_PYTHON: python_version(),
|
|
214
|
+
CLITelemetryField.COMMAND_CI_ENVIRONMENT: _get_ci_environment_type(),
|
|
151
215
|
CLITelemetryField.CONFIG_FEATURE_FLAGS: {
|
|
152
216
|
k: str(v) for k, v in get_feature_flags_section().items()
|
|
153
217
|
},
|
|
@@ -202,7 +266,7 @@ def log_command_result(execution: ExecutionMetadata):
|
|
|
202
266
|
@ignore_exceptions()
|
|
203
267
|
def log_command_execution_error(exception: Exception, execution: ExecutionMetadata):
|
|
204
268
|
exception_type: str = type(exception).__name__
|
|
205
|
-
is_cli_exception: bool =
|
|
269
|
+
is_cli_exception: bool = _is_cli_exception(exception)
|
|
206
270
|
_telemetry.send(
|
|
207
271
|
{
|
|
208
272
|
TelemetryField.KEY_TYPE: TelemetryEvent.CMD_EXECUTION_ERROR.value,
|
|
@@ -210,6 +274,7 @@ def log_command_execution_error(exception: Exception, execution: ExecutionMetada
|
|
|
210
274
|
CLITelemetryField.ERROR_TYPE: exception_type,
|
|
211
275
|
CLITelemetryField.IS_CLI_EXCEPTION: is_cli_exception,
|
|
212
276
|
CLITelemetryField.COMMAND_EXECUTION_TIME: execution.get_duration(),
|
|
277
|
+
**_get_additional_exception_information(exception),
|
|
213
278
|
**_get_command_metrics(),
|
|
214
279
|
}
|
|
215
280
|
)
|
|
@@ -18,9 +18,10 @@ import logging
|
|
|
18
18
|
import os.path
|
|
19
19
|
|
|
20
20
|
import typer
|
|
21
|
-
from click import ClickException, Context, Parameter # type: ignore
|
|
21
|
+
from click import ClickException, Context, Parameter, UsageError # type: ignore
|
|
22
22
|
from click.core import ParameterSource # type: ignore
|
|
23
23
|
from click.types import StringParamType
|
|
24
|
+
from snowflake import connector
|
|
24
25
|
from snowflake.cli._plugins.connection.util import (
|
|
25
26
|
strip_and_check_if_exists,
|
|
26
27
|
strip_if_value_present,
|
|
@@ -342,7 +343,7 @@ def test(
|
|
|
342
343
|
@app.command(requires_connection=False)
|
|
343
344
|
def set_default(
|
|
344
345
|
name: str = typer.Argument(
|
|
345
|
-
help="Name of the connection, as defined in your `config.toml`",
|
|
346
|
+
help="Name of the connection, as defined in your `config.toml` file",
|
|
346
347
|
show_default=False,
|
|
347
348
|
),
|
|
348
349
|
**options,
|
|
@@ -351,3 +352,40 @@ def set_default(
|
|
|
351
352
|
get_connection_dict(connection_name=name)
|
|
352
353
|
set_config_value(section=None, key="default_connection_name", value=name)
|
|
353
354
|
return MessageResult(f"Default connection set to: {name}")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@app.command(requires_connection=True)
|
|
358
|
+
def generate_jwt(
|
|
359
|
+
**options,
|
|
360
|
+
) -> CommandResult:
|
|
361
|
+
"""Generate and display a JWT token."""
|
|
362
|
+
connection_details = get_cli_context().connection_context.update_from_config()
|
|
363
|
+
|
|
364
|
+
msq_template = (
|
|
365
|
+
"{} is not set in the connection context, but required for JWT generation."
|
|
366
|
+
)
|
|
367
|
+
if not connection_details.user:
|
|
368
|
+
raise UsageError(msq_template.format("User"))
|
|
369
|
+
if not connection_details.account:
|
|
370
|
+
raise UsageError(msq_template.format("Account"))
|
|
371
|
+
if not connection_details.private_key_file:
|
|
372
|
+
raise UsageError(msq_template.format("Private key file"))
|
|
373
|
+
passphrase = os.getenv("PRIVATE_KEY_PASSPHRASE", None)
|
|
374
|
+
if not passphrase:
|
|
375
|
+
passphrase = typer.prompt(
|
|
376
|
+
"Enter private key file password (Press enter if none)",
|
|
377
|
+
hide_input=True,
|
|
378
|
+
type=str,
|
|
379
|
+
default="",
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
token = connector.auth.get_token_from_private_key(
|
|
384
|
+
user=connection_details.user,
|
|
385
|
+
account=connection_details.account,
|
|
386
|
+
privatekey_path=connection_details.private_key_file,
|
|
387
|
+
key_password=passphrase,
|
|
388
|
+
)
|
|
389
|
+
return MessageResult(token)
|
|
390
|
+
except ValueError as err:
|
|
391
|
+
raise ClickException(str(err))
|
|
@@ -333,12 +333,15 @@ def execute(
|
|
|
333
333
|
**options,
|
|
334
334
|
):
|
|
335
335
|
"""
|
|
336
|
-
Execute immediate all files from the repository path. Files can be filtered with glob
|
|
336
|
+
Execute immediate all files from the repository path. Files can be filtered with a glob-like pattern,
|
|
337
337
|
e.g. `@my_repo/branches/main/*.sql`, `@my_repo/branches/main/dev/*`. Only files with `.sql`
|
|
338
|
-
extension will be executed.
|
|
338
|
+
or `.py` extension will be executed.
|
|
339
339
|
"""
|
|
340
340
|
results = GitManager().execute(
|
|
341
|
-
|
|
341
|
+
stage_path_str=repository_path,
|
|
342
|
+
on_error=on_error,
|
|
343
|
+
variables=variables,
|
|
344
|
+
requires_temporary_stage=True,
|
|
342
345
|
)
|
|
343
346
|
return CollectionResult(results)
|
|
344
347
|
|
|
@@ -26,6 +26,7 @@ from snowflake.cli._plugins.stage.manager import (
|
|
|
26
26
|
UserStagePathParts,
|
|
27
27
|
)
|
|
28
28
|
from snowflake.cli.api.identifiers import FQN
|
|
29
|
+
from snowflake.cli.api.stage_path import StagePath
|
|
29
30
|
from snowflake.connector.cursor import SnowflakeCursor
|
|
30
31
|
|
|
31
32
|
# Replace magic numbers with constants
|
|
@@ -78,6 +79,10 @@ class GitStagePathParts(StagePathParts):
|
|
|
78
79
|
|
|
79
80
|
|
|
80
81
|
class GitManager(StageManager):
|
|
82
|
+
@staticmethod
|
|
83
|
+
def build_path(stage_path: str) -> StagePathParts:
|
|
84
|
+
return StagePath.from_git_str(stage_path)
|
|
85
|
+
|
|
81
86
|
def show_branches(self, repo_name: str, like: str) -> SnowflakeCursor:
|
|
82
87
|
return self._execute_query(f"show git branches like '{like}' in {repo_name}")
|
|
83
88
|
|
|
@@ -225,9 +225,7 @@ class _ArtifactPathMap:
|
|
|
225
225
|
|
|
226
226
|
current_is_dir = self._dest_is_dir.get(dest, None)
|
|
227
227
|
if current_is_dir is not None and current_is_dir != is_dir:
|
|
228
|
-
raise ArtifactError(
|
|
229
|
-
"Conflicting type for destination path: {canonical_dest}"
|
|
230
|
-
)
|
|
228
|
+
raise ArtifactError(f"Conflicting type for destination path: {dest}")
|
|
231
229
|
|
|
232
230
|
parent = dest.parent
|
|
233
231
|
if parent != dest:
|
|
@@ -240,9 +238,21 @@ class BundleMap:
|
|
|
240
238
|
"""
|
|
241
239
|
Computes the mapping between project directory artifacts (aka source artifacts) to their deploy root location
|
|
242
240
|
(aka destination artifact). This information is primarily used when bundling a native applications project.
|
|
241
|
+
|
|
242
|
+
:param project_root: The root directory of the project and base for all relative paths. Must be an absolute path.
|
|
243
|
+
:param deploy_root: The directory where artifacts should be copied to. Must be an absolute path.
|
|
243
244
|
"""
|
|
244
245
|
|
|
245
246
|
def __init__(self, *, project_root: Path, deploy_root: Path):
|
|
247
|
+
# If a relative path ends up here, it's a bug in the app and can lead to other
|
|
248
|
+
# subtle bugs as paths would be resolved relative to the current working directory.
|
|
249
|
+
assert (
|
|
250
|
+
project_root.is_absolute()
|
|
251
|
+
), f"Project root {project_root} must be an absolute path."
|
|
252
|
+
assert (
|
|
253
|
+
deploy_root.is_absolute()
|
|
254
|
+
), f"Deploy root {deploy_root} must be an absolute path."
|
|
255
|
+
|
|
246
256
|
self._project_root: Path = resolve_without_follow(project_root)
|
|
247
257
|
self._deploy_root: Path = resolve_without_follow(deploy_root)
|
|
248
258
|
self._artifact_map = _ArtifactPathMap(project_root=self._project_root)
|
|
@@ -36,7 +36,7 @@ class UnsupportedArtifactProcessorError(ClickException):
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def is_python_file_artifact(src: Path, _: Path):
|
|
39
|
-
"""Determines whether the provided source path is an existing
|
|
39
|
+
"""Determines whether the provided source path is an existing Python file."""
|
|
40
40
|
return src.is_file() and src.suffix == ".py"
|
|
41
41
|
|
|
42
42
|
|
|
@@ -65,10 +65,17 @@ class NativeAppCompiler:
|
|
|
65
65
|
self,
|
|
66
66
|
bundle_ctx: BundleContext,
|
|
67
67
|
):
|
|
68
|
+
self._assert_absolute_paths(bundle_ctx)
|
|
68
69
|
self._bundle_ctx = bundle_ctx
|
|
69
70
|
# dictionary of all processors created and shared between different artifact objects.
|
|
70
71
|
self.cached_processors: Dict[str, ArtifactProcessor] = {}
|
|
71
72
|
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _assert_absolute_paths(bundle_ctx: BundleContext):
|
|
75
|
+
for name in ["Project", "Deploy", "Bundle", "Generated"]:
|
|
76
|
+
path = getattr(bundle_ctx, f"{name.lower()}_root")
|
|
77
|
+
assert path.is_absolute(), f"{name} root {path} must be an absolute path."
|
|
78
|
+
|
|
72
79
|
def compile_artifacts(self):
|
|
73
80
|
"""
|
|
74
81
|
Go through every artifact object in the project definition of a native app, and execute processors in order of specification for each of the artifact object.
|
|
@@ -29,10 +29,10 @@ EnvVars = Mapping[str, str] # Only support str -> str for cross-platform compat
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class SandboxExecutionError(ClickException):
|
|
32
|
-
"""An error occurred while executing a
|
|
32
|
+
"""An error occurred while executing a Python script."""
|
|
33
33
|
|
|
34
34
|
def __init__(self, error: str):
|
|
35
|
-
super().__init__(f"Failed to execute
|
|
35
|
+
super().__init__(f"Failed to execute Python script. {error}")
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def _get_active_venv_dir() -> Optional[str]:
|
|
@@ -63,7 +63,7 @@ def _execute_python_interpreter(
|
|
|
63
63
|
env_vars: Optional[EnvVars],
|
|
64
64
|
) -> subprocess.CompletedProcess:
|
|
65
65
|
if not python_executable:
|
|
66
|
-
raise SandboxExecutionError("No
|
|
66
|
+
raise SandboxExecutionError("No Python executable found")
|
|
67
67
|
|
|
68
68
|
if isinstance(python_executable, str) or isinstance(python_executable, Path):
|
|
69
69
|
args = [python_executable]
|
|
@@ -106,7 +106,7 @@ def _execute_in_venv(
|
|
|
106
106
|
f"venv path must be an existing directory: {resolved_venv_path}"
|
|
107
107
|
)
|
|
108
108
|
|
|
109
|
-
# find the
|
|
109
|
+
# find the Python interpreter for this environment. There is no need to activate environment prior to invoking the
|
|
110
110
|
# interpreter, as venv maintains the invariant that invoking any of the scripts will set up the virtual environment
|
|
111
111
|
# correctly. activation scripts are only used for convenience in interactive shells.
|
|
112
112
|
if _is_ms_windows():
|
|
@@ -116,7 +116,7 @@ def _execute_in_venv(
|
|
|
116
116
|
|
|
117
117
|
if not python_executable.is_file():
|
|
118
118
|
raise SandboxExecutionError(
|
|
119
|
-
f"No venv
|
|
119
|
+
f"No venv Python executable found: {resolved_venv_path}"
|
|
120
120
|
)
|
|
121
121
|
|
|
122
122
|
return _execute_python_interpreter(
|
|
@@ -189,14 +189,14 @@ def execute_script_in_sandbox(
|
|
|
189
189
|
**kwargs,
|
|
190
190
|
) -> subprocess.CompletedProcess:
|
|
191
191
|
"""
|
|
192
|
-
Executes a
|
|
192
|
+
Executes a Python script in a sandboxed environment, and returns its output. The script is executed in a different
|
|
193
193
|
process. The execution environment is determined by the `env_type` argument. By default, the logic will attempt
|
|
194
194
|
to auto-detect the correct environment by looking for an active venv or conda environment. If none can be found, it
|
|
195
|
-
will use the system's default
|
|
196
|
-
|
|
195
|
+
will use the system's default Python executable, as determined by the user's path. As a last resort, the current
|
|
196
|
+
Python execution environment will be used (still in a subprocess).
|
|
197
197
|
|
|
198
198
|
Parameters:
|
|
199
|
-
script_source (str): The
|
|
199
|
+
script_source (str): The Python script to be executed, as a string.
|
|
200
200
|
env_type: The type of execution environment to use (default: ExecutionEnvironmentType.AUTO_DETECT).
|
|
201
201
|
cwd (Optional[Union[str, Path]]): An optional path to use as the current directory when executing the script.
|
|
202
202
|
timeout (Optional[int]): An optional timeout in seconds when executing the script. Defaults to no timeout.
|
|
@@ -248,7 +248,7 @@ def execute_script_in_sandbox(
|
|
|
248
248
|
class SandboxEnvBuilder(EnvBuilder):
|
|
249
249
|
"""
|
|
250
250
|
A virtual environment builder that can be used to build an environment suitable for
|
|
251
|
-
executing user-provided
|
|
251
|
+
executing user-provided Python scripts in an isolated sandbox.
|
|
252
252
|
"""
|
|
253
253
|
|
|
254
254
|
def __init__(self, path: Path, **kwargs) -> None:
|
|
@@ -167,14 +167,14 @@ class NativeAppSetupProcessor(ArtifactProcessor):
|
|
|
167
167
|
)
|
|
168
168
|
except Exception as e:
|
|
169
169
|
raise ClickException(
|
|
170
|
-
f"Exception while executing
|
|
170
|
+
f"Exception while executing Python setup script logic: {e}"
|
|
171
171
|
)
|
|
172
172
|
|
|
173
173
|
if result.returncode == 0:
|
|
174
174
|
return json.loads(result.stdout)
|
|
175
175
|
else:
|
|
176
176
|
raise ClickException(
|
|
177
|
-
f"Failed to execute
|
|
177
|
+
f"Failed to execute Python setup script logic: {result.stderr}"
|
|
178
178
|
)
|
|
179
179
|
|
|
180
180
|
def _edit_setup_sql(self, modifications: List[dict]) -> None:
|
|
@@ -194,7 +194,7 @@ def deannotate_module_source(
|
|
|
194
194
|
|
|
195
195
|
module_lines = module_source.splitlines()
|
|
196
196
|
for definition in definitions:
|
|
197
|
-
# Comment out all decorators. As per the
|
|
197
|
+
# Comment out all decorators. As per the Python grammar, decorators must be terminated by a
|
|
198
198
|
# new line, so the line ranges can't overlap.
|
|
199
199
|
for decorator in definition.decorator_list:
|
|
200
200
|
decorator_id = _get_decorator_id(decorator)
|
|
@@ -132,12 +132,12 @@ def _execute_in_sandbox(
|
|
|
132
132
|
)
|
|
133
133
|
except SandboxExecutionError as sdbx_err:
|
|
134
134
|
cc.warning(
|
|
135
|
-
f"Could not fetch Snowpark objects from {py_file} due to {sdbx_err}, continuing execution for the rest of the
|
|
135
|
+
f"Could not fetch Snowpark objects from {py_file} due to {sdbx_err}, continuing execution for the rest of the Python files."
|
|
136
136
|
)
|
|
137
137
|
return None
|
|
138
138
|
except Exception as err:
|
|
139
139
|
cc.warning(
|
|
140
|
-
f"Could not fetch Snowpark objects from {py_file} due to {err}, continuing execution for the rest of the
|
|
140
|
+
f"Could not fetch Snowpark objects from {py_file} due to {err}, continuing execution for the rest of the Python files."
|
|
141
141
|
)
|
|
142
142
|
return None
|
|
143
143
|
|
|
@@ -145,22 +145,22 @@ def _execute_in_sandbox(
|
|
|
145
145
|
cc.warning(
|
|
146
146
|
f"Could not fetch Snowpark objects from {py_file} due to the following error:\n {completed_process.stderr}"
|
|
147
147
|
)
|
|
148
|
-
cc.warning("Continuing execution for the rest of the
|
|
148
|
+
cc.warning("Continuing execution for the rest of the Python files.")
|
|
149
149
|
return None
|
|
150
150
|
|
|
151
151
|
try:
|
|
152
152
|
return json.loads(completed_process.stdout)
|
|
153
153
|
except Exception as exc:
|
|
154
154
|
cc.warning(
|
|
155
|
-
f"Could not load JSON into
|
|
155
|
+
f"Could not load JSON into Python due to the following exception: {exc}"
|
|
156
156
|
)
|
|
157
|
-
cc.warning(f"Continuing execution for the rest of the
|
|
157
|
+
cc.warning(f"Continuing execution for the rest of the Python files.")
|
|
158
158
|
return None
|
|
159
159
|
|
|
160
160
|
|
|
161
161
|
class SnowparkAnnotationProcessor(ArtifactProcessor):
|
|
162
162
|
"""
|
|
163
|
-
Built-in Processor to discover Snowpark-annotated objects in a given set of
|
|
163
|
+
Built-in Processor to discover Snowpark-annotated objects in a given set of Python files,
|
|
164
164
|
and generate SQL code for creation of extension functions based on those discovered objects.
|
|
165
165
|
"""
|
|
166
166
|
|
|
@@ -174,7 +174,7 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
|
|
|
174
174
|
**kwargs,
|
|
175
175
|
) -> None:
|
|
176
176
|
"""
|
|
177
|
-
Collects code annotations from Snowpark
|
|
177
|
+
Collects code annotations from Snowpark Python files containing extension functions and augments the existing
|
|
178
178
|
setup script with generated SQL that registers these functions.
|
|
179
179
|
"""
|
|
180
180
|
|
|
@@ -360,7 +360,7 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
|
|
|
360
360
|
|
|
361
361
|
def generate_new_sql_file_name(self, py_file: Path) -> Path:
|
|
362
362
|
"""
|
|
363
|
-
Generates a SQL filename for the generated root from the
|
|
363
|
+
Generates a SQL filename for the generated root from the Python file, and creates its parent directories.
|
|
364
364
|
"""
|
|
365
365
|
relative_py_file = py_file.relative_to(self._bundle_ctx.deploy_root)
|
|
366
366
|
sql_file = Path(
|