snowflake-cli-labs 3.0.0rc3__py3-none-any.whl → 3.0.0rc4__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 (43) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/telemetry.py +28 -0
  3. snowflake/cli/_plugins/connection/commands.py +9 -4
  4. snowflake/cli/_plugins/helpers/commands.py +5 -1
  5. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +5 -0
  6. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +4 -0
  7. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +3 -0
  8. snowflake/cli/_plugins/nativeapp/commands.py +9 -86
  9. snowflake/cli/_plugins/nativeapp/entities/__init__.py +0 -0
  10. snowflake/cli/_plugins/nativeapp/{application_entity.py → entities/application.py} +266 -39
  11. snowflake/cli/_plugins/nativeapp/{application_package_entity.py → entities/application_package.py} +357 -72
  12. snowflake/cli/_plugins/nativeapp/manager.py +62 -183
  13. snowflake/cli/_plugins/nativeapp/run_processor.py +6 -6
  14. snowflake/cli/_plugins/nativeapp/teardown_processor.py +2 -4
  15. snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +2 -4
  16. snowflake/cli/_plugins/nativeapp/version/commands.py +1 -15
  17. snowflake/cli/_plugins/nativeapp/version/version_processor.py +16 -82
  18. snowflake/cli/_plugins/object/manager.py +36 -15
  19. snowflake/cli/_plugins/streamlit/commands.py +12 -0
  20. snowflake/cli/_plugins/streamlit/manager.py +4 -0
  21. snowflake/cli/_plugins/workspace/commands.py +33 -0
  22. snowflake/cli/api/cli_global_context.py +7 -0
  23. snowflake/cli/api/commands/decorators.py +14 -0
  24. snowflake/cli/api/commands/flags.py +18 -0
  25. snowflake/cli/api/config.py +25 -6
  26. snowflake/cli/api/connections.py +3 -1
  27. snowflake/cli/api/entities/common.py +1 -0
  28. snowflake/cli/api/entities/utils.py +3 -0
  29. snowflake/cli/api/metrics.py +92 -0
  30. snowflake/cli/api/project/definition_conversion.py +51 -9
  31. snowflake/cli/api/project/schemas/entities/entities.py +3 -5
  32. snowflake/cli/api/project/schemas/project_definition.py +1 -3
  33. snowflake/cli/api/rendering/sql_templates.py +6 -0
  34. snowflake/cli/api/rest_api.py +11 -5
  35. snowflake/cli/api/utils/definition_rendering.py +24 -4
  36. {snowflake_cli_labs-3.0.0rc3.dist-info → snowflake_cli_labs-3.0.0rc4.dist-info}/METADATA +4 -2
  37. {snowflake_cli_labs-3.0.0rc3.dist-info → snowflake_cli_labs-3.0.0rc4.dist-info}/RECORD +40 -41
  38. snowflake/cli/_plugins/nativeapp/application_entity_model.py +0 -56
  39. snowflake/cli/_plugins/nativeapp/application_package_entity_model.py +0 -94
  40. snowflake/cli/_plugins/nativeapp/init.py +0 -345
  41. {snowflake_cli_labs-3.0.0rc3.dist-info → snowflake_cli_labs-3.0.0rc4.dist-info}/WHEEL +0 -0
  42. {snowflake_cli_labs-3.0.0rc3.dist-info → snowflake_cli_labs-3.0.0rc4.dist-info}/entry_points.txt +0 -0
  43. {snowflake_cli_labs-3.0.0rc3.dist-info → snowflake_cli_labs-3.0.0rc4.dist-info}/licenses/LICENSE +0 -0
@@ -1,19 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ import time
1
4
  from contextlib import contextmanager
5
+ from datetime import datetime
2
6
  from pathlib import Path
3
7
  from textwrap import dedent
4
- from typing import Callable, List, Optional, TypedDict
8
+ from typing import Callable, Generator, List, Literal, Optional, TypedDict
5
9
 
6
10
  import typer
7
11
  from click import ClickException, UsageError
8
- from snowflake.cli._plugins.nativeapp.application_entity_model import (
9
- ApplicationEntityModel,
10
- )
11
- from snowflake.cli._plugins.nativeapp.application_package_entity import (
12
- ApplicationPackageEntity,
13
- )
14
- from snowflake.cli._plugins.nativeapp.application_package_entity_model import (
15
- ApplicationPackageEntityModel,
16
- )
12
+ from pydantic import Field, field_validator
13
+ from snowflake.cli._plugins.connection.util import make_snowsight_url
17
14
  from snowflake.cli._plugins.nativeapp.common_flags import (
18
15
  ForceOption,
19
16
  InteractiveOption,
@@ -26,8 +23,13 @@ from snowflake.cli._plugins.nativeapp.constants import (
26
23
  OWNER_COL,
27
24
  SPECIAL_COMMENT,
28
25
  )
26
+ from snowflake.cli._plugins.nativeapp.entities.application_package import (
27
+ ApplicationPackageEntity,
28
+ ApplicationPackageEntityModel,
29
+ )
29
30
  from snowflake.cli._plugins.nativeapp.exceptions import (
30
31
  ApplicationPackageDoesNotExistError,
32
+ NoEventTableForAccount,
31
33
  )
32
34
  from snowflake.cli._plugins.nativeapp.policy import (
33
35
  AllowAlwaysPolicy,
@@ -38,10 +40,9 @@ from snowflake.cli._plugins.nativeapp.policy import (
38
40
  from snowflake.cli._plugins.nativeapp.same_account_install_method import (
39
41
  SameAccountInstallMethod,
40
42
  )
41
- from snowflake.cli._plugins.nativeapp.utils import (
42
- needs_confirmation,
43
- )
43
+ from snowflake.cli._plugins.nativeapp.utils import needs_confirmation
44
44
  from snowflake.cli._plugins.workspace.action_context import ActionContext
45
+ from snowflake.cli.api.cli_global_context import get_cli_context
45
46
  from snowflake.cli.api.console.abc import AbstractConsole
46
47
  from snowflake.cli.api.entities.common import EntityBase, get_sql_executor
47
48
  from snowflake.cli.api.entities.utils import (
@@ -57,11 +58,21 @@ from snowflake.cli.api.errno import (
57
58
  NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS,
58
59
  ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS,
59
60
  )
60
- from snowflake.cli.api.project.schemas.entities.common import PostDeployHook
61
+ from snowflake.cli.api.metrics import CLICounterField
62
+ from snowflake.cli.api.project.schemas.entities.common import (
63
+ EntityModelBase,
64
+ Identifier,
65
+ PostDeployHook,
66
+ TargetField,
67
+ )
68
+ from snowflake.cli.api.project.schemas.updatable_model import DiscriminatorField
61
69
  from snowflake.cli.api.project.util import (
70
+ append_test_resource_suffix,
62
71
  extract_schema,
72
+ identifier_for_url,
73
+ unquote_identifier,
63
74
  )
64
- from snowflake.connector import ProgrammingError
75
+ from snowflake.connector import DictCursor, ProgrammingError
65
76
 
66
77
  # Reasons why an `alter application ... upgrade` might fail
67
78
  UPGRADE_RESTRICTION_CODES = {
@@ -75,6 +86,31 @@ UPGRADE_RESTRICTION_CODES = {
75
86
  ApplicationOwnedObject = TypedDict("ApplicationOwnedObject", {"name": str, "type": str})
76
87
 
77
88
 
89
+ class ApplicationEntityModel(EntityModelBase):
90
+ type: Literal["application"] = DiscriminatorField() # noqa A003
91
+ from_: TargetField[ApplicationPackageEntityModel] = Field(
92
+ alias="from",
93
+ title="An application package this entity should be created from",
94
+ )
95
+ debug: Optional[bool] = Field(
96
+ title="Whether to enable debug mode when using a named stage to create an application object",
97
+ default=None,
98
+ )
99
+
100
+ @field_validator("identifier")
101
+ @classmethod
102
+ def append_test_resource_suffix_to_identifier(
103
+ cls, input_value: Identifier | str
104
+ ) -> Identifier | str:
105
+ identifier = (
106
+ input_value.name if isinstance(input_value, Identifier) else input_value
107
+ )
108
+ with_suffix = append_test_resource_suffix(identifier)
109
+ if isinstance(input_value, Identifier):
110
+ return input_value.model_copy(update=dict(name=with_suffix))
111
+ return with_suffix
112
+
113
+
78
114
  class ApplicationEntity(EntityBase[ApplicationEntityModel]):
79
115
  """
80
116
  A Native App application object, created from an application package.
@@ -528,14 +564,13 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
528
564
  )
529
565
 
530
566
  # hooks always executed after a create or upgrade
531
- if post_deploy_hooks:
532
- cls.execute_post_deploy_hooks(
533
- console=console,
534
- project_root=project_root,
535
- post_deploy_hooks=post_deploy_hooks,
536
- app_name=app_name,
537
- app_warehouse=app_warehouse,
538
- )
567
+ cls.execute_post_deploy_hooks(
568
+ console=console,
569
+ project_root=project_root,
570
+ post_deploy_hooks=post_deploy_hooks,
571
+ app_name=app_name,
572
+ app_warehouse=app_warehouse,
573
+ )
539
574
  return
540
575
 
541
576
  except ProgrammingError as err:
@@ -587,14 +622,13 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
587
622
  print_messages(console, create_cursor)
588
623
 
589
624
  # hooks always executed after a create or upgrade
590
- if post_deploy_hooks:
591
- cls.execute_post_deploy_hooks(
592
- console=console,
593
- project_root=project_root,
594
- post_deploy_hooks=post_deploy_hooks,
595
- app_name=app_name,
596
- app_warehouse=app_warehouse,
597
- )
625
+ cls.execute_post_deploy_hooks(
626
+ console=console,
627
+ project_root=project_root,
628
+ post_deploy_hooks=post_deploy_hooks,
629
+ app_name=app_name,
630
+ app_warehouse=app_warehouse,
631
+ )
598
632
 
599
633
  except ProgrammingError as err:
600
634
  generic_sql_error_handler(err)
@@ -608,14 +642,19 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
608
642
  app_name: str,
609
643
  app_warehouse: Optional[str],
610
644
  ):
611
- with cls.use_application_warehouse(app_warehouse):
612
- execute_post_deploy_hooks(
613
- console=console,
614
- project_root=project_root,
615
- post_deploy_hooks=post_deploy_hooks,
616
- deployed_object_type="application",
617
- database_name=app_name,
618
- )
645
+ get_cli_context().metrics.set_counter_default(
646
+ CLICounterField.POST_DEPLOY_SCRIPTS, 0
647
+ )
648
+
649
+ if post_deploy_hooks:
650
+ with cls.use_application_warehouse(app_warehouse):
651
+ execute_post_deploy_hooks(
652
+ console=console,
653
+ project_root=project_root,
654
+ post_deploy_hooks=post_deploy_hooks,
655
+ deployed_object_type="application",
656
+ database_name=app_name,
657
+ )
619
658
 
620
659
  @staticmethod
621
660
  @contextmanager
@@ -649,3 +688,191 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
649
688
  return sql_executor.show_specific_object(
650
689
  "applications", app_name, name_col=NAME_COL
651
690
  )
691
+
692
+ @classmethod
693
+ def get_events(
694
+ cls,
695
+ app_name: str,
696
+ package_name: str,
697
+ since: str | datetime | None = None,
698
+ until: str | datetime | None = None,
699
+ record_types: list[str] | None = None,
700
+ scopes: list[str] | None = None,
701
+ consumer_org: str = "",
702
+ consumer_account: str = "",
703
+ consumer_app_hash: str = "",
704
+ first: int = -1,
705
+ last: int = -1,
706
+ ):
707
+
708
+ record_types = record_types or []
709
+ scopes = scopes or []
710
+
711
+ if first >= 0 and last >= 0:
712
+ raise ValueError("first and last cannot be used together")
713
+
714
+ account_event_table = cls.get_account_event_table()
715
+ if not account_event_table:
716
+ raise NoEventTableForAccount()
717
+
718
+ # resource_attributes uses the unquoted/uppercase app and package name
719
+ app_name = unquote_identifier(app_name)
720
+ package_name = unquote_identifier(package_name)
721
+ org_name = unquote_identifier(consumer_org)
722
+ account_name = unquote_identifier(consumer_account)
723
+
724
+ # Filter on record attributes
725
+ if consumer_org and consumer_account:
726
+ # Look for events shared from a consumer account
727
+ app_clause = (
728
+ f"resource_attributes:\"snow.application.package.name\" = '{package_name}' "
729
+ f"and resource_attributes:\"snow.application.consumer.organization\" = '{org_name}' "
730
+ f"and resource_attributes:\"snow.application.consumer.name\" = '{account_name}'"
731
+ )
732
+ if consumer_app_hash:
733
+ # If the user has specified a hash of a specific app installation
734
+ # in the consumer account, filter events to that installation only
735
+ app_clause += f" and resource_attributes:\"snow.database.hash\" = '{consumer_app_hash.lower()}'"
736
+ else:
737
+ # Otherwise look for events from an app installed in the same account as the package
738
+ app_clause = f"resource_attributes:\"snow.database.name\" = '{app_name}'"
739
+
740
+ # Filter on event time
741
+ if isinstance(since, datetime):
742
+ since_clause = f"and timestamp >= '{since}'"
743
+ elif isinstance(since, str) and since:
744
+ since_clause = f"and timestamp >= sysdate() - interval '{since}'"
745
+ else:
746
+ since_clause = ""
747
+ if isinstance(until, datetime):
748
+ until_clause = f"and timestamp <= '{until}'"
749
+ elif isinstance(until, str) and until:
750
+ until_clause = f"and timestamp <= sysdate() - interval '{until}'"
751
+ else:
752
+ until_clause = ""
753
+
754
+ # Filter on event type (log, span, span_event)
755
+ type_in_values = ",".join(f"'{v}'" for v in record_types)
756
+ types_clause = (
757
+ f"and record_type in ({type_in_values})" if type_in_values else ""
758
+ )
759
+
760
+ # Filter on event scope (e.g. the logger name)
761
+ scope_in_values = ",".join(f"'{v}'" for v in scopes)
762
+ scopes_clause = (
763
+ f"and scope:name in ({scope_in_values})" if scope_in_values else ""
764
+ )
765
+
766
+ # Limit event count
767
+ first_clause = f"limit {first}" if first >= 0 else ""
768
+ last_clause = f"limit {last}" if last >= 0 else ""
769
+
770
+ query = dedent(
771
+ f"""\
772
+ select * from (
773
+ select timestamp, value::varchar value
774
+ from {account_event_table}
775
+ where ({app_clause})
776
+ {since_clause}
777
+ {until_clause}
778
+ {types_clause}
779
+ {scopes_clause}
780
+ order by timestamp desc
781
+ {last_clause}
782
+ ) order by timestamp asc
783
+ {first_clause}
784
+ """
785
+ )
786
+ sql_executor = get_sql_executor()
787
+ try:
788
+ return sql_executor.execute_query(query, cursor_class=DictCursor).fetchall()
789
+ except ProgrammingError as err:
790
+ generic_sql_error_handler(err)
791
+
792
+ @classmethod
793
+ def stream_events(
794
+ cls,
795
+ app_name: str,
796
+ package_name: str,
797
+ interval_seconds: int,
798
+ since: str | datetime | None = None,
799
+ record_types: list[str] | None = None,
800
+ scopes: list[str] | None = None,
801
+ consumer_org: str = "",
802
+ consumer_account: str = "",
803
+ consumer_app_hash: str = "",
804
+ last: int = -1,
805
+ ) -> Generator[dict, None, None]:
806
+ try:
807
+ events = cls.get_events(
808
+ app_name=app_name,
809
+ package_name=package_name,
810
+ since=since,
811
+ record_types=record_types,
812
+ scopes=scopes,
813
+ consumer_org=consumer_org,
814
+ consumer_account=consumer_account,
815
+ consumer_app_hash=consumer_app_hash,
816
+ last=last,
817
+ )
818
+ yield from events # Yield the initial batch of events
819
+ last_event_time = events[-1]["TIMESTAMP"] if events else None
820
+
821
+ while True: # Then infinite poll for new events
822
+ time.sleep(interval_seconds)
823
+ previous_events = events
824
+ events = cls.get_events(
825
+ app_name=app_name,
826
+ package_name=package_name,
827
+ since=last_event_time,
828
+ record_types=record_types,
829
+ scopes=scopes,
830
+ consumer_org=consumer_org,
831
+ consumer_account=consumer_account,
832
+ consumer_app_hash=consumer_app_hash,
833
+ )
834
+ if not events:
835
+ continue
836
+
837
+ yield from _new_events_only(previous_events, events)
838
+ last_event_time = events[-1]["TIMESTAMP"]
839
+ except KeyboardInterrupt:
840
+ return
841
+
842
+ @staticmethod
843
+ def get_account_event_table():
844
+ query = "show parameters like 'event_table' in account"
845
+ sql_executor = get_sql_executor()
846
+ results = sql_executor.execute_query(query, cursor_class=DictCursor)
847
+ return next((r["value"] for r in results if r["key"] == "EVENT_TABLE"), "")
848
+
849
+ @classmethod
850
+ def get_snowsight_url(cls, app_name: str, app_warehouse: str | None) -> str:
851
+ """Returns the URL that can be used to visit this app via Snowsight."""
852
+ name = identifier_for_url(app_name)
853
+ with cls.use_application_warehouse(app_warehouse):
854
+ sql_executor = get_sql_executor()
855
+ return make_snowsight_url(
856
+ sql_executor._conn, f"/#/apps/application/{name}" # noqa: SLF001
857
+ )
858
+
859
+
860
+ def _new_events_only(previous_events: list[dict], new_events: list[dict]) -> list[dict]:
861
+ # The timestamp that overlaps between both sets of events
862
+ overlap_time = new_events[0]["TIMESTAMP"]
863
+
864
+ # Remove all the events from the new result set
865
+ # if they were already printed. We iterate and remove
866
+ # instead of filtering in order to handle duplicates
867
+ # (i.e. if an event is present 3 times in new_events
868
+ # but only once in previous_events, it should still
869
+ # appear twice in new_events at the end
870
+ new_events = new_events.copy()
871
+ for event in reversed(previous_events):
872
+ if event["TIMESTAMP"] < overlap_time:
873
+ break
874
+ # No need to handle ValueError here since we know
875
+ # that events that pass the above if check will
876
+ # either be in both lists or in new_events only
877
+ new_events.remove(event)
878
+ return new_events