snowflake-cli-labs 2.6.1__py3-none-any.whl → 2.7.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.
Files changed (86) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/api/cli_global_context.py +9 -0
  3. snowflake/cli/api/commands/decorators.py +9 -4
  4. snowflake/cli/api/commands/execution_metadata.py +40 -0
  5. snowflake/cli/api/commands/flags.py +45 -36
  6. snowflake/cli/api/commands/project_initialisation.py +5 -2
  7. snowflake/cli/api/commands/snow_typer.py +20 -9
  8. snowflake/cli/api/config.py +1 -0
  9. snowflake/cli/api/errno.py +27 -0
  10. snowflake/cli/api/feature_flags.py +5 -0
  11. snowflake/cli/api/identifiers.py +20 -3
  12. snowflake/cli/api/output/types.py +9 -0
  13. snowflake/cli/api/project/definition_manager.py +2 -2
  14. snowflake/cli/api/project/project_verification.py +23 -0
  15. snowflake/cli/api/project/schemas/entities/application_entity.py +50 -0
  16. snowflake/cli/api/project/schemas/entities/application_package_entity.py +63 -0
  17. snowflake/cli/api/project/schemas/entities/common.py +85 -0
  18. snowflake/cli/api/project/schemas/entities/entities.py +30 -0
  19. snowflake/cli/api/project/schemas/project_definition.py +114 -22
  20. snowflake/cli/api/project/schemas/streamlit/streamlit.py +5 -4
  21. snowflake/cli/api/project/schemas/template.py +77 -0
  22. snowflake/cli/{plugins/nativeapp/errno.py → api/rendering/__init__.py} +0 -2
  23. snowflake/cli/api/{utils/rendering.py → rendering/jinja.py} +3 -48
  24. snowflake/cli/api/rendering/project_definition_templates.py +39 -0
  25. snowflake/cli/api/rendering/project_templates.py +97 -0
  26. snowflake/cli/api/rendering/sql_templates.py +56 -0
  27. snowflake/cli/api/sql_execution.py +40 -1
  28. snowflake/cli/api/utils/definition_rendering.py +8 -5
  29. snowflake/cli/app/commands_registration/builtin_plugins.py +4 -0
  30. snowflake/cli/app/dev/docs/project_definition_docs_generator.py +2 -2
  31. snowflake/cli/app/loggers.py +3 -1
  32. snowflake/cli/app/printing.py +17 -7
  33. snowflake/cli/app/snow_connector.py +9 -1
  34. snowflake/cli/app/telemetry.py +41 -2
  35. snowflake/cli/plugins/connection/commands.py +13 -3
  36. snowflake/cli/plugins/connection/util.py +73 -18
  37. snowflake/cli/plugins/cortex/commands.py +2 -1
  38. snowflake/cli/plugins/git/commands.py +20 -4
  39. snowflake/cli/plugins/git/manager.py +44 -20
  40. snowflake/cli/plugins/init/__init__.py +13 -0
  41. snowflake/cli/plugins/init/commands.py +242 -0
  42. snowflake/cli/plugins/init/plugin_spec.py +30 -0
  43. snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +40 -0
  44. snowflake/cli/plugins/nativeapp/codegen/compiler.py +57 -27
  45. snowflake/cli/plugins/nativeapp/codegen/sandbox.py +99 -10
  46. snowflake/cli/plugins/nativeapp/codegen/setup/native_app_setup_processor.py +172 -0
  47. snowflake/cli/plugins/nativeapp/codegen/setup/setup_driver.py.source +56 -0
  48. snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +21 -21
  49. snowflake/cli/plugins/nativeapp/commands.py +100 -6
  50. snowflake/cli/plugins/nativeapp/constants.py +0 -6
  51. snowflake/cli/plugins/nativeapp/exceptions.py +37 -12
  52. snowflake/cli/plugins/nativeapp/init.py +1 -1
  53. snowflake/cli/plugins/nativeapp/manager.py +114 -39
  54. snowflake/cli/plugins/nativeapp/project_model.py +8 -4
  55. snowflake/cli/plugins/nativeapp/run_processor.py +117 -102
  56. snowflake/cli/plugins/nativeapp/teardown_processor.py +7 -2
  57. snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +146 -0
  58. snowflake/cli/plugins/nativeapp/version/commands.py +19 -3
  59. snowflake/cli/plugins/nativeapp/version/version_processor.py +11 -3
  60. snowflake/cli/plugins/snowpark/commands.py +34 -26
  61. snowflake/cli/plugins/snowpark/common.py +88 -27
  62. snowflake/cli/plugins/snowpark/manager.py +16 -5
  63. snowflake/cli/plugins/snowpark/models.py +6 -0
  64. snowflake/cli/plugins/sql/commands.py +3 -5
  65. snowflake/cli/plugins/sql/manager.py +1 -1
  66. snowflake/cli/plugins/stage/commands.py +2 -2
  67. snowflake/cli/plugins/stage/diff.py +27 -64
  68. snowflake/cli/plugins/stage/manager.py +290 -86
  69. snowflake/cli/plugins/stage/md5.py +160 -0
  70. snowflake/cli/plugins/streamlit/commands.py +20 -6
  71. snowflake/cli/plugins/streamlit/manager.py +46 -32
  72. snowflake/cli/plugins/workspace/__init__.py +13 -0
  73. snowflake/cli/plugins/workspace/commands.py +35 -0
  74. snowflake/cli/plugins/workspace/plugin_spec.py +30 -0
  75. snowflake/cli/templates/default_snowpark/app/__init__.py +0 -13
  76. snowflake/cli/templates/default_snowpark/app/common.py +0 -15
  77. snowflake/cli/templates/default_snowpark/app/functions.py +0 -14
  78. snowflake/cli/templates/default_snowpark/app/procedures.py +0 -14
  79. snowflake/cli/templates/default_streamlit/common/hello.py +0 -15
  80. snowflake/cli/templates/default_streamlit/pages/my_page.py +0 -14
  81. snowflake/cli/templates/default_streamlit/streamlit_app.py +0 -14
  82. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0.dist-info}/METADATA +7 -6
  83. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0.dist-info}/RECORD +86 -65
  84. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0.dist-info}/WHEEL +0 -0
  85. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0.dist-info}/entry_points.txt +0 -0
  86. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -38,9 +38,7 @@ from snowflake.cli.api.constants import (
38
38
  DEPLOYMENT_STAGE,
39
39
  ObjectType,
40
40
  )
41
- from snowflake.cli.api.exceptions import (
42
- SecretsWithoutExternalAccessIntegrationError,
43
- )
41
+ from snowflake.cli.api.exceptions import SecretsWithoutExternalAccessIntegrationError
44
42
  from snowflake.cli.api.identifiers import FQN
45
43
  from snowflake.cli.api.output.types import (
46
44
  CollectionResult,
@@ -48,6 +46,7 @@ from snowflake.cli.api.output.types import (
48
46
  MessageResult,
49
47
  SingleQueryResult,
50
48
  )
49
+ from snowflake.cli.api.project.project_verification import assert_project_type
51
50
  from snowflake.cli.api.project.schemas.snowpark.callable import (
52
51
  FunctionSchema,
53
52
  ProcedureSchema,
@@ -68,7 +67,8 @@ from snowflake.cli.plugins.object.commands import (
68
67
  from snowflake.cli.plugins.object.manager import ObjectManager
69
68
  from snowflake.cli.plugins.snowpark import package_utils
70
69
  from snowflake.cli.plugins.snowpark.common import (
71
- build_udf_sproc_identifier,
70
+ FunctionOrProcedure,
71
+ UdfSprocIdentifier,
72
72
  check_if_replace_is_required,
73
73
  )
74
74
  from snowflake.cli.plugins.snowpark.manager import FunctionManager, ProcedureManager
@@ -116,7 +116,7 @@ add_init_command(app, project_type="Snowpark", template="default_snowpark")
116
116
 
117
117
 
118
118
  @app.command("deploy", requires_connection=True)
119
- @with_project_definition("snowpark")
119
+ @with_project_definition()
120
120
  def deploy(
121
121
  replace: bool = ReplaceOption(
122
122
  help="Replaces procedure or function, even if no detected changes to metadata"
@@ -128,6 +128,9 @@ def deploy(
128
128
  By default, if any of the objects exist already the commands will fail unless `--replace` flag is provided.
129
129
  All deployed objects use the same artifact which is deployed only once.
130
130
  """
131
+
132
+ assert_project_type("snowpark")
133
+
131
134
  snowpark = cli_context.project_definition.snowpark
132
135
  paths = SnowparkPackagePaths.for_snowpark_project(
133
136
  project_root=SecurePath(cli_context.project_root),
@@ -218,7 +221,7 @@ def deploy(
218
221
 
219
222
 
220
223
  def _assert_object_definitions_are_correct(
221
- object_type, object_definitions: List[FunctionSchema | ProcedureSchema]
224
+ object_type, object_definitions: List[FunctionOrProcedure]
222
225
  ):
223
226
  for definition in object_definitions:
224
227
  database = definition.database
@@ -237,14 +240,14 @@ def _assert_object_definitions_are_correct(
237
240
 
238
241
  def _find_existing_objects(
239
242
  object_type: ObjectType,
240
- objects: List[Dict],
243
+ objects: List[FunctionOrProcedure],
241
244
  om: ObjectManager,
242
245
  ):
243
246
  existing_objects = {}
244
247
  for object_definition in objects:
245
- identifier = build_udf_sproc_identifier(
246
- object_definition, om, include_parameter_names=False
247
- )
248
+ identifier = UdfSprocIdentifier.from_definition(
249
+ object_definition
250
+ ).identifier_with_arg_types
248
251
  try:
249
252
  current_state = om.describe(
250
253
  object_type=object_type.value.sf_name,
@@ -293,50 +296,52 @@ def get_app_stage_path(stage_name: Optional[str], project_name: str) -> str:
293
296
  def _deploy_single_object(
294
297
  manager: FunctionManager | ProcedureManager,
295
298
  object_type: ObjectType,
296
- object_definition: FunctionSchema | ProcedureSchema,
299
+ object_definition: FunctionOrProcedure,
297
300
  existing_objects: Dict[str, Dict],
298
301
  snowflake_dependencies: List[str],
299
302
  stage_artifact_path: str,
300
303
  ):
301
- identifier = build_udf_sproc_identifier(
302
- object_definition, manager, include_parameter_names=False
303
- )
304
- identifier_with_default_values = build_udf_sproc_identifier(
305
- object_definition,
306
- manager,
307
- include_parameter_names=True,
308
- include_default_values=True,
304
+
305
+ identifiers = UdfSprocIdentifier.from_definition(object_definition)
306
+
307
+ log.info(
308
+ "Deploying %s: %s", object_type, identifiers.identifier_with_arg_names_types
309
309
  )
310
- log.info("Deploying %s: %s", object_type, identifier_with_default_values)
311
310
 
312
311
  handler = object_definition.handler
313
312
  returns = object_definition.returns
314
313
  imports = object_definition.imports
315
314
  external_access_integrations = object_definition.external_access_integrations
315
+ runtime_ver = object_definition.runtime
316
+ execute_as_caller = None
317
+ if object_type == ObjectType.PROCEDURE:
318
+ execute_as_caller = object_definition.execute_as_caller
316
319
  replace_object = False
317
320
 
318
- object_exists = identifier in existing_objects
321
+ object_exists = identifiers.identifier_with_arg_types in existing_objects
319
322
  if object_exists:
320
323
  replace_object = check_if_replace_is_required(
321
324
  object_type=object_type,
322
- current_state=existing_objects[identifier],
325
+ current_state=existing_objects[identifiers.identifier_with_arg_types],
323
326
  handler=handler,
324
327
  return_type=returns,
325
328
  snowflake_dependencies=snowflake_dependencies,
326
329
  external_access_integrations=external_access_integrations,
327
330
  imports=imports,
328
331
  stage_artifact_file=stage_artifact_path,
332
+ runtime_ver=runtime_ver,
333
+ execute_as_caller=execute_as_caller,
329
334
  )
330
335
 
331
336
  if object_exists and not replace_object:
332
337
  return {
333
- "object": identifier_with_default_values,
338
+ "object": identifiers.identifier_with_arg_names_types_defaults,
334
339
  "type": str(object_type),
335
340
  "status": "packages updated",
336
341
  }
337
342
 
338
343
  create_or_replace_kwargs = {
339
- "identifier": identifier_with_default_values,
344
+ "identifier": identifiers,
340
345
  "handler": handler,
341
346
  "return_type": returns,
342
347
  "artifact_file": stage_artifact_path,
@@ -354,7 +359,7 @@ def _deploy_single_object(
354
359
 
355
360
  status = "created" if not object_exists else "definition updated"
356
361
  return {
357
- "object": identifier_with_default_values,
362
+ "object": identifiers.identifier_with_arg_names_types_defaults,
358
363
  "type": str(object_type),
359
364
  "status": status,
360
365
  }
@@ -379,7 +384,7 @@ def _read_snowflake_requrements_file(file_path: SecurePath):
379
384
 
380
385
 
381
386
  @app.command("build", requires_connection=True)
382
- @with_project_definition("snowpark")
387
+ @with_project_definition()
383
388
  def build(
384
389
  ignore_anaconda: bool = IgnoreAnacondaOption,
385
390
  allow_shared_libraries: bool = AllowSharedLibrariesOption,
@@ -396,6 +401,9 @@ def build(
396
401
  Builds the Snowpark project as a `.zip` archive that can be used by `deploy` command.
397
402
  The archive is built using only the `src` directory specified in the project file.
398
403
  """
404
+
405
+ assert_project_type("snowpark")
406
+
399
407
  if not deprecated_check_anaconda_for_pypi_deps:
400
408
  ignore_anaconda = True
401
409
  snowpark_paths = SnowparkPackagePaths.for_snowpark_project(
@@ -15,11 +15,14 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import re
18
- from typing import Dict, List, Optional, Set
18
+ from typing import Dict, List, Optional, Set, Union
19
19
 
20
20
  from snowflake.cli.api.constants import ObjectType
21
21
  from snowflake.cli.api.identifiers import FQN
22
- from snowflake.cli.api.project.schemas.snowpark.argument import Argument
22
+ from snowflake.cli.api.project.schemas.snowpark.callable import (
23
+ FunctionSchema,
24
+ ProcedureSchema,
25
+ )
23
26
  from snowflake.cli.api.sql_execution import SqlExecutionMixin
24
27
  from snowflake.cli.plugins.snowpark.models import Requirement
25
28
  from snowflake.cli.plugins.snowpark.package_utils import (
@@ -28,6 +31,7 @@ from snowflake.cli.plugins.snowpark.package_utils import (
28
31
  from snowflake.connector.cursor import SnowflakeCursor
29
32
 
30
33
  DEFAULT_RUNTIME = "3.8"
34
+ FunctionOrProcedure = Union[FunctionSchema, ProcedureSchema]
31
35
 
32
36
 
33
37
  def check_if_replace_is_required(
@@ -39,6 +43,8 @@ def check_if_replace_is_required(
39
43
  external_access_integrations: List[str],
40
44
  imports: List[str],
41
45
  stage_artifact_file: str,
46
+ runtime_ver: Optional[str] = None,
47
+ execute_as_caller: Optional[bool] = None,
42
48
  ) -> bool:
43
49
  import logging
44
50
 
@@ -78,6 +84,22 @@ def check_if_replace_is_required(
78
84
  return True
79
85
 
80
86
  if _compare_imports(resource_json, imports, stage_artifact_file):
87
+ log.info("Imports do not match. Replacing the %s", object_type)
88
+ return True
89
+
90
+ if runtime_ver is not None and runtime_ver != resource_json.get(
91
+ "runtime_version", "RUNTIME_NOT_SET"
92
+ ):
93
+ log.info("Runtime versions do not match. Replacing the %s", object_type)
94
+ return True
95
+
96
+ if execute_as_caller is not None and (
97
+ resource_json.get("execute as", "OWNER")
98
+ != ("CALLER" if execute_as_caller else "OWNER")
99
+ ):
100
+ log.info(
101
+ "Execute as caller settings do not match. Replacing the %s", object_type
102
+ )
81
103
  return True
82
104
 
83
105
  return False
@@ -150,7 +172,7 @@ class SnowparkObjectManager(SqlExecutionMixin):
150
172
 
151
173
  def create_query(
152
174
  self,
153
- identifier: str,
175
+ identifier: UdfSprocIdentifier,
154
176
  return_type: str,
155
177
  handler: str,
156
178
  artifact_file: str,
@@ -166,7 +188,7 @@ class SnowparkObjectManager(SqlExecutionMixin):
166
188
  packages_list = ",".join(f"'{p}'" for p in packages)
167
189
 
168
190
  query = [
169
- f"create or replace {self._object_type.value.sf_name} {identifier}",
191
+ f"create or replace {self._object_type.value.sf_name} {identifier.identifier_for_sql}",
170
192
  f"copy grants",
171
193
  f"returns {return_type}",
172
194
  "language python",
@@ -198,30 +220,69 @@ def _is_signature_type_a_string(sig_type: str) -> bool:
198
220
  return sig_type.lower() in ["string", "varchar"]
199
221
 
200
222
 
201
- def build_udf_sproc_identifier(
202
- udf_sproc,
203
- slq_exec_mixin,
204
- include_parameter_names,
205
- include_default_values=False,
206
- ):
207
- def format_arg(arg: Argument):
208
- result = f"{arg.arg_type}"
209
- if include_parameter_names:
210
- result = f"{arg.name} {result}"
211
- if include_default_values and arg.default:
212
- val = f"{arg.default}"
213
- if _is_signature_type_a_string(arg.arg_type):
214
- val = f"'{val}'"
215
- result += f" default {val}"
216
- return result
217
-
218
- if udf_sproc.signature and udf_sproc.signature != "null":
219
- arguments = ", ".join(format_arg(arg) for arg in udf_sproc.signature)
220
- else:
221
- arguments = ""
223
+ class UdfSprocIdentifier:
224
+ def __init__(self, identifier: FQN, arg_names, arg_types, arg_defaults):
225
+ self._identifier = identifier
226
+ self._arg_names = arg_names
227
+ self._arg_types = arg_types
228
+ self._arg_defaults = arg_defaults
229
+
230
+ def _identifier_from_signature(self, sig: List[str], for_sql: bool = False):
231
+ signature = self._comma_join(sig)
232
+ id_ = self._identifier.sql_identifier if for_sql else self._identifier
233
+ return f"{id_}({signature})"
222
234
 
223
- name = FQN.from_identifier_model(udf_sproc).using_context().identifier
224
- return f"{name}({arguments})"
235
+ @staticmethod
236
+ def _comma_join(*args):
237
+ return ", ".join(*args)
238
+
239
+ @property
240
+ def identifier_with_arg_names(self):
241
+ return self._identifier_from_signature(self._arg_names)
242
+
243
+ @property
244
+ def identifier_with_arg_types(self):
245
+ return self._identifier_from_signature(self._arg_types)
246
+
247
+ @property
248
+ def identifier_with_arg_names_types(self):
249
+ sig = [f"{n} {t}" for n, t in zip(self._arg_names, self._arg_types)]
250
+ return self._identifier_from_signature(sig)
251
+
252
+ @property
253
+ def identifier_with_arg_names_types_defaults(self):
254
+ return self._identifier_from_signature(self._full_signature())
255
+
256
+ def _full_signature(self):
257
+ sig = []
258
+ for name, _type, _default in zip(
259
+ self._arg_names, self._arg_types, self._arg_defaults
260
+ ):
261
+ s = f"{name} {_type}"
262
+ if _default:
263
+ if _is_signature_type_a_string(_type):
264
+ _default = f"'{_default}'"
265
+ s += f" default {_default}"
266
+ sig.append(s)
267
+ return sig
268
+
269
+ @property
270
+ def identifier_for_sql(self):
271
+ return self._identifier_from_signature(self._full_signature(), for_sql=True)
272
+
273
+ @classmethod
274
+ def from_definition(cls, udf_sproc: FunctionOrProcedure):
275
+ names = []
276
+ types = []
277
+ defaults = []
278
+ if udf_sproc.signature and udf_sproc.signature != "null":
279
+ for arg in udf_sproc.signature:
280
+ names.append(arg.name)
281
+ types.append(arg.arg_type)
282
+ defaults.append(arg.default)
283
+
284
+ identifier = FQN.from_identifier_model(udf_sproc).using_context()
285
+ return cls(identifier, names, types, defaults)
225
286
 
226
287
 
227
288
  def _compare_imports(
@@ -18,7 +18,10 @@ import logging
18
18
  from typing import Dict, List, Optional
19
19
 
20
20
  from snowflake.cli.api.constants import ObjectType
21
- from snowflake.cli.plugins.snowpark.common import SnowparkObjectManager
21
+ from snowflake.cli.plugins.snowpark.common import (
22
+ SnowparkObjectManager,
23
+ UdfSprocIdentifier,
24
+ )
22
25
  from snowflake.connector.cursor import SnowflakeCursor
23
26
 
24
27
  log = logging.getLogger(__name__)
@@ -35,7 +38,7 @@ class FunctionManager(SnowparkObjectManager):
35
38
 
36
39
  def create_or_replace(
37
40
  self,
38
- identifier: str,
41
+ identifier: UdfSprocIdentifier,
39
42
  return_type: str,
40
43
  handler: str,
41
44
  artifact_file: str,
@@ -45,7 +48,11 @@ class FunctionManager(SnowparkObjectManager):
45
48
  secrets: Optional[Dict[str, str]] = None,
46
49
  runtime: Optional[str] = None,
47
50
  ) -> SnowflakeCursor:
48
- log.debug("Creating function %s using @%s", identifier, artifact_file)
51
+ log.debug(
52
+ "Creating function %s using @%s",
53
+ identifier.identifier_with_arg_names_types_defaults,
54
+ artifact_file,
55
+ )
49
56
  query = self.create_query(
50
57
  identifier,
51
58
  return_type,
@@ -71,7 +78,7 @@ class ProcedureManager(SnowparkObjectManager):
71
78
 
72
79
  def create_or_replace(
73
80
  self,
74
- identifier: str,
81
+ identifier: UdfSprocIdentifier,
75
82
  return_type: str,
76
83
  handler: str,
77
84
  artifact_file: str,
@@ -82,7 +89,11 @@ class ProcedureManager(SnowparkObjectManager):
82
89
  runtime: Optional[str] = None,
83
90
  execute_as_caller: bool = False,
84
91
  ) -> SnowflakeCursor:
85
- log.debug("Creating procedure %s using @%s", identifier, artifact_file)
92
+ log.debug(
93
+ "Creating procedure %s using @%s",
94
+ identifier.identifier_with_arg_names_types_defaults,
95
+ artifact_file,
96
+ )
86
97
  query = self.create_query(
87
98
  identifier,
88
99
  return_type,
@@ -33,6 +33,10 @@ class YesNoAsk(Enum):
33
33
  class Requirement(requirement.Requirement):
34
34
  extra_pattern = re.compile("'([^']*)'")
35
35
 
36
+ def __init__(self, *args, **kwargs):
37
+ super().__init__(*args, **kwargs)
38
+ self.package_name = None
39
+
36
40
  @classmethod
37
41
  def parse_line(cls, line: str) -> Requirement:
38
42
  if len(line_elements := line.split(";")) > 1:
@@ -44,6 +48,8 @@ class Requirement(requirement.Requirement):
44
48
  if "extra" in element and (extras := cls.extra_pattern.search(element)):
45
49
  result.extras.extend(extras.groups())
46
50
 
51
+ result.package_name = result.name
52
+
47
53
  if result.uri and not result.name:
48
54
  result.name = get_package_name(result.uri)
49
55
  result.name = cls.standardize_name(result.name)
@@ -21,6 +21,7 @@ import typer
21
21
  from snowflake.cli.api.commands.decorators import with_project_definition
22
22
  from snowflake.cli.api.commands.flags import (
23
23
  parse_key_value_variables,
24
+ variables_option,
24
25
  )
25
26
  from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
26
27
  from snowflake.cli.api.output.types import CommandResult, MultipleResults, QueryResult
@@ -55,11 +56,8 @@ def execute_sql(
55
56
  "-i",
56
57
  help="Read the query from standard input. Use it when piping input to this command.",
57
58
  ),
58
- data_override: List[str] = typer.Option(
59
- None,
60
- "--variable",
61
- "-D",
62
- help="String in format of key=value. If provided the SQL content will "
59
+ data_override: List[str] = variables_option(
60
+ "String in format of key=value. If provided the SQL content will "
63
61
  "be treated as template and rendered using provided data.",
64
62
  ),
65
63
  **options,
@@ -22,9 +22,9 @@ from typing import Dict, Iterable, List, Tuple
22
22
 
23
23
  from click import ClickException, UsageError
24
24
  from jinja2 import UndefinedError
25
+ from snowflake.cli.api.rendering.sql_templates import snowflake_sql_jinja_render
25
26
  from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
26
27
  from snowflake.cli.api.sql_execution import SqlExecutionMixin, VerboseCursor
27
- from snowflake.cli.api.utils.rendering import snowflake_sql_jinja_render
28
28
  from snowflake.cli.plugins.sql.snowsql_templating import transpile_snowsql_templates
29
29
  from snowflake.connector.cursor import SnowflakeCursor
30
30
  from snowflake.connector.util_text import split_statements
@@ -23,9 +23,9 @@ import click
23
23
  import typer
24
24
  from snowflake.cli.api.cli_global_context import cli_context
25
25
  from snowflake.cli.api.commands.flags import (
26
+ ExecuteVariablesOption,
26
27
  OnErrorOption,
27
28
  PatternOption,
28
- VariablesOption,
29
29
  like_option,
30
30
  )
31
31
  from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
@@ -201,7 +201,7 @@ def execute(
201
201
  show_default=False,
202
202
  ),
203
203
  on_error: OnErrorType = OnErrorOption,
204
- variables: Optional[List[str]] = VariablesOption,
204
+ variables: Optional[List[str]] = ExecuteVariablesOption,
205
205
  **options,
206
206
  ):
207
207
  """
@@ -14,9 +14,7 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- import hashlib
18
17
  import logging
19
- import re
20
18
  from dataclasses import dataclass, field
21
19
  from pathlib import Path, PurePosixPath
22
20
  from typing import Collection, Dict, List, Optional, Tuple
@@ -25,14 +23,11 @@ from snowflake.cli.api.console import cli_console as cc
25
23
  from snowflake.cli.api.exceptions import (
26
24
  SnowflakeSQLExecutionError,
27
25
  )
28
- from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
29
26
  from snowflake.cli.plugins.nativeapp.artifacts import BundleMap
30
27
  from snowflake.connector.cursor import DictCursor
31
28
 
32
29
  from .manager import StageManager
33
-
34
- MD5SUM_REGEX = r"^[A-Fa-f0-9]{32}$"
35
- CHUNK_SIZE_BYTES = 8192
30
+ from .md5 import UnknownMD5FormatError, file_matches_md5sum
36
31
 
37
32
  log = logging.getLogger(__name__)
38
33
 
@@ -72,43 +67,6 @@ class DiffResult:
72
67
  }
73
68
 
74
69
 
75
- def is_valid_md5sum(checksum: str) -> bool:
76
- """
77
- Could the provided hexadecimal checksum represent a valid md5sum?
78
- """
79
- return re.match(MD5SUM_REGEX, checksum) is not None
80
-
81
-
82
- def compute_md5sum(file: Path) -> str:
83
- """
84
- Returns a hexadecimal checksum for the file located at the given path.
85
- """
86
- if not file.is_file():
87
- raise ValueError(
88
- "The provided file does not exist or not a (symlink to a) regular file"
89
- )
90
-
91
- # FIXME: there are two cases in which this will fail to provide a matching
92
- # md5sum, even when the underlying file is the same:
93
- # 1. when the stage uses SNOWFLAKE_FULL encryption
94
- # 2. when the file was uploaded in multiple parts
95
-
96
- # We can re-create the second if we know what chunk size was used by the
97
- # upload process to the backing object store (e.g. S3, azure blob, etc.)
98
- # but we cannot re-create the first as the encrpytion key is hidden.
99
-
100
- # We are assuming that we will not get accidental collisions here due to the
101
- # large space of the md5sum (32 * 4 = 128 bits means 1-in-9-trillion chance)
102
- # combined with the fact that the file name + path must also match elsewhere.
103
-
104
- with SecurePath(file).open("rb", read_file_limit_mb=UNLIMITED) as f:
105
- file_hash = hashlib.md5()
106
- while chunk := f.read(CHUNK_SIZE_BYTES):
107
- file_hash.update(chunk)
108
-
109
- return file_hash.hexdigest()
110
-
111
-
112
70
  def enumerate_files(path: Path) -> List[Path]:
113
71
  """
114
72
  Get a list of all files in a directory (recursively).
@@ -131,7 +89,7 @@ def strip_stage_name(path: str) -> StagePath:
131
89
  return StagePath(*path.split("/")[1:])
132
90
 
133
91
 
134
- def build_md5_map(list_stage_cursor: DictCursor) -> Dict[StagePath, str]:
92
+ def build_md5_map(list_stage_cursor: DictCursor) -> Dict[StagePath, Optional[str]]:
135
93
  """
136
94
  Returns a mapping of relative stage paths to their md5sums.
137
95
  """
@@ -173,30 +131,35 @@ def compute_stage_diff(
173
131
 
174
132
  for local_file in local_files:
175
133
  relpath = local_file.relative_to(local_root)
176
- stage_filename = to_stage_path(relpath)
177
- if stage_filename not in remote_md5:
134
+ stage_path = to_stage_path(relpath)
135
+ if stage_path not in remote_md5:
178
136
  # doesn't exist on the stage
179
- result.only_local.append(stage_filename)
137
+ result.only_local.append(stage_path)
180
138
  else:
181
- # N.B. we could compare local size vs remote size to skip the relatively-
182
- # expensive md5sum operation, but after seeing a comment that says the value
183
- # may not always be correctly populated, we'll ignore that column.
184
- stage_md5sum = remote_md5[stage_filename]
185
- if is_valid_md5sum(stage_md5sum) and stage_md5sum == compute_md5sum(
186
- local_file
187
- ):
188
- # the file definitely hasn't changed
189
- result.identical.append(stage_filename)
190
- else:
191
- # either the file has changed, or we can't tell if it has
192
- result.different.append(stage_filename)
139
+ # N.B. file size on stage is not always accurate, so cannot fail fast
140
+ try:
141
+ if file_matches_md5sum(local_file, remote_md5[stage_path]):
142
+ # We are assuming that we will not get accidental collisions here due to the
143
+ # large space of the md5sum (32 * 4 = 128 bits means 1-in-9-trillion chance)
144
+ # combined with the fact that the file name + path must also match elsewhere.
145
+ result.identical.append(stage_path)
146
+ else:
147
+ # either the file has changed, or we can't tell if it has
148
+ result.different.append(stage_path)
149
+ except UnknownMD5FormatError:
150
+ log.warning(
151
+ "Could not compare md5 for %s, assuming file has changed",
152
+ local_file,
153
+ exc_info=True,
154
+ )
155
+ result.different.append(stage_path)
193
156
 
194
157
  # mark this file as seen
195
- del remote_md5[stage_filename]
158
+ del remote_md5[stage_path]
196
159
 
197
160
  # every entry here is a file we never saw locally
198
- for stage_filename in remote_md5.keys():
199
- result.only_on_stage.append(stage_filename)
161
+ for stage_path in remote_md5.keys():
162
+ result.only_on_stage.append(stage_path)
200
163
 
201
164
  return result
202
165
 
@@ -230,8 +193,8 @@ def delete_only_on_stage_files(
230
193
  """
231
194
  Deletes all files from a Snowflake stage according to the input list of filenames, using a custom role.
232
195
  """
233
- for _stage_filename in only_on_stage:
234
- stage_manager.remove(stage_name=stage_fqn, path=str(_stage_filename), role=role)
196
+ for _stage_path in only_on_stage:
197
+ stage_manager.remove(stage_name=stage_fqn, path=str(_stage_path), role=role)
235
198
 
236
199
 
237
200
  def put_files_on_stage(