snowflake-cli 3.3.0__py3-none-any.whl → 3.4.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.
Files changed (77) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/__main__.py +2 -2
  3. snowflake/cli/_app/cli_app.py +224 -192
  4. snowflake/cli/_app/commands_registration/commands_registration_with_callbacks.py +1 -27
  5. snowflake/cli/_plugins/cortex/commands.py +2 -4
  6. snowflake/cli/_plugins/git/manager.py +1 -1
  7. snowflake/cli/_plugins/nativeapp/artifacts.py +6 -624
  8. snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
  9. snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
  10. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +1 -3
  11. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
  12. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +2 -2
  13. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +2 -2
  14. snowflake/cli/_plugins/nativeapp/commands.py +21 -19
  15. snowflake/cli/_plugins/nativeapp/entities/application.py +16 -19
  16. snowflake/cli/_plugins/nativeapp/entities/application_package.py +142 -55
  17. snowflake/cli/_plugins/nativeapp/release_channel/commands.py +37 -3
  18. snowflake/cli/_plugins/nativeapp/release_directive/commands.py +80 -2
  19. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +224 -44
  20. snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +2 -2
  21. snowflake/cli/_plugins/nativeapp/version/commands.py +1 -1
  22. snowflake/cli/_plugins/notebook/commands.py +55 -2
  23. snowflake/cli/_plugins/notebook/exceptions.py +1 -1
  24. snowflake/cli/_plugins/notebook/manager.py +3 -3
  25. snowflake/cli/_plugins/notebook/notebook_entity.py +120 -0
  26. snowflake/cli/_plugins/notebook/notebook_entity_model.py +42 -0
  27. snowflake/cli/_plugins/notebook/notebook_project_paths.py +15 -0
  28. snowflake/cli/_plugins/notebook/types.py +3 -0
  29. snowflake/cli/_plugins/snowpark/commands.py +48 -30
  30. snowflake/cli/_plugins/snowpark/common.py +47 -2
  31. snowflake/cli/_plugins/snowpark/snowpark_entity.py +38 -25
  32. snowflake/cli/_plugins/snowpark/snowpark_entity_model.py +18 -30
  33. snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +156 -23
  34. snowflake/cli/_plugins/snowpark/zipper.py +33 -1
  35. snowflake/cli/_plugins/spcs/services/commands.py +0 -3
  36. snowflake/cli/_plugins/stage/commands.py +2 -1
  37. snowflake/cli/_plugins/stage/diff.py +60 -39
  38. snowflake/cli/_plugins/stage/manager.py +24 -11
  39. snowflake/cli/_plugins/stage/utils.py +1 -1
  40. snowflake/cli/_plugins/streamlit/commands.py +10 -1
  41. snowflake/cli/_plugins/streamlit/manager.py +62 -21
  42. snowflake/cli/_plugins/streamlit/streamlit_entity.py +20 -41
  43. snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +14 -24
  44. snowflake/cli/_plugins/streamlit/streamlit_project_paths.py +30 -0
  45. snowflake/cli/_plugins/workspace/commands.py +3 -3
  46. snowflake/cli/_plugins/workspace/manager.py +1 -1
  47. snowflake/cli/api/artifacts/__init__.py +13 -0
  48. snowflake/cli/api/artifacts/bundle_map.py +500 -0
  49. snowflake/cli/api/artifacts/common.py +78 -0
  50. snowflake/cli/api/artifacts/utils.py +82 -0
  51. snowflake/cli/api/cli_global_context.py +14 -1
  52. snowflake/cli/api/commands/flags.py +10 -4
  53. snowflake/cli/api/commands/utils.py +28 -2
  54. snowflake/cli/api/constants.py +1 -0
  55. snowflake/cli/api/entities/common.py +14 -32
  56. snowflake/cli/api/entities/resolver.py +160 -0
  57. snowflake/cli/api/entities/utils.py +56 -15
  58. snowflake/cli/api/errno.py +3 -0
  59. snowflake/cli/api/feature_flags.py +1 -2
  60. snowflake/cli/api/project/definition_conversion.py +3 -2
  61. snowflake/cli/api/project/project_paths.py +28 -0
  62. snowflake/cli/api/project/schemas/entities/common.py +130 -1
  63. snowflake/cli/api/project/schemas/entities/entities.py +4 -0
  64. snowflake/cli/api/project/schemas/project_definition.py +27 -0
  65. snowflake/cli/api/project/schemas/updatable_model.py +2 -2
  66. snowflake/cli/api/project/schemas/v1/native_app/native_app.py +5 -7
  67. snowflake/cli/api/secure_path.py +6 -0
  68. snowflake/cli/api/sql_execution.py +5 -1
  69. snowflake/cli/api/stage_path.py +7 -2
  70. snowflake/cli/api/utils/graph.py +3 -0
  71. snowflake/cli/api/utils/path_utils.py +24 -0
  72. {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.4.1.dist-info}/METADATA +8 -9
  73. {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.4.1.dist-info}/RECORD +76 -67
  74. snowflake/cli/api/project/schemas/v1/native_app/path_mapping.py +0 -65
  75. {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.4.1.dist-info}/WHEEL +0 -0
  76. {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.4.1.dist-info}/entry_points.txt +0 -0
  77. {snowflake_cli-3.3.0.dist-info → snowflake_cli-3.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -45,6 +45,7 @@ from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import (
45
45
  UserScriptError,
46
46
  handle_unclassified_error,
47
47
  )
48
+ from snowflake.cli._plugins.stage.manager import StageManager
48
49
  from snowflake.cli.api.cli_global_context import get_cli_context
49
50
  from snowflake.cli.api.constants import ObjectType
50
51
  from snowflake.cli.api.errno import (
@@ -53,6 +54,8 @@ from snowflake.cli.api.errno import (
53
54
  APPLICATION_PACKAGE_MAX_VERSIONS_HIT,
54
55
  APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS,
55
56
  APPLICATION_REQUIRES_TELEMETRY_SHARING,
57
+ CANNOT_ADD_PATCH_WITH_NON_INCREASING_PATCH_NUMBER,
58
+ CANNOT_CREATE_VERSION_WITH_NON_ZERO_PATCH,
56
59
  CANNOT_DEREGISTER_VERSION_ASSOCIATED_WITH_CHANNEL,
57
60
  CANNOT_DISABLE_MANDATORY_TELEMETRY,
58
61
  CANNOT_DISABLE_RELEASE_CHANNELS,
@@ -64,6 +67,7 @@ from snowflake.cli.api.errno import (
64
67
  MAX_VERSIONS_IN_RELEASE_CHANNEL_REACHED,
65
68
  NO_WAREHOUSE_SELECTED_IN_SESSION,
66
69
  RELEASE_DIRECTIVE_DOES_NOT_EXIST,
70
+ RELEASE_DIRECTIVE_UNAPPROVED_VERSION_OR_PATCH,
67
71
  RELEASE_DIRECTIVES_VERSION_PATCH_NOT_FOUND,
68
72
  SQL_COMPILATION_ERROR,
69
73
  TARGET_ACCOUNT_USED_BY_OTHER_RELEASE_DIRECTIVE,
@@ -289,7 +293,7 @@ class SnowflakeSQLFacade:
289
293
  def create_version_in_package(
290
294
  self,
291
295
  package_name: str,
292
- stage_fqn: str,
296
+ path_to_version_directory: str,
293
297
  version: str,
294
298
  label: str | None = None,
295
299
  role: str | None = None,
@@ -297,7 +301,7 @@ class SnowflakeSQLFacade:
297
301
  """
298
302
  Creates a new version in an existing application package.
299
303
  @param package_name: Name of the application package to alter.
300
- @param stage_fqn: Stage fully qualified name.
304
+ @param path_to_version_directory: Path to artifacts on the stage to create a version from.
301
305
  @param version: Version name to create.
302
306
  @param [Optional] role: Switch to this role while executing create version.
303
307
  @param [Optional] label: Label for this version, visible to consumers.
@@ -312,6 +316,9 @@ class SnowflakeSQLFacade:
312
316
  with_label_clause = (
313
317
  f"label={to_string_literal(label)}" if label is not None else ""
314
318
  )
319
+ using_clause = (
320
+ f"using {StageManager.quote_stage_name(path_to_version_directory)}"
321
+ )
315
322
 
316
323
  action = "register" if available_release_channels else "add"
317
324
 
@@ -320,7 +327,7 @@ class SnowflakeSQLFacade:
320
327
  f"""\
321
328
  alter application package {package_name}
322
329
  {action} version {version}
323
- using @{stage_fqn}
330
+ {using_clause}
324
331
  {with_label_clause}
325
332
  """
326
333
  )
@@ -342,6 +349,10 @@ class SnowflakeSQLFacade:
342
349
  f"Maximum versions reached for application package {package_name}. "
343
350
  "Please drop the other versions first."
344
351
  ) from err
352
+ if err.errno == CANNOT_CREATE_VERSION_WITH_NON_ZERO_PATCH:
353
+ raise UserInputError(
354
+ "Cannot create a new version with a non-zero patch in the manifest file."
355
+ ) from err
345
356
  handle_unclassified_error(
346
357
  err,
347
358
  f"Failed to {action} version {version} to application package {package_name}.",
@@ -389,7 +400,7 @@ class SnowflakeSQLFacade:
389
400
  def add_patch_to_package_version(
390
401
  self,
391
402
  package_name: str,
392
- stage_fqn: str,
403
+ path_to_version_directory: str,
393
404
  version: str,
394
405
  patch: int | None = None,
395
406
  label: str | None = None,
@@ -398,7 +409,7 @@ class SnowflakeSQLFacade:
398
409
  """
399
410
  Add a new patch, optionally a custom one, to an existing version in an application package.
400
411
  @param package_name: Name of the application package to alter.
401
- @param stage_fqn: Stage fully qualified name.
412
+ @param path_to_version_directory: Path to artifacts on the stage to create a version from.
402
413
  @param version: Version name to create.
403
414
  @param [Optional] patch: Patch number to create.
404
415
  @param [Optional] label: Label for this patch, visible to consumers.
@@ -416,13 +427,13 @@ class SnowflakeSQLFacade:
416
427
  )
417
428
 
418
429
  patch_query = f" {patch}" if patch is not None else ""
419
-
430
+ using_clause = StageManager.quote_stage_name(path_to_version_directory)
420
431
  # No space between patch and patch{patch_query} to avoid extra space when patch is None
421
432
  add_patch_query = dedent(
422
433
  f"""\
423
434
  alter application package {package_name}
424
435
  add patch{patch_query} for version {version}
425
- using @{stage_fqn}{with_label_clause}
436
+ using {using_clause}{with_label_clause}
426
437
  """
427
438
  )
428
439
  with self._use_role_optional(role):
@@ -433,8 +444,17 @@ class SnowflakeSQLFacade:
433
444
  except Exception as err:
434
445
  if isinstance(err, ProgrammingError):
435
446
  if err.errno == APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS:
447
+ extra_message = (
448
+ " Check the manifest file for any hard-coded patch value."
449
+ if patch is None
450
+ else ""
451
+ )
436
452
  raise UserInputError(
437
- f"Patch {patch} already exists for version {version} in application package {package_name}."
453
+ f"Patch{patch_query} already exists for version {version} in application package {package_name}.{extra_message}"
454
+ ) from err
455
+ if err.errno == CANNOT_ADD_PATCH_WITH_NON_INCREASING_PATCH_NUMBER:
456
+ raise UserInputError(
457
+ f"Cannot add a patch with a non-increasing patch number to version {version} in application package {package_name}."
438
458
  ) from err
439
459
  handle_unclassified_error(
440
460
  err,
@@ -713,7 +733,7 @@ class SnowflakeSQLFacade:
713
733
  self,
714
734
  name: str,
715
735
  install_method: SameAccountInstallMethod,
716
- stage_fqn: str,
736
+ path_to_version_directory: str,
717
737
  role: str,
718
738
  warehouse: str,
719
739
  debug_mode: bool | None,
@@ -725,7 +745,7 @@ class SnowflakeSQLFacade:
725
745
 
726
746
  @param name: Name of the application object
727
747
  @param install_method: Method of installing the application
728
- @param stage_fqn: FQN of the stage housing the application artifacts
748
+ @param path_to_version_directory: Path to directory in stage housing the application artifacts
729
749
  @param role: Role to use when creating the application and provider-side objects
730
750
  @param warehouse: Warehouse which is required to create an application object
731
751
  @param debug_mode: Whether to enable debug mode; None means not explicitly enabled or disabled
@@ -750,7 +770,7 @@ class SnowflakeSQLFacade:
750
770
 
751
771
  with self._use_role_optional(role), self._use_warehouse_optional(warehouse):
752
772
  try:
753
- using_clause = install_method.using_clause(stage_fqn)
773
+ using_clause = install_method.using_clause(path_to_version_directory)
754
774
 
755
775
  current_release_channel = (
756
776
  get_app_properties().get(CHANNEL_COL) or DEFAULT_CHANNEL
@@ -828,7 +848,7 @@ class SnowflakeSQLFacade:
828
848
  name: str,
829
849
  package_name: str,
830
850
  install_method: SameAccountInstallMethod,
831
- stage_fqn: str,
851
+ path_to_version_directory: str,
832
852
  role: str,
833
853
  warehouse: str,
834
854
  debug_mode: bool | None,
@@ -842,7 +862,7 @@ class SnowflakeSQLFacade:
842
862
  @param name: Name of the application object
843
863
  @param package_name: Name of the application package to install the application from
844
864
  @param install_method: Method of installing the application
845
- @param stage_fqn: FQN of the stage housing the application artifacts
865
+ @param path_to_version_directory: Path to directory in stage housing the application artifacts
846
866
  @param role: Role to use when creating the application and provider-side objects
847
867
  @param warehouse: Warehouse which is required to create an application object
848
868
  @param debug_mode: Whether to enable debug mode; None means not explicitly enabled or disabled
@@ -868,7 +888,7 @@ class SnowflakeSQLFacade:
868
888
  )
869
889
  authorize_telemetry_clause = f"AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(should_authorize_event_sharing).upper()}"
870
890
 
871
- using_clause = install_method.using_clause(stage_fqn)
891
+ using_clause = install_method.using_clause(path_to_version_directory)
872
892
  release_channel_clause = (
873
893
  f"using release channel {release_channel}" if release_channel else ""
874
894
  )
@@ -1096,13 +1116,22 @@ class SnowflakeSQLFacade:
1096
1116
  raise UserInputError(
1097
1117
  f"Some target accounts are already referenced by other release directives in application package {package_name}.\n{str(err.msg)}"
1098
1118
  ) from err
1099
- _handle_release_directive_version_error(
1100
- err,
1101
- package_name=package_name,
1102
- release_channel=release_channel,
1103
- version=version,
1104
- patch=patch,
1105
- )
1119
+ if err.errno == VERSION_NOT_ADDED_TO_RELEASE_CHANNEL:
1120
+ raise UserInputError(
1121
+ f"Version {version} is not added to release channel {release_channel}. Please add it to the release channel first."
1122
+ ) from err
1123
+ if err.errno == RELEASE_DIRECTIVES_VERSION_PATCH_NOT_FOUND:
1124
+ raise UserInputError(
1125
+ f"Patch {patch} for version {version} not found in application package {package_name}."
1126
+ ) from err
1127
+ if err.errno == RELEASE_DIRECTIVE_UNAPPROVED_VERSION_OR_PATCH:
1128
+ raise UserInputError(
1129
+ f"Version {version}, patch {patch} has not yet been approved to release to accounts outside of this organization."
1130
+ ) from err
1131
+ if err.errno == VERSION_DOES_NOT_EXIST:
1132
+ raise UserInputError(
1133
+ f"Version {version} does not exist in application package {package_name}."
1134
+ ) from err
1106
1135
  handle_unclassified_error(
1107
1136
  err,
1108
1137
  f"Failed to set release directive {release_directive} for application package {package_name}.",
@@ -1154,6 +1183,138 @@ class SnowflakeSQLFacade:
1154
1183
  f"Failed to unset release directive {release_directive} for application package {package_name}.",
1155
1184
  )
1156
1185
 
1186
+ def add_accounts_to_release_directive(
1187
+ self,
1188
+ package_name: str,
1189
+ release_directive: str,
1190
+ release_channel: str | None,
1191
+ target_accounts: List[str],
1192
+ role: str | None = None,
1193
+ ):
1194
+ """
1195
+ Adds target accounts to a release directive of a release channel in an application package.
1196
+ Release directive must already exist in the application package.
1197
+ Default release directive does not support target accounts.
1198
+
1199
+ @param package_name: Name of the application package to alter.
1200
+ @param release_directive: Name of the release directive to add target accounts to.
1201
+ @param release_channel: Name of the release channel where the release directive belongs to.
1202
+ @param target_accounts: List of target accounts to add to the release directive.
1203
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1204
+ """
1205
+ package_name = to_identifier(package_name)
1206
+ release_channel = to_identifier(release_channel) if release_channel else None
1207
+ release_directive = to_identifier(release_directive)
1208
+
1209
+ if same_identifiers(release_directive, DEFAULT_DIRECTIVE):
1210
+ raise UserInputError(
1211
+ "Default release directive does not support adding accounts. Please specify a non-default release directive."
1212
+ )
1213
+
1214
+ release_channel_statement = ""
1215
+ if release_channel:
1216
+ release_channel_statement = f"modify release channel {release_channel}"
1217
+
1218
+ with self._use_role_optional(role):
1219
+ try:
1220
+ self._sql_executor.execute_query(
1221
+ dedent(
1222
+ _strip_empty_lines(
1223
+ f"""\
1224
+ alter application package {package_name}
1225
+ {release_channel_statement}
1226
+ modify release directive {release_directive}
1227
+ add accounts = ({','.join(target_accounts)})
1228
+ """
1229
+ )
1230
+ )
1231
+ )
1232
+ except Exception as err:
1233
+ if isinstance(err, ProgrammingError):
1234
+ if (
1235
+ err.errno == ACCOUNT_DOES_NOT_EXIST
1236
+ or err.errno == ACCOUNT_HAS_TOO_MANY_QUALIFIERS
1237
+ ):
1238
+ raise UserInputError(
1239
+ f"Invalid account passed in.\n{str(err.msg)}"
1240
+ ) from err
1241
+ if err.errno == RELEASE_DIRECTIVE_DOES_NOT_EXIST:
1242
+ raise UserInputError(
1243
+ f"Release directive {release_directive} does not exist in application package {package_name}."
1244
+ ) from err
1245
+ if err.errno == TARGET_ACCOUNT_USED_BY_OTHER_RELEASE_DIRECTIVE:
1246
+ raise UserInputError(
1247
+ f"Some target accounts are already referenced by other release directives in application package {package_name}.\n{str(err.msg)}"
1248
+ ) from err
1249
+ handle_unclassified_error(
1250
+ err,
1251
+ f"Failed to add accounts to release directive {release_directive} for application package {package_name}.",
1252
+ )
1253
+
1254
+ def remove_accounts_from_release_directive(
1255
+ self,
1256
+ package_name: str,
1257
+ release_directive: str,
1258
+ release_channel: str | None,
1259
+ target_accounts: List[str],
1260
+ role: str | None = None,
1261
+ ):
1262
+ """
1263
+ Removes target accounts from a release directive of a release channel in an application package.
1264
+ Release directive must already exist in the application package.
1265
+ Default release directive does not support target accounts.
1266
+
1267
+ @param package_name: Name of the application package to alter.
1268
+ @param release_directive: Name of the release directive to remove target accounts from.
1269
+ @param release_channel: Name of the release channel where the release directive belongs to.
1270
+ @param target_accounts: List of target accounts to remove from the release directive.
1271
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1272
+ """
1273
+ package_name = to_identifier(package_name)
1274
+ release_channel = to_identifier(release_channel) if release_channel else None
1275
+ release_directive = to_identifier(release_directive)
1276
+
1277
+ if same_identifiers(release_directive, DEFAULT_DIRECTIVE):
1278
+ raise UserInputError(
1279
+ "Default release directive does not support removing accounts. Please specify a non-default release directive."
1280
+ )
1281
+
1282
+ release_channel_statement = ""
1283
+ if release_channel:
1284
+ release_channel_statement = f"modify release channel {release_channel}"
1285
+
1286
+ with self._use_role_optional(role):
1287
+ try:
1288
+ self._sql_executor.execute_query(
1289
+ dedent(
1290
+ _strip_empty_lines(
1291
+ f"""\
1292
+ alter application package {package_name}
1293
+ {release_channel_statement}
1294
+ modify release directive {release_directive}
1295
+ remove accounts = ({','.join(target_accounts)})
1296
+ """
1297
+ )
1298
+ )
1299
+ )
1300
+ except Exception as err:
1301
+ if isinstance(err, ProgrammingError):
1302
+ if (
1303
+ err.errno == ACCOUNT_DOES_NOT_EXIST
1304
+ or err.errno == ACCOUNT_HAS_TOO_MANY_QUALIFIERS
1305
+ ):
1306
+ raise UserInputError(
1307
+ f"Invalid account passed in.\n{str(err.msg)}"
1308
+ ) from err
1309
+ if err.errno == RELEASE_DIRECTIVE_DOES_NOT_EXIST:
1310
+ raise UserInputError(
1311
+ f"Release directive {release_directive} does not exist in application package {package_name}."
1312
+ ) from err
1313
+ handle_unclassified_error(
1314
+ err,
1315
+ f"Failed to remove accounts from release directive {release_directive} for application package {package_name}.",
1316
+ )
1317
+
1157
1318
  def show_release_channels(
1158
1319
  self, package_name: str, role: str | None = None
1159
1320
  ) -> list[ReleaseChannel]:
@@ -1295,6 +1456,48 @@ class SnowflakeSQLFacade:
1295
1456
  f"Failed to remove accounts from release channel {release_channel} in application package {package_name}.",
1296
1457
  )
1297
1458
 
1459
+ def set_accounts_for_release_channel(
1460
+ self,
1461
+ package_name: str,
1462
+ release_channel: str,
1463
+ target_accounts: List[str],
1464
+ role: str | None = None,
1465
+ ):
1466
+ """
1467
+ Sets accounts for a release channel.
1468
+
1469
+ @param package_name: Name of the application package
1470
+ @param release_channel: Name of the release channel
1471
+ @param target_accounts: List of target accounts to set for the release channel
1472
+ @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in.
1473
+ """
1474
+
1475
+ package_name = to_identifier(package_name)
1476
+ release_channel = to_identifier(release_channel)
1477
+
1478
+ with self._use_role_optional(role):
1479
+ try:
1480
+ self._sql_executor.execute_query(
1481
+ f"alter application package {package_name} modify release channel {release_channel} set accounts = ({','.join(target_accounts)})"
1482
+ )
1483
+ except Exception as err:
1484
+ if isinstance(err, ProgrammingError):
1485
+ if (
1486
+ err.errno == ACCOUNT_DOES_NOT_EXIST
1487
+ or err.errno == ACCOUNT_HAS_TOO_MANY_QUALIFIERS
1488
+ ):
1489
+ raise UserInputError(
1490
+ f"Invalid account passed in.\n{str(err.msg)}"
1491
+ ) from err
1492
+ if err.errno == CANNOT_MODIFY_RELEASE_CHANNEL_ACCOUNTS:
1493
+ raise UserInputError(
1494
+ f"Cannot modify accounts for release channel {release_channel} in application package {package_name}."
1495
+ ) from err
1496
+ handle_unclassified_error(
1497
+ err,
1498
+ f"Failed to set accounts for release channel {release_channel} in application package {package_name}.",
1499
+ )
1500
+
1298
1501
  def add_version_to_release_channel(
1299
1502
  self,
1300
1503
  package_name: str,
@@ -1424,26 +1627,3 @@ def _strip_empty_lines(text: str) -> str:
1424
1627
  other_lines = [line for line in all_lines[:-1] if line.strip()]
1425
1628
 
1426
1629
  return "\n".join(other_lines) + "\n" + last_line
1427
-
1428
-
1429
- def _handle_release_directive_version_error(
1430
- err: ProgrammingError,
1431
- *,
1432
- package_name: str,
1433
- release_channel: str | None,
1434
- version: str,
1435
- patch: int,
1436
- ) -> None:
1437
-
1438
- if err.errno == VERSION_NOT_ADDED_TO_RELEASE_CHANNEL:
1439
- raise UserInputError(
1440
- f"Version {version} is not added to release channel {release_channel}. Please add it to the release channel first."
1441
- ) from err
1442
- if err.errno == RELEASE_DIRECTIVES_VERSION_PATCH_NOT_FOUND:
1443
- raise UserInputError(
1444
- f"Patch {patch} for version {version} not found in application package {package_name}."
1445
- ) from err
1446
- if err.errno == VERSION_DOES_NOT_EXIST:
1447
- raise UserInputError(
1448
- f"Version {version} does not exist in application package {package_name}."
1449
- ) from err
@@ -46,7 +46,7 @@ APP_AND_PACKAGE_OPTIONS = [
46
46
  annotation=Optional[str],
47
47
  default=typer.Option(
48
48
  default="",
49
- help="The ID of the package entity on which to operate when definition_version is 2 or higher.",
49
+ help="The ID of the package entity on which to operate when the definition_version is 2 or higher.",
50
50
  ),
51
51
  ),
52
52
  inspect.Parameter(
@@ -55,7 +55,7 @@ APP_AND_PACKAGE_OPTIONS = [
55
55
  annotation=Optional[str],
56
56
  default=typer.Option(
57
57
  default="",
58
- help="The ID of the application entity on which to operate when definition_version is 2 or higher.",
58
+ help="The ID of the application entity on which to operate when the definition_version is 2 or higher.",
59
59
  ),
60
60
  ),
61
61
  ]
@@ -29,7 +29,7 @@ from snowflake.cli.api.commands.decorators import (
29
29
  with_project_definition,
30
30
  )
31
31
  from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
32
- from snowflake.cli.api.entities.common import EntityActions
32
+ from snowflake.cli.api.entities.utils import EntityActions
33
33
  from snowflake.cli.api.output.formats import OutputFormat
34
34
  from snowflake.cli.api.output.types import (
35
35
  CollectionResult,
@@ -15,12 +15,28 @@
15
15
  import logging
16
16
 
17
17
  import typer
18
+ from click import UsageError
18
19
  from snowflake.cli._plugins.notebook.manager import NotebookManager
20
+ from snowflake.cli._plugins.notebook.notebook_entity_model import NotebookEntityModel
19
21
  from snowflake.cli._plugins.notebook.types import NotebookStagePath
20
- from snowflake.cli.api.commands.flags import identifier_argument
22
+ from snowflake.cli._plugins.workspace.manager import WorkspaceManager
23
+ from snowflake.cli.api.cli_global_context import get_cli_context
24
+ from snowflake.cli.api.commands.decorators import (
25
+ with_project_definition,
26
+ )
27
+ from snowflake.cli.api.commands.flags import (
28
+ ReplaceOption,
29
+ entity_argument,
30
+ identifier_argument,
31
+ )
21
32
  from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
33
+ from snowflake.cli.api.commands.utils import get_entity_for_operation
34
+ from snowflake.cli.api.entities.common import EntityActions
22
35
  from snowflake.cli.api.identifiers import FQN
23
- from snowflake.cli.api.output.types import MessageResult
36
+ from snowflake.cli.api.output.types import (
37
+ CommandResult,
38
+ MessageResult,
39
+ )
24
40
  from typing_extensions import Annotated
25
41
 
26
42
  app = SnowTyperFactory(
@@ -84,3 +100,40 @@ def create(
84
100
  notebook_file=notebook_file,
85
101
  )
86
102
  return MessageResult(message=notebook_url)
103
+
104
+
105
+ @app.command(requires_connection=True)
106
+ @with_project_definition()
107
+ def deploy(
108
+ entity_id: str = entity_argument("notebook"),
109
+ replace: bool = ReplaceOption(
110
+ help="Replace notebook object if it already exists.",
111
+ ),
112
+ **options,
113
+ ) -> CommandResult:
114
+ """Uploads a notebook and required files to a stage and creates a Snowflake notebook."""
115
+ cli_context = get_cli_context()
116
+ pd = cli_context.project_definition
117
+ if not pd.meets_version_requirement("2"):
118
+ raise UsageError(
119
+ "This command requires project definition of version at least 2."
120
+ )
121
+
122
+ notebook: NotebookEntityModel = get_entity_for_operation(
123
+ cli_context=cli_context,
124
+ entity_id=entity_id,
125
+ project_definition=pd,
126
+ entity_type="notebook",
127
+ )
128
+ ws = WorkspaceManager(
129
+ project_definition=cli_context.project_definition,
130
+ project_root=cli_context.project_root,
131
+ )
132
+ notebook_url = ws.perform_action(
133
+ notebook.entity_id,
134
+ EntityActions.DEPLOY,
135
+ replace=replace,
136
+ )
137
+ return MessageResult(
138
+ f"Notebook successfully deployed and available under {notebook_url}"
139
+ )
@@ -15,6 +15,6 @@
15
15
  from click.exceptions import ClickException
16
16
 
17
17
 
18
- class NotebookStagePathError(ClickException):
18
+ class NotebookFilePathError(ClickException):
19
19
  def __init__(self, path: str):
20
20
  super().__init__(f"Cannot extract notebook file name from {path=}")
@@ -16,7 +16,7 @@ from pathlib import Path
16
16
  from textwrap import dedent
17
17
 
18
18
  from snowflake.cli._plugins.connection.util import make_snowsight_url
19
- from snowflake.cli._plugins.notebook.exceptions import NotebookStagePathError
19
+ from snowflake.cli._plugins.notebook.exceptions import NotebookFilePathError
20
20
  from snowflake.cli._plugins.notebook.types import NotebookStagePath
21
21
  from snowflake.cli.api.cli_global_context import get_cli_context
22
22
  from snowflake.cli.api.identifiers import FQN
@@ -40,11 +40,11 @@ class NotebookManager(SqlExecutionMixin):
40
40
  def parse_stage_as_path(notebook_file: str) -> Path:
41
41
  """Parses notebook file path to pathlib.Path."""
42
42
  if not notebook_file.endswith(".ipynb"):
43
- raise NotebookStagePathError(notebook_file)
43
+ raise NotebookFilePathError(notebook_file)
44
44
  # we don't perform any operations on the path, so we don't need to differentiate git repository paths
45
45
  stage_path = StagePath.from_stage_str(notebook_file)
46
46
  if len(stage_path.parts) < 1:
47
- raise NotebookStagePathError(notebook_file)
47
+ raise NotebookFilePathError(notebook_file)
48
48
 
49
49
  return stage_path
50
50
 
@@ -0,0 +1,120 @@
1
+ import functools
2
+
3
+ from click import ClickException
4
+ from snowflake.cli._plugins.connection.util import make_snowsight_url
5
+ from snowflake.cli._plugins.notebook.notebook_entity_model import NotebookEntityModel
6
+ from snowflake.cli._plugins.notebook.notebook_project_paths import NotebookProjectPaths
7
+ from snowflake.cli._plugins.stage.manager import StageManager
8
+ from snowflake.cli._plugins.workspace.context import ActionContext
9
+ from snowflake.cli.api.artifacts.utils import bundle_artifacts
10
+ from snowflake.cli.api.cli_global_context import get_cli_context
11
+ from snowflake.cli.api.console.console import cli_console
12
+ from snowflake.cli.api.entities.common import EntityBase
13
+ from snowflake.cli.api.stage_path import StagePath
14
+ from snowflake.connector import ProgrammingError
15
+ from snowflake.connector.cursor import SnowflakeCursor
16
+
17
+ _DEFAULT_NOTEBOOK_STAGE_NAME = "@notebooks"
18
+
19
+
20
+ class NotebookEntity(EntityBase[NotebookEntityModel]):
21
+ """
22
+ A notebook.
23
+ """
24
+
25
+ @functools.cached_property
26
+ def _stage_path(self) -> StagePath:
27
+ stage_path = self.model.stage_path
28
+ if stage_path is None:
29
+ stage_path = f"{_DEFAULT_NOTEBOOK_STAGE_NAME}/{self.fqn.name}"
30
+ return StagePath.from_stage_str(stage_path)
31
+
32
+ @functools.cached_property
33
+ def _project_paths(self):
34
+ return NotebookProjectPaths(get_cli_context().project_root)
35
+
36
+ def _object_exists(self) -> bool:
37
+ # currently notebook objects are not supported by object manager - re-implementing "exists"
38
+ try:
39
+ self.action_describe()
40
+ return True
41
+ except ProgrammingError:
42
+ return False
43
+
44
+ def _upload_artifacts(self):
45
+ stage_fqn = self._stage_path.stage_fqn
46
+ stage_manager = StageManager()
47
+ cli_console.step(f"Creating stage {stage_fqn} if not exists")
48
+ stage_manager.create(fqn=stage_fqn)
49
+
50
+ cli_console.step("Uploading artifacts")
51
+
52
+ # creating bundle map to handle glob patterns logic
53
+ bundle_map = bundle_artifacts(self._project_paths, self.model.artifacts)
54
+ for absolute_src, absolute_dest in bundle_map.all_mappings(
55
+ absolute=True, expand_directories=True
56
+ ):
57
+ artifact_stage_path = self._stage_path / (
58
+ absolute_dest.relative_to(self._project_paths.bundle_root).parent
59
+ )
60
+ stage_manager.put(
61
+ local_path=absolute_src, stage_path=artifact_stage_path, overwrite=True
62
+ )
63
+
64
+ def get_create_sql(self, replace: bool) -> str:
65
+ main_file_stage_path = self._stage_path / (
66
+ self.model.notebook_file.absolute().relative_to(
67
+ self._project_paths.project_root
68
+ )
69
+ )
70
+ query = "CREATE OR REPLACE " if replace else "CREATE "
71
+ query += (
72
+ f"NOTEBOOK {self.fqn.sql_identifier}\n"
73
+ f"FROM '{main_file_stage_path.stage_with_at}'\n"
74
+ f"QUERY_WAREHOUSE = '{self.model.query_warehouse}'\n"
75
+ f"MAIN_FILE = '{main_file_stage_path.path}'"
76
+ )
77
+ if self.model.compute_pool:
78
+ query += f"\nCOMPUTE_POOL = '{self.model.compute_pool}'"
79
+ if self.model.runtime_name:
80
+ query += f"\nRUNTIME_NAME = '{self.model.runtime_name}'"
81
+
82
+ query += (
83
+ ";\n// Cannot use IDENTIFIER(...)"
84
+ f"\nALTER NOTEBOOK {self.fqn.identifier} ADD LIVE VERSION FROM LAST;"
85
+ )
86
+ return query
87
+
88
+ def action_describe(self) -> SnowflakeCursor:
89
+ return self._sql_executor.execute_query(self.get_describe_sql())
90
+
91
+ def action_create(self, replace: bool) -> str:
92
+ self._sql_executor.execute_query(self.get_create_sql(replace))
93
+ return make_snowsight_url(
94
+ self._conn,
95
+ f"/#/notebooks/{self.fqn.using_connection(self._conn).url_identifier}",
96
+ )
97
+
98
+ def action_deploy(
99
+ self,
100
+ action_ctx: ActionContext,
101
+ replace: bool,
102
+ *args,
103
+ **kwargs,
104
+ ) -> str:
105
+ if self._object_exists():
106
+ if not replace:
107
+ raise ClickException(
108
+ f"Notebook {self.fqn.name} already exists. Consider using --replace."
109
+ )
110
+ with cli_console.phase(f"Uploading artifacts to {self._stage_path}"):
111
+ self._upload_artifacts()
112
+ with cli_console.phase(f"Creating notebook {self.fqn}"):
113
+ return self.action_create(replace=replace)
114
+
115
+ # complementary actions, currently not used - to be implemented in future
116
+ def action_drop(self, *args, **kwargs):
117
+ raise ClickException("action DROP not supported by NOTEBOOK entity")
118
+
119
+ def action_teardown(self, *args, **kwargs):
120
+ raise ClickException("action TEARDOWN not supported by NOTEBOOK entity")