snowflake-cli-labs 2.8.0rc0__py3-none-any.whl → 2.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/api/commands/flags.py +26 -4
- snowflake/cli/api/identifiers.py +17 -4
- snowflake/cli/api/project/errors.py +16 -1
- snowflake/cli/api/sql_execution.py +4 -4
- snowflake/cli/plugins/git/commands.py +68 -19
- snowflake/cli/plugins/git/manager.py +19 -10
- snowflake/cli/plugins/init/commands.py +8 -4
- snowflake/cli/plugins/nativeapp/manager.py +17 -13
- snowflake/cli/plugins/notebook/commands.py +6 -5
- snowflake/cli/plugins/notebook/manager.py +10 -10
- snowflake/cli/plugins/notebook/types.py +0 -1
- snowflake/cli/plugins/object/command_aliases.py +3 -2
- snowflake/cli/plugins/object/commands.py +13 -6
- snowflake/cli/plugins/object/manager.py +7 -6
- snowflake/cli/plugins/snowpark/commands.py +4 -6
- snowflake/cli/plugins/snowpark/models.py +2 -1
- snowflake/cli/plugins/snowpark/package/manager.py +2 -1
- snowflake/cli/plugins/spcs/compute_pool/commands.py +21 -20
- snowflake/cli/plugins/spcs/image_repository/commands.py +19 -13
- snowflake/cli/plugins/spcs/services/commands.py +23 -22
- snowflake/cli/plugins/stage/commands.py +7 -5
- snowflake/cli/plugins/stage/manager.py +51 -18
- snowflake/cli/plugins/streamlit/commands.py +7 -14
- snowflake/cli/plugins/streamlit/manager.py +1 -1
- {snowflake_cli_labs-2.8.0rc0.dist-info → snowflake_cli_labs-2.8.1.dist-info}/METADATA +1 -1
- {snowflake_cli_labs-2.8.0rc0.dist-info → snowflake_cli_labs-2.8.1.dist-info}/RECORD +30 -30
- {snowflake_cli_labs-2.8.0rc0.dist-info → snowflake_cli_labs-2.8.1.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-2.8.0rc0.dist-info → snowflake_cli_labs-2.8.1.dist-info}/entry_points.txt +0 -0
- {snowflake_cli_labs-2.8.0rc0.dist-info → snowflake_cli_labs-2.8.1.dist-info}/licenses/LICENSE +0 -0
snowflake/cli/__about__.py
CHANGED
|
@@ -28,6 +28,7 @@ from snowflake.cli.api.cli_global_context import cli_context_manager
|
|
|
28
28
|
from snowflake.cli.api.commands.typer_pre_execute import register_pre_execute_command
|
|
29
29
|
from snowflake.cli.api.console import cli_console
|
|
30
30
|
from snowflake.cli.api.exceptions import MissingConfiguration
|
|
31
|
+
from snowflake.cli.api.identifiers import FQN
|
|
31
32
|
from snowflake.cli.api.output.formats import OutputFormat
|
|
32
33
|
from snowflake.cli.api.project.definition_manager import DefinitionManager
|
|
33
34
|
from snowflake.cli.api.rendering.jinja import CONTEXT_KEY
|
|
@@ -350,13 +351,23 @@ EnableDiagOption = typer.Option(
|
|
|
350
351
|
rich_help_panel=_CONNECTION_SECTION,
|
|
351
352
|
)
|
|
352
353
|
|
|
354
|
+
# Set default via callback to avoid including tempdir path in generated docs (snow --docs).
|
|
355
|
+
# Use constant instead of None, as None is removed from telemetry data.
|
|
356
|
+
_DIAG_LOG_DEFAULT_VALUE = "<temporary_directory>"
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _diag_log_path_callback(path: str):
|
|
360
|
+
if path == _DIAG_LOG_DEFAULT_VALUE:
|
|
361
|
+
path = tempfile.gettempdir()
|
|
362
|
+
cli_context_manager.connection_context.set_diag_log_path(Path(path))
|
|
363
|
+
return path
|
|
364
|
+
|
|
365
|
+
|
|
353
366
|
DiagLogPathOption: Path = typer.Option(
|
|
354
367
|
tempfile.gettempdir(),
|
|
355
368
|
"--diag-log-path",
|
|
356
369
|
help="Diagnostic report path",
|
|
357
|
-
callback=
|
|
358
|
-
lambda: cli_context_manager.connection_context.set_diag_log_path
|
|
359
|
-
),
|
|
370
|
+
callback=_diag_log_path_callback,
|
|
360
371
|
show_default=False,
|
|
361
372
|
rich_help_panel=_CONNECTION_SECTION,
|
|
362
373
|
exists=True,
|
|
@@ -514,11 +525,15 @@ def experimental_option(
|
|
|
514
525
|
)
|
|
515
526
|
|
|
516
527
|
|
|
517
|
-
def identifier_argument(
|
|
528
|
+
def identifier_argument(
|
|
529
|
+
sf_object: str, example: str, callback: Callable | None = None
|
|
530
|
+
) -> typer.Argument:
|
|
518
531
|
return typer.Argument(
|
|
519
532
|
...,
|
|
520
533
|
help=f"Identifier of the {sf_object}. For example: {example}",
|
|
521
534
|
show_default=False,
|
|
535
|
+
click_type=IdentifierType(),
|
|
536
|
+
callback=callback,
|
|
522
537
|
)
|
|
523
538
|
|
|
524
539
|
|
|
@@ -638,3 +653,10 @@ def parse_key_value_variables(variables: Optional[List[str]]) -> List[Variable]:
|
|
|
638
653
|
key, value = p.split("=", 1)
|
|
639
654
|
result.append(Variable(key.strip(), value.strip()))
|
|
640
655
|
return result
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
class IdentifierType(click.ParamType):
|
|
659
|
+
name = "TEXT"
|
|
660
|
+
|
|
661
|
+
def convert(self, value, param, ctx):
|
|
662
|
+
return FQN.from_string(value)
|
snowflake/cli/api/identifiers.py
CHANGED
|
@@ -35,10 +35,17 @@ class FQN:
|
|
|
35
35
|
fqn = FQN.from_string("my_name").set_database("db").set_schema("foo")
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
|
-
def __init__(
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
database: str | None,
|
|
41
|
+
schema: str | None,
|
|
42
|
+
name: str,
|
|
43
|
+
signature: str | None = None,
|
|
44
|
+
):
|
|
39
45
|
self._database = database
|
|
40
46
|
self._schema = schema
|
|
41
47
|
self._name = name
|
|
48
|
+
self.signature = signature
|
|
42
49
|
|
|
43
50
|
@property
|
|
44
51
|
def database(self) -> str | None:
|
|
@@ -72,6 +79,8 @@ class FQN:
|
|
|
72
79
|
|
|
73
80
|
@property
|
|
74
81
|
def sql_identifier(self) -> str:
|
|
82
|
+
if self.signature:
|
|
83
|
+
return f"IDENTIFIER('{self.identifier}'){self.signature}"
|
|
75
84
|
return f"IDENTIFIER('{self.identifier}')"
|
|
76
85
|
|
|
77
86
|
def __str__(self):
|
|
@@ -98,9 +107,13 @@ class FQN:
|
|
|
98
107
|
else:
|
|
99
108
|
database = None
|
|
100
109
|
schema = result.group("first_qualifier")
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
|
|
111
|
+
signature = None
|
|
112
|
+
if result.group("signature"):
|
|
113
|
+
signature = result.group("signature")
|
|
114
|
+
return cls(
|
|
115
|
+
name=unqualified_name, schema=schema, database=database, signature=signature
|
|
116
|
+
)
|
|
104
117
|
|
|
105
118
|
@classmethod
|
|
106
119
|
def from_stage(cls, stage: str) -> "FQN":
|
|
@@ -29,10 +29,25 @@ class SchemaValidationError(ClickException):
|
|
|
29
29
|
def __init__(self, error: ValidationError):
|
|
30
30
|
errors = error.errors()
|
|
31
31
|
message = f"During evaluation of {error.title} in project definition following errors were encountered:\n"
|
|
32
|
+
|
|
33
|
+
def calculate_location(e):
|
|
34
|
+
if e["loc"] is None:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
# show numbers as list indexes and strings as dictionary keys. Example: key1[0].key2
|
|
38
|
+
result = "".join(
|
|
39
|
+
f"[{item}]" if isinstance(item, int) else f".{item}"
|
|
40
|
+
for item in e["loc"]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# remove leading dot from the string if any:
|
|
44
|
+
return result[1:] if result.startswith(".") else result
|
|
45
|
+
|
|
32
46
|
message += "\n".join(
|
|
33
47
|
[
|
|
34
48
|
self.message_templates.get(e["type"], self.generic_message).format(
|
|
35
|
-
**e,
|
|
49
|
+
**e,
|
|
50
|
+
location=calculate_location(e),
|
|
36
51
|
)
|
|
37
52
|
for e in errors
|
|
38
53
|
]
|
|
@@ -147,11 +147,11 @@ class SqlExecutionMixin:
|
|
|
147
147
|
self.use(object_type=ObjectType.WAREHOUSE, name=prev_wh)
|
|
148
148
|
|
|
149
149
|
def create_password_secret(
|
|
150
|
-
self, name:
|
|
150
|
+
self, name: FQN, username: str, password: str
|
|
151
151
|
) -> SnowflakeCursor:
|
|
152
152
|
return self._execute_query(
|
|
153
153
|
f"""
|
|
154
|
-
create secret {name}
|
|
154
|
+
create secret {name.sql_identifier}
|
|
155
155
|
type = password
|
|
156
156
|
username = '{username}'
|
|
157
157
|
password = '{password}'
|
|
@@ -159,11 +159,11 @@ class SqlExecutionMixin:
|
|
|
159
159
|
)
|
|
160
160
|
|
|
161
161
|
def create_api_integration(
|
|
162
|
-
self, name:
|
|
162
|
+
self, name: FQN, api_provider: str, allowed_prefix: str, secret: Optional[str]
|
|
163
163
|
) -> SnowflakeCursor:
|
|
164
164
|
return self._execute_query(
|
|
165
165
|
f"""
|
|
166
|
-
create api integration {name}
|
|
166
|
+
create api integration {name.sql_identifier}
|
|
167
167
|
api_provider = {api_provider}
|
|
168
168
|
api_allowed_prefixes = ('{allowed_prefix}')
|
|
169
169
|
allowed_authentication_secrets = ({secret if secret else ''})
|
|
@@ -18,7 +18,7 @@ import itertools
|
|
|
18
18
|
import logging
|
|
19
19
|
from os import path
|
|
20
20
|
from pathlib import Path
|
|
21
|
-
from typing import List, Optional
|
|
21
|
+
from typing import Dict, List, Optional
|
|
22
22
|
|
|
23
23
|
import typer
|
|
24
24
|
from click import ClickException
|
|
@@ -41,6 +41,7 @@ from snowflake.cli.plugins.object.command_aliases import (
|
|
|
41
41
|
)
|
|
42
42
|
from snowflake.cli.plugins.object.manager import ObjectManager
|
|
43
43
|
from snowflake.cli.plugins.stage.manager import OnErrorType
|
|
44
|
+
from snowflake.connector import DictCursor
|
|
44
45
|
|
|
45
46
|
app = SnowTyperFactory(
|
|
46
47
|
name="git",
|
|
@@ -82,10 +83,12 @@ add_object_command_aliases(
|
|
|
82
83
|
scope_option=scope_option(help_example="`list --in database my_db`"),
|
|
83
84
|
)
|
|
84
85
|
|
|
86
|
+
from snowflake.cli.api.identifiers import FQN
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
|
|
89
|
+
def _assure_repository_does_not_exist(om: ObjectManager, repository_name: FQN) -> None:
|
|
87
90
|
if om.object_exists(
|
|
88
|
-
object_type=ObjectType.GIT_REPOSITORY.value.cli_name,
|
|
91
|
+
object_type=ObjectType.GIT_REPOSITORY.value.cli_name, fqn=repository_name
|
|
89
92
|
):
|
|
90
93
|
raise ClickException(f"Repository '{repository_name}' already exists")
|
|
91
94
|
|
|
@@ -95,9 +98,27 @@ def _validate_origin_url(url: str) -> None:
|
|
|
95
98
|
raise ClickException("Url address should start with 'https'")
|
|
96
99
|
|
|
97
100
|
|
|
101
|
+
def _unique_new_object_name(
|
|
102
|
+
om: ObjectManager, object_type: ObjectType, proposed_fqn: FQN
|
|
103
|
+
) -> str:
|
|
104
|
+
existing_objects: List[Dict] = om.show(
|
|
105
|
+
object_type=object_type.value.cli_name,
|
|
106
|
+
like=f"{proposed_fqn.name}%",
|
|
107
|
+
cursor_class=DictCursor,
|
|
108
|
+
).fetchall()
|
|
109
|
+
existing_names = set(o["name"].upper() for o in existing_objects)
|
|
110
|
+
|
|
111
|
+
result = proposed_fqn.name
|
|
112
|
+
i = 1
|
|
113
|
+
while result.upper() in existing_names:
|
|
114
|
+
result = proposed_fqn.name + str(i)
|
|
115
|
+
i += 1
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
|
|
98
119
|
@app.command("setup", requires_connection=True)
|
|
99
120
|
def setup(
|
|
100
|
-
repository_name:
|
|
121
|
+
repository_name: FQN = RepoNameArgument,
|
|
101
122
|
**options,
|
|
102
123
|
) -> CommandResult:
|
|
103
124
|
"""
|
|
@@ -123,12 +144,29 @@ def setup(
|
|
|
123
144
|
should_create_secret = False
|
|
124
145
|
secret_name = None
|
|
125
146
|
if secret_needed:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
147
|
+
default_secret_name = (
|
|
148
|
+
FQN.from_string(f"{repository_name.name}_secret")
|
|
149
|
+
.set_schema(repository_name.schema)
|
|
150
|
+
.set_database(repository_name.database)
|
|
151
|
+
)
|
|
152
|
+
default_secret_name.set_name(
|
|
153
|
+
_unique_new_object_name(
|
|
154
|
+
om, object_type=ObjectType.SECRET, proposed_fqn=default_secret_name
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
secret_name = FQN.from_string(
|
|
158
|
+
typer.prompt(
|
|
159
|
+
"Secret identifier (will be created if not exists)",
|
|
160
|
+
default=default_secret_name.name,
|
|
161
|
+
)
|
|
129
162
|
)
|
|
163
|
+
if not secret_name.database:
|
|
164
|
+
secret_name.set_database(repository_name.database)
|
|
165
|
+
if not secret_name.schema:
|
|
166
|
+
secret_name.set_schema(repository_name.schema)
|
|
167
|
+
|
|
130
168
|
if om.object_exists(
|
|
131
|
-
object_type=ObjectType.SECRET.value.cli_name,
|
|
169
|
+
object_type=ObjectType.SECRET.value.cli_name, fqn=secret_name
|
|
132
170
|
):
|
|
133
171
|
cli_console.step(f"Using existing secret '{secret_name}'")
|
|
134
172
|
else:
|
|
@@ -137,10 +175,17 @@ def setup(
|
|
|
137
175
|
secret_username = typer.prompt("username")
|
|
138
176
|
secret_password = typer.prompt("password/token", hide_input=True)
|
|
139
177
|
|
|
140
|
-
|
|
141
|
-
api_integration =
|
|
142
|
-
|
|
143
|
-
|
|
178
|
+
# API integration is an account-level object
|
|
179
|
+
api_integration = FQN.from_string(f"{repository_name.name}_api_integration")
|
|
180
|
+
api_integration.set_name(
|
|
181
|
+
typer.prompt(
|
|
182
|
+
"API integration identifier (will be created if not exists)",
|
|
183
|
+
default=_unique_new_object_name(
|
|
184
|
+
om,
|
|
185
|
+
object_type=ObjectType.INTEGRATION,
|
|
186
|
+
proposed_fqn=api_integration,
|
|
187
|
+
),
|
|
188
|
+
)
|
|
144
189
|
)
|
|
145
190
|
|
|
146
191
|
if should_create_secret:
|
|
@@ -150,7 +195,7 @@ def setup(
|
|
|
150
195
|
cli_console.step(f"Secret '{secret_name}' successfully created.")
|
|
151
196
|
|
|
152
197
|
if not om.object_exists(
|
|
153
|
-
object_type=ObjectType.INTEGRATION.value.cli_name,
|
|
198
|
+
object_type=ObjectType.INTEGRATION.value.cli_name, fqn=api_integration
|
|
154
199
|
):
|
|
155
200
|
manager.create_api_integration(
|
|
156
201
|
name=api_integration,
|
|
@@ -177,7 +222,7 @@ def setup(
|
|
|
177
222
|
requires_connection=True,
|
|
178
223
|
)
|
|
179
224
|
def list_branches(
|
|
180
|
-
repository_name:
|
|
225
|
+
repository_name: FQN = RepoNameArgument,
|
|
181
226
|
like=like_option(
|
|
182
227
|
help_example='`list-branches --like "%_test"` lists all branches that end with "_test"'
|
|
183
228
|
),
|
|
@@ -186,7 +231,9 @@ def list_branches(
|
|
|
186
231
|
"""
|
|
187
232
|
List all branches in the repository.
|
|
188
233
|
"""
|
|
189
|
-
return QueryResult(
|
|
234
|
+
return QueryResult(
|
|
235
|
+
GitManager().show_branches(repo_name=repository_name.identifier, like=like)
|
|
236
|
+
)
|
|
190
237
|
|
|
191
238
|
|
|
192
239
|
@app.command(
|
|
@@ -194,7 +241,7 @@ def list_branches(
|
|
|
194
241
|
requires_connection=True,
|
|
195
242
|
)
|
|
196
243
|
def list_tags(
|
|
197
|
-
repository_name:
|
|
244
|
+
repository_name: FQN = RepoNameArgument,
|
|
198
245
|
like=like_option(
|
|
199
246
|
help_example='`list-tags --like "v2.0%"` lists all tags that start with "v2.0"'
|
|
200
247
|
),
|
|
@@ -203,7 +250,9 @@ def list_tags(
|
|
|
203
250
|
"""
|
|
204
251
|
List all tags in the repository.
|
|
205
252
|
"""
|
|
206
|
-
return QueryResult(
|
|
253
|
+
return QueryResult(
|
|
254
|
+
GitManager().show_tags(repo_name=repository_name.identifier, like=like)
|
|
255
|
+
)
|
|
207
256
|
|
|
208
257
|
|
|
209
258
|
@app.command(
|
|
@@ -228,13 +277,13 @@ def list_files(
|
|
|
228
277
|
requires_connection=True,
|
|
229
278
|
)
|
|
230
279
|
def fetch(
|
|
231
|
-
repository_name:
|
|
280
|
+
repository_name: FQN = RepoNameArgument,
|
|
232
281
|
**options,
|
|
233
282
|
) -> CommandResult:
|
|
234
283
|
"""
|
|
235
284
|
Fetch changes from origin to Snowflake repository.
|
|
236
285
|
"""
|
|
237
|
-
return QueryResult(GitManager().fetch(
|
|
286
|
+
return QueryResult(GitManager().fetch(fqn=repository_name))
|
|
238
287
|
|
|
239
288
|
|
|
240
289
|
@app.command(
|
|
@@ -18,6 +18,7 @@ from pathlib import Path
|
|
|
18
18
|
from textwrap import dedent
|
|
19
19
|
from typing import List
|
|
20
20
|
|
|
21
|
+
from snowflake.cli.api.identifiers import FQN
|
|
21
22
|
from snowflake.cli.plugins.stage.manager import (
|
|
22
23
|
USER_STAGE_PREFIX,
|
|
23
24
|
StageManager,
|
|
@@ -40,17 +41,25 @@ class GitStagePathParts(StagePathParts):
|
|
|
40
41
|
|
|
41
42
|
@property
|
|
42
43
|
def path(self) -> str:
|
|
43
|
-
return (
|
|
44
|
-
f"{self.stage_name}{self.directory}"
|
|
45
|
-
if self.stage_name.endswith("/")
|
|
46
|
-
else f"{self.stage_name}/{self.directory}"
|
|
47
|
-
)
|
|
44
|
+
return f"{self.stage_name.rstrip('/')}/{self.directory}"
|
|
48
45
|
|
|
49
|
-
|
|
46
|
+
@classmethod
|
|
47
|
+
def get_directory(cls, stage_path: str) -> str:
|
|
48
|
+
return "/".join(Path(stage_path).parts[3:])
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def full_path(self) -> str:
|
|
52
|
+
return f"{self.stage.rstrip('/')}/{self.directory}"
|
|
53
|
+
|
|
54
|
+
def replace_stage_prefix(self, file_path: str) -> str:
|
|
50
55
|
stage = Path(self.stage).parts[0]
|
|
51
56
|
file_path_without_prefix = Path(file_path).parts[1:]
|
|
52
57
|
return f"{stage}/{'/'.join(file_path_without_prefix)}"
|
|
53
58
|
|
|
59
|
+
def add_stage_prefix(self, file_path: str) -> str:
|
|
60
|
+
stage = self.stage.rstrip("/")
|
|
61
|
+
return f"{stage}/{file_path.lstrip('/')}"
|
|
62
|
+
|
|
54
63
|
def get_directory_from_file_path(self, file_path: str) -> List[str]:
|
|
55
64
|
stage_path_length = len(Path(self.directory).parts)
|
|
56
65
|
return list(Path(file_path).parts[3 + stage_path_length : -1])
|
|
@@ -63,15 +72,15 @@ class GitManager(StageManager):
|
|
|
63
72
|
def show_tags(self, repo_name: str, like: str) -> SnowflakeCursor:
|
|
64
73
|
return self._execute_query(f"show git tags like '{like}' in {repo_name}")
|
|
65
74
|
|
|
66
|
-
def fetch(self,
|
|
67
|
-
return self._execute_query(f"alter git repository {
|
|
75
|
+
def fetch(self, fqn: FQN) -> SnowflakeCursor:
|
|
76
|
+
return self._execute_query(f"alter git repository {fqn} fetch")
|
|
68
77
|
|
|
69
78
|
def create(
|
|
70
|
-
self, repo_name:
|
|
79
|
+
self, repo_name: FQN, api_integration: str, url: str, secret: str
|
|
71
80
|
) -> SnowflakeCursor:
|
|
72
81
|
query = dedent(
|
|
73
82
|
f"""
|
|
74
|
-
create git repository {repo_name}
|
|
83
|
+
create git repository {repo_name.sql_identifier}
|
|
75
84
|
api_integration = {api_integration}
|
|
76
85
|
origin = '{url}'
|
|
77
86
|
"""
|
|
@@ -68,7 +68,8 @@ TemplateOption = typer.Option(
|
|
|
68
68
|
show_default=False,
|
|
69
69
|
)
|
|
70
70
|
SourceOption = typer.Option(
|
|
71
|
-
|
|
71
|
+
DEFAULT_SOURCE,
|
|
72
|
+
"--template-source",
|
|
72
73
|
help=f"local path to template directory or URL to git repository with templates.",
|
|
73
74
|
)
|
|
74
75
|
VariablesOption = variables_option(
|
|
@@ -132,13 +133,13 @@ def _fetch_remote_template(
|
|
|
132
133
|
return template_root
|
|
133
134
|
|
|
134
135
|
|
|
135
|
-
def _read_template_metadata(template_root: SecurePath) -> Template:
|
|
136
|
+
def _read_template_metadata(template_root: SecurePath, args_error_msg: str) -> Template:
|
|
136
137
|
"""Parse template.yml file."""
|
|
137
138
|
template_metadata_path = template_root / TEMPLATE_METADATA_FILE_NAME
|
|
138
139
|
log.debug("Reading template metadata from %s", template_metadata_path.path)
|
|
139
140
|
if not template_metadata_path.exists():
|
|
140
141
|
raise InvalidTemplate(
|
|
141
|
-
f"
|
|
142
|
+
f"File {TEMPLATE_METADATA_FILE_NAME} not found. {args_error_msg}"
|
|
142
143
|
)
|
|
143
144
|
with template_metadata_path.open(read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB) as fd:
|
|
144
145
|
yaml_contents = yaml.safe_load(fd) or {}
|
|
@@ -203,6 +204,7 @@ def init(
|
|
|
203
204
|
is_remote = any(
|
|
204
205
|
template_source.startswith(prefix) for prefix in ["git@", "http://", "https://"] # type: ignore
|
|
205
206
|
)
|
|
207
|
+
args_error_msg = f"Check whether {TemplateOption.param_decls[0]} and {SourceOption.param_decls[0]} arguments are correct."
|
|
206
208
|
|
|
207
209
|
# copy/download template into tmpdir, so it is going to be removed in case command ends with an error
|
|
208
210
|
with SecurePath.temporary_directory() as tmpdir:
|
|
@@ -217,7 +219,9 @@ def init(
|
|
|
217
219
|
destination=tmpdir,
|
|
218
220
|
)
|
|
219
221
|
|
|
220
|
-
template_metadata = _read_template_metadata(
|
|
222
|
+
template_metadata = _read_template_metadata(
|
|
223
|
+
template_root, args_error_msg=args_error_msg
|
|
224
|
+
)
|
|
221
225
|
if template_metadata.minimum_cli_version:
|
|
222
226
|
_validate_cli_version(template_metadata.minimum_cli_version)
|
|
223
227
|
|
|
@@ -45,6 +45,7 @@ from snowflake.cli.api.project.util import (
|
|
|
45
45
|
from snowflake.cli.api.rendering.sql_templates import (
|
|
46
46
|
get_sql_cli_jinja_env,
|
|
47
47
|
)
|
|
48
|
+
from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
|
|
48
49
|
from snowflake.cli.api.sql_execution import SqlExecutionMixin
|
|
49
50
|
from snowflake.cli.plugins.connection.util import make_snowsight_url
|
|
50
51
|
from snowflake.cli.plugins.nativeapp.artifacts import (
|
|
@@ -577,7 +578,10 @@ class NativeAppManager(SqlExecutionMixin):
|
|
|
577
578
|
)
|
|
578
579
|
|
|
579
580
|
def _expand_script_templates(
|
|
580
|
-
self,
|
|
581
|
+
self,
|
|
582
|
+
env: jinja2.Environment,
|
|
583
|
+
jinja_context: dict[str, Any],
|
|
584
|
+
scripts: List[str],
|
|
581
585
|
) -> List[str]:
|
|
582
586
|
"""
|
|
583
587
|
Input:
|
|
@@ -589,20 +593,22 @@ class NativeAppManager(SqlExecutionMixin):
|
|
|
589
593
|
Size of the return list is the same as the size of the input scripts list.
|
|
590
594
|
"""
|
|
591
595
|
scripts_contents = []
|
|
592
|
-
for
|
|
596
|
+
for script in scripts:
|
|
597
|
+
full_path = SecurePath(self.project_root) / script
|
|
593
598
|
try:
|
|
594
|
-
|
|
599
|
+
template_content = full_path.read_text(file_size_limit_mb=UNLIMITED)
|
|
600
|
+
template = env.from_string(template_content)
|
|
595
601
|
result = template.render(**jinja_context)
|
|
596
602
|
scripts_contents.append(result)
|
|
597
603
|
|
|
598
|
-
except
|
|
599
|
-
raise MissingScriptError(
|
|
604
|
+
except FileNotFoundError as e:
|
|
605
|
+
raise MissingScriptError(script) from e
|
|
600
606
|
|
|
601
607
|
except jinja2.TemplateSyntaxError as e:
|
|
602
|
-
raise InvalidScriptError(
|
|
608
|
+
raise InvalidScriptError(script, e, e.lineno) from e
|
|
603
609
|
|
|
604
610
|
except jinja2.UndefinedError as e:
|
|
605
|
-
raise InvalidScriptError(
|
|
611
|
+
raise InvalidScriptError(script, e) from e
|
|
606
612
|
|
|
607
613
|
return scripts_contents
|
|
608
614
|
|
|
@@ -618,7 +624,7 @@ class NativeAppManager(SqlExecutionMixin):
|
|
|
618
624
|
)
|
|
619
625
|
|
|
620
626
|
env = jinja2.Environment(
|
|
621
|
-
loader=jinja2.
|
|
627
|
+
loader=jinja2.BaseLoader(),
|
|
622
628
|
keep_trailing_newline=True,
|
|
623
629
|
undefined=jinja2.StrictUndefined,
|
|
624
630
|
)
|
|
@@ -668,19 +674,17 @@ class NativeAppManager(SqlExecutionMixin):
|
|
|
668
674
|
if not post_deploy_hooks:
|
|
669
675
|
return
|
|
670
676
|
|
|
671
|
-
with cc.phase(f"Executing {deployed_object_type}
|
|
677
|
+
with cc.phase(f"Executing {deployed_object_type} post_deploy actions"):
|
|
672
678
|
sql_scripts_paths = []
|
|
673
679
|
for hook in post_deploy_hooks:
|
|
674
680
|
if hook.sql_script:
|
|
675
681
|
sql_scripts_paths.append(hook.sql_script)
|
|
676
682
|
else:
|
|
677
683
|
raise ValueError(
|
|
678
|
-
f"Unsupported {deployed_object_type}
|
|
684
|
+
f"Unsupported {deployed_object_type} post_deploy hook type: {hook}"
|
|
679
685
|
)
|
|
680
686
|
|
|
681
|
-
env = get_sql_cli_jinja_env(
|
|
682
|
-
loader=jinja2.loaders.FileSystemLoader(self.project_root)
|
|
683
|
-
)
|
|
687
|
+
env = get_sql_cli_jinja_env()
|
|
684
688
|
scripts_content_list = self._expand_script_templates(
|
|
685
689
|
env, cli_context.template_context, sql_scripts_paths
|
|
686
690
|
)
|
|
@@ -17,9 +17,10 @@ import logging
|
|
|
17
17
|
import typer
|
|
18
18
|
from snowflake.cli.api.commands.flags import identifier_argument
|
|
19
19
|
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
20
|
+
from snowflake.cli.api.identifiers import FQN
|
|
20
21
|
from snowflake.cli.api.output.types import MessageResult
|
|
21
22
|
from snowflake.cli.plugins.notebook.manager import NotebookManager
|
|
22
|
-
from snowflake.cli.plugins.notebook.types import
|
|
23
|
+
from snowflake.cli.plugins.notebook.types import NotebookStagePath
|
|
23
24
|
from typing_extensions import Annotated
|
|
24
25
|
|
|
25
26
|
app = SnowTyperFactory(
|
|
@@ -38,7 +39,7 @@ NotebookFile: NotebookStagePath = typer.Option(
|
|
|
38
39
|
|
|
39
40
|
@app.command(requires_connection=True)
|
|
40
41
|
def execute(
|
|
41
|
-
identifier:
|
|
42
|
+
identifier: FQN = NOTEBOOK_IDENTIFIER,
|
|
42
43
|
**options,
|
|
43
44
|
):
|
|
44
45
|
"""
|
|
@@ -51,7 +52,7 @@ def execute(
|
|
|
51
52
|
|
|
52
53
|
@app.command(requires_connection=True)
|
|
53
54
|
def get_url(
|
|
54
|
-
identifier:
|
|
55
|
+
identifier: FQN = NOTEBOOK_IDENTIFIER,
|
|
55
56
|
**options,
|
|
56
57
|
):
|
|
57
58
|
"""Return a url to a notebook."""
|
|
@@ -61,7 +62,7 @@ def get_url(
|
|
|
61
62
|
|
|
62
63
|
@app.command(name="open", requires_connection=True)
|
|
63
64
|
def open_cmd(
|
|
64
|
-
identifier:
|
|
65
|
+
identifier: FQN = NOTEBOOK_IDENTIFIER,
|
|
65
66
|
**options,
|
|
66
67
|
):
|
|
67
68
|
"""Opens a notebook in default browser"""
|
|
@@ -72,7 +73,7 @@ def open_cmd(
|
|
|
72
73
|
|
|
73
74
|
@app.command(requires_connection=True)
|
|
74
75
|
def create(
|
|
75
|
-
identifier: Annotated[
|
|
76
|
+
identifier: Annotated[FQN, NOTEBOOK_IDENTIFIER],
|
|
76
77
|
notebook_file: Annotated[NotebookStagePath, NotebookFile],
|
|
77
78
|
**options,
|
|
78
79
|
):
|
|
@@ -20,23 +20,23 @@ from snowflake.cli.api.identifiers import FQN
|
|
|
20
20
|
from snowflake.cli.api.sql_execution import SqlExecutionMixin
|
|
21
21
|
from snowflake.cli.plugins.connection.util import make_snowsight_url
|
|
22
22
|
from snowflake.cli.plugins.notebook.exceptions import NotebookStagePathError
|
|
23
|
-
from snowflake.cli.plugins.notebook.types import
|
|
23
|
+
from snowflake.cli.plugins.notebook.types import NotebookStagePath
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class NotebookManager(SqlExecutionMixin):
|
|
27
|
-
def execute(self, notebook_name:
|
|
28
|
-
query = f"EXECUTE NOTEBOOK {notebook_name}()"
|
|
27
|
+
def execute(self, notebook_name: FQN):
|
|
28
|
+
query = f"EXECUTE NOTEBOOK {notebook_name.sql_identifier}()"
|
|
29
29
|
return self._execute_query(query=query)
|
|
30
30
|
|
|
31
|
-
def get_url(self, notebook_name:
|
|
32
|
-
fqn =
|
|
31
|
+
def get_url(self, notebook_name: FQN):
|
|
32
|
+
fqn = notebook_name.using_connection(self._conn)
|
|
33
33
|
return make_snowsight_url(
|
|
34
34
|
self._conn,
|
|
35
35
|
f"/#/notebooks/{fqn.url_identifier}",
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
@staticmethod
|
|
39
|
-
def parse_stage_as_path(notebook_file:
|
|
39
|
+
def parse_stage_as_path(notebook_file: str) -> Path:
|
|
40
40
|
"""Parses notebook file path to pathlib.Path."""
|
|
41
41
|
if not notebook_file.endswith(".ipynb"):
|
|
42
42
|
raise NotebookStagePathError(notebook_file)
|
|
@@ -48,19 +48,19 @@ class NotebookManager(SqlExecutionMixin):
|
|
|
48
48
|
|
|
49
49
|
def create(
|
|
50
50
|
self,
|
|
51
|
-
notebook_name:
|
|
51
|
+
notebook_name: FQN,
|
|
52
52
|
notebook_file: NotebookStagePath,
|
|
53
53
|
) -> str:
|
|
54
|
-
notebook_fqn =
|
|
54
|
+
notebook_fqn = notebook_name.using_connection(self._conn)
|
|
55
55
|
stage_path = self.parse_stage_as_path(notebook_file)
|
|
56
56
|
|
|
57
57
|
queries = dedent(
|
|
58
58
|
f"""
|
|
59
|
-
CREATE OR REPLACE NOTEBOOK {notebook_fqn.
|
|
59
|
+
CREATE OR REPLACE NOTEBOOK {notebook_fqn.sql_identifier}
|
|
60
60
|
FROM '{stage_path.parent}'
|
|
61
61
|
QUERY_WAREHOUSE = '{cli_context.connection.warehouse}'
|
|
62
62
|
MAIN_FILE = '{stage_path.name}';
|
|
63
|
-
|
|
63
|
+
// Cannot use IDENTIFIER(...)
|
|
64
64
|
ALTER NOTEBOOK {notebook_fqn.identifier} ADD LIVE VERSION FROM LAST;
|
|
65
65
|
"""
|
|
66
66
|
)
|
|
@@ -20,6 +20,7 @@ import typer
|
|
|
20
20
|
from click import ClickException
|
|
21
21
|
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
22
22
|
from snowflake.cli.api.constants import ObjectType
|
|
23
|
+
from snowflake.cli.api.identifiers import FQN
|
|
23
24
|
from snowflake.cli.plugins.object.commands import (
|
|
24
25
|
ScopeOption,
|
|
25
26
|
describe,
|
|
@@ -72,7 +73,7 @@ def add_object_command_aliases(
|
|
|
72
73
|
if "drop" not in ommit_commands:
|
|
73
74
|
|
|
74
75
|
@app.command("drop", requires_connection=True)
|
|
75
|
-
def drop_cmd(name:
|
|
76
|
+
def drop_cmd(name: FQN = name_argument, **options):
|
|
76
77
|
return drop(
|
|
77
78
|
object_type=object_type.value.cli_name,
|
|
78
79
|
object_name=name,
|
|
@@ -84,7 +85,7 @@ def add_object_command_aliases(
|
|
|
84
85
|
if "describe" not in ommit_commands:
|
|
85
86
|
|
|
86
87
|
@app.command("describe", requires_connection=True)
|
|
87
|
-
def describe_cmd(name:
|
|
88
|
+
def describe_cmd(name: FQN = name_argument, **options):
|
|
88
89
|
return describe(
|
|
89
90
|
object_type=object_type.value.cli_name,
|
|
90
91
|
object_name=name,
|