-
Notifications
You must be signed in to change notification settings - Fork 62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Snow 1761248 introduce notebook entity #2007
Changes from all commits
5464f82
a1e1967
6005d36
8560ac0
212ea38
8a4b172
29437cc
2f1b7cd
78c1880
d5e7ef3
667bd9d
bc2c2a5
78e7a58
740a15b
16b6b6c
2c8ec66
d75c216
4d7e437
86f8771
fd4a6ba
3d7a269
0adb41f
a48f8f9
bb665a6
ebf8d07
73f2108
1aed9fc
fe9d572
ba670ed
168dc39
a1aaf6d
48c5a1c
aa41f4b
294067c
011c0b4
394c7af
98f2b2e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import functools | ||
from textwrap import dedent | ||
|
||
from click import ClickException | ||
from snowflake.cli._plugins.connection.util import make_snowsight_url | ||
from snowflake.cli._plugins.notebook.notebook_entity_model import NotebookEntityModel | ||
from snowflake.cli._plugins.notebook.notebook_project_paths import NotebookProjectPaths | ||
from snowflake.cli._plugins.stage.manager import StageManager | ||
from snowflake.cli._plugins.workspace.context import ActionContext | ||
from snowflake.cli.api.artifacts.utils import bundle_artifacts | ||
from snowflake.cli.api.cli_global_context import get_cli_context | ||
from snowflake.cli.api.console.console import cli_console | ||
from snowflake.cli.api.entities.common import EntityBase | ||
from snowflake.cli.api.stage_path import StagePath | ||
from snowflake.connector import ProgrammingError | ||
from snowflake.connector.cursor import SnowflakeCursor | ||
|
||
_DEFAULT_NOTEBOOK_STAGE_NAME = "@notebooks" | ||
|
||
|
||
class NotebookEntity(EntityBase[NotebookEntityModel]): | ||
""" | ||
A notebook. | ||
""" | ||
|
||
@functools.cached_property | ||
def _stage_path(self) -> StagePath: | ||
stage_path = self.model.stage_path | ||
if stage_path is None: | ||
stage_path = f"{_DEFAULT_NOTEBOOK_STAGE_NAME}/{self.fqn.name}" | ||
return StagePath.from_stage_str(stage_path) | ||
|
||
@functools.cached_property | ||
def _project_paths(self): | ||
return NotebookProjectPaths(get_cli_context().project_root) | ||
|
||
def _object_exists(self) -> bool: | ||
# currently notebook objects are not supported by object manager - re-implementing "exists" | ||
try: | ||
self.action_describe() | ||
return True | ||
except ProgrammingError: | ||
return False | ||
|
||
def _upload_artifacts(self): | ||
stage_fqn = self._stage_path.stage_fqn | ||
stage_manager = StageManager() | ||
cli_console.step(f"Creating stage {stage_fqn} if not exists") | ||
stage_manager.create(fqn=stage_fqn) | ||
|
||
cli_console.step("Uploading artifacts") | ||
|
||
# creating bundle map to handle glob patterns logic | ||
bundle_map = bundle_artifacts(self._project_paths, self.model.artifacts) | ||
for absolute_src, absolute_dest in bundle_map.all_mappings( | ||
absolute=True, expand_directories=True | ||
): | ||
artifact_stage_path = self._stage_path / ( | ||
absolute_dest.relative_to(self._project_paths.bundle_root).parent | ||
) | ||
stage_manager.put( | ||
local_path=absolute_src, stage_path=artifact_stage_path, overwrite=True | ||
) | ||
|
||
def get_create_sql(self, replace: bool) -> str: | ||
main_file_stage_path = self._stage_path / ( | ||
self.model.notebook_file.absolute().relative_to( | ||
self._project_paths.project_root | ||
) | ||
) | ||
create_str = "CREATE OR REPLACE" if replace else "CREATE" | ||
return dedent( | ||
f""" | ||
{create_str} NOTEBOOK {self.fqn.sql_identifier} | ||
FROM '{main_file_stage_path.stage_with_at}' | ||
QUERY_WAREHOUSE = '{self.model.query_warehouse}' | ||
MAIN_FILE = '{main_file_stage_path.path}'; | ||
// Cannot use IDENTIFIER(...) | ||
ALTER NOTEBOOK {self.fqn.identifier} ADD LIVE VERSION FROM LAST; | ||
""" | ||
) | ||
|
||
def action_describe(self) -> SnowflakeCursor: | ||
return self._sql_executor.execute_query(self.get_describe_sql()) | ||
|
||
def action_create(self, replace: bool) -> str: | ||
self._sql_executor.execute_query(self.get_create_sql(replace)) | ||
return make_snowsight_url( | ||
self._conn, | ||
f"/#/notebooks/{self.fqn.using_connection(self._conn).url_identifier}", | ||
) | ||
|
||
def action_deploy( | ||
self, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It currently won't be used by any command - should we implement it in this PR? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we introduce an entity- i think we should at least make some dummy methods to show, what is left to implement There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added placeholders |
||
action_ctx: ActionContext, | ||
replace: bool, | ||
*args, | ||
**kwargs, | ||
) -> str: | ||
if self._object_exists(): | ||
if not replace: | ||
raise ClickException( | ||
f"Notebook {self.fqn.name} already exists. Consider using --replace." | ||
) | ||
with cli_console.phase(f"Uploading artifacts to {self._stage_path}"): | ||
self._upload_artifacts() | ||
with cli_console.phase(f"Creating notebook {self.fqn}"): | ||
return self.action_create(replace=replace) | ||
|
||
# complementary actions, currently not used - to be implemented in future | ||
def action_drop(self, *args, **kwargs): | ||
raise ClickException("action DROP not supported by NOTEBOOK entity") | ||
|
||
def action_teardown(self, *args, **kwargs): | ||
raise ClickException("action TEARDOWN not supported by NOTEBOOK entity") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from __future__ import annotations | ||
|
||
from pathlib import Path | ||
from typing import Literal, Optional | ||
|
||
from pydantic import Field, model_validator | ||
from snowflake.cli._plugins.notebook.exceptions import NotebookFilePathError | ||
from snowflake.cli.api.project.schemas.entities.common import ( | ||
EntityModelBaseWithArtifacts, | ||
) | ||
from snowflake.cli.api.project.schemas.updatable_model import ( | ||
DiscriminatorField, | ||
) | ||
|
||
|
||
class NotebookEntityModel(EntityModelBaseWithArtifacts): | ||
type: Literal["notebook"] = DiscriminatorField() # noqa: A003 | ||
stage_path: Optional[str] = Field( | ||
title="Stage directory in which the notebook file will be stored", default=None | ||
) | ||
notebook_file: Path = Field(title="Notebook file") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we consider using artifact approach with notbeook_file being a pointer (similar to streamlit main file)? In this way you can upload more than one file, for example .py file with scripts or a .csv data. WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea, added |
||
query_warehouse: str = Field(title="Snowflake warehouse to execute the notebook") | ||
|
||
@model_validator(mode="after") | ||
def validate_notebook_file(self): | ||
if not self.notebook_file.exists(): | ||
raise ValueError(f"Notebook file {self.notebook_file} does not exist") | ||
if self.notebook_file.suffix.lower() != ".ipynb": | ||
raise NotebookFilePathError(str(self.notebook_file)) | ||
return self |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
from dataclasses import dataclass | ||
from pathlib import Path | ||
|
||
from snowflake.cli.api.project.project_paths import ProjectPaths, bundle_root | ||
|
||
|
||
@dataclass | ||
class NotebookProjectPaths(ProjectPaths): | ||
""" | ||
This class allows you to manage files paths related to given project. | ||
""" | ||
|
||
@property | ||
def bundle_root(self) -> Path: | ||
return bundle_root(self.project_root, "notebook") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed 😺