snowflake-cli-labs 3.0.0rc1__py3-none-any.whl → 3.0.0rc3__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.
Files changed (92) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/cli_app.py +10 -1
  3. snowflake/cli/_app/commands_registration/builtin_plugins.py +2 -0
  4. snowflake/cli/_app/secret.py +9 -0
  5. snowflake/cli/_app/snow_connector.py +110 -51
  6. snowflake/cli/_app/telemetry.py +8 -4
  7. snowflake/cli/_app/version_check.py +74 -0
  8. snowflake/cli/_plugins/git/commands.py +55 -14
  9. snowflake/cli/_plugins/git/manager.py +53 -7
  10. snowflake/cli/_plugins/helpers/commands.py +57 -0
  11. snowflake/cli/{api/commands/typer_pre_execute.py → _plugins/helpers/plugin_spec.py} +14 -10
  12. snowflake/cli/_plugins/nativeapp/application_entity.py +651 -0
  13. snowflake/cli/{api/project/schemas/entities → _plugins/nativeapp}/application_entity_model.py +2 -2
  14. snowflake/cli/_plugins/nativeapp/application_package_entity.py +1107 -0
  15. snowflake/cli/{api/project/schemas/entities → _plugins/nativeapp}/application_package_entity_model.py +3 -3
  16. snowflake/cli/_plugins/nativeapp/artifacts.py +10 -9
  17. snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
  18. snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
  19. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +1 -1
  20. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +1 -1
  21. snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +1 -1
  22. snowflake/cli/_plugins/nativeapp/codegen/snowpark/models.py +1 -1
  23. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +3 -6
  24. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +50 -32
  25. snowflake/cli/_plugins/nativeapp/commands.py +84 -16
  26. snowflake/cli/_plugins/nativeapp/exceptions.py +0 -9
  27. snowflake/cli/_plugins/nativeapp/manager.py +56 -92
  28. snowflake/cli/_plugins/nativeapp/policy.py +3 -0
  29. snowflake/cli/_plugins/nativeapp/project_model.py +2 -2
  30. snowflake/cli/_plugins/nativeapp/run_processor.py +65 -272
  31. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +70 -0
  32. snowflake/cli/_plugins/nativeapp/teardown_processor.py +11 -154
  33. snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +150 -40
  34. snowflake/cli/_plugins/nativeapp/version/commands.py +6 -24
  35. snowflake/cli/_plugins/nativeapp/version/version_processor.py +35 -235
  36. snowflake/cli/_plugins/snowpark/commands.py +5 -5
  37. snowflake/cli/_plugins/snowpark/common.py +4 -4
  38. snowflake/cli/_plugins/snowpark/models.py +2 -1
  39. snowflake/cli/{api/entities → _plugins/snowpark}/snowpark_entity.py +2 -2
  40. snowflake/cli/{api/project/schemas/entities/snowpark_entity.py → _plugins/snowpark/snowpark_entity_model.py} +3 -6
  41. snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +1 -1
  42. snowflake/cli/_plugins/stage/manager.py +9 -4
  43. snowflake/cli/_plugins/streamlit/commands.py +4 -4
  44. snowflake/cli/_plugins/streamlit/manager.py +17 -4
  45. snowflake/cli/{api/entities → _plugins/streamlit}/streamlit_entity.py +2 -2
  46. snowflake/cli/{api/project/schemas/entities → _plugins/streamlit}/streamlit_entity_model.py +5 -12
  47. snowflake/cli/_plugins/workspace/action_context.py +2 -1
  48. snowflake/cli/_plugins/workspace/commands.py +127 -48
  49. snowflake/cli/_plugins/workspace/manager.py +1 -0
  50. snowflake/cli/_plugins/workspace/plugin_spec.py +1 -1
  51. snowflake/cli/api/cli_global_context.py +136 -313
  52. snowflake/cli/api/commands/flags.py +76 -91
  53. snowflake/cli/api/commands/snow_typer.py +7 -5
  54. snowflake/cli/api/config.py +1 -1
  55. snowflake/cli/api/connections.py +214 -0
  56. snowflake/cli/api/console/abc.py +4 -2
  57. snowflake/cli/api/entities/common.py +4 -0
  58. snowflake/cli/api/entities/utils.py +41 -31
  59. snowflake/cli/api/errno.py +1 -0
  60. snowflake/cli/api/identifiers.py +7 -3
  61. snowflake/cli/api/project/definition.py +11 -0
  62. snowflake/cli/api/project/definition_conversion.py +175 -16
  63. snowflake/cli/api/project/schemas/entities/common.py +15 -14
  64. snowflake/cli/api/project/schemas/entities/entities.py +13 -10
  65. snowflake/cli/api/project/schemas/project_definition.py +107 -45
  66. snowflake/cli/api/project/schemas/v1/__init__.py +0 -0
  67. snowflake/cli/api/project/schemas/{identifier_model.py → v1/identifier_model.py} +0 -7
  68. snowflake/cli/api/project/schemas/v1/native_app/__init__.py +0 -0
  69. snowflake/cli/api/project/schemas/{native_app → v1/native_app}/native_app.py +4 -4
  70. snowflake/cli/api/project/schemas/v1/snowpark/__init__.py +0 -0
  71. snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/callable.py +2 -2
  72. snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/snowpark.py +2 -2
  73. snowflake/cli/api/project/schemas/v1/streamlit/__init__.py +0 -0
  74. snowflake/cli/api/project/schemas/{streamlit → v1/streamlit}/streamlit.py +2 -1
  75. snowflake/cli/api/rendering/project_definition_templates.py +4 -0
  76. snowflake/cli/api/rendering/sql_templates.py +7 -0
  77. snowflake/cli/api/sql_execution.py +6 -15
  78. snowflake/cli/api/utils/definition_rendering.py +3 -1
  79. {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc3.dist-info}/METADATA +9 -9
  80. {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc3.dist-info}/RECORD +88 -81
  81. snowflake/cli/api/entities/application_entity.py +0 -12
  82. snowflake/cli/api/entities/application_package_entity.py +0 -553
  83. snowflake/cli/api/project/schemas/snowpark/__init__.py +0 -13
  84. snowflake/cli/api/project/schemas/streamlit/__init__.py +0 -13
  85. /snowflake/cli/{api/project/schemas/native_app → _plugins/helpers}/__init__.py +0 -0
  86. /snowflake/cli/api/project/schemas/{native_app → v1/native_app}/application.py +0 -0
  87. /snowflake/cli/api/project/schemas/{native_app → v1/native_app}/package.py +0 -0
  88. /snowflake/cli/api/project/schemas/{native_app → v1/native_app}/path_mapping.py +0 -0
  89. /snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/argument.py +0 -0
  90. {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc3.dist-info}/WHEEL +0 -0
  91. {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc3.dist-info}/entry_points.txt +0 -0
  92. {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc3.dist-info}/licenses/LICENSE +0 -0
@@ -14,4 +14,4 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- VERSION = "3.0.0rc1"
17
+ VERSION = "3.0.0rc3"
@@ -38,6 +38,10 @@ from snowflake.cli._app.dev.pycharm_remote_debug import (
38
38
  )
39
39
  from snowflake.cli._app.main_typer import SnowCliMainTyper
40
40
  from snowflake.cli._app.printing import MessageResult, print_result
41
+ from snowflake.cli._app.version_check import (
42
+ get_new_version_msg,
43
+ show_new_version_banner_callback,
44
+ )
41
45
  from snowflake.cli.api import Api, api_provider
42
46
  from snowflake.cli.api.config import config_init
43
47
  from snowflake.cli.api.output.formats import OutputFormat
@@ -145,8 +149,13 @@ def _info_callback(value: bool):
145
149
 
146
150
  def app_factory() -> SnowCliMainTyper:
147
151
  app = SnowCliMainTyper()
152
+ new_version_msg = get_new_version_msg()
148
153
 
149
- @app.callback(invoke_without_command=True)
154
+ @app.callback(
155
+ invoke_without_command=True,
156
+ epilog=new_version_msg,
157
+ result_callback=show_new_version_banner_callback(new_version_msg),
158
+ )
150
159
  def default(
151
160
  ctx: typer.Context,
152
161
  version: bool = typer.Option(
@@ -15,6 +15,7 @@
15
15
  from snowflake.cli._plugins.connection import plugin_spec as connection_plugin_spec
16
16
  from snowflake.cli._plugins.cortex import plugin_spec as cortex_plugin_spec
17
17
  from snowflake.cli._plugins.git import plugin_spec as git_plugin_spec
18
+ from snowflake.cli._plugins.helpers import plugin_spec as migrate_plugin_spec
18
19
  from snowflake.cli._plugins.init import plugin_spec as init_plugin_spec
19
20
  from snowflake.cli._plugins.nativeapp import plugin_spec as nativeapp_plugin_spec
20
21
  from snowflake.cli._plugins.notebook import plugin_spec as notebook_plugin_spec
@@ -31,6 +32,7 @@ from snowflake.cli._plugins.workspace import plugin_spec as workspace_plugin_spe
31
32
  def get_builtin_plugin_name_to_plugin_spec():
32
33
  plugin_specs = {
33
34
  "connection": connection_plugin_spec,
35
+ "helpers": migrate_plugin_spec,
34
36
  "spcs": spcs_plugin_spec,
35
37
  "nativeapp": nativeapp_plugin_spec,
36
38
  "object": object_plugin_spec,
@@ -0,0 +1,9 @@
1
+ class SecretType:
2
+ def __init__(self, value):
3
+ self.value = value
4
+
5
+ def __repr__(self):
6
+ return "SecretType(***)"
7
+
8
+ def __str___(self):
9
+ return "***"
@@ -24,12 +24,11 @@ from click.exceptions import ClickException
24
24
  from snowflake.cli._app.constants import (
25
25
  PARAM_APPLICATION_NAME,
26
26
  )
27
+ from snowflake.cli._app.secret import SecretType
27
28
  from snowflake.cli._app.telemetry import command_info
28
- from snowflake.cli.api.cli_global_context import get_cli_context
29
29
  from snowflake.cli.api.config import (
30
30
  get_connection_dict,
31
- get_default_connection_dict,
32
- get_default_connection_name,
31
+ get_env_value,
33
32
  )
34
33
  from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB
35
34
  from snowflake.cli.api.exceptions import (
@@ -46,6 +45,33 @@ log = logging.getLogger(__name__)
46
45
  ENCRYPTED_PKCS8_PK_HEADER = b"-----BEGIN ENCRYPTED PRIVATE KEY-----"
47
46
  UNENCRYPTED_PKCS8_PK_HEADER = b"-----BEGIN PRIVATE KEY-----"
48
47
 
48
+ # connection keys that can be set using SNOWFLAKE_* env vars
49
+ SUPPORTED_ENV_OVERRIDES = [
50
+ "account",
51
+ "user",
52
+ "password",
53
+ "authenticator",
54
+ "private_key_file",
55
+ "private_key_path",
56
+ "database",
57
+ "schema",
58
+ "role",
59
+ "warehouse",
60
+ "session_token",
61
+ "master_token",
62
+ "token_file_path",
63
+ ]
64
+
65
+ # mapping of found key -> key to set
66
+ CONNECTION_KEY_ALIASES = {"private_key_path": "private_key_file"}
67
+
68
+
69
+ def _resolve_alias(key_or_alias: str):
70
+ """
71
+ Given the key of an override / env var, what key should it be set as in the connection parameters?
72
+ """
73
+ return CONNECTION_KEY_ALIASES.get(key_or_alias, key_or_alias)
74
+
49
75
 
50
76
  def connect_to_snowflake(
51
77
  temporary_connection: bool = False,
@@ -58,6 +84,10 @@ def connect_to_snowflake(
58
84
  ) -> SnowflakeConnection:
59
85
  if temporary_connection and connection_name:
60
86
  raise ClickException("Can't use connection name and temporary connection.")
87
+ elif not temporary_connection and not connection_name:
88
+ raise ClickException(
89
+ "One of connection name or temporary connection is required."
90
+ )
61
91
 
62
92
  using_session_token = (
63
93
  "session_token" in overrides and overrides["session_token"] is not None
@@ -70,37 +100,33 @@ def connect_to_snowflake(
70
100
  )
71
101
 
72
102
  if connection_name:
73
- connection_parameters = get_connection_dict(connection_name)
74
- connection_parameters = get_connection_dict(connection_name)
103
+ connection_parameters = {
104
+ _resolve_alias(k): v
105
+ for k, v in get_connection_dict(connection_name).items()
106
+ }
75
107
  elif temporary_connection:
76
108
  connection_parameters = {} # we will apply overrides in next step
77
- else:
78
- connection_parameters = get_default_connection_dict()
79
- get_cli_context().connection_context.set_connection_name(
80
- get_default_connection_name()
81
- )
82
109
 
83
110
  # Apply overrides to connection details
111
+ # (1) Command line override case
84
112
  for key, value in overrides.items():
85
- # Command line override case
86
- if value:
87
- connection_parameters[key] = value
88
- continue
113
+ if value is not None:
114
+ connection_parameters[_resolve_alias(key)] = value
89
115
 
90
- # Generic environment variable case, apply only if value not passed via flag or connection variable
91
- generic_env_value = os.environ.get(f"SNOWFLAKE_{key}".upper())
92
- if key not in connection_parameters and generic_env_value:
93
- connection_parameters[key] = generic_env_value
94
- continue
116
+ # (2) Generic environment variable case
117
+ # ... apply only if value not passed via flag or connection variable
118
+ for key in SUPPORTED_ENV_OVERRIDES:
119
+ generic_env_value = get_env_value(key=key)
120
+ connection_key = _resolve_alias(key)
121
+ if connection_key not in connection_parameters and generic_env_value:
122
+ connection_parameters[connection_key] = generic_env_value
95
123
 
96
124
  # Clean up connection params
97
125
  connection_parameters = {
98
126
  k: v for k, v in connection_parameters.items() if v is not None
99
127
  }
100
128
 
101
- connection_parameters = update_connection_details_with_private_key(
102
- connection_parameters
103
- )
129
+ update_connection_details_with_private_key(connection_parameters)
104
130
 
105
131
  if mfa_passcode:
106
132
  connection_parameters["passcode"] = mfa_passcode
@@ -169,13 +195,34 @@ def update_connection_details_with_private_key(connection_parameters: Dict):
169
195
  _load_private_key(connection_parameters, "private_key_file")
170
196
  elif "private_key_path" in connection_parameters:
171
197
  _load_private_key(connection_parameters, "private_key_path")
198
+ elif "private_key_raw" in connection_parameters:
199
+ _load_private_key_from_parameters(connection_parameters, "private_key_raw")
172
200
  return connection_parameters
173
201
 
174
202
 
175
203
  def _load_private_key(connection_parameters: Dict, private_key_var_name: str) -> None:
176
204
  if connection_parameters.get("authenticator") == "SNOWFLAKE_JWT":
177
- private_key = _load_pem_to_der(connection_parameters[private_key_var_name])
178
- connection_parameters["private_key"] = private_key
205
+ private_key_pem = _load_pem_from_file(
206
+ connection_parameters[private_key_var_name]
207
+ )
208
+ private_key = _load_pem_to_der(private_key_pem)
209
+ connection_parameters["private_key"] = private_key.value
210
+ del connection_parameters[private_key_var_name]
211
+ else:
212
+ raise ClickException(
213
+ "Private Key authentication requires authenticator set to SNOWFLAKE_JWT"
214
+ )
215
+
216
+
217
+ def _load_private_key_from_parameters(
218
+ connection_parameters: Dict, private_key_var_name: str
219
+ ) -> None:
220
+ if connection_parameters.get("authenticator") == "SNOWFLAKE_JWT":
221
+ private_key_pem = _load_pem_from_parameters(
222
+ connection_parameters[private_key_var_name]
223
+ )
224
+ private_key = _load_pem_to_der(private_key_pem)
225
+ connection_parameters["private_key"] = private_key.value
179
226
  del connection_parameters[private_key_var_name]
180
227
  else:
181
228
  raise ClickException(
@@ -191,41 +238,49 @@ def _update_connection_application_name(connection_parameters: Dict):
191
238
  connection_parameters.update(connection_application_params)
192
239
 
193
240
 
194
- def _load_pem_to_der(private_key_file: str) -> bytes:
195
- """
196
- Given a private key file path (in PEM format), decode key data into DER
197
- format
198
- """
199
-
241
+ def _load_pem_from_file(private_key_file: str) -> SecretType:
200
242
  with SecurePath(private_key_file).open(
201
243
  "rb", read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB
202
244
  ) as f:
203
- private_key_pem = f.read()
245
+ private_key_pem = SecretType(f.read())
246
+ return private_key_pem
247
+
248
+
249
+ def _load_pem_from_parameters(private_key_raw: str) -> SecretType:
250
+ return SecretType(private_key_raw.encode("utf-8"))
204
251
 
205
- private_key_passphrase = os.getenv("PRIVATE_KEY_PASSPHRASE", None)
252
+
253
+ def _load_pem_to_der(private_key_pem: SecretType) -> SecretType:
254
+ """
255
+ Given a private key file path (in PEM format), decode key data into DER
256
+ format
257
+ """
258
+ private_key_passphrase = SecretType(os.getenv("PRIVATE_KEY_PASSPHRASE", None))
206
259
  if (
207
- private_key_pem.startswith(ENCRYPTED_PKCS8_PK_HEADER)
208
- and private_key_passphrase is None
260
+ private_key_pem.value.startswith(ENCRYPTED_PKCS8_PK_HEADER)
261
+ and private_key_passphrase.value is None
209
262
  ):
210
263
  raise ClickException(
211
264
  "Encrypted private key, you must provide the"
212
265
  "passphrase in the environment variable PRIVATE_KEY_PASSPHRASE"
213
266
  )
214
267
 
215
- if not private_key_pem.startswith(
268
+ if not private_key_pem.value.startswith(
216
269
  ENCRYPTED_PKCS8_PK_HEADER
217
- ) and not private_key_pem.startswith(UNENCRYPTED_PKCS8_PK_HEADER):
270
+ ) and not private_key_pem.value.startswith(UNENCRYPTED_PKCS8_PK_HEADER):
218
271
  raise ClickException(
219
272
  "Private key provided is not in PKCS#8 format. Please use correct format."
220
273
  )
221
274
 
222
- if private_key_pem.startswith(UNENCRYPTED_PKCS8_PK_HEADER):
223
- private_key_passphrase = None
275
+ if private_key_pem.value.startswith(UNENCRYPTED_PKCS8_PK_HEADER):
276
+ private_key_passphrase = SecretType(None)
224
277
 
225
278
  return prepare_private_key(private_key_pem, private_key_passphrase)
226
279
 
227
280
 
228
- def prepare_private_key(private_key_pem, private_key_passphrase=None):
281
+ def prepare_private_key(
282
+ private_key_pem: SecretType, private_key_passphrase: SecretType = SecretType(None)
283
+ ):
229
284
  from cryptography.hazmat.backends import default_backend
230
285
  from cryptography.hazmat.primitives.serialization import (
231
286
  Encoding,
@@ -234,17 +289,21 @@ def prepare_private_key(private_key_pem, private_key_passphrase=None):
234
289
  load_pem_private_key,
235
290
  )
236
291
 
237
- private_key = load_pem_private_key(
238
- private_key_pem,
239
- (
240
- str.encode(private_key_passphrase)
241
- if private_key_passphrase is not None
242
- else private_key_passphrase
243
- ),
244
- default_backend(),
292
+ private_key = SecretType(
293
+ load_pem_private_key(
294
+ private_key_pem.value,
295
+ (
296
+ str.encode(private_key_passphrase.value)
297
+ if private_key_passphrase.value is not None
298
+ else private_key_passphrase.value
299
+ ),
300
+ default_backend(),
301
+ )
245
302
  )
246
- return private_key.private_bytes(
247
- encoding=Encoding.DER,
248
- format=PrivateFormat.PKCS8,
249
- encryption_algorithm=NoEncryption(),
303
+ return SecretType(
304
+ private_key.value.private_bytes(
305
+ encoding=Encoding.DER,
306
+ format=PrivateFormat.PKCS8,
307
+ encryption_algorithm=NoEncryption(),
308
+ )
250
309
  )
@@ -22,7 +22,10 @@ from typing import Any, Dict, Union
22
22
  import click
23
23
  from snowflake.cli.__about__ import VERSION
24
24
  from snowflake.cli._app.constants import PARAM_APPLICATION_NAME
25
- from snowflake.cli.api.cli_global_context import get_cli_context
25
+ from snowflake.cli.api.cli_global_context import (
26
+ _CliGlobalContextAccess,
27
+ get_cli_context,
28
+ )
26
29
  from snowflake.cli.api.commands.execution_metadata import ExecutionMetadata
27
30
  from snowflake.cli.api.config import get_feature_flags_section
28
31
  from snowflake.cli.api.output.formats import OutputFormat
@@ -106,8 +109,9 @@ def python_version() -> str:
106
109
 
107
110
 
108
111
  class CLITelemetryClient:
109
- def __init__(self, ctx):
110
- self._ctx = ctx
112
+ @property
113
+ def _ctx(self) -> _CliGlobalContextAccess:
114
+ return get_cli_context()
111
115
 
112
116
  @staticmethod
113
117
  def generate_telemetry_data_dict(
@@ -143,7 +147,7 @@ class CLITelemetryClient:
143
147
  self._telemetry.send_batch()
144
148
 
145
149
 
146
- _telemetry = CLITelemetryClient(ctx=get_cli_context())
150
+ _telemetry = CLITelemetryClient()
147
151
 
148
152
 
149
153
  @ignore_exceptions()
@@ -0,0 +1,74 @@
1
+ import json
2
+ import time
3
+
4
+ import requests
5
+ from packaging.version import Version
6
+ from snowflake.cli.__about__ import VERSION
7
+ from snowflake.cli.api.console import cli_console
8
+ from snowflake.cli.api.secure_path import SecurePath
9
+ from snowflake.connector.config_manager import CONFIG_MANAGER
10
+
11
+
12
+ def get_new_version_msg() -> str | None:
13
+ last = _VersionCache().get_last_version()
14
+ current = Version(VERSION)
15
+ if last and last > current:
16
+ return f"\nNew version of Snowflake CLI available. Newest: {last}, current: {VERSION}\n"
17
+ return None
18
+
19
+
20
+ def show_new_version_banner_callback(msg):
21
+ def _callback(*args, **kwargs):
22
+ if msg:
23
+ cli_console.message(msg)
24
+
25
+ return _callback
26
+
27
+
28
+ class _VersionCache:
29
+ _last_time = "last_time_check"
30
+ _version = "version"
31
+ _version_cache_file = SecurePath(
32
+ CONFIG_MANAGER.file_path.parent / ".cli_version.cache"
33
+ )
34
+
35
+ def __init__(self):
36
+ self._cache_file = _VersionCache._version_cache_file
37
+
38
+ def _save_latest_version(self, version: str):
39
+ data = {
40
+ _VersionCache._last_time: time.time(),
41
+ _VersionCache._version: str(version),
42
+ }
43
+ self._cache_file.write_text(json.dumps(data))
44
+
45
+ @staticmethod
46
+ def _get_version_from_pypi() -> str | None:
47
+ headers = {"Content-Type": "application/vnd.pypi.simple.v1+json"}
48
+ response = requests.get(
49
+ "https://pypi.org/pypi/snowflake-cli-labs/json", headers=headers, timeout=3
50
+ )
51
+ response.raise_for_status()
52
+ return response.json()["info"]["version"]
53
+
54
+ def _update_latest_version(self) -> Version | None:
55
+ version = self._get_version_from_pypi()
56
+ if version is None:
57
+ return None
58
+ self._save_latest_version(version)
59
+ return Version(version)
60
+
61
+ def _read_latest_version(self) -> Version | None:
62
+ if self._cache_file.exists():
63
+ data = json.loads(self._cache_file.read_text())
64
+ now = time.time()
65
+ if data[_VersionCache._last_time] > now - 60 * 60:
66
+ return Version(data[_VersionCache._version])
67
+
68
+ return self._update_latest_version()
69
+
70
+ def get_last_version(self) -> Version | None:
71
+ try:
72
+ return self._read_latest_version()
73
+ except: # anything, this it not crucial feature
74
+ return None
@@ -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.api.console.console import cli_console
41
41
  from snowflake.cli.api.constants import ObjectType
42
42
  from snowflake.cli.api.output.types import CollectionResult, CommandResult, QueryResult
43
43
  from snowflake.cli.api.utils.path_utils import is_stage_path
44
+ from snowflake.connector import DictCursor
44
45
 
45
46
  app = SnowTyperFactory(
46
47
  name="git",
@@ -98,6 +99,24 @@ def _validate_origin_url(url: str) -> None:
98
99
  raise ClickException("Url address should start with 'https'")
99
100
 
100
101
 
102
+ def _unique_new_object_name(
103
+ om: ObjectManager, object_type: ObjectType, proposed_fqn: FQN
104
+ ) -> str:
105
+ existing_objects: List[Dict] = om.show(
106
+ object_type=object_type.value.cli_name,
107
+ like=f"{proposed_fqn.name}%",
108
+ cursor_class=DictCursor,
109
+ ).fetchall()
110
+ existing_names = set(o["name"].upper() for o in existing_objects)
111
+
112
+ result = proposed_fqn.name
113
+ i = 1
114
+ while result.upper() in existing_names:
115
+ result = proposed_fqn.name + str(i)
116
+ i += 1
117
+ return result
118
+
119
+
101
120
  @app.command("setup", requires_connection=True)
102
121
  def setup(
103
122
  repository_name: FQN = RepoNameArgument,
@@ -128,13 +147,29 @@ def setup(
128
147
  should_create_secret = False
129
148
  secret_name = None
130
149
  if secret_needed:
131
- secret_name = f"{repository_name}_secret"
132
- secret_name = typer.prompt(
133
- "Secret identifier (will be created if not exists)", default=secret_name
150
+ default_secret_name = (
151
+ FQN.from_string(f"{repository_name.name}_secret")
152
+ .set_schema(repository_name.schema)
153
+ .set_database(repository_name.database)
154
+ )
155
+ default_secret_name.set_name(
156
+ _unique_new_object_name(
157
+ om, object_type=ObjectType.SECRET, proposed_fqn=default_secret_name
158
+ ),
134
159
  )
135
- secret_fqn = FQN.from_string(secret_name)
160
+ secret_name = FQN.from_string(
161
+ typer.prompt(
162
+ "Secret identifier (will be created if not exists)",
163
+ default=default_secret_name.name,
164
+ )
165
+ )
166
+ if not secret_name.database:
167
+ secret_name.set_database(repository_name.database)
168
+ if not secret_name.schema:
169
+ secret_name.set_schema(repository_name.schema)
170
+
136
171
  if om.object_exists(
137
- object_type=ObjectType.SECRET.value.cli_name, fqn=secret_fqn
172
+ object_type=ObjectType.SECRET.value.cli_name, fqn=secret_name
138
173
  ):
139
174
  cli_console.step(f"Using existing secret '{secret_name}'")
140
175
  else:
@@ -143,24 +178,30 @@ def setup(
143
178
  secret_username = typer.prompt("username")
144
179
  secret_password = typer.prompt("password/token", hide_input=True)
145
180
 
146
- api_integration = f"{repository_name}_api_integration"
147
- api_integration = typer.prompt(
148
- "API integration identifier (will be created if not exists)",
149
- default=api_integration,
181
+ # API integration is an account-level object
182
+ api_integration = FQN.from_string(f"{repository_name.name}_api_integration")
183
+ api_integration.set_name(
184
+ typer.prompt(
185
+ "API integration identifier (will be created if not exists)",
186
+ default=_unique_new_object_name(
187
+ om,
188
+ object_type=ObjectType.INTEGRATION,
189
+ proposed_fqn=api_integration,
190
+ ),
191
+ )
150
192
  )
151
- api_integration_fqn = FQN.from_string(api_integration)
152
193
 
153
194
  if should_create_secret:
154
195
  manager.create_password_secret(
155
- name=secret_fqn, username=secret_username, password=secret_password
196
+ name=secret_name, username=secret_username, password=secret_password
156
197
  )
157
198
  cli_console.step(f"Secret '{secret_name}' successfully created.")
158
199
 
159
200
  if not om.object_exists(
160
- object_type=ObjectType.INTEGRATION.value.cli_name, fqn=api_integration_fqn
201
+ object_type=ObjectType.INTEGRATION.value.cli_name, fqn=api_integration
161
202
  ):
162
203
  manager.create_api_integration(
163
- name=api_integration_fqn,
204
+ name=api_integration,
164
205
  api_provider="git_https_api",
165
206
  allowed_prefix=url,
166
207
  secret=secret_name,
@@ -14,10 +14,11 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- from pathlib import Path
17
+ from pathlib import Path, PurePosixPath
18
18
  from textwrap import dedent
19
19
  from typing import List
20
20
 
21
+ from click import UsageError
21
22
  from snowflake.cli._plugins.stage.manager import (
22
23
  USER_STAGE_PREFIX,
23
24
  StageManager,
@@ -27,16 +28,22 @@ from snowflake.cli._plugins.stage.manager import (
27
28
  from snowflake.cli.api.identifiers import FQN
28
29
  from snowflake.connector.cursor import SnowflakeCursor
29
30
 
31
+ # Replace magic numbers with constants
32
+ OMIT_FIRST = slice(1, None)
33
+ OMIT_STAGE = slice(3, None)
34
+ OMIT_STAGE_IN_NEW_LIST_FILES = slice(2, None)
35
+ ONLY_STAGE = slice(3)
36
+
30
37
 
31
38
  class GitStagePathParts(StagePathParts):
32
39
  def __init__(self, stage_path: str):
33
40
  self.stage = GitManager.get_stage_from_path(stage_path)
34
- stage_path_parts = Path(stage_path).parts
41
+ stage_path_parts = GitManager.split_git_path(stage_path)
35
42
  git_repo_name = stage_path_parts[0].split(".")[-1]
36
43
  if git_repo_name.startswith("@"):
37
- git_repo_name = git_repo_name[1:]
44
+ git_repo_name = git_repo_name[OMIT_FIRST]
38
45
  self.stage_name = "/".join([git_repo_name, *stage_path_parts[1:3], ""])
39
- self.directory = "/".join(stage_path_parts[3:])
46
+ self.directory = "/".join(stage_path_parts[OMIT_STAGE])
40
47
  self.is_directory = True if stage_path.endswith("/") else False
41
48
 
42
49
  @property
@@ -45,7 +52,12 @@ class GitStagePathParts(StagePathParts):
45
52
 
46
53
  @classmethod
47
54
  def get_directory(cls, stage_path: str) -> str:
48
- return "/".join(Path(stage_path).parts[3:])
55
+ git_path_parts = GitManager.split_git_path(stage_path)
56
+ # New file list does not have a stage name at the beginning
57
+ if stage_path.startswith("/"):
58
+ return "/".join(git_path_parts[OMIT_STAGE_IN_NEW_LIST_FILES])
59
+ else:
60
+ return "/".join(git_path_parts[OMIT_STAGE])
49
61
 
50
62
  @property
51
63
  def full_path(self) -> str:
@@ -53,7 +65,7 @@ class GitStagePathParts(StagePathParts):
53
65
 
54
66
  def replace_stage_prefix(self, file_path: str) -> str:
55
67
  stage = Path(self.stage).parts[0]
56
- file_path_without_prefix = Path(file_path).parts[1:]
68
+ file_path_without_prefix = Path(file_path).parts[OMIT_FIRST]
57
69
  return f"{stage}/{'/'.join(file_path_without_prefix)}"
58
70
 
59
71
  def add_stage_prefix(self, file_path: str) -> str:
@@ -95,7 +107,8 @@ class GitManager(StageManager):
95
107
  Returns stage name from potential path on stage. For example
96
108
  repo/branches/main/foo/bar -> repo/branches/main/
97
109
  """
98
- return f"{'/'.join(Path(path).parts[0:3])}/"
110
+ path_parts = GitManager.split_git_path(path)
111
+ return f"{'/'.join(path_parts[ONLY_STAGE])}/"
99
112
 
100
113
  @staticmethod
101
114
  def _stage_path_part_factory(stage_path: str) -> StagePathParts:
@@ -103,3 +116,36 @@ class GitManager(StageManager):
103
116
  if stage_path.startswith(USER_STAGE_PREFIX):
104
117
  return UserStagePathParts(stage_path)
105
118
  return GitStagePathParts(stage_path)
119
+
120
+ @staticmethod
121
+ def split_git_path(path: str):
122
+ # Check if path contains quotes and split it accordingly
123
+ if '/"' in path and '"/' in path:
124
+ if path.count('"') > 2:
125
+ raise UsageError(
126
+ f'Invalid string {path}, too much " in path, expected 2.'
127
+ )
128
+
129
+ path_parts = path.split('"')
130
+ before_quoted_part = GitManager._split_path_without_empty_parts(
131
+ path_parts[0]
132
+ )
133
+
134
+ if path_parts[2] == "/":
135
+ after_quoted_part = []
136
+ else:
137
+ after_quoted_part = GitManager._split_path_without_empty_parts(
138
+ path_parts[2]
139
+ )
140
+
141
+ return [
142
+ *before_quoted_part,
143
+ f'"{path_parts[1]}"',
144
+ *after_quoted_part,
145
+ ]
146
+ else:
147
+ return GitManager._split_path_without_empty_parts(path)
148
+
149
+ @staticmethod
150
+ def _split_path_without_empty_parts(path: str):
151
+ return [e for e in PurePosixPath(path).parts if e != "/"]