snowflake-cli 3.0.2__py3-none-any.whl → 3.2.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 +152 -99
- snowflake/cli/_plugins/connection/util.py +54 -9
- snowflake/cli/_plugins/cortex/manager.py +1 -1
- snowflake/cli/_plugins/git/commands.py +6 -3
- snowflake/cli/_plugins/git/manager.py +9 -4
- snowflake/cli/_plugins/nativeapp/artifacts.py +77 -13
- 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/codegen/templates/templates_processor.py +5 -3
- snowflake/cli/_plugins/nativeapp/commands.py +144 -188
- snowflake/cli/_plugins/nativeapp/constants.py +1 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +564 -351
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +583 -929
- snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
- snowflake/cli/_plugins/nativeapp/exceptions.py +12 -0
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
- snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
- snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
- snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
- snowflake/cli/_plugins/nativeapp/v2_conversions/{v2_to_v1_decorator.py → compat.py} +88 -117
- snowflake/cli/_plugins/nativeapp/version/commands.py +36 -32
- snowflake/cli/_plugins/notebook/manager.py +2 -2
- snowflake/cli/_plugins/object/commands.py +10 -1
- snowflake/cli/_plugins/object/manager.py +13 -5
- snowflake/cli/_plugins/snowpark/common.py +63 -21
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +3 -3
- snowflake/cli/_plugins/spcs/common.py +29 -0
- snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
- snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -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 +100 -17
- snowflake/cli/_plugins/spcs/services/manager.py +108 -16
- snowflake/cli/_plugins/sql/commands.py +9 -1
- snowflake/cli/_plugins/sql/manager.py +9 -4
- snowflake/cli/_plugins/stage/commands.py +28 -19
- snowflake/cli/_plugins/stage/diff.py +17 -17
- snowflake/cli/_plugins/stage/manager.py +304 -84
- snowflake/cli/_plugins/stage/md5.py +1 -1
- snowflake/cli/_plugins/streamlit/manager.py +5 -5
- snowflake/cli/_plugins/workspace/commands.py +27 -4
- snowflake/cli/_plugins/workspace/context.py +38 -0
- snowflake/cli/_plugins/workspace/manager.py +23 -13
- snowflake/cli/api/cli_global_context.py +4 -3
- snowflake/cli/api/commands/flags.py +23 -7
- snowflake/cli/api/config.py +30 -9
- snowflake/cli/api/connections.py +12 -1
- snowflake/cli/api/console/console.py +4 -19
- snowflake/cli/api/entities/common.py +4 -2
- snowflake/cli/api/entities/utils.py +36 -69
- snowflake/cli/api/errno.py +2 -0
- snowflake/cli/api/exceptions.py +41 -0
- snowflake/cli/api/identifiers.py +8 -0
- snowflake/cli/api/metrics.py +223 -7
- snowflake/cli/api/output/types.py +1 -1
- snowflake/cli/api/project/definition_conversion.py +293 -77
- snowflake/cli/api/project/schemas/entities/common.py +11 -0
- snowflake/cli/api/project/schemas/project_definition.py +30 -25
- snowflake/cli/api/rest_api.py +26 -4
- snowflake/cli/api/secure_utils.py +1 -1
- snowflake/cli/api/sql_execution.py +40 -29
- snowflake/cli/api/stage_path.py +244 -0
- snowflake/cli/api/utils/definition_rendering.py +3 -5
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +14 -15
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +78 -77
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
- snowflake/cli/_plugins/nativeapp/manager.py +0 -415
- snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
- snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +0 -70
- snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -98
- snowflake/cli/_plugins/workspace/action_context.py +0 -18
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -16,7 +16,7 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import inspect
|
|
18
18
|
from functools import wraps
|
|
19
|
-
from typing import
|
|
19
|
+
from typing import Optional, Type, TypeVar
|
|
20
20
|
|
|
21
21
|
import typer
|
|
22
22
|
from click import ClickException
|
|
@@ -29,35 +29,44 @@ from snowflake.cli.api.cli_global_context import (
|
|
|
29
29
|
get_cli_context_manager,
|
|
30
30
|
)
|
|
31
31
|
from snowflake.cli.api.commands.decorators import _options_decorator_factory
|
|
32
|
+
from snowflake.cli.api.project.definition_conversion import (
|
|
33
|
+
convert_project_definition_to_v2,
|
|
34
|
+
)
|
|
32
35
|
from snowflake.cli.api.project.schemas.entities.common import EntityModelBase
|
|
33
36
|
from snowflake.cli.api.project.schemas.project_definition import (
|
|
34
|
-
DefinitionV11,
|
|
35
37
|
DefinitionV20,
|
|
38
|
+
ProjectDefinition,
|
|
39
|
+
ProjectDefinitionV1,
|
|
36
40
|
)
|
|
37
|
-
from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import PathMapping
|
|
38
|
-
from snowflake.cli.api.utils.definition_rendering import render_definition_template
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _convert_v2_artifact_to_v1_dict(
|
|
42
|
-
v2_artifact: Union[PathMapping, str]
|
|
43
|
-
) -> Union[Dict, str]:
|
|
44
|
-
if isinstance(v2_artifact, PathMapping):
|
|
45
|
-
return {
|
|
46
|
-
"src": v2_artifact.src,
|
|
47
|
-
"dest": v2_artifact.dest,
|
|
48
|
-
"processors": v2_artifact.processors,
|
|
49
|
-
}
|
|
50
|
-
return v2_artifact
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
|
|
42
|
+
APP_AND_PACKAGE_OPTIONS = [
|
|
43
|
+
inspect.Parameter(
|
|
44
|
+
"package_entity_id",
|
|
45
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
46
|
+
annotation=Optional[str],
|
|
47
|
+
default=typer.Option(
|
|
48
|
+
default="",
|
|
49
|
+
help="The ID of the package entity on which to operate when definition_version is 2 or higher.",
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
inspect.Parameter(
|
|
53
|
+
"app_entity_id",
|
|
54
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
55
|
+
annotation=Optional[str],
|
|
56
|
+
default=typer.Option(
|
|
57
|
+
default="",
|
|
58
|
+
help="The ID of the application entity on which to operate when definition_version is 2 or higher.",
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _find_app_and_package_entities(
|
|
54
65
|
v2_definition: DefinitionV20,
|
|
55
|
-
package_entity_id: str
|
|
56
|
-
app_entity_id: str
|
|
57
|
-
app_required: bool
|
|
58
|
-
)
|
|
59
|
-
pdfv1: Dict[str, Any] = {"definition_version": "1.1", "native_app": {}}
|
|
60
|
-
|
|
66
|
+
package_entity_id: str,
|
|
67
|
+
app_entity_id: str,
|
|
68
|
+
app_required: bool,
|
|
69
|
+
):
|
|
61
70
|
# Determine the application entity to convert, there can be zero or one
|
|
62
71
|
app_definition = find_entity(
|
|
63
72
|
v2_definition,
|
|
@@ -66,7 +75,6 @@ def _pdf_v2_to_v1(
|
|
|
66
75
|
disambiguation_option="--app-entity-id",
|
|
67
76
|
required=app_required,
|
|
68
77
|
)
|
|
69
|
-
|
|
70
78
|
# Infer or verify the package if we have an app entity to convert
|
|
71
79
|
if app_definition:
|
|
72
80
|
target_package = app_definition.from_.target
|
|
@@ -87,7 +95,6 @@ def _pdf_v2_to_v1(
|
|
|
87
95
|
):
|
|
88
96
|
# If the user didn't target a specific package entity, use the one the app entity targets
|
|
89
97
|
package_entity_id = target_package
|
|
90
|
-
|
|
91
98
|
# Determine the package entity to convert, there must be one
|
|
92
99
|
app_package_definition = find_entity(
|
|
93
100
|
v2_definition,
|
|
@@ -97,63 +104,7 @@ def _pdf_v2_to_v1(
|
|
|
97
104
|
required=True,
|
|
98
105
|
)
|
|
99
106
|
assert app_package_definition is not None # satisfy mypy
|
|
100
|
-
|
|
101
|
-
# NativeApp
|
|
102
|
-
if app_definition and app_definition.fqn.identifier:
|
|
103
|
-
pdfv1["native_app"]["name"] = app_definition.fqn.identifier
|
|
104
|
-
else:
|
|
105
|
-
pdfv1["native_app"]["name"] = app_package_definition.fqn.identifier.split(
|
|
106
|
-
"_pkg_"
|
|
107
|
-
)[0]
|
|
108
|
-
pdfv1["native_app"]["artifacts"] = [
|
|
109
|
-
_convert_v2_artifact_to_v1_dict(a) for a in app_package_definition.artifacts
|
|
110
|
-
]
|
|
111
|
-
pdfv1["native_app"]["source_stage"] = app_package_definition.stage
|
|
112
|
-
pdfv1["native_app"]["bundle_root"] = app_package_definition.bundle_root
|
|
113
|
-
pdfv1["native_app"]["generated_root"] = app_package_definition.generated_root
|
|
114
|
-
pdfv1["native_app"]["deploy_root"] = app_package_definition.deploy_root
|
|
115
|
-
pdfv1["native_app"]["scratch_stage"] = app_package_definition.scratch_stage
|
|
116
|
-
|
|
117
|
-
# Package
|
|
118
|
-
pdfv1["native_app"]["package"] = {}
|
|
119
|
-
pdfv1["native_app"]["package"]["name"] = app_package_definition.fqn.identifier
|
|
120
|
-
if app_package_definition.distribution:
|
|
121
|
-
pdfv1["native_app"]["package"][
|
|
122
|
-
"distribution"
|
|
123
|
-
] = app_package_definition.distribution
|
|
124
|
-
if app_package_definition.meta and app_package_definition.meta.post_deploy:
|
|
125
|
-
pdfv1["native_app"]["package"][
|
|
126
|
-
"post_deploy"
|
|
127
|
-
] = app_package_definition.meta.post_deploy
|
|
128
|
-
if app_package_definition.meta:
|
|
129
|
-
if app_package_definition.meta.role:
|
|
130
|
-
pdfv1["native_app"]["package"]["role"] = app_package_definition.meta.role
|
|
131
|
-
if app_package_definition.meta.warehouse:
|
|
132
|
-
pdfv1["native_app"]["package"][
|
|
133
|
-
"warehouse"
|
|
134
|
-
] = app_package_definition.meta.warehouse
|
|
135
|
-
|
|
136
|
-
# Application
|
|
137
|
-
if app_definition:
|
|
138
|
-
pdfv1["native_app"]["application"] = {}
|
|
139
|
-
pdfv1["native_app"]["application"]["name"] = app_definition.fqn.identifier
|
|
140
|
-
if app_definition.debug:
|
|
141
|
-
pdfv1["native_app"]["application"]["debug"] = app_definition.debug
|
|
142
|
-
if app_definition.meta:
|
|
143
|
-
if app_definition.meta.role:
|
|
144
|
-
pdfv1["native_app"]["application"]["role"] = app_definition.meta.role
|
|
145
|
-
if app_definition.meta.warehouse:
|
|
146
|
-
pdfv1["native_app"]["application"][
|
|
147
|
-
"warehouse"
|
|
148
|
-
] = app_definition.meta.warehouse
|
|
149
|
-
if app_definition.meta.post_deploy:
|
|
150
|
-
pdfv1["native_app"]["application"][
|
|
151
|
-
"post_deploy"
|
|
152
|
-
] = app_definition.meta.post_deploy
|
|
153
|
-
|
|
154
|
-
result = render_definition_template(pdfv1, {})
|
|
155
|
-
# Override the definition object in global context
|
|
156
|
-
return result.project_definition
|
|
107
|
+
return app_definition, app_package_definition
|
|
157
108
|
|
|
158
109
|
|
|
159
110
|
T = TypeVar("T", bound=EntityModelBase)
|
|
@@ -209,54 +160,74 @@ def find_entity(
|
|
|
209
160
|
return entity
|
|
210
161
|
|
|
211
162
|
|
|
212
|
-
def
|
|
163
|
+
def force_project_definition_v2(
|
|
164
|
+
*, single_app_and_package: bool = True, app_required: bool = False
|
|
165
|
+
):
|
|
213
166
|
"""
|
|
214
|
-
A command decorator that
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
167
|
+
A command decorator that forces the project definition to be converted to v2.
|
|
168
|
+
|
|
169
|
+
If a v1 definition is found, it is converted to v2 in-memory and the global context
|
|
170
|
+
is updated with the new definition object.
|
|
171
|
+
|
|
172
|
+
If a v2 definition is already found, it is used as-is, optionally limiting the number
|
|
173
|
+
of application and package entities to one each (true by default).
|
|
174
|
+
|
|
175
|
+
Assumes with_project_definition() has already been called.
|
|
219
176
|
"""
|
|
220
177
|
|
|
221
178
|
def decorator(func):
|
|
222
179
|
@wraps(func)
|
|
223
180
|
def wrapper(*args, **kwargs):
|
|
224
|
-
|
|
181
|
+
cli_context = get_cli_context()
|
|
182
|
+
original_pdf: Optional[ProjectDefinition] = cli_context.project_definition
|
|
225
183
|
if not original_pdf:
|
|
226
184
|
raise ValueError(
|
|
227
|
-
"Project definition could not be found.
|
|
185
|
+
"Project definition could not be found. "
|
|
186
|
+
"The single_app_and_package() command decorator assumes "
|
|
187
|
+
"that with_project_definition() was called before it."
|
|
228
188
|
)
|
|
229
|
-
if original_pdf
|
|
189
|
+
if isinstance(original_pdf, ProjectDefinitionV1):
|
|
190
|
+
pdfv2 = convert_project_definition_to_v2(
|
|
191
|
+
cli_context.project_root,
|
|
192
|
+
original_pdf,
|
|
193
|
+
accept_templates=False, # Templates should all be rendered by now
|
|
194
|
+
template_context=None, # Force inclusion of all fields
|
|
195
|
+
in_memory=True, # Convert the definition knowing it will be used immediately
|
|
196
|
+
)
|
|
197
|
+
for entity_id, entity in pdfv2.entities.items():
|
|
198
|
+
# Backfill kwargs for the command to use,
|
|
199
|
+
# there can only be one entity of each type
|
|
200
|
+
is_package = isinstance(entity, ApplicationPackageEntityModel)
|
|
201
|
+
key = "package_entity_id" if is_package else "app_entity_id"
|
|
202
|
+
kwargs[key] = entity_id
|
|
203
|
+
|
|
204
|
+
cm = get_cli_context_manager()
|
|
205
|
+
|
|
206
|
+
# Override the project definition so that the command operates on the new entities
|
|
207
|
+
cm.override_project_definition = pdfv2
|
|
208
|
+
elif single_app_and_package:
|
|
230
209
|
package_entity_id = kwargs.get("package_entity_id", "")
|
|
231
210
|
app_entity_id = kwargs.get("app_entity_id", "")
|
|
232
|
-
|
|
211
|
+
app_definition, app_package_definition = _find_app_and_package_entities(
|
|
233
212
|
original_pdf, package_entity_id, app_entity_id, app_required
|
|
234
213
|
)
|
|
235
|
-
|
|
214
|
+
entities_to_keep = {app_package_definition.entity_id}
|
|
215
|
+
kwargs["package_entity_id"] = app_package_definition.entity_id
|
|
216
|
+
if app_definition:
|
|
217
|
+
entities_to_keep.add(app_definition.entity_id)
|
|
218
|
+
kwargs["app_entity_id"] = app_definition.entity_id
|
|
219
|
+
for entity_id in list(original_pdf.entities):
|
|
220
|
+
if entity_id not in entities_to_keep:
|
|
221
|
+
# This happens after templates are rendered,
|
|
222
|
+
# so we can safely remove the entity
|
|
223
|
+
del original_pdf.entities[entity_id]
|
|
236
224
|
return func(*args, **kwargs)
|
|
237
225
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
annotation=Optional[str],
|
|
245
|
-
default=typer.Option(
|
|
246
|
-
default="",
|
|
247
|
-
help="The ID of the package entity on which to operate when definition_version is 2 or higher.",
|
|
248
|
-
),
|
|
249
|
-
),
|
|
250
|
-
inspect.Parameter(
|
|
251
|
-
"app_entity_id",
|
|
252
|
-
inspect.Parameter.KEYWORD_ONLY,
|
|
253
|
-
annotation=Optional[str],
|
|
254
|
-
default=typer.Option(
|
|
255
|
-
default="",
|
|
256
|
-
help="The ID of the application entity on which to operate when definition_version is 2 or higher.",
|
|
257
|
-
),
|
|
258
|
-
),
|
|
259
|
-
],
|
|
260
|
-
)
|
|
226
|
+
if single_app_and_package:
|
|
227
|
+
# Add the --app-entity-id and --package-entity-id options to the command
|
|
228
|
+
return _options_decorator_factory(
|
|
229
|
+
wrapper, additional_options=APP_AND_PACKAGE_OPTIONS
|
|
230
|
+
)
|
|
231
|
+
return wrapper
|
|
261
232
|
|
|
262
233
|
return decorator
|
|
@@ -18,23 +18,18 @@ import logging
|
|
|
18
18
|
from typing import Optional
|
|
19
19
|
|
|
20
20
|
import typer
|
|
21
|
-
from click import MissingParameter
|
|
22
21
|
from snowflake.cli._plugins.nativeapp.common_flags import ForceOption, InteractiveOption
|
|
23
|
-
from snowflake.cli._plugins.nativeapp.
|
|
24
|
-
|
|
25
|
-
nativeapp_definition_v2_to_v1,
|
|
26
|
-
)
|
|
27
|
-
from snowflake.cli._plugins.nativeapp.version.version_processor import (
|
|
28
|
-
NativeAppVersionCreateProcessor,
|
|
29
|
-
NativeAppVersionDropProcessor,
|
|
22
|
+
from snowflake.cli._plugins.nativeapp.v2_conversions.compat import (
|
|
23
|
+
force_project_definition_v2,
|
|
30
24
|
)
|
|
25
|
+
from snowflake.cli._plugins.workspace.manager import WorkspaceManager
|
|
31
26
|
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
32
27
|
from snowflake.cli.api.commands.decorators import (
|
|
33
28
|
with_project_definition,
|
|
34
29
|
)
|
|
35
30
|
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
31
|
+
from snowflake.cli.api.entities.common import EntityActions
|
|
36
32
|
from snowflake.cli.api.output.types import CommandResult, MessageResult, QueryResult
|
|
37
|
-
from snowflake.cli.api.project.project_verification import assert_project_type
|
|
38
33
|
|
|
39
34
|
app = SnowTyperFactory(
|
|
40
35
|
name="version",
|
|
@@ -46,7 +41,7 @@ log = logging.getLogger(__name__)
|
|
|
46
41
|
|
|
47
42
|
@app.command(requires_connection=True)
|
|
48
43
|
@with_project_definition()
|
|
49
|
-
@
|
|
44
|
+
@force_project_definition_v2()
|
|
50
45
|
def create(
|
|
51
46
|
version: Optional[str] = typer.Argument(
|
|
52
47
|
None,
|
|
@@ -58,6 +53,11 @@ def create(
|
|
|
58
53
|
help=f"""The patch number you want to create for an existing version.
|
|
59
54
|
Defaults to undefined if it is not set, which means the Snowflake CLI either uses the patch specified in the `manifest.yml` file or automatically generates a new patch number.""",
|
|
60
55
|
),
|
|
56
|
+
label: Optional[str] = typer.Option(
|
|
57
|
+
None,
|
|
58
|
+
"--label",
|
|
59
|
+
help="A label for the version that is displayed to consumers. If unset, the version label specified in `manifest.yml` file is used.",
|
|
60
|
+
),
|
|
61
61
|
skip_git_check: Optional[bool] = typer.Option(
|
|
62
62
|
False,
|
|
63
63
|
"--skip-git-check",
|
|
@@ -72,19 +72,18 @@ def create(
|
|
|
72
72
|
Adds a new patch to the provided version defined in your application package. If the version does not exist, creates a version with patch 0.
|
|
73
73
|
"""
|
|
74
74
|
|
|
75
|
-
assert_project_type("native_app")
|
|
76
|
-
|
|
77
|
-
if version is None and patch is not None:
|
|
78
|
-
raise MissingParameter("Cannot provide a patch without version!")
|
|
79
|
-
|
|
80
75
|
cli_context = get_cli_context()
|
|
81
|
-
|
|
82
|
-
project_definition=cli_context.project_definition
|
|
76
|
+
ws = WorkspaceManager(
|
|
77
|
+
project_definition=cli_context.project_definition,
|
|
83
78
|
project_root=cli_context.project_root,
|
|
84
79
|
)
|
|
85
|
-
|
|
80
|
+
package_id = options["package_entity_id"]
|
|
81
|
+
ws.perform_action(
|
|
82
|
+
package_id,
|
|
83
|
+
EntityActions.VERSION_CREATE,
|
|
86
84
|
version=version,
|
|
87
85
|
patch=patch,
|
|
86
|
+
label=label,
|
|
88
87
|
force=force,
|
|
89
88
|
interactive=interactive,
|
|
90
89
|
skip_git_check=skip_git_check,
|
|
@@ -94,28 +93,29 @@ def create(
|
|
|
94
93
|
|
|
95
94
|
@app.command("list", requires_connection=True)
|
|
96
95
|
@with_project_definition()
|
|
97
|
-
@
|
|
96
|
+
@force_project_definition_v2()
|
|
98
97
|
def version_list(
|
|
99
98
|
**options,
|
|
100
99
|
) -> CommandResult:
|
|
101
100
|
"""
|
|
102
101
|
Lists all versions defined in an application package.
|
|
103
102
|
"""
|
|
104
|
-
|
|
105
|
-
assert_project_type("native_app")
|
|
106
|
-
|
|
107
103
|
cli_context = get_cli_context()
|
|
108
|
-
|
|
109
|
-
project_definition=cli_context.project_definition
|
|
104
|
+
ws = WorkspaceManager(
|
|
105
|
+
project_definition=cli_context.project_definition,
|
|
110
106
|
project_root=cli_context.project_root,
|
|
111
107
|
)
|
|
112
|
-
|
|
108
|
+
package_id = options["package_entity_id"]
|
|
109
|
+
cursor = ws.perform_action(
|
|
110
|
+
package_id,
|
|
111
|
+
EntityActions.VERSION_LIST,
|
|
112
|
+
)
|
|
113
113
|
return QueryResult(cursor)
|
|
114
114
|
|
|
115
115
|
|
|
116
116
|
@app.command(requires_connection=True)
|
|
117
117
|
@with_project_definition()
|
|
118
|
-
@
|
|
118
|
+
@force_project_definition_v2()
|
|
119
119
|
def drop(
|
|
120
120
|
version: Optional[str] = typer.Argument(
|
|
121
121
|
None,
|
|
@@ -129,13 +129,17 @@ def drop(
|
|
|
129
129
|
Drops a version defined in your application package. Versions can either be passed in as an argument to the command or read from the `manifest.yml` file.
|
|
130
130
|
Dropping patches is not allowed.
|
|
131
131
|
"""
|
|
132
|
-
|
|
133
|
-
assert_project_type("native_app")
|
|
134
|
-
|
|
135
132
|
cli_context = get_cli_context()
|
|
136
|
-
|
|
137
|
-
project_definition=cli_context.project_definition
|
|
133
|
+
ws = WorkspaceManager(
|
|
134
|
+
project_definition=cli_context.project_definition,
|
|
138
135
|
project_root=cli_context.project_root,
|
|
139
136
|
)
|
|
140
|
-
|
|
137
|
+
package_id = options["package_entity_id"]
|
|
138
|
+
ws.perform_action(
|
|
139
|
+
package_id,
|
|
140
|
+
EntityActions.VERSION_DROP,
|
|
141
|
+
version=version,
|
|
142
|
+
interactive=interactive,
|
|
143
|
+
force=force,
|
|
144
|
+
)
|
|
141
145
|
return MessageResult(f"Version drop is now complete.")
|
|
@@ -26,7 +26,7 @@ from snowflake.cli.api.sql_execution import SqlExecutionMixin
|
|
|
26
26
|
class NotebookManager(SqlExecutionMixin):
|
|
27
27
|
def execute(self, notebook_name: FQN):
|
|
28
28
|
query = f"EXECUTE NOTEBOOK {notebook_name.sql_identifier}()"
|
|
29
|
-
return self.
|
|
29
|
+
return self.execute_query(query=query)
|
|
30
30
|
|
|
31
31
|
def get_url(self, notebook_name: FQN):
|
|
32
32
|
fqn = notebook_name.using_connection(self._conn)
|
|
@@ -64,7 +64,7 @@ class NotebookManager(SqlExecutionMixin):
|
|
|
64
64
|
ALTER NOTEBOOK {notebook_fqn.identifier} ADD LIVE VERSION FROM LAST;
|
|
65
65
|
"""
|
|
66
66
|
)
|
|
67
|
-
self.
|
|
67
|
+
self.execute_queries(queries=queries)
|
|
68
68
|
|
|
69
69
|
return make_snowsight_url(
|
|
70
70
|
self._conn, f"/#/notebooks/{notebook_fqn.url_identifier}"
|
|
@@ -21,6 +21,8 @@ from click import ClickException
|
|
|
21
21
|
from snowflake.cli._plugins.object.manager import ObjectManager
|
|
22
22
|
from snowflake.cli.api.commands.flags import (
|
|
23
23
|
IdentifierType,
|
|
24
|
+
IfNotExistsOption,
|
|
25
|
+
ReplaceOption,
|
|
24
26
|
like_option,
|
|
25
27
|
)
|
|
26
28
|
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
@@ -145,6 +147,8 @@ def create(
|
|
|
145
147
|
object_type: str = ObjectArgument,
|
|
146
148
|
object_attributes: Optional[List[str]] = ObjectAttributesArgument,
|
|
147
149
|
object_json: str = ObjectDefinitionJsonOption,
|
|
150
|
+
if_not_exists: bool = IfNotExistsOption(),
|
|
151
|
+
replace: bool = ReplaceOption(),
|
|
148
152
|
**options,
|
|
149
153
|
):
|
|
150
154
|
"""
|
|
@@ -176,5 +180,10 @@ def create(
|
|
|
176
180
|
"Provide either list of object attributes, or object definition in JSON format"
|
|
177
181
|
)
|
|
178
182
|
|
|
179
|
-
result = ObjectManager().create(
|
|
183
|
+
result = ObjectManager().create(
|
|
184
|
+
object_type=object_type,
|
|
185
|
+
object_data=object_data,
|
|
186
|
+
if_not_exists=if_not_exists,
|
|
187
|
+
replace=replace,
|
|
188
|
+
)
|
|
180
189
|
return MessageResult(result)
|
|
@@ -52,11 +52,11 @@ class ObjectManager(SqlExecutionMixin):
|
|
|
52
52
|
query += f" like '{like}'"
|
|
53
53
|
if scope[0] is not None:
|
|
54
54
|
query += f" in {scope[0].replace('-', ' ')} {scope[1]}"
|
|
55
|
-
return self.
|
|
55
|
+
return self.execute_query(query, **kwargs)
|
|
56
56
|
|
|
57
57
|
def drop(self, *, object_type: str, fqn: FQN) -> SnowflakeCursor:
|
|
58
58
|
object_name = _get_object_names(object_type).sf_name
|
|
59
|
-
return self.
|
|
59
|
+
return self.execute_query(f"drop {object_name} {fqn.sql_identifier}")
|
|
60
60
|
|
|
61
61
|
def describe(self, *, object_type: str, fqn: FQN):
|
|
62
62
|
# Image repository is the only supported object that does not have a DESCRIBE command.
|
|
@@ -65,7 +65,7 @@ class ObjectManager(SqlExecutionMixin):
|
|
|
65
65
|
f"Describe is currently not supported for object of type image-repository"
|
|
66
66
|
)
|
|
67
67
|
object_name = _get_object_names(object_type).sf_name
|
|
68
|
-
return self.
|
|
68
|
+
return self.execute_query(f"describe {object_name} {fqn.sql_identifier}")
|
|
69
69
|
|
|
70
70
|
def object_exists(self, *, object_type: str, fqn: FQN):
|
|
71
71
|
try:
|
|
@@ -74,9 +74,17 @@ class ObjectManager(SqlExecutionMixin):
|
|
|
74
74
|
except ProgrammingError:
|
|
75
75
|
return False
|
|
76
76
|
|
|
77
|
-
def create(
|
|
77
|
+
def create(
|
|
78
|
+
self,
|
|
79
|
+
object_type: str,
|
|
80
|
+
object_data: Dict[str, Any],
|
|
81
|
+
replace: bool = False,
|
|
82
|
+
if_not_exists: bool = False,
|
|
83
|
+
) -> str:
|
|
78
84
|
rest = RestApi(self._conn)
|
|
79
|
-
url = rest.determine_url_for_create_query(
|
|
85
|
+
url = rest.determine_url_for_create_query(
|
|
86
|
+
object_type=object_type, replace=replace, if_not_exists=if_not_exists
|
|
87
|
+
)
|
|
80
88
|
try:
|
|
81
89
|
response = rest.send_rest_request(url=url, method="post", data=object_data)
|
|
82
90
|
except Exception as err:
|
|
@@ -58,9 +58,9 @@ class SnowparkObjectManager(SqlExecutionMixin):
|
|
|
58
58
|
self, execution_identifier: str, object_type: SnowparkObject
|
|
59
59
|
) -> SnowflakeCursor:
|
|
60
60
|
if object_type == SnowparkObject.FUNCTION:
|
|
61
|
-
return self.
|
|
61
|
+
return self.execute_query(f"select {execution_identifier}")
|
|
62
62
|
if object_type == SnowparkObject.PROCEDURE:
|
|
63
|
-
return self.
|
|
63
|
+
return self.execute_query(f"call {execution_identifier}")
|
|
64
64
|
raise UsageError(f"Unknown object type: {object_type}.")
|
|
65
65
|
|
|
66
66
|
def create_or_replace(
|
|
@@ -95,7 +95,7 @@ class SnowparkObjectManager(SqlExecutionMixin):
|
|
|
95
95
|
if isinstance(entity, ProcedureEntityModel) and entity.execute_as_caller:
|
|
96
96
|
query.append("execute as caller")
|
|
97
97
|
|
|
98
|
-
return self.
|
|
98
|
+
return self.execute_query("\n".join(query))
|
|
99
99
|
|
|
100
100
|
def deploy_entity(
|
|
101
101
|
self,
|
|
@@ -158,10 +158,8 @@ def _check_if_replace_is_required(
|
|
|
158
158
|
)
|
|
159
159
|
return True
|
|
160
160
|
|
|
161
|
-
if (
|
|
162
|
-
resource_json["
|
|
163
|
-
or _sql_to_python_return_type_mapper(resource_json["returns"]).lower()
|
|
164
|
-
!= entity.returns.lower()
|
|
161
|
+
if resource_json["handler"].lower() != entity.handler.lower() or not same_type(
|
|
162
|
+
resource_json["returns"], entity.returns
|
|
165
163
|
):
|
|
166
164
|
log.info(
|
|
167
165
|
"Return type or handler types do not match. Replacing the %s.", object_type
|
|
@@ -216,24 +214,68 @@ def _snowflake_dependencies_differ(
|
|
|
216
214
|
return _standardize(old_dependencies) != _standardize(new_dependencies)
|
|
217
215
|
|
|
218
216
|
|
|
219
|
-
def
|
|
220
|
-
|
|
221
|
-
Some of the Python data types get converted to SQL types, when function/procedure is created.
|
|
222
|
-
So, to properly compare types, we use mapping based on:
|
|
223
|
-
https://docs.snowflake.com/en/developer-guide/udf-stored-procedure-data-type-mapping#sql-python-data-type-mappings
|
|
217
|
+
def same_type(sf_type: str, local_type: str) -> bool:
|
|
218
|
+
sf_type, local_type = sf_type.upper(), local_type.upper()
|
|
224
219
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
220
|
+
# 1. Types are equal out of the box
|
|
221
|
+
if sf_type == local_type:
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
# 2. Local type is alias for Snowflake type
|
|
225
|
+
local_type = user_to_sql_type_mapper(local_type).upper()
|
|
226
|
+
if sf_type == local_type:
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
# 3. Local type is a subset of Snowflake type, e.g. VARCHAR(N) == VARCHAR
|
|
230
|
+
# We solved for local VARCHAR(N) in point 1 & 2 as those are explicit types
|
|
231
|
+
if sf_type.startswith(local_type):
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
# 4. Snowflake types is subset of local type
|
|
235
|
+
if local_type.startswith(sf_type):
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def user_to_sql_type_mapper(user_provided_type: str) -> str:
|
|
229
242
|
mapping = {
|
|
230
|
-
"
|
|
231
|
-
"
|
|
232
|
-
"
|
|
233
|
-
|
|
243
|
+
("VARCHAR", "(16777216)"): ("CHAR", "TEXT", "STRING"),
|
|
244
|
+
("BINARY", "(8388608)"): ("BINARY", "VARBINARY"),
|
|
245
|
+
("NUMBER", "(38,0)"): (
|
|
246
|
+
"NUMBER",
|
|
247
|
+
"DECIMAL",
|
|
248
|
+
"INT",
|
|
249
|
+
"INTEGER",
|
|
250
|
+
"BIGINT",
|
|
251
|
+
"SMALLINT",
|
|
252
|
+
"TINYINT",
|
|
253
|
+
"BYTEINT",
|
|
254
|
+
),
|
|
255
|
+
("FLOAT", ""): (
|
|
256
|
+
"FLOAT",
|
|
257
|
+
"DOUBLE",
|
|
258
|
+
"DOUBLE PRECISION",
|
|
259
|
+
"REAL",
|
|
260
|
+
"FLOAT",
|
|
261
|
+
"FLOAT4",
|
|
262
|
+
"FLOAT8",
|
|
263
|
+
),
|
|
264
|
+
("TIMESTAMP_NTZ", ""): ("TIMESTAMP_NTZ", "TIMESTAMPNTZ", "DATETIME"),
|
|
265
|
+
("TIMESTAMP_LTZ", ""): ("TIMESTAMP_LTZ", "TIMESTAMPLTZ"),
|
|
266
|
+
("TIMESTAMP_TZ", ""): ("TIMESTAMP_TZ", "TIMESTAMPTZ"),
|
|
234
267
|
}
|
|
235
268
|
|
|
236
|
-
|
|
269
|
+
user_provided_type = user_provided_type.upper()
|
|
270
|
+
for (cast_type, default), matching_types in mapping.items():
|
|
271
|
+
for type_ in matching_types:
|
|
272
|
+
if user_provided_type == type_:
|
|
273
|
+
# TEXT -> VARCHAR(16777216)
|
|
274
|
+
return cast_type + default
|
|
275
|
+
if user_provided_type.startswith(type_):
|
|
276
|
+
# TEXT(30) -> VARCHAR(30)
|
|
277
|
+
return user_provided_type.replace(type_, cast_type + default)
|
|
278
|
+
return user_provided_type
|
|
237
279
|
|
|
238
280
|
|
|
239
281
|
def _compare_imports(
|
|
@@ -169,7 +169,7 @@ class AnacondaPackagesManager(SqlExecutionMixin):
|
|
|
169
169
|
|
|
170
170
|
def find_packages_available_in_snowflake_anaconda(self) -> AnacondaPackages:
|
|
171
171
|
"""
|
|
172
|
-
Finds
|
|
172
|
+
Finds Python packages available in Snowflake to use in functions and stored procedures.
|
|
173
173
|
It tries to get the list of packages using SQL query
|
|
174
174
|
but if the try fails then the fallback is to parse JSON containing info about Snowflake's Anaconda channel.
|
|
175
175
|
"""
|
|
@@ -177,8 +177,8 @@ class AnacondaPackagesManager(SqlExecutionMixin):
|
|
|
177
177
|
return AnacondaPackages(packages)
|
|
178
178
|
|
|
179
179
|
def _query_snowflake_for_available_packages(self) -> dict[str, AvailablePackage]:
|
|
180
|
-
cursor = self.
|
|
181
|
-
"select package_name, version from information_schema.packages where language = 'python'",
|
|
180
|
+
cursor = self.execute_query(
|
|
181
|
+
"select package_name, version from snowflake.information_schema.packages where language = 'python'",
|
|
182
182
|
cursor_class=DictCursor,
|
|
183
183
|
)
|
|
184
184
|
if cursor.rowcount is None or cursor.rowcount == 0:
|
|
@@ -95,5 +95,34 @@ def handle_object_already_exists(
|
|
|
95
95
|
raise error
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
def filter_log_timestamp(log: str, include_timestamps: bool) -> str:
|
|
99
|
+
if include_timestamps:
|
|
100
|
+
return log
|
|
101
|
+
else:
|
|
102
|
+
return log.split(" ", 1)[1] if " " in log else log
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def new_logs_only(prev_log_records: list[str], new_log_records: list[str]) -> list[str]:
|
|
106
|
+
# Sort the log records, we get time-ordered logs
|
|
107
|
+
# due to ISO 8601 timestamp format in the log content
|
|
108
|
+
# eg: 2024-10-22T01:12:29.873896187Z Count: 1
|
|
109
|
+
new_log_records_sorted = sorted(new_log_records)
|
|
110
|
+
|
|
111
|
+
# Get the first new log record to establish the overlap point
|
|
112
|
+
first_new_log_record = new_log_records_sorted[0]
|
|
113
|
+
|
|
114
|
+
# Traverse previous logs in reverse and remove duplicates from new logs
|
|
115
|
+
for prev_log in reversed(prev_log_records):
|
|
116
|
+
# Stop if the previous log is earlier than the first new log
|
|
117
|
+
if prev_log < first_new_log_record:
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
# Remove matching previous logs from the new logs list
|
|
121
|
+
if prev_log in new_log_records_sorted:
|
|
122
|
+
new_log_records_sorted.remove(prev_log)
|
|
123
|
+
|
|
124
|
+
return new_log_records_sorted
|
|
125
|
+
|
|
126
|
+
|
|
98
127
|
class NoPropertiesProvidedError(ClickException):
|
|
99
128
|
pass
|