diff --git a/docs/resources/streamlit.md b/docs/resources/streamlit.md new file mode 100644 index 0000000..aa95443 --- /dev/null +++ b/docs/resources/streamlit.md @@ -0,0 +1,69 @@ +--- +description: >- + +--- + +# Streamlit + +[Snowflake Documentation](https://docs.snowflake.com/en/sql-reference/sql/create-streamlit) + +Represents a Streamlit app in Snowflake, which is a schema-scoped resource for creating interactive applications using Python code. + +## Examples + +### Python + +```python +# Creating a Streamlit app from a stage +streamlit_stage = Streamlit( + name="my_db.my_schema.my_streamlit", + from_="@my_stage", + main_file="app.py", + title="My Streamlit App", + query_warehouse="my_warehouse", + comment="A sample Streamlit app from a stage", + tags={"project": "demo"} +) + +# Creating a Streamlit app from a Git repository +streamlit_repo = Streamlit( + name="my_streamlit", + from_="https://github.com/user/repo.git", + version="main", + main_file="app.py", + title="Repo Streamlit App", + owner="SYSADMIN" +) +``` + +### YAML + +```yaml +streamlits: + - name: my_db.my_schema.my_streamlit + from: "@my_stage" + main_file: "app.py" + title: "My Streamlit App" + query_warehouse: "my_warehouse" + comment: "A sample Streamlit app from a stage" + owner: SYSADMIN + tags: + project: demo + - name: my_streamlit + from: "https://github.com/user/repo.git" + version: "main" + main_file: "app.py" + title: "Repo Streamlit App" +``` + +## Fields + +* `name` (string, required) - The name of the Streamlit app. Can be a fully qualified name (e.g., "database.schema.app_name"). +* `from_` (string, required) - The source of the Streamlit app. This can be either a stage (e.g., '@mystage') or a repository URL (e.g., 'https://github.com/user/repo.git'). +* `version` (string) - The version or branch of the repository to use. Only applicable if from_ is a repository URL. +* `main_file` (string) - The name of the main Python file for the Streamlit app (e.g., 'app.py'). +* `title` (string) - The display title of the Streamlit app. +* `query_warehouse` (string) - The name of the warehouse to use for queries in the app. +* `comment` (string) - A comment or description for the Streamlit app. +* `owner` (string or Role) - The role that owns the Streamlit app. Defaults to "SYSADMIN". +* `tags` (dict) - A dictionary of tags to associate with the Streamlit app. \ No newline at end of file diff --git a/titan/enums.py b/titan/enums.py index 08896bd..4dbda4f 100644 --- a/titan/enums.py +++ b/titan/enums.py @@ -106,6 +106,7 @@ class ResourceType(ParseableEnum): STAGE = "STAGE" STORAGE_INTEGRATION = "STORAGE INTEGRATION" STREAM = "STREAM" + STREAMLIT = "STREAMLIT" TABLE = "TABLE" TAG = "TAG" TAG_REFERENCE = "TAG REFERENCE" diff --git a/titan/privs.py b/titan/privs.py index f885ed8..668d28b 100644 --- a/titan/privs.py +++ b/titan/privs.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from typing import Optional, Union + from .enums import ParseableEnum, ResourceType @@ -297,6 +298,12 @@ class StreamPriv(Priv): SELECT = "SELECT" +class StreamlitPriv(Priv): + ALL = "ALL" + OWNERSHIP = "OWNERSHIP" + USAGE = "USAGE" + + class TablePriv(Priv): ALL = "ALL" APPLYBUDGET = "APPLYBUDGET" @@ -407,6 +414,7 @@ class WarehousePriv(Priv): ResourceType.STAGE: StagePriv, ResourceType.STORAGE_INTEGRATION: IntegrationPriv, ResourceType.STREAM: StreamPriv, + ResourceType.STREAMLIT: StreamlitPriv, ResourceType.TABLE: TablePriv, ResourceType.TAG_REFERENCE: None, ResourceType.TAG: TagPriv, @@ -541,3 +549,4 @@ def system_role_for_priv(priv: str): except ValueError: return None return GLOBAL_PRIV_DEFAULT_OWNERS.get(account_priv) + return GLOBAL_PRIV_DEFAULT_OWNERS.get(account_priv) diff --git a/titan/props.py b/titan/props.py index 7371876..3f588f9 100644 --- a/titan/props.py +++ b/titan/props.py @@ -654,3 +654,23 @@ def render(self, values): column_str = f"{name} {data_type}{not_null}{default}{comment}" columns.append(column_str) return f"({', '.join(columns)})" + + +class FromProp(Prop): + """ + Custom property to handle the FROM clause, which can be either a stage or a repository URL. + """ + + def __init__(self): + value_expr = pp.sgl_quoted_string | (pp.Literal("@") + FullyQualifiedIdentifier()) + super().__init__(label="FROM", value_expr=value_expr) + + def typecheck(self, prop_value): + if isinstance(prop_value, list): + return "".join(prop_value) # e.g., '@stage_name' + return prop_value # e.g., 'https://github.com/user/repo.git' + + def render(self, value): + if value.startswith("@"): + return f"FROM {value}" + return f"FROM '{value}'" diff --git a/titan/resources/__init__.py b/titan/resources/__init__.py index 5d29bbd..f16fc3c 100644 --- a/titan/resources/__init__.py +++ b/titan/resources/__init__.py @@ -16,7 +16,7 @@ from .failover_group import FailoverGroup from .file_format import CSVFileFormat, JSONFileFormat, ParquetFileFormat from .function import JavascriptUDF, PythonUDF -from .grant import FutureGrant, Grant, GrantOnAll, RoleGrant, DatabaseRoleGrant +from .grant import DatabaseRoleGrant, FutureGrant, Grant, GrantOnAll, RoleGrant from .hybrid_table import HybridTable from .iceberg_table import SnowflakeIcebergTable from .image_repository import ImageRepository @@ -41,6 +41,7 @@ from .resource import Resource from .resource_monitor import ResourceMonitor from .role import DatabaseRole, Role +from .scanner_package import ScannerPackage from .schema import Schema from .secret import GenericSecret, OAuthSecret, PasswordSecret from .security_integration import ( @@ -58,10 +59,10 @@ S3StorageIntegration, ) from .stream import StageStream, TableStream, ViewStream # ExternalTableStream +from .streamlit import Streamlit from .table import Table # , CreateTableAsSelect from .tag import Tag, TagReference from .task import Task -from .scanner_package import ScannerPackage from .user import User from .view import View from .warehouse import Warehouse @@ -135,6 +136,7 @@ "SnowflakePartnerOAuthSecurityIntegration", "SnowservicesOAuthSecurityIntegration", "StageStream", + "Streamlit", "Table", "TableStream", "Tag", diff --git a/titan/resources/streamlit.py b/titan/resources/streamlit.py new file mode 100644 index 0000000..1685bc6 --- /dev/null +++ b/titan/resources/streamlit.py @@ -0,0 +1,131 @@ +from dataclasses import dataclass +from typing import Optional + +from ..enums import ResourceType +from ..props import FromProp, IdentifierProp, Props, StringProp +from ..resource_name import ResourceName +from ..role_ref import RoleRef +from ..scope import SchemaScope +from .resource import NamedResource, Resource, ResourceSpec +from .tag import TaggableResource + + +@dataclass(unsafe_hash=True) +class _Streamlit(ResourceSpec): + """ + Specification for a Streamlit resource, defining its data structure. + """ + + name: ResourceName + from_: str + version: Optional[str] = None + main_file: Optional[str] = None + title: Optional[str] = None + query_warehouse: Optional[str] = None + comment: Optional[str] = None + owner: RoleRef = "SYSADMIN" + + def __post_init__(self): + super().__post_init__() + if self.from_.startswith("@"): + if self.version is not None: + raise ValueError("Version should not be set when the source is a stage") + + +class Streamlit(NamedResource, TaggableResource, Resource): + """ + Description: + Represents a Streamlit app in Snowflake, which is a schema-scoped resource for creating + interactive applications. + + Snowflake Docs: + https://docs.snowflake.com/en/sql-reference/sql/create-streamlit + + Fields: + name (string, required): The name of the Streamlit app. Can be fully qualified (e.g., "db.schema.app"). + from_ (string, required): The source of the Streamlit app, either a stage (e.g., '@mystage') or a + repository URL (e.g., 'https://github.com/user/repo.git'). + version (string): The version of the repository, applicable only if from_ is a repository URL. + main_file (string): The main Python file for the Streamlit app (e.g., 'app.py'). + title (string): The display title of the Streamlit app. + query_warehouse (string): The warehouse used for queries in the app. + comment (string): A descriptive comment about the Streamlit app. + owner (string or Role): The role that owns the Streamlit app. Defaults to "SYSADMIN". + tags (dict): A dictionary of key-value pairs for tagging the Streamlit app. + + Python Example: + # Creating a Streamlit app from a stage + streamlit_stage = Streamlit( + name="my_db.my_schema.my_streamlit", + from_="@my_stage", + main_file="app.py", + title="My Streamlit App", + query_warehouse="my_warehouse", + comment="A sample Streamlit app from a stage", + tags={"project": "demo"} + ) + + # Creating a Streamlit app from a Git repository + streamlit_repo = Streamlit( + name="my_streamlit", + from_="https://github.com/user/repo.git", + version="main", + main_file="app.py", + title="Repo Streamlit App", + owner="SYSADMIN" + ) + YAML Example: + streamlits: + - name: my_db.my_schema.my_streamlit + from: "@my_stage" + main_file: "app.py" + title: "My Streamlit App" + query_warehouse: "my_warehouse" + comment: "A sample Streamlit app from a stage" + owner: SYSADMIN + tags: + project: demo + - name: my_streamlit + from: "https://github.com/user/repo.git" + version: "main" + main_file: "app.py" + title: "Repo Streamlit App" + """ + + resource_type = ResourceType.STREAMLIT + props = Props( + from_=FromProp(), + version=StringProp("VERSION"), + main_file=StringProp("MAIN_FILE"), + title=StringProp("TITLE"), + query_warehouse=IdentifierProp("QUERY_WAREHOUSE"), + comment=StringProp("COMMENT"), + ) + scope = SchemaScope() + spec = _Streamlit + + def init( + self, + name: str, + from_: str, + version: Optional[str] = None, + main_file: Optional[str] = None, + title: Optional[str] = None, + query_warehouse: Optional[str] = None, + comment: Optional[str] = None, + owner: str = "SYSADMIN", + tags: dict[str, str] = None, + **kwargs, + ): + super().__init__(name, **kwargs) + self._data: _Streamlit = _Streamlit( + name=self.name, + from_=from_, + version=version, + main_file=main_file, + title=title, + query_warehouse=query_warehouse, + comment=comment, + owner=owner, + ) + self.set_tags(tags)