snowflake-cli-labs 2.6.1__py3-none-any.whl → 2.7.0rc0__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 (84) 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 +4 -1
  7. snowflake/cli/api/commands/snow_typer.py +20 -9
  8. snowflake/cli/api/errno.py +27 -0
  9. snowflake/cli/api/feature_flags.py +1 -0
  10. snowflake/cli/api/identifiers.py +20 -3
  11. snowflake/cli/api/output/types.py +9 -0
  12. snowflake/cli/api/project/definition_manager.py +2 -2
  13. snowflake/cli/api/project/project_verification.py +23 -0
  14. snowflake/cli/api/project/schemas/entities/application_entity.py +50 -0
  15. snowflake/cli/api/project/schemas/entities/application_package_entity.py +63 -0
  16. snowflake/cli/api/project/schemas/entities/common.py +85 -0
  17. snowflake/cli/api/project/schemas/entities/entities.py +30 -0
  18. snowflake/cli/api/project/schemas/project_definition.py +114 -22
  19. snowflake/cli/api/project/schemas/streamlit/streamlit.py +5 -4
  20. snowflake/cli/api/project/schemas/template.py +77 -0
  21. snowflake/cli/{plugins/nativeapp/errno.py → api/rendering/__init__.py} +0 -2
  22. snowflake/cli/api/{utils/rendering.py → rendering/jinja.py} +3 -48
  23. snowflake/cli/api/rendering/project_definition_templates.py +39 -0
  24. snowflake/cli/api/rendering/project_templates.py +97 -0
  25. snowflake/cli/api/rendering/sql_templates.py +56 -0
  26. snowflake/cli/api/sql_execution.py +40 -1
  27. snowflake/cli/api/utils/definition_rendering.py +8 -5
  28. snowflake/cli/app/commands_registration/builtin_plugins.py +4 -0
  29. snowflake/cli/app/dev/docs/project_definition_docs_generator.py +2 -2
  30. snowflake/cli/app/loggers.py +3 -1
  31. snowflake/cli/app/printing.py +17 -7
  32. snowflake/cli/app/snow_connector.py +9 -1
  33. snowflake/cli/app/telemetry.py +41 -2
  34. snowflake/cli/plugins/connection/commands.py +4 -3
  35. snowflake/cli/plugins/connection/util.py +73 -18
  36. snowflake/cli/plugins/cortex/commands.py +2 -1
  37. snowflake/cli/plugins/git/commands.py +20 -4
  38. snowflake/cli/plugins/git/manager.py +44 -20
  39. snowflake/cli/plugins/init/__init__.py +13 -0
  40. snowflake/cli/plugins/init/commands.py +242 -0
  41. snowflake/cli/plugins/init/plugin_spec.py +30 -0
  42. snowflake/cli/plugins/nativeapp/codegen/artifact_processor.py +40 -0
  43. snowflake/cli/plugins/nativeapp/codegen/compiler.py +57 -27
  44. snowflake/cli/plugins/nativeapp/codegen/sandbox.py +99 -10
  45. snowflake/cli/plugins/nativeapp/codegen/setup/native_app_setup_processor.py +172 -0
  46. snowflake/cli/plugins/nativeapp/codegen/setup/setup_driver.py.source +56 -0
  47. snowflake/cli/plugins/nativeapp/codegen/snowpark/python_processor.py +21 -21
  48. snowflake/cli/plugins/nativeapp/commands.py +69 -6
  49. snowflake/cli/plugins/nativeapp/constants.py +0 -6
  50. snowflake/cli/plugins/nativeapp/exceptions.py +37 -12
  51. snowflake/cli/plugins/nativeapp/init.py +1 -1
  52. snowflake/cli/plugins/nativeapp/manager.py +114 -39
  53. snowflake/cli/plugins/nativeapp/project_model.py +8 -4
  54. snowflake/cli/plugins/nativeapp/run_processor.py +117 -102
  55. snowflake/cli/plugins/nativeapp/teardown_processor.py +7 -2
  56. snowflake/cli/plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +146 -0
  57. snowflake/cli/plugins/nativeapp/version/commands.py +19 -3
  58. snowflake/cli/plugins/nativeapp/version/version_processor.py +11 -3
  59. snowflake/cli/plugins/snowpark/commands.py +34 -26
  60. snowflake/cli/plugins/snowpark/common.py +88 -27
  61. snowflake/cli/plugins/snowpark/manager.py +16 -5
  62. snowflake/cli/plugins/snowpark/models.py +6 -0
  63. snowflake/cli/plugins/sql/commands.py +3 -5
  64. snowflake/cli/plugins/sql/manager.py +1 -1
  65. snowflake/cli/plugins/stage/commands.py +2 -2
  66. snowflake/cli/plugins/stage/diff.py +4 -2
  67. snowflake/cli/plugins/stage/manager.py +290 -86
  68. snowflake/cli/plugins/streamlit/commands.py +20 -6
  69. snowflake/cli/plugins/streamlit/manager.py +29 -27
  70. snowflake/cli/plugins/workspace/__init__.py +13 -0
  71. snowflake/cli/plugins/workspace/commands.py +35 -0
  72. snowflake/cli/plugins/workspace/plugin_spec.py +30 -0
  73. snowflake/cli/templates/default_snowpark/app/__init__.py +0 -13
  74. snowflake/cli/templates/default_snowpark/app/common.py +0 -15
  75. snowflake/cli/templates/default_snowpark/app/functions.py +0 -14
  76. snowflake/cli/templates/default_snowpark/app/procedures.py +0 -14
  77. snowflake/cli/templates/default_streamlit/common/hello.py +0 -15
  78. snowflake/cli/templates/default_streamlit/pages/my_page.py +0 -14
  79. snowflake/cli/templates/default_streamlit/streamlit_app.py +0 -14
  80. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0rc0.dist-info}/METADATA +7 -6
  81. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0rc0.dist-info}/RECORD +84 -64
  82. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0rc0.dist-info}/WHEEL +0 -0
  83. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0rc0.dist-info}/entry_points.txt +0 -0
  84. {snowflake_cli_labs-2.6.1.dist-info → snowflake_cli_labs-2.7.0rc0.dist-info}/licenses/LICENSE +0 -0
@@ -25,9 +25,3 @@ PATCH_COL = "patch"
25
25
 
26
26
  INTERNAL_DISTRIBUTION = "internal"
27
27
  EXTERNAL_DISTRIBUTION = "external"
28
-
29
- ERROR_MESSAGE_2003 = "does not exist or not authorized"
30
- ERROR_MESSAGE_2043 = "Object does not exist, or operation cannot be performed."
31
- ERROR_MESSAGE_606 = "No active warehouse selected in the current session."
32
- ERROR_MESSAGE_093079 = "Application is no longer available for use"
33
- ERROR_MESSAGE_093128 = "The application owns one or more objects within the account"
@@ -12,7 +12,10 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ from __future__ import annotations
16
+
15
17
  from textwrap import dedent
18
+ from typing import Optional
16
19
 
17
20
  import jinja2
18
21
  from click.exceptions import ClickException
@@ -36,12 +39,12 @@ class ApplicationPackageDoesNotExistError(ClickException):
36
39
  )
37
40
 
38
41
 
39
- class ApplicationAlreadyExistsError(ClickException):
42
+ class ApplicationCreatedExternallyError(ClickException):
40
43
  """An application object not created by Snowflake CLI exists with the same name."""
41
44
 
42
45
  def __init__(self, name: str):
43
46
  super().__init__(
44
- f'A application object "{name}" not created in development mode using files on a named stage already exists in the account.'
47
+ f'An application object "{name}" not created by Snowflake CLI already exists in the account.'
45
48
  )
46
49
 
47
50
 
@@ -54,18 +57,23 @@ class UnexpectedOwnerError(ClickException):
54
57
  )
55
58
 
56
59
 
57
- class MissingPackageScriptError(ClickException):
58
- """A referenced package script was not found."""
60
+ class MissingScriptError(ClickException):
61
+ """A referenced script was not found."""
59
62
 
60
63
  def __init__(self, relpath: str):
61
- super().__init__(f'Package script "{relpath}" does not exist')
64
+ super().__init__(f'Script "{relpath}" does not exist')
62
65
 
63
66
 
64
- class InvalidPackageScriptError(ClickException):
65
- """A referenced package script had syntax error(s)."""
67
+ class InvalidScriptError(ClickException):
68
+ """A referenced script had syntax error(s)."""
66
69
 
67
- def __init__(self, relpath: str, err: jinja2.TemplateError):
68
- super().__init__(f'Package script "{relpath}" is not a valid jinja2 template')
70
+ def __init__(
71
+ self, relpath: str, err: jinja2.TemplateError, lineno: Optional[int] = None
72
+ ):
73
+ lineno_str = f":{lineno}" if lineno is not None else ""
74
+ super().__init__(
75
+ f'Script "{relpath}{lineno_str}" does not contain a valid template: {err.message}'
76
+ )
69
77
  self.err = err
70
78
 
71
79
 
@@ -79,14 +87,14 @@ class MissingSchemaError(ClickException):
79
87
  class CouldNotDropApplicationPackageWithVersions(ClickException):
80
88
  """Application package could not be dropped as it has versions associated with it."""
81
89
 
82
- def __init__(self):
90
+ def __init__(self, additional_msg: str = ""):
83
91
  super().__init__(
84
92
  dedent(
85
93
  f"""
86
94
  {self.__doc__}
87
- Versions must be dropped first using “snow app version drop”.
95
+ {additional_msg}
88
96
  """
89
- )
97
+ ).strip()
90
98
  )
91
99
 
92
100
 
@@ -95,3 +103,20 @@ class SetupScriptFailedValidation(ClickException):
95
103
 
96
104
  def __init__(self):
97
105
  super().__init__(self.__doc__)
106
+
107
+
108
+ class NoEventTableForAccount(ClickException):
109
+ """No event table was found for this Snowflake account."""
110
+
111
+ INSTRUCTIONS = dedent(
112
+ """\
113
+ Ask your Snowflake administrator to set up an event table for your account by following the docs at
114
+ https://docs.snowflake.com/en/developer-guide/logging-tracing/event-table-setting-up.
115
+
116
+ If your account is configured to send events to an organization event account, create a new
117
+ connection to this account using `snow connection add` and re-run this command using the new connection.
118
+ More information on event accounts is available at https://docs.snowflake.com/en/developer-guide/native-apps/setting-up-logging-and-events#configure-an-account-to-store-shared-events."""
119
+ )
120
+
121
+ def __init__(self):
122
+ super().__init__(f"{self.__doc__}\n\n{self.INSTRUCTIONS}")
@@ -28,8 +28,8 @@ from snowflake.cli.api.project.util import (
28
28
  is_valid_unquoted_identifier,
29
29
  to_identifier,
30
30
  )
31
+ from snowflake.cli.api.rendering.jinja import jinja_render_from_file
31
32
  from snowflake.cli.api.secure_path import SecurePath
32
- from snowflake.cli.api.utils.rendering import jinja_render_from_file
33
33
  from yaml import dump, safe_dump, safe_load
34
34
 
35
35
  log = logging.getLogger(__name__)
@@ -17,14 +17,20 @@ from __future__ import annotations
17
17
  import json
18
18
  import os
19
19
  from abc import ABC, abstractmethod
20
+ from contextlib import contextmanager
20
21
  from functools import cached_property
21
22
  from pathlib import Path
22
23
  from textwrap import dedent
23
- from typing import List, Optional, TypedDict
24
+ from typing import Any, List, NoReturn, Optional, TypedDict
24
25
 
25
26
  import jinja2
26
27
  from click import ClickException
27
28
  from snowflake.cli.api.console import cli_console as cc
29
+ from snowflake.cli.api.errno import (
30
+ DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED,
31
+ DOES_NOT_EXIST_OR_NOT_AUTHORIZED,
32
+ NO_WAREHOUSE_SELECTED_IN_SESSION,
33
+ )
28
34
  from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
29
35
  from snowflake.cli.api.project.schemas.native_app.application import (
30
36
  ApplicationPostDeployHook,
@@ -48,9 +54,6 @@ from snowflake.cli.plugins.nativeapp.codegen.compiler import (
48
54
  from snowflake.cli.plugins.nativeapp.constants import (
49
55
  ALLOWED_SPECIAL_COMMENTS,
50
56
  COMMENT_COL,
51
- ERROR_MESSAGE_606,
52
- ERROR_MESSAGE_2003,
53
- ERROR_MESSAGE_2043,
54
57
  INTERNAL_DISTRIBUTION,
55
58
  NAME_COL,
56
59
  OWNER_COL,
@@ -59,8 +62,9 @@ from snowflake.cli.plugins.nativeapp.constants import (
59
62
  from snowflake.cli.plugins.nativeapp.exceptions import (
60
63
  ApplicationPackageAlreadyExistsError,
61
64
  ApplicationPackageDoesNotExistError,
62
- InvalidPackageScriptError,
63
- MissingPackageScriptError,
65
+ InvalidScriptError,
66
+ MissingScriptError,
67
+ NoEventTableForAccount,
64
68
  SetupScriptFailedValidation,
65
69
  UnexpectedOwnerError,
66
70
  )
@@ -78,16 +82,16 @@ from snowflake.cli.plugins.stage.diff import (
78
82
  to_stage_path,
79
83
  )
80
84
  from snowflake.cli.plugins.stage.manager import StageManager
81
- from snowflake.connector import ProgrammingError
85
+ from snowflake.connector import DictCursor, ProgrammingError
82
86
 
83
87
  ApplicationOwnedObject = TypedDict("ApplicationOwnedObject", {"name": str, "type": str})
84
88
 
85
89
 
86
90
  def generic_sql_error_handler(
87
91
  err: ProgrammingError, role: Optional[str] = None, warehouse: Optional[str] = None
88
- ):
92
+ ) -> NoReturn:
89
93
  # Potential refactor: If moving away from Python 3.8 and 3.9 to >= 3.10, use match ... case
90
- if err.errno == 2043 or err.msg.__contains__(ERROR_MESSAGE_2043):
94
+ if err.errno == DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED:
91
95
  raise ProgrammingError(
92
96
  msg=dedent(
93
97
  f"""\
@@ -98,7 +102,7 @@ def generic_sql_error_handler(
98
102
  ),
99
103
  errno=err.errno,
100
104
  )
101
- elif err.errno == 606 or err.msg.__contains__(ERROR_MESSAGE_606):
105
+ elif err.errno == NO_WAREHOUSE_SELECTED_IN_SESSION:
102
106
  raise ProgrammingError(
103
107
  msg=dedent(
104
108
  f"""\
@@ -108,7 +112,7 @@ def generic_sql_error_handler(
108
112
  ),
109
113
  errno=err.errno,
110
114
  )
111
- elif err.msg.__contains__("does not exist or not authorized"):
115
+ elif "does not exist or not authorized" in err.msg:
112
116
  raise ProgrammingError(
113
117
  msg=dedent(
114
118
  f"""\
@@ -216,10 +220,40 @@ class NativeAppManager(SqlExecutionMixin):
216
220
  def package_warehouse(self) -> Optional[str]:
217
221
  return self.na_project.package_warehouse
218
222
 
223
+ @contextmanager
224
+ def use_package_warehouse(self):
225
+ if self.package_warehouse:
226
+ with self.use_warehouse(self.package_warehouse):
227
+ yield
228
+ else:
229
+ raise ClickException(
230
+ dedent(
231
+ f"""\
232
+ Application package warehouse cannot be empty.
233
+ Please provide a value for it in your connection information or your project definition file.
234
+ """
235
+ )
236
+ )
237
+
219
238
  @property
220
239
  def application_warehouse(self) -> Optional[str]:
221
240
  return self.na_project.application_warehouse
222
241
 
242
+ @contextmanager
243
+ def use_application_warehouse(self):
244
+ if self.application_warehouse:
245
+ with self.use_warehouse(self.application_warehouse):
246
+ yield
247
+ else:
248
+ raise ClickException(
249
+ dedent(
250
+ f"""\
251
+ Application warehouse cannot be empty.
252
+ Please provide a value for it in your connection information or your project definition file.
253
+ """
254
+ )
255
+ )
256
+
223
257
  @property
224
258
  def project_identifier(self) -> str:
225
259
  return self.na_project.project_identifier
@@ -280,6 +314,12 @@ class NativeAppManager(SqlExecutionMixin):
280
314
  )
281
315
  )
282
316
 
317
+ @cached_property
318
+ def account_event_table(self) -> str:
319
+ query = "show parameters like 'event_table' in account"
320
+ results = self._execute_query(query, cursor_class=DictCursor)
321
+ return next((r["value"] for r in results if r["key"] == "EVENT_TABLE"), "")
322
+
283
323
  def verify_project_distribution(
284
324
  self, expected_distribution: Optional[str] = None
285
325
  ) -> bool:
@@ -482,7 +522,8 @@ class NativeAppManager(SqlExecutionMixin):
482
522
  def get_snowsight_url(self) -> str:
483
523
  """Returns the URL that can be used to visit this app via Snowsight."""
484
524
  name = identifier_for_url(self.app_name)
485
- return make_snowsight_url(self._conn, f"/#/apps/application/{name}")
525
+ with self.use_application_warehouse():
526
+ return make_snowsight_url(self._conn, f"/#/apps/application/{name}")
486
527
 
487
528
  def create_app_package(self) -> None:
488
529
  """
@@ -527,6 +568,36 @@ class NativeAppManager(SqlExecutionMixin):
527
568
  )
528
569
  )
529
570
 
571
+ def _expand_script_templates(
572
+ self, env: jinja2.Environment, jinja_context: dict[str, Any], scripts: List[str]
573
+ ) -> List[str]:
574
+ """
575
+ Input:
576
+ - env: Jinja2 environment
577
+ - jinja_context: a dictionary with the jinja context
578
+ - scripts: list of scripts that need to be expanded with Jinja
579
+ Returns:
580
+ - List of expanded scripts content.
581
+ Size of the return list is the same as the size of the input scripts list.
582
+ """
583
+ scripts_contents = []
584
+ for relpath in scripts:
585
+ try:
586
+ template = env.get_template(relpath)
587
+ result = template.render(**jinja_context)
588
+ scripts_contents.append(result)
589
+
590
+ except jinja2.TemplateNotFound as e:
591
+ raise MissingScriptError(e.name) from e
592
+
593
+ except jinja2.TemplateSyntaxError as e:
594
+ raise InvalidScriptError(e.name, e, e.lineno) from e
595
+
596
+ except jinja2.UndefinedError as e:
597
+ raise InvalidScriptError(relpath, e) from e
598
+
599
+ return scripts_contents
600
+
530
601
  def _apply_package_scripts(self) -> None:
531
602
  """
532
603
  Assuming the application package exists and we are using the correct role,
@@ -538,34 +609,20 @@ class NativeAppManager(SqlExecutionMixin):
538
609
  undefined=jinja2.StrictUndefined,
539
610
  )
540
611
 
541
- queued_queries = []
542
- for relpath in self.package_scripts:
543
- try:
544
- template = env.get_template(relpath)
545
- result = template.render(dict(package_name=self.package_name))
546
- queued_queries.append(result)
547
-
548
- except jinja2.TemplateNotFound as e:
549
- raise MissingPackageScriptError(e.name)
550
-
551
- except jinja2.TemplateSyntaxError as e:
552
- raise InvalidPackageScriptError(e.name, e)
553
-
554
- except jinja2.UndefinedError as e:
555
- raise InvalidPackageScriptError(relpath, e)
612
+ queued_queries = self._expand_script_templates(
613
+ env, dict(package_name=self.package_name), self.package_scripts
614
+ )
556
615
 
557
616
  # once we're sure all the templates expanded correctly, execute all of them
558
- try:
559
- if self.package_warehouse:
560
- self._execute_query(f"use warehouse {self.package_warehouse}")
561
-
562
- for i, queries in enumerate(queued_queries):
563
- cc.step(f"Applying package script: {self.package_scripts[i]}")
564
- self._execute_queries(queries)
565
- except ProgrammingError as err:
566
- generic_sql_error_handler(
567
- err, role=self.package_role, warehouse=self.package_warehouse
568
- )
617
+ with self.use_package_warehouse():
618
+ try:
619
+ for i, queries in enumerate(queued_queries):
620
+ cc.step(f"Applying package script: {self.package_scripts[i]}")
621
+ self._execute_queries(queries)
622
+ except ProgrammingError as err:
623
+ generic_sql_error_handler(
624
+ err, role=self.package_role, warehouse=self.package_warehouse
625
+ )
569
626
 
570
627
  def deploy(
571
628
  self,
@@ -642,7 +699,7 @@ class NativeAppManager(SqlExecutionMixin):
642
699
  f"call system$validate_native_app_setup('{prefixed_stage_fqn}')"
643
700
  )
644
701
  except ProgrammingError as err:
645
- if err.errno == 2003 and ERROR_MESSAGE_2003 in err.msg:
702
+ if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED:
646
703
  raise ApplicationPackageDoesNotExistError(self.package_name)
647
704
  generic_sql_error_handler(err)
648
705
  else:
@@ -657,6 +714,24 @@ class NativeAppManager(SqlExecutionMixin):
657
714
  f"drop stage if exists {self.scratch_stage_fqn}"
658
715
  )
659
716
 
717
+ def get_events(self) -> list[dict]:
718
+ if not self.account_event_table:
719
+ raise NoEventTableForAccount()
720
+
721
+ # resource_attributes:"snow.database.name" uses the unquoted/uppercase app name
722
+ app_name = unquote_identifier(self.app_name)
723
+ query = dedent(
724
+ f"""\
725
+ select timestamp, value::varchar value
726
+ from {self.account_event_table}
727
+ where resource_attributes:"snow.database.name" = '{app_name}'
728
+ order by timestamp asc;"""
729
+ )
730
+ try:
731
+ return self._execute_query(query, cursor_class=DictCursor).fetchall()
732
+ except ProgrammingError as err:
733
+ generic_sql_error_handler(err)
734
+
660
735
 
661
736
  def _validation_item_to_str(item: dict[str, str | int]):
662
737
  s = item["message"]
@@ -105,16 +105,20 @@ class NativeAppProjectModel:
105
105
  @cached_property
106
106
  def package_warehouse(self) -> Optional[str]:
107
107
  if self.definition.package and self.definition.package.warehouse:
108
- return self.definition.package.warehouse
108
+ return to_identifier(self.definition.package.warehouse)
109
109
  else:
110
- return cli_context.connection.warehouse
110
+ if cli_context.connection.warehouse:
111
+ return to_identifier(cli_context.connection.warehouse)
112
+ return None
111
113
 
112
114
  @cached_property
113
115
  def application_warehouse(self) -> Optional[str]:
114
116
  if self.definition.application and self.definition.application.warehouse:
115
- return self.definition.application.warehouse
117
+ return to_identifier(self.definition.application.warehouse)
116
118
  else:
117
- return cli_context.connection.warehouse
119
+ if cli_context.connection.warehouse:
120
+ return to_identifier(cli_context.connection.warehouse)
121
+ return None
118
122
 
119
123
  @cached_property
120
124
  def project_identifier(self) -> str: