Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-cgorrie committed Sep 9, 2024
1 parent 20985f7 commit 16a4f7b
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 65 deletions.
10 changes: 5 additions & 5 deletions src/snowflake/cli/_plugins/nativeapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@
from typing import Generator, Iterable, List, Optional, cast

import typer
from snowflake.cli._plugins.nativeapp.common_flags import (
ForceOption,
InteractiveOption,
ValidateOption,
)
from snowflake.cli._plugins.nativeapp.init import (
OFFICIAL_TEMPLATES_GITHUB_URL,
nativeapp_init,
Expand Down Expand Up @@ -59,6 +54,11 @@
with_project_definition,
)
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
from snowflake.cli.api.entities.parameters import (
ForceOption,
InteractiveOption,
ValidateOption,
)
from snowflake.cli.api.exceptions import IncompatibleParametersError
from snowflake.cli.api.output.formats import OutputFormat
from snowflake.cli.api.output.types import (
Expand Down
5 changes: 0 additions & 5 deletions src/snowflake/cli/_plugins/nativeapp/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import os
from pathlib import Path
from sys import stdin, stdout
from typing import Iterable, Optional, Union

from click import ClickException
Expand All @@ -26,10 +25,6 @@ def needs_confirmation(needs_confirm: bool, auto_yes: bool) -> bool:
return needs_confirm and not auto_yes


def is_tty_interactive():
return stdin.isatty() and stdout.isatty()


def get_first_paragraph_from_markdown_file(file_path: Path) -> Optional[str]:
"""
Reads a Markdown file at the given file path and finds the first paragraph
Expand Down
2 changes: 1 addition & 1 deletion src/snowflake/cli/_plugins/nativeapp/version/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

import typer
from click import MissingParameter
from snowflake.cli._plugins.nativeapp.common_flags import ForceOption, InteractiveOption
from snowflake.cli._plugins.nativeapp.policy import (
AllowAlwaysPolicy,
AskAlwaysPolicy,
Expand All @@ -38,6 +37,7 @@
with_project_definition,
)
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
from snowflake.cli.api.entities.parameters import ForceOption, InteractiveOption
from snowflake.cli.api.output.types import CommandResult, MessageResult, QueryResult
from snowflake.cli.api.project.project_verification import assert_project_type

Expand Down
7 changes: 2 additions & 5 deletions src/snowflake/cli/_plugins/workspace/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,13 @@
import typer
import yaml
from snowflake.cli._plugins.nativeapp.artifacts import BundleMap
from snowflake.cli._plugins.nativeapp.common_flags import (
ForceOption,
ValidateOption,
)
from snowflake.cli._plugins.workspace.entity_commands import generate_entity_commands
from snowflake.cli._plugins.workspace.manager import WorkspaceManager
from snowflake.cli.api.cli_global_context import get_cli_context
from snowflake.cli.api.commands.decorators import with_project_definition
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
from snowflake.cli.api.entities.common import EntityActions
from snowflake.cli.api.entities.actions import EntityActions
from snowflake.cli.api.entities.parameters import ForceOption, ValidateOption
from snowflake.cli.api.exceptions import IncompatibleParametersError
from snowflake.cli.api.output.types import MessageResult
from snowflake.cli.api.project.definition_conversion import (
Expand Down
43 changes: 25 additions & 18 deletions src/snowflake/cli/_plugins/workspace/entity_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,24 @@

import logging
from pathlib import Path
from typing import Callable

from click import ClickException
from snowflake.cli._plugins.workspace.manager import WorkspaceManager
from snowflake.cli.api.cli_global_context import get_cli_context
from snowflake.cli.api.commands.decorators import with_project_definition
from snowflake.cli.api.commands.decorators import (
_options_decorator_factory,
with_project_definition,
)
from snowflake.cli.api.commands.snow_typer import (
SnowTyper,
SnowTyperCommandData,
SnowTyperFactory,
)
from snowflake.cli.api.entities.common import EntityActions
from snowflake.cli.api.entities.actions import EntityAction, EntityActions
from snowflake.cli.api.output.types import CommandResult, MessageResult
from snowflake.cli.api.project.definition_manager import DefinitionManager
from snowflake.cli.api.project.schemas.entities.entities import (
Entity,
v2_entity_model_to_entity_map,
v2_entity_model_types_map,
)
Expand All @@ -37,6 +40,11 @@


class EntityCommandGroup(SnowTyperFactory):
"""
Command group for entity actions on a particular concrete entity.
Currently, all such command groups are hidden.
"""

help: str # noqa: A003
target_id: str
_tree_path: list[str]
Expand All @@ -50,7 +58,7 @@ def __init__(
help_text: str | None = None,
tree_path: list[str] = [],
):
super().__init__(name=name, help=help_text)
super().__init__(name=name, help=help_text, is_hidden=lambda: True)
self.target_id = target_id
self._tree_path = tree_path
self._command_map = {}
Expand All @@ -76,7 +84,7 @@ def create_instance(self) -> SnowTyper:
*self._command_map.keys(),
]
)
self.help = "\+ " + ", ".join(subcommands)
self.help = r"\+ " + ", ".join(subcommands)

return super().create_instance()

Expand Down Expand Up @@ -106,17 +114,14 @@ def _get_subtree(self, group_path: list[str]) -> "EntityCommandGroup":
return subtree

def register_command_leaf(
self, name: str, action: EntityActions, action_callable: Callable
self, name: str, entity_type: Entity, action: EntityAction
):
"""Registers the provided action at the given name"""

@self.command(name)
@with_project_definition()
def _action_executor(**options) -> CommandResult:
# TODO: get args for action and turn into typer options
# TODO: what message result are we returning? do we throw them away for multi-step actions (i.e deps?)
# TODO: how do we know if a command needs connection?

cli_context = get_cli_context()
ws = WorkspaceManager(
project_definition=cli_context.project_definition,
Expand All @@ -128,18 +133,23 @@ def _action_executor(**options) -> CommandResult:
f"Successfully performed {action.verb} on {self.target_id}."
)

_action_executor.__doc__ = action_callable.__doc__
# add typer options/arguments and metadata
fn = entity_type.get_action_callable()
params = entity_type.get_action_params_as_inspect()
_action_executor = _options_decorator_factory(_action_executor, params)
_action_executor.__doc__ = fn.__doc__

def register_command_in_tree(
self, action: EntityActions, action_callable: Callable
):
# queue this command for registration
self.command(name)(_action_executor)

def register_command_in_tree(self, entity_type: Entity, action: EntityAction):
"""
Recurses into subtrees created on-demand to register
an action based on its command path and implementation.
"""
[*group_path, verb] = action.command_path
subtree = self._get_subtree(group_path)
subtree.register_command_leaf(verb, action, action_callable)
subtree.register_command_leaf(verb, entity_type, action)


def generate_entity_commands(
Expand Down Expand Up @@ -175,9 +185,6 @@ def generate_entity_commands(
[action for action in EntityActions if entity_type.supports(action)]
)
for action in supported_actions:
tree_group.register_command_in_tree(
action, entity_type.get_action_callable(action)
)
tree_group.register_command_in_tree(entity_type, action)

# TODO: hide, by default
ws.add_typer(tree_group)
108 changes: 108 additions & 0 deletions src/snowflake/cli/api/entities/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import inspect
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from textwrap import dedent
from typing import Annotated, Callable, List, Optional


@dataclass
class HelpText:
value: str


class ParameterDeclarations:
decls: List[str]

def __init__(self, *decls: str):
self.decls = list(decls)


class EntityAction:
key: str
_declaration: inspect.Signature

def __init__(self, key: str):
self.key = key

def __str__(self) -> str:
return f"EntityAction[{self.key}]"

# @functools.cached_property
# def params_map(self) -> Dict[str, ActionParameter]:
# return {param.name: param for param in (self.params or [])}

@property
def verb(self) -> str:
return self.key.replace("_", " ")

@property
def command_path(self) -> list[str]:
return self.key.split("_")

def declaration(self, func: Callable):
"""
Inspects the function signature of the wrapped function to define required
and default parameters for an entity action. The function is never called.
"""
self._declaration = inspect.signature(func)

def implementation(self):
"""
Validates the wrapped function's signature against the stored declaration
signature.
"""
if not self._declaration:
raise RuntimeError(
f"{str(self)} has no base declaration; cannot register implementation."
)

def wrapper(func):
func.entity_action = self
return func

return wrapper


class EntityActions(EntityAction, Enum):
BUNDLE = ("bundle",)
DEPLOY = ("deploy",)
DROP = ("drop",)
VALIDATE = ("validate",)
VERSION_CREATE = (("version_create",),)
VERSION_DROP = (("version_drop",),)
VERSION_LIST = ("version_list",)


@EntityActions.DEPLOY.declaration
def deploy(
prune: Annotated[
Optional[bool],
HelpText(
"Whether to delete specified files from the stage if they don't exist locally. If set, the command deletes files that exist in the stage, but not in the local filesystem. This option cannot be used when paths are specified."
),
] = None,
recursive: Annotated[
Optional[bool],
ParameterDeclarations("--recursive/--no-recursive", "-r"),
HelpText(
"Whether to traverse and deploy files from subdirectories. If set, the command deploys all files and subdirectories; otherwise, only files in the current directory are deployed."
),
] = None,
paths: Annotated[
Optional[List[Path]],
HelpText(
dedent(
f"""
Paths, relative to the the project root, of files or directories you want to upload to a stage. If a file is
specified, it must match one of the artifacts src pattern entries in snowflake.yml. If a directory is
specified, it will be searched for subfolders or files to deploy based on artifacts src pattern entries. If
unspecified, the command syncs all local changes to the stage."""
).strip()
),
] = None,
):
"""
Generic help text for deploy.
"""
raise NotImplementedError()
Loading

0 comments on commit 16a4f7b

Please sign in to comment.